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