summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java374
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginTransitionSession.java40
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/SurfaceUIComponent.java169
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/Transactions.java86
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/UIComponent.java72
-rw-r--r--packages/SystemUI/animation/lib/src/com/android/systemui/animation/ViewUIComponent.java278
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();
+ }
+ }
+}