diff options
6 files changed, 1019 insertions, 0 deletions
diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java new file mode 100644 index 000000000000..08db95e5a795 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java @@ -0,0 +1,374 @@ +/* + * 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.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ValueAnimator; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.SurfaceControl; +import android.window.IRemoteTransition; +import android.window.IRemoteTransitionFinishedCallback; +import android.window.TransitionInfo; +import android.window.TransitionInfo.Change; +import android.window.WindowAnimationState; + +import com.android.internal.policy.ScreenDecorationsUtils; +import com.android.wm.shell.shared.TransitionUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * An implementation of {@link IRemoteTransition} that accepts a {@link UIComponent} as the origin + * and automatically attaches it to the transition leash before the transition starts. + */ +public class OriginRemoteTransition extends IRemoteTransition.Stub { + private static final String TAG = "OriginRemoteTransition"; + + private final Context mContext; + private final boolean mIsEntry; + private final UIComponent mOrigin; + private final TransitionPlayer mPlayer; + private final long mDuration; + private final Handler mHandler; + + @Nullable private SurfaceControl.Transaction mStartTransaction; + @Nullable private IRemoteTransitionFinishedCallback mFinishCallback; + @Nullable private UIComponent.Transaction mOriginTransaction; + @Nullable private ValueAnimator mAnimator; + @Nullable private SurfaceControl mOriginLeash; + private boolean mCancelled; + + OriginRemoteTransition( + Context context, + boolean isEntry, + UIComponent origin, + TransitionPlayer player, + long duration, + Handler handler) { + mContext = context; + mIsEntry = isEntry; + mOrigin = origin; + mPlayer = player; + mDuration = duration; + mHandler = handler; + } + + @Override + public void startAnimation( + IBinder token, + TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) { + logD("startAnimation - " + info); + mHandler.post( + () -> { + mStartTransaction = t; + mFinishCallback = finishCallback; + startAnimationInternal(info); + }); + } + + @Override + public void mergeAnimation( + IBinder transition, + TransitionInfo info, + SurfaceControl.Transaction t, + IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) { + logD("mergeAnimation - " + info); + mHandler.post(this::cancel); + } + + @Override + public void takeOverAnimation( + IBinder transition, + TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback, + WindowAnimationState[] states) { + logD("takeOverAnimation - " + info); + } + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted) { + logD("onTransitionConsumed - aborted: " + aborted); + mHandler.post(this::cancel); + } + + private void startAnimationInternal(TransitionInfo info) { + if (!prepareUIs(info)) { + logE("Unable to prepare UI!"); + finishAnimation(/* finished= */ false); + return; + } + // Notify player that we are starting. + mPlayer.onStart(info, mStartTransaction, mOrigin, mOriginTransaction); + + // Start the animator. + mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + mAnimator.setDuration(mDuration); + mAnimator.addListener( + new AnimatorListener() { + @Override + public void onAnimationStart(Animator a) {} + + @Override + public void onAnimationEnd(Animator a) { + finishAnimation(/* finished= */ !mCancelled); + } + + @Override + public void onAnimationCancel(Animator a) { + mCancelled = true; + } + + @Override + public void onAnimationRepeat(Animator a) {} + }); + mAnimator.addUpdateListener( + a -> { + mPlayer.onProgress((float) a.getAnimatedValue()); + }); + mAnimator.start(); + } + + private boolean prepareUIs(TransitionInfo info) { + if (info.getRootCount() == 0) { + logE("prepareUIs: no root leash!"); + return false; + } + if (info.getRootCount() > 1) { + logE("prepareUIs: multi-display transition is not supported yet!"); + return false; + } + if (info.getChanges().isEmpty()) { + logE("prepareUIs: no changes!"); + return false; + } + + SurfaceControl rootLeash = info.getRoot(0).getLeash(); + int displayId = info.getChanges().get(0).getEndDisplayId(); + Rect displayBounds = getDisplayBounds(displayId); + float windowRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); + logD("prepareUIs: windowRadius=" + windowRadius + ", displayBounds=" + displayBounds); + + // Create the origin leash and add to the transition root leash. + mOriginLeash = + new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build(); + mStartTransaction + .reparent(mOriginLeash, rootLeash) + .show(mOriginLeash) + .setCornerRadius(mOriginLeash, windowRadius) + .setWindowCrop(mOriginLeash, displayBounds.width(), displayBounds.height()); + + // Process surfaces + List<SurfaceControl> openingSurfaces = new ArrayList<>(); + List<SurfaceControl> closingSurfaces = new ArrayList<>(); + for (Change change : info.getChanges()) { + int mode = change.getMode(); + SurfaceControl leash = change.getLeash(); + // Reparent leash to the transition root. + mStartTransaction.reparent(leash, rootLeash); + if (TransitionUtil.isOpeningMode(mode)) { + openingSurfaces.add(change.getLeash()); + // For opening surfaces, ending bounds are base bound. Apply corner radius if + // it's full screen. + Rect bounds = change.getEndAbsBounds(); + if (displayBounds.equals(bounds)) { + mStartTransaction + .setCornerRadius(leash, windowRadius) + .setWindowCrop(leash, bounds.width(), bounds.height()); + } + } else if (TransitionUtil.isClosingMode(mode)) { + closingSurfaces.add(change.getLeash()); + // For closing surfaces, starting bounds are base bounds. Apply corner radius if + // it's full screen. + Rect bounds = change.getStartAbsBounds(); + if (displayBounds.equals(bounds)) { + mStartTransaction + .setCornerRadius(leash, windowRadius) + .setWindowCrop(leash, bounds.width(), bounds.height()); + } + } + } + + // Set relative order: + // ---- App1 ---- + // ---- origin ---- + // ---- App2 ---- + if (mIsEntry) { + mStartTransaction + .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1) + .setRelativeLayer( + openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + } else { + mStartTransaction + .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1) + .setRelativeLayer( + closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + } + + // Attach origin UIComponent to origin leash. + mOriginTransaction = mOrigin.newTransaction(); + mOriginTransaction + .attachToTransitionLeash( + mOrigin, mOriginLeash, displayBounds.width(), displayBounds.height()) + .commit(); + + // Apply all surface changes. + mStartTransaction.apply(); + return true; + } + + private Rect getDisplayBounds(int displayId) { + DisplayManager dm = mContext.getSystemService(DisplayManager.class); + DisplayMetrics metrics = new DisplayMetrics(); + dm.getDisplay(displayId).getMetrics(metrics); + return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels); + } + + private void finishAnimation(boolean finished) { + logD("finishAnimation: finished=" + finished); + if (mAnimator == null) { + // The transition didn't start. Ensure we apply the start transaction and report + // finish afterwards. + mStartTransaction + .addTransactionCommittedListener( + mContext.getMainExecutor(), this::finishInternal) + .apply(); + return; + } + mAnimator = null; + // Notify client that we have ended. + mPlayer.onEnd(finished); + // Detach the origin from the transition leash and report finish after it's done. + mOriginTransaction + .detachFromTransitionLeash( + mOrigin, mContext.getMainExecutor(), this::finishInternal) + .commit(); + } + + private void finishInternal() { + logD("finishInternal"); + if (mOriginLeash != null) { + // Release origin leash. + mOriginLeash.release(); + mOriginLeash = null; + } + try { + mFinishCallback.onTransitionFinished(null, null); + } catch (RemoteException e) { + logE("Unable to report transition finish!", e); + } + mStartTransaction = null; + mOriginTransaction = null; + mFinishCallback = null; + } + + private void cancel() { + if (mAnimator != null) { + mAnimator.cancel(); + } + } + + private static void logD(String msg) { + if (OriginTransitionSession.DEBUG) { + Log.d(TAG, msg); + } + } + + private static void logE(String msg) { + Log.e(TAG, msg); + } + + private static void logE(String msg, Throwable e) { + Log.e(TAG, msg, e); + } + + private static UIComponent wrapSurfaces(TransitionInfo info, boolean isOpening) { + List<SurfaceControl> surfaces = new ArrayList<>(); + Rect maxBounds = new Rect(); + for (Change change : info.getChanges()) { + int mode = change.getMode(); + if (TransitionUtil.isOpeningMode(mode) == isOpening) { + surfaces.add(change.getLeash()); + Rect bounds = isOpening ? change.getEndAbsBounds() : change.getStartAbsBounds(); + maxBounds.union(bounds); + } + } + return new SurfaceUIComponent( + surfaces, + /* alpha= */ 1.0f, + /* visible= */ true, + /* bounds= */ maxBounds, + /* baseBounds= */ maxBounds); + } + + /** An interface that represents an origin transitions. */ + public interface TransitionPlayer { + + /** + * Called when an origin transition starts. This method exposes the raw {@link + * TransitionInfo} so that clients can extract more information from it. + */ + default void onStart( + TransitionInfo transitionInfo, + SurfaceControl.Transaction sfTransaction, + UIComponent origin, + UIComponent.Transaction uiTransaction) { + // Wrap transactions. + Transactions transactions = + new Transactions() + .registerTransactionForClass(origin.getClass(), uiTransaction) + .registerTransactionForClass( + SurfaceUIComponent.class, + new SurfaceUIComponent.Transaction(sfTransaction)); + // Wrap surfaces and start. + onStart( + transactions, + origin, + wrapSurfaces(transitionInfo, /* isOpening= */ false), + wrapSurfaces(transitionInfo, /* isOpening= */ true)); + } + + /** + * Called when an origin transition starts. This method exposes the opening and closing + * windows as wrapped {@link UIComponent} to provide simplified interface to clients. + */ + void onStart( + UIComponent.Transaction transaction, + UIComponent origin, + UIComponent closingApp, + UIComponent openingApp); + + /** Called to update the transition frame. */ + void onProgress(float progress); + + /** Called when the transition ended. */ + void onEnd(boolean finished); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java index 64bedd347d7a..23693b68a920 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java @@ -24,11 +24,14 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.os.RemoteException; import android.util.Log; import android.window.IRemoteTransition; import android.window.RemoteTransition; +import com.android.systemui.animation.OriginRemoteTransition.TransitionPlayer; import com.android.systemui.animation.shared.IOriginTransitions; import java.lang.annotation.Retention; @@ -182,6 +185,7 @@ public class OriginTransitionSession { @Nullable private final IOriginTransitions mOriginTransitions; @Nullable private Supplier<IRemoteTransition> mEntryTransitionSupplier; @Nullable private Supplier<IRemoteTransition> mExitTransitionSupplier; + private Handler mHandler = new Handler(Looper.getMainLooper()); private String mName; @Nullable private Predicate<RemoteTransition> mIntentStarter; @@ -259,12 +263,48 @@ public class OriginTransitionSession { return this; } + /** Add an origin entry transition to the builder. */ + public Builder withEntryTransition( + UIComponent entryOrigin, TransitionPlayer entryPlayer, long entryDuration) { + mEntryTransitionSupplier = + () -> + new OriginRemoteTransition( + mContext, + /* isEntry= */ true, + entryOrigin, + entryPlayer, + entryDuration, + mHandler); + return this; + } + /** Add an exit transition to the builder. */ public Builder withExitTransition(IRemoteTransition transition) { mExitTransitionSupplier = () -> transition; return this; } + /** Add an origin exit transition to the builder. */ + public Builder withExitTransition( + UIComponent exitTarget, TransitionPlayer exitPlayer, long exitDuration) { + mExitTransitionSupplier = + () -> + new OriginRemoteTransition( + mContext, + /* isEntry= */ false, + exitTarget, + exitPlayer, + exitDuration, + mHandler); + return this; + } + + /** Supply a handler where transition callbacks will run. */ + public Builder withHandler(Handler handler) { + mHandler = handler; + return this; + } + /** Build an {@link OriginTransitionSession}. */ public OriginTransitionSession build() { if (mIntentStarter == null) { diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java new file mode 100644 index 000000000000..24387360936b --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java @@ -0,0 +1,169 @@ +/* + * 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.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.SurfaceControl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.concurrent.Executor; + +/** A {@link UIComponent} representing a {@link SurfaceControl}. */ +public class SurfaceUIComponent implements UIComponent { + private final Collection<SurfaceControl> mSurfaces; + private final Rect mBaseBounds; + private final float[] mFloat9 = new float[9]; + + private float mAlpha; + private boolean mVisible; + private Rect mBounds; + + public SurfaceUIComponent( + SurfaceControl sc, float alpha, boolean visible, Rect bounds, Rect baseBounds) { + this(Arrays.asList(sc), alpha, visible, bounds, baseBounds); + } + + public SurfaceUIComponent( + Collection<SurfaceControl> surfaces, + float alpha, + boolean visible, + Rect bounds, + Rect baseBounds) { + mSurfaces = surfaces; + mAlpha = alpha; + mVisible = visible; + mBounds = bounds; + mBaseBounds = baseBounds; + } + + @Override + public float getAlpha() { + return mAlpha; + } + + @Override + public boolean isVisible() { + return mVisible; + } + + @Override + public Rect getBounds() { + return mBounds; + } + + @Override + public Transaction newTransaction() { + return new Transaction(new SurfaceControl.Transaction()); + } + + @Override + public String toString() { + return "SurfaceUIComponent{mSurfaces=" + + mSurfaces + + ", mAlpha=" + + mAlpha + + ", mVisible=" + + mVisible + + ", mBounds=" + + mBounds + + ", mBaseBounds=" + + mBaseBounds + + "}"; + } + + /** A {@link Transaction} wrapping a {@link SurfaceControl.Transaction}. */ + public static class Transaction implements UIComponent.Transaction<SurfaceUIComponent> { + private final SurfaceControl.Transaction mTransaction; + private final ArrayList<Runnable> mChanges = new ArrayList<>(); + + public Transaction(SurfaceControl.Transaction transaction) { + mTransaction = transaction; + } + + @Override + public Transaction setAlpha(SurfaceUIComponent ui, float alpha) { + mChanges.add( + () -> { + ui.mAlpha = alpha; + ui.mSurfaces.forEach(s -> mTransaction.setAlpha(s, alpha)); + }); + return this; + } + + @Override + public Transaction setVisible(SurfaceUIComponent ui, boolean visible) { + mChanges.add( + () -> { + ui.mVisible = visible; + if (visible) { + ui.mSurfaces.forEach(s -> mTransaction.show(s)); + } else { + ui.mSurfaces.forEach(s -> mTransaction.hide(s)); + } + }); + return this; + } + + @Override + public Transaction setBounds(SurfaceUIComponent ui, Rect bounds) { + mChanges.add( + () -> { + if (ui.mBounds.equals(bounds)) { + return; + } + ui.mBounds = bounds; + Matrix matrix = new Matrix(); + matrix.setRectToRect( + new RectF(ui.mBaseBounds), + new RectF(ui.mBounds), + Matrix.ScaleToFit.FILL); + ui.mSurfaces.forEach(s -> mTransaction.setMatrix(s, matrix, ui.mFloat9)); + }); + return this; + } + + @Override + public Transaction attachToTransitionLeash( + SurfaceUIComponent ui, SurfaceControl transitionLeash, int w, int h) { + mChanges.add( + () -> ui.mSurfaces.forEach(s -> mTransaction.reparent(s, transitionLeash))); + return this; + } + + @Override + public Transaction detachFromTransitionLeash( + SurfaceUIComponent ui, Executor executor, Runnable onDone) { + mChanges.add( + () -> { + ui.mSurfaces.forEach(s -> mTransaction.reparent(s, null)); + mTransaction.addTransactionCommittedListener(executor, onDone::run); + }); + return this; + } + + @Override + public void commit() { + mChanges.forEach(Runnable::run); + mChanges.clear(); + mTransaction.apply(); + } + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java new file mode 100644 index 000000000000..5240d99a9217 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java @@ -0,0 +1,86 @@ +/* + * 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.FloatRange; +import android.graphics.Rect; +import android.util.ArrayMap; +import android.view.SurfaceControl; + +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * A composite {@link UIComponent.Transaction} that combines multiple other transactions for each ui + * type. + */ +public class Transactions implements UIComponent.Transaction<UIComponent> { + private final Map<Class, UIComponent.Transaction> mTransactions = new ArrayMap<>(); + + /** Register a transaction object for updating a certain {@link UIComponent} type. */ + public <T extends UIComponent> Transactions registerTransactionForClass( + Class<T> clazz, UIComponent.Transaction transaction) { + mTransactions.put(clazz, transaction); + return this; + } + + private UIComponent.Transaction getTransactionFor(UIComponent ui) { + UIComponent.Transaction transaction = mTransactions.get(ui.getClass()); + if (transaction == null) { + transaction = ui.newTransaction(); + mTransactions.put(ui.getClass(), transaction); + } + return transaction; + } + + @Override + public Transactions setAlpha(UIComponent ui, @FloatRange(from = 0.0, to = 1.0) float alpha) { + getTransactionFor(ui).setAlpha(ui, alpha); + return this; + } + + @Override + public Transactions setVisible(UIComponent ui, boolean visible) { + getTransactionFor(ui).setVisible(ui, visible); + return this; + } + + @Override + public Transactions setBounds(UIComponent ui, Rect bounds) { + getTransactionFor(ui).setBounds(ui, bounds); + return this; + } + + @Override + public Transactions attachToTransitionLeash( + UIComponent ui, SurfaceControl transitionLeash, int w, int h) { + getTransactionFor(ui).attachToTransitionLeash(ui, transitionLeash, w, h); + return this; + } + + @Override + public Transactions detachFromTransitionLeash( + UIComponent ui, Executor executor, Runnable onDone) { + getTransactionFor(ui).detachFromTransitionLeash(ui, executor, onDone); + return this; + } + + @Override + public void commit() { + mTransactions.values().forEach(UIComponent.Transaction::commit); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java new file mode 100644 index 000000000000..747e4d1eb278 --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java @@ -0,0 +1,72 @@ +/* + * 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.FloatRange; +import android.graphics.Rect; +import android.view.SurfaceControl; + +import java.util.concurrent.Executor; + +/** An interface representing an UI component on the display. */ +public interface UIComponent { + + /** Get the current alpha of this UI. */ + float getAlpha(); + + /** Check if this UI is visible. */ + boolean isVisible(); + + /** Get the bounds of this UI in its display. */ + Rect getBounds(); + + /** Create a new {@link Transaction} that can update this UI. */ + Transaction newTransaction(); + + /** + * A transaction class for updating {@link UIComponent}. + * + * @param <T> the subtype of {@link UIComponent} that this {@link Transaction} can handle. + */ + interface Transaction<T extends UIComponent> { + /** Update alpha of an UI. Execution will be delayed until {@link #commit()} is called. */ + Transaction setAlpha(T ui, @FloatRange(from = 0.0, to = 1.0) float alpha); + + /** + * Update visibility of an UI. Execution will be delayed until {@link #commit()} is called. + */ + Transaction setVisible(T ui, boolean visible); + + /** Update bounds of an UI. Execution will be delayed until {@link #commit()} is called. */ + Transaction setBounds(T ui, Rect bounds); + + /** + * Attach a ui to the transition leash. Execution will be delayed until {@link #commit()} is + * called. + */ + Transaction attachToTransitionLeash(T ui, SurfaceControl transitionLeash, int w, int h); + + /** + * Detach a ui from the transition leash. Execution will be delayed until {@link #commit} is + * called. + */ + Transaction detachFromTransitionLeash(T ui, Executor executor, Runnable onDone); + + /** Commit any pending changes added to this transaction. */ + void commit(); + } +} diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java new file mode 100644 index 000000000000..313789c4ca7e --- /dev/null +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java @@ -0,0 +1,278 @@ +/* + * 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.PorterDuff; +import android.graphics.Rect; +import android.os.Build; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver.OnDrawListener; + +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. + */ +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 OnDrawListener mOnDrawListener = this::postDraw; + private final View mView; + + @Nullable private SurfaceControl mSurfaceControl; + @Nullable private Surface mSurface; + @Nullable private Rect mViewBoundsOverride; + private boolean mVisibleOverride; + private boolean mDirty; + + public ViewUIComponent(View view) { + mView = view; + } + + @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); + forceDraw(); + + // Attach surface to transition leash + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(mSurfaceControl, transitionLeash).show(mSurfaceControl); + + // Make sure view draw triggers surface draw. + mView.getViewTreeObserver().addOnDrawListener(mOnDrawListener); + + // Make the view invisible AFTER the surface is shown. + t.addTransactionCommittedListener( + mView.getContext().getMainExecutor(), + () -> mView.setVisibility(View.INVISIBLE)) + .apply(); + } + + private void detachFromTransitionLeash(Executor executor, Runnable onDone) { + logD("detachFromTransitionLeash"); + Surface s = mSurface; + SurfaceControl sc = mSurfaceControl; + mSurface = null; + mSurfaceControl = null; + mView.getViewTreeObserver().removeOnDrawListener(mOnDrawListener); + // Restore view visibility + mView.setVisibility(mVisibleOverride ? View.VISIBLE : View.INVISIBLE); + mView.invalidate(); + // Clean up surfaces. + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + t.reparent(sc, null) + .addTransactionCommittedListener( + mView.getContext().getMainExecutor(), + () -> { + s.release(); + sc.release(); + executor.execute(onDone); + }); + // Apply transaction AFTER the view is drawn. + mView.getRootSurfaceControl().applyTransactionOnDraw(t); + } + + @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 || params.width == 0 || params.height == 0) { + // layout pass didn't happen. + logD("draw: skipped - no layout"); + return; + } + Canvas canvas = mSurface.lockHardwareCanvas(); + // Clear the canvas first. + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + if (mVisibleOverride) { + Rect realBounds = getRealBounds(); + Rect renderBounds = getBounds(); + canvas.translate(renderBounds.left, renderBounds.top); + canvas.scale( + (float) renderBounds.width() / realBounds.width(), + (float) renderBounds.height() / realBounds.height()); + 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; + mView.post(this::draw); + } + + 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.setAlpha(alpha)); + return this; + } + + @Override + public Transaction setVisible(ViewUIComponent ui, boolean visible) { + mChanges.add(() -> ui.setVisible(visible)); + return this; + } + + @Override + public Transaction setBounds(ViewUIComponent ui, Rect bounds) { + mChanges.add(() -> ui.setBounds(bounds)); + return this; + } + + @Override + public Transaction attachToTransitionLeash( + ViewUIComponent ui, SurfaceControl transitionLeash, int w, int h) { + mChanges.add(() -> ui.attachToTransitionLeash(transitionLeash, w, h)); + return this; + } + + @Override + public Transaction detachFromTransitionLeash( + ViewUIComponent ui, Executor executor, Runnable onDone) { + mChanges.add(() -> ui.detachFromTransitionLeash(executor, onDone)); + return this; + } + + @Override + public void commit() { + mChanges.forEach(Runnable::run); + mChanges.clear(); + } + } +} |