Files
Lawnchair/systemUI/anim/lib/src/com/android/systemui/animation/ViewUIComponent.java
T
Pun Butrach 11f7abcb66 chore: Cleanup, and hidden feature logged
Signed-off-by: Pun Butrach <pun.butrach@gmail.com>
2026-01-10 20:48:25 +07:00

389 lines
12 KiB
Java

/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.animation;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.Outline;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* A {@link UIComponent} wrapping a {@link View}. After being attached to the transition leash, this
* class will draw the content of the {@link View} directly into the leash, and the actual View will
* be changed to INVISIBLE in its view tree. This allows the {@link View} to transform in the
* full-screen size leash without being constrained by the view tree's boundary or inheriting its
* parent's alpha and transformation.
*
* @hide
*/
public class ViewUIComponent implements UIComponent {
private static final String TAG = "ViewUIComponent";
private static final boolean DEBUG = Build.IS_USERDEBUG || Log.isLoggable(TAG, Log.DEBUG);
private final Path mClippingPath = new Path();
private final Outline mClippingOutline = new Outline();
private final LifecycleListener mLifecycleListener = new LifecycleListener();
private final View mView;
private final Handler mMainHandler;
@Nullable private SurfaceControl mSurfaceControl;
@Nullable private Surface mSurface;
@Nullable private Rect mViewBoundsOverride;
private boolean mVisibleOverride;
private boolean mDirty;
public ViewUIComponent(View view) {
mView = view;
mMainHandler = new Handler(Looper.getMainLooper());
}
/**
* @return the view wrapped by this UI component.
* @hide
*/
public View getView() {
return mView;
}
@Override
public float getAlpha() {
return mView.getAlpha();
}
@Override
public boolean isVisible() {
return isAttachedToLeash() ? mVisibleOverride : mView.getVisibility() == View.VISIBLE;
}
@Override
public Rect getBounds() {
if (isAttachedToLeash() && mViewBoundsOverride != null) {
return mViewBoundsOverride;
}
return getRealBounds();
}
@Override
public Transaction newTransaction() {
return new Transaction();
}
private void attachToTransitionLeash(SurfaceControl transitionLeash, int w, int h) {
logD("attachToTransitionLeash");
// Remember current visibility.
mVisibleOverride = mView.getVisibility() == View.VISIBLE;
// Create the surface
mSurfaceControl =
new SurfaceControl.Builder().setName("ViewUIComponent").setBufferSize(w, h).build();
mSurface = new Surface(mSurfaceControl);
// Attach surface to transition leash
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl);
// Make sure view draw triggers surface draw.
mLifecycleListener.register();
// Make the view invisible AFTER the surface is shown.
t.addTransactionCommittedListener(
this::post,
() -> {
logD("Surface attached!");
forceDraw();
mView.setVisibility(View.INVISIBLE);
})
.apply();
}
private void detachFromTransitionLeash(Executor executor, Runnable onDone) {
logD("detachFromTransitionLeash");
Surface s = mSurface;
SurfaceControl sc = mSurfaceControl;
mSurface = null;
mSurfaceControl = null;
mLifecycleListener.unregister();
// Restore view visibility
mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE);
// Clean up surfaces.
SurfaceControl.Transaction t = new SurfaceControl.Transaction();
t.reparent(sc, null)
.addTransactionCommittedListener(
this::post,
() -> {
s.release();
sc.release();
executor.execute(onDone);
});
ViewRootImpl viewRoot = mView.getViewRootImpl();
if (viewRoot == null) {
t.apply();
} else {
// Apply transaction AFTER the view is drawn.
viewRoot.applyTransactionOnDraw(t);
// Request layout to force redrawing the entire view tree, so that the transaction is
// guaranteed to be applied.
viewRoot.requestLayout();
}
}
@Override
public String toString() {
return "ViewUIComponent{"
+ "alpha="
+ getAlpha()
+ ", visible="
+ isVisible()
+ ", bounds="
+ getBounds()
+ ", attached="
+ isAttachedToLeash()
+ "}";
}
private void draw() {
if (!mDirty) {
// No need to draw. This is probably a duplicate call.
logD("draw: skipped - clean");
return;
}
mDirty = false;
if (!isAttachedToLeash()) {
// Not attached.
logD("draw: skipped - not attached");
return;
}
ViewGroup.LayoutParams params = mView.getLayoutParams();
if (params == null) {
// layout pass didn't happen.
logD("draw: skipped - no layout");
return;
}
final Rect realBounds = getRealBounds();
if (realBounds.width() == 0 || realBounds.height() == 0) {
// bad bounds.
logD("draw: skipped - zero bounds");
return;
}
Canvas canvas = mSurface.lockHardwareCanvas();
// Clear the canvas first.
canvas.drawColor(0, PorterDuff.Mode.CLEAR);
if (mVisibleOverride) {
Rect renderBounds = getBounds();
canvas.translate(renderBounds.left, renderBounds.top);
canvas.scale(
(float) renderBounds.width() / realBounds.width(),
(float) renderBounds.height() / realBounds.height());
if (mView.getClipToOutline()) {
mView.getOutlineProvider().getOutline(mView, mClippingOutline);
mClippingPath.reset();
RectF rect = new RectF(0, 0, mView.getWidth(), mView.getHeight());
final float cornerRadius = mClippingOutline.getRadius();
mClippingPath.addRoundRect(rect, cornerRadius, cornerRadius, Path.Direction.CW);
mClippingPath.close();
canvas.clipPath(mClippingPath);
}
canvas.saveLayerAlpha(null, (int) (255 * mView.getAlpha()));
mView.draw(canvas);
canvas.restore();
}
mSurface.unlockCanvasAndPost(canvas);
logD("draw: done");
}
private void forceDraw() {
mDirty = true;
draw();
}
private Rect getRealBounds() {
Rect output = new Rect();
mView.getBoundsOnScreen(output);
return output;
}
private boolean isAttachedToLeash() {
return mSurfaceControl != null && mSurface != null;
}
private void logD(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
private void setVisible(boolean visible) {
logD("setVisibility: " + visible);
if (isAttachedToLeash()) {
mVisibleOverride = visible;
postDraw();
} else {
mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
}
private void setBounds(Rect bounds) {
logD("setBounds: " + bounds);
mViewBoundsOverride = bounds;
if (isAttachedToLeash()) {
postDraw();
} else {
Log.w(TAG, "setBounds: not attached to leash!");
}
}
private void setAlpha(float alpha) {
logD("setAlpha: " + alpha);
mView.setAlpha(alpha);
if (isAttachedToLeash()) {
postDraw();
}
}
private void postDraw() {
if (mDirty) {
return;
}
mDirty = true;
post(this::draw);
}
private void post(Runnable r) {
if (mView.isAttachedToWindow()) {
mView.post(r);
} else {
// If the view is detached from window, {@code View.post()} will postpone the action
// until the view is attached again. However, we don't know if the view will be attached
// again, so we post the action to the main thread in this case. This could lead to race
// condition if the attachment change caused a thread switching, and it's the caller's
// responsibility to ensure the window attachment state doesn't change unexpectedly.
if (DEBUG) {
Log.w(TAG, mView + " is not attached. Posting action to main thread!");
}
mMainHandler.post(r);
}
}
/** A listener for monitoring view life cycles. */
private class LifecycleListener
implements ViewTreeObserver.OnDrawListener, View.OnAttachStateChangeListener {
private boolean mRegistered;
@Override
public void onDraw() {
// View draw should trigger surface draw.
postDraw();
}
@Override
public void onViewAttachedToWindow(View v) {
// empty
}
@Override
public void onViewDetachedFromWindow(View v) {
Log.w(
TAG,
v + " is detached from the window. Unregistering the life cycle listener ...");
unregister();
}
public void register() {
if (mRegistered) {
return;
}
mRegistered = true;
mView.getViewTreeObserver().addOnDrawListener(this);
mView.addOnAttachStateChangeListener(this);
}
public void unregister() {
if (!mRegistered) {
return;
}
mRegistered = false;
mView.getViewTreeObserver().removeOnDrawListener(this);
mView.removeOnAttachStateChangeListener(this);
}
}
/** @hide */
public static class Transaction implements UIComponent.Transaction<ViewUIComponent> {
private final List<Runnable> mChanges = new ArrayList<>();
@Override
public Transaction setAlpha(ViewUIComponent ui, float alpha) {
mChanges.add(() -> ui.post(() -> ui.setAlpha(alpha)));
return this;
}
@Override
public Transaction setVisible(ViewUIComponent ui, boolean visible) {
mChanges.add(() -> ui.post(() -> ui.setVisible(visible)));
return this;
}
@Override
public Transaction setBounds(ViewUIComponent ui, Rect bounds) {
mChanges.add(() -> ui.post(() -> ui.setBounds(bounds)));
return this;
}
@Override
public Transaction attachToTransitionLeash(
ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) {
mChanges.add(() -> ui.post(() -> ui.attachToTransitionLeash(transitionLeash, w, h)));
return this;
}
@Override
public Transaction detachFromTransitionLeash(
ViewUIComponent ui, Executor executor, Runnable onDone) {
mChanges.add(() -> ui.post(() -> ui.detachFromTransitionLeash(executor, onDone)));
return this;
}
@Override
public void commit() {
mChanges.forEach(Runnable::run);
mChanges.clear();
}
}
}