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.util.Log;
28 import android.view.Surface;
29 import android.view.SurfaceControl;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewTreeObserver.OnDrawListener;
33 
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.concurrent.Executor;
37 
38 /**
39  * A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
40  * class will draw the content of the {@link View} directly into the leash, and the actual View will
41  * be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
42  * full-screen size leash without being constrained by the view tree's boundary or inheriting its
43  * parent's alpha and transformation.
44  *
45  * @hide
46  */
47 public class ViewUIComponent implements UIComponent {
48     private static final String TAG = "ViewUIComponent";
49     private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
50     private final Path mClippingPath = new Path();
51     private final Outline mClippingOutline = new Outline();
52 
53     private final OnDrawListener mOnDrawListener = this::postDraw;
54     private final View mView;
55 
56     @Nullable private SurfaceControl mSurfaceControl;
57     @Nullable private Surface mSurface;
58     @Nullable private Rect mViewBoundsOverride;
59     private boolean mVisibleOverride;
60     private boolean mDirty;
61 
ViewUIComponent(View view)62     public ViewUIComponent(View view) {
63         mView = view;
64     }
65 
66     /**
67      * @return the view wrapped by this UI component.
68      * @hide
69      */
getView()70     public View getView() {
71         return mView;
72     }
73 
74     @Override
getAlpha()75     public float getAlpha() {
76         return mView.getAlpha();
77     }
78 
79     @Override
isVisible()80     public boolean isVisible() {
81         return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
82     }
83 
84     @Override
getBounds()85     public Rect getBounds() {
86         if (isAttachedToLeash() && mViewBoundsOverride != null) {
87             return mViewBoundsOverride;
88         }
89         return getRealBounds();
90     }
91 
92     @Override
newTransaction()93     public Transaction newTransaction() {
94         return new Transaction();
95     }
96 
attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h)97     private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
98         logD("attachToTransitionLeash");
99         // Remember current visibility.
100         mVisibleOverride = mView.getVisibility() == View.VISIBLE;
101 
102         // Create the surface
103         mSurfaceControl =
104                 new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
105         mSurface = new Surface(mSurfaceControl);
106 
107         // Attach surface to transition leash
108         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
109         t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
110 
111         // Make sure view draw triggers surface draw.
112         mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener);
113 
114         // Make the view invisible AFTER the surface is shown.
115         t.addTransactionCommittedListener(
116                         mView::post,
117                         () -> {
118                             logD("Surface attached!");
119                             forceDraw();
120                             mView.setVisibility(View.INVISIBLE);
121                         })
122                 .apply();
123     }
124 
detachFromTransitionLeash(Executor executor, Runnable onDone)125     private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
126         logD("detachFromTransitionLeash");
127         Surface s = mSurface;
128         SurfaceControl sc = mSurfaceControl;
129         mSurface = null;
130         mSurfaceControl = null;
131         mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener);
132         // Restore view visibility
133         mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
134         mView.invalidate();
135         // Clean up surfaces.
136         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
137         t.reparent(sc, null)
138                 .addTransactionCommittedListener(
139                         mView::post,
140                         () -> {
141                             s.release();
142                             sc.release();
143                             executor.execute(onDone);
144                         });
145         // Apply transaction AFTER the view is drawn.
146         mView.getRootSurfaceControl().applyTransactionOnDraw(t);
147     }
148 
149     @Override
toString()150     public String toString() {
151         return "ViewUIComponent{"
152                 + "alpha="
153                 + getAlpha()
154                 + ", visible="
155                 + isVisible()
156                 + ", bounds="
157                 + getBounds()
158                 + ", attached="
159                 + isAttachedToLeash()
160                 + "}";
161     }
162 
draw()163     private void draw() {
164         if (!mDirty) {
165             // No need to draw. This is probably a duplicate call.
166             logD("draw: skipped - clean");
167             return;
168         }
169         mDirty = false;
170         if (!isAttachedToLeash()) {
171             // Not attached.
172             logD("draw: skipped - not attached");
173             return;
174         }
175         ViewGroup.LayoutParams params = mView.getLayoutParams();
176         if (params == null || params.width == 0 || params.height == 0) {
177             // layout pass didn't happen.
178             logD("draw: skipped - no layout");
179             return;
180         }
181         Canvas canvas = mSurface.lockHardwareCanvas();
182         // Clear the canvas first.
183         canvas.drawColor(0, PorterDuff.Mode.CLEAR);
184         if (mVisibleOverride) {
185             Rect realBounds = getRealBounds();
186             Rect renderBounds = getBounds();
187             canvas.translate(renderBounds.left, renderBounds.top);
188             canvas.scale(
189                     (float) renderBounds.width() / realBounds.width(),
190                     (float) renderBounds.height() / realBounds.height());
191 
192             if (mView.getClipToOutline()) {
193                 mView.getOutlineProvider().getOutline(mView, mClippingOutline);
194                 mClippingPath.reset();
195                 RectF rect = new RectF(0, 0, mView.getWidth(), mView.getHeight());
196                 final float cornerRadius = mClippingOutline.getRadius();
197                 mClippingPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);
198                 mClippingPath.close();
199                 canvas.clipPath(mClippingPath);
200             }
201 
202             canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
203             mView.draw(canvas);
204             canvas.restore();
205         }
206         mSurface.unlockCanvasAndPost(canvas);
207         logD("draw: done");
208     }
209 
forceDraw()210     private void forceDraw() {
211         mDirty = true;
212         draw();
213     }
214 
getRealBounds()215     private Rect getRealBounds() {
216         Rect output = new Rect();
217         mView.getBoundsOnScreen(output);
218         return output;
219     }
220 
isAttachedToLeash()221     private boolean isAttachedToLeash() {
222         return mSurfaceControl != null && mSurface != null;
223     }
224 
logD(String msg)225     private void logD(String msg) {
226         if (DEBUG) {
227             Log.d(TAG, msg);
228         }
229     }
230 
setVisible(boolean visible)231     private void setVisible(boolean visible) {
232         logD("setVisibility: " + visible);
233         if (isAttachedToLeash()) {
234             mVisibleOverride = visible;
235             postDraw();
236         } else {
237             mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
238         }
239     }
240 
setBounds(Rect bounds)241     private void setBounds(Rect bounds) {
242         logD("setBounds: " + bounds);
243         mViewBoundsOverride = bounds;
244         if (isAttachedToLeash()) {
245             postDraw();
246         } else {
247             Log.w(TAG, "setBounds: not attached to leash!");
248         }
249     }
250 
setAlpha(float alpha)251     private void setAlpha(float alpha) {
252         logD("setAlpha: " + alpha);
253         mView.setAlpha(alpha);
254         if (isAttachedToLeash()) {
255             postDraw();
256         }
257     }
258 
postDraw()259     private void postDraw() {
260         if (mDirty) {
261             return;
262         }
263         mDirty = true;
264         mView.post(this::draw);
265     }
266 
267     /** @hide */
268     public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
269         private final List<Runnable> mChanges = new ArrayList<>();
270 
271         @Override
setAlpha(ViewUIComponent ui, float alpha)272         public Transaction setAlpha(ViewUIComponent ui, float alpha) {
273             mChanges.add(() -> ui.mView.post(() -> ui.setAlpha(alpha)));
274             return this;
275         }
276 
277         @Override
setVisible(ViewUIComponent ui, boolean visible)278         public Transaction setVisible(ViewUIComponent ui, boolean visible) {
279             mChanges.add(() -> ui.mView.post(() -> ui.setVisible(visible)));
280             return this;
281         }
282 
283         @Override
setBounds(ViewUIComponent ui, Rect bounds)284         public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
285             mChanges.add(() -> ui.mView.post(() -> ui.setBounds(bounds)));
286             return this;
287         }
288 
289         @Override
attachToTransitionLeash( ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h)290         public Transaction attachToTransitionLeash(
291                 ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
292             mChanges.add(
293                     () -> ui.mView.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
294             return this;
295         }
296 
297         @Override
detachFromTransitionLeash( ViewUIComponent ui, Executor executor, Runnable onDone)298         public Transaction detachFromTransitionLeash(
299                 ViewUIComponent ui, Executor executor, Runnable onDone) {
300             mChanges.add(() -> ui.mView.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
301             return this;
302         }
303 
304         @Override
commit()305         public void commit() {
306             mChanges.forEach(Runnable::run);
307             mChanges.clear();
308         }
309     }
310 }
311