• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.animation;
18 
19 import android.annotation.Nullable;
20 import android.graphics.Canvas;
21 import android.graphics.Outline;
22 import android.graphics.Path;
23 import android.graphics.PorterDuff;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.os.Build;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.util.Log;
30 import android.view.Surface;
31 import android.view.SurfaceControl;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.ViewRootImpl;
35 import android.view.ViewTreeObserver;
36 
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.concurrent.Executor;
40 
41 /**
42  * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
43  * class will draw the content of the {@link View} directly into the leash, and the actual View will
44  * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
45  * full-screen size leash without being constrained by the view tree's boundary or inheriting its
46  * parent's alpha and transformation.
47  *
48  * @hide
49  */
50 public class ViewUIComponent implements UIComponent {
51     private static final String TAG = "ViewUIComponent";
52     private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
53     private final Path mClippingPath = new Path();
54     private final Outline mClippingOutline = new Outline();
55 
56     private final LifecycleListener mLifecycleListener = new LifecycleListener();
57     private final View mView;
58     private final Handler mMainHandler;
59 
60     @Nullable private SurfaceControl mSurfaceControl;
61     @Nullable private Surface mSurface;
62     @Nullable private Rect mViewBoundsOverride;
63     private boolean mVisibleOverride;
64     private boolean mDirty;
65 
ViewUIComponent(View view)66     public ViewUIComponent(View view) {
67         mView = view;
68         mMainHandler = new Handler(Looper.getMainLooper());
69     }
70 
71     /**
72      * @return the view wrapped by this UI component.
73      * @hide
74      */
getView()75     public View getView() {
76         return mView;
77     }
78 
79     @Override
getAlpha()80     public float getAlpha() {
81         return mView.getAlpha();
82     }
83 
84     @Override
isVisible()85     public boolean isVisible() {
86         return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
87     }
88 
89     @Override
getBounds()90     public Rect getBounds() {
91         if (isAttachedToLeash() && mViewBoundsOverride != null) {
92             return mViewBoundsOverride;
93         }
94         return getRealBounds();
95     }
96 
97     @Override
newTransaction()98     public Transaction newTransaction() {
99         return new Transaction();
100     }
101 
attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h)102     private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
103         logD("attachToTransitionLeash");
104         // Remember current visibility.
105         mVisibleOverride = mView.getVisibility() == View.VISIBLE;
106 
107         // Create the surface
108         mSurfaceControl =
109                 new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
110         mSurface = new Surface(mSurfaceControl);
111 
112         // Attach surface to transition leash
113         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
114         t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
115 
116         // Make sure view draw triggers surface draw.
117         mLifecycleListener.register();
118 
119         // Make the view invisible AFTER the surface is shown.
120         t.addTransactionCommittedListener(
121                         this::post,
122                         () -> {
123                             logD("Surface attached!");
124                             forceDraw();
125                             mView.setVisibility(View.INVISIBLE);
126                         })
127                 .apply();
128     }
129 
detachFromTransitionLeash(Executor executor, Runnable onDone)130     private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
131         logD("detachFromTransitionLeash");
132         Surface s = mSurface;
133         SurfaceControl sc = mSurfaceControl;
134         mSurface = null;
135         mSurfaceControl = null;
136         mLifecycleListener.unregister();
137         // Restore view visibility
138         mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
139         // Clean up surfaces.
140         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
141         t.reparent(sc, null)
142                 .addTransactionCommittedListener(
143                         this::post,
144                         () -> {
145                             s.release();
146                             sc.release();
147                             executor.execute(onDone);
148                         });
149         ViewRootImpl viewRoot = mView.getViewRootImpl();
150         if (viewRoot == null) {
151             t.apply();
152         } else {
153             // Apply transaction AFTER the view is drawn.
154             viewRoot.applyTransactionOnDraw(t);
155             // Request layout to force redrawing the entire view tree, so that the transaction is
156             // guaranteed to be applied.
157             viewRoot.requestLayout();
158         }
159     }
160 
161     @Override
toString()162     public String toString() {
163         return "ViewUIComponent{"
164                 + "alpha="
165                 + getAlpha()
166                 + ", visible="
167                 + isVisible()
168                 + ", bounds="
169                 + getBounds()
170                 + ", attached="
171                 + isAttachedToLeash()
172                 + "}";
173     }
174 
draw()175     private void draw() {
176         if (!mDirty) {
177             // No need to draw. This is probably a duplicate call.
178             logD("draw: skipped - clean");
179             return;
180         }
181         mDirty = false;
182         if (!isAttachedToLeash()) {
183             // Not attached.
184             logD("draw: skipped - not attached");
185             return;
186         }
187         ViewGroup.LayoutParams params = mView.getLayoutParams();
188         if (params == null) {
189             // layout pass didn't happen.
190             logD("draw: skipped - no layout");
191             return;
192         }
193 
194         final Rect realBounds = getRealBounds();
195         if (realBounds.width() == 0 || realBounds.height() == 0) {
196             // bad bounds.
197             logD("draw: skipped - zero bounds");
198             return;
199         }
200 
201 
202         Canvas canvas = mSurface.lockHardwareCanvas();
203         // Clear the canvas first.
204         canvas.drawColor(0, PorterDuff.Mode.CLEAR);
205         if (mVisibleOverride) {
206             Rect renderBounds = getBounds();
207             canvas.translate(renderBounds.left, renderBounds.top);
208             canvas.scale(
209                     (float) renderBounds.width() / realBounds.width(),
210                     (float) renderBounds.height() / realBounds.height());
211 
212             if (mView.getClipToOutline()) {
213                 mView.getOutlineProvider().getOutline(mView, mClippingOutline);
214                 mClippingPath.reset();
215                 RectF rect = new RectF(0, 0, mView.getWidth(), mView.getHeight());
216                 final float cornerRadius = mClippingOutline.getRadius();
217                 mClippingPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);
218                 mClippingPath.close();
219                 canvas.clipPath(mClippingPath);
220             }
221 
222             canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
223             mView.draw(canvas);
224             canvas.restore();
225         }
226         mSurface.unlockCanvasAndPost(canvas);
227         logD("draw: done");
228     }
229 
forceDraw()230     private void forceDraw() {
231         mDirty = true;
232         draw();
233     }
234 
getRealBounds()235     private Rect getRealBounds() {
236         Rect output = new Rect();
237         mView.getBoundsOnScreen(output);
238         return output;
239     }
240 
isAttachedToLeash()241     private boolean isAttachedToLeash() {
242         return mSurfaceControl != null && mSurface != null;
243     }
244 
logD(String msg)245     private void logD(String msg) {
246         if (DEBUG) {
247             Log.d(TAG, msg);
248         }
249     }
250 
setVisible(boolean visible)251     private void setVisible(boolean visible) {
252         logD("setVisibility: " + visible);
253         if (isAttachedToLeash()) {
254             mVisibleOverride = visible;
255             postDraw();
256         } else {
257             mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
258         }
259     }
260 
setBounds(Rect bounds)261     private void setBounds(Rect bounds) {
262         logD("setBounds: " + bounds);
263         mViewBoundsOverride = bounds;
264         if (isAttachedToLeash()) {
265             postDraw();
266         } else {
267             Log.w(TAG, "setBounds: not attached to leash!");
268         }
269     }
270 
setAlpha(float alpha)271     private void setAlpha(float alpha) {
272         logD("setAlpha: " + alpha);
273         mView.setAlpha(alpha);
274         if (isAttachedToLeash()) {
275             postDraw();
276         }
277     }
278 
postDraw()279     private void postDraw() {
280         if (mDirty) {
281             return;
282         }
283         mDirty = true;
284         post(this::draw);
285     }
286 
post(Runnable r)287     private void post(Runnable r) {
288         if (mView.isAttachedToWindow()) {
289             mView.post(r);
290         } else {
291             // If the view is detached from window, {@code View.post()} will postpone the action
292             // until the view is attached again. However, we don't know if the view will be attached
293             // again, so we post the action to the main thread in this case. This could lead to race
294             // condition if the attachment change caused a thread switching, and it's the caller's
295             // responsibility to ensure the window attachment state doesn't change unexpectedly.
296             if (DEBUG) {
297                 Log.w(TAG, mView + " is not attached. Posting action to main thread!");
298             }
299             mMainHandler.post(r);
300         }
301     }
302 
303     /** A listener for monitoring view life cycles. */
304     private class LifecycleListener
305             implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener {
306         private boolean mRegistered;
307 
308         @Override
onDraw()309         public void onDraw() {
310             // View draw should trigger surface draw.
311             postDraw();
312         }
313 
314         @Override
onViewAttachedToWindow(View v)315         public void onViewAttachedToWindow(View v) {
316             // empty
317         }
318 
319         @Override
onViewDetachedFromWindow(View v)320         public void onViewDetachedFromWindow(View v) {
321             Log.w(
322                     TAG,
323                     v + " is detached from the window. Unregistering the life cycle listener ...");
324             unregister();
325         }
326 
register()327         public void register() {
328             if (mRegistered) {
329                 return;
330             }
331             mRegistered = true;
332             mView.getViewTreeObserver().addOnDrawListener(this);
333             mView.addOnAttachStateChangeListener(this);
334         }
335 
unregister()336         public void unregister() {
337             if (!mRegistered) {
338                 return;
339             }
340             mRegistered = false;
341             mView.getViewTreeObserver().removeOnDrawListener(this);
342             mView.removeOnAttachStateChangeListener(this);
343         }
344     }
345 
346     /** @hide */
347     public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
348         private final List<Runnable> mChanges = new ArrayList<>();
349 
350         @Override
setAlpha(ViewUIComponent ui, float alpha)351         public Transaction setAlpha(ViewUIComponent ui, float alpha) {
352             mChanges.add(() -> ui.post(() -> ui.setAlpha(alpha)));
353             return this;
354         }
355 
356         @Override
setVisible(ViewUIComponent ui, boolean visible)357         public Transaction setVisible(ViewUIComponent ui, boolean visible) {
358             mChanges.add(() -> ui.post(() -> ui.setVisible(visible)));
359             return this;
360         }
361 
362         @Override
setBounds(ViewUIComponent ui, Rect bounds)363         public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
364             mChanges.add(() -> ui.post(() -> ui.setBounds(bounds)));
365             return this;
366         }
367 
368         @Override
attachToTransitionLeash( ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h)369         public Transaction attachToTransitionLeash(
370                 ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
371             mChanges.add(() -> ui.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
372             return this;
373         }
374 
375         @Override
detachFromTransitionLeash( ViewUIComponent ui, Executor executor, Runnable onDone)376         public Transaction detachFromTransitionLeash(
377                 ViewUIComponent ui, Executor executor, Runnable onDone) {
378             mChanges.add(() -> ui.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
379             return this;
380         }
381 
382         @Override
commit()383         public void commit() {
384             mChanges.forEach(Runnable::run);
385             mChanges.clear();
386         }
387     }
388 }
389