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