From 5bb571dc403b4384111ae987ed7b44aaef76ace0 Mon Sep 17 00:00:00 2001 From: Jorim Jaggi Date: Tue, 6 Nov 2018 14:42:04 +0100 Subject: [PATCH] A brave new world for window insets (5/n) Implement controlWindowInsetsAnimation Based on the leashes we have on the client, and the insets the client has requested, we are able to move the surfaces around such that the resulting insets will match what the client requested. Bug: 118118435 Change-Id: I0616e53455a6544aaf374c1b0eb10e258aced21d --- api/current.txt | 3 + core/java/android/util/SparseSetArray.java | 7 + .../view/InsetsAnimationControlImpl.java | 197 ++++++++++++++++++ core/java/android/view/InsetsController.java | 33 ++- core/java/android/view/InsetsSource.java | 3 +- core/java/android/view/InsetsState.java | 57 ++++- .../SyncRtSurfaceTransactionApplier.java | 82 ++++---- core/java/android/view/View.java | 1 + .../WindowInsetsAnimationControlListener.java | 50 +++++ .../view/WindowInsetsAnimationController.java | 81 +++++++ .../android/view/WindowInsetsController.java | 12 ++ .../view/InsetsAnimationControlImplTest.java | 121 +++++++++++ .../android/view/InsetsControllerTest.java | 1 + .../src/android/view/InsetsStateTest.java | 14 +- graphics/java/android/graphics/Insets.java | 35 ++++ ...SyncRtSurfaceTransactionApplierCompat.java | 103 +++++++++ .../notification/ActivityLaunchAnimator.java | 8 +- 17 files changed, 752 insertions(+), 56 deletions(-) create mode 100644 core/java/android/view/InsetsAnimationControlImpl.java rename {packages/SystemUI/shared/src/com/android/systemui/shared/system => core/java/android/view}/SyncRtSurfaceTransactionApplier.java (72%) create mode 100644 core/java/android/view/WindowInsetsAnimationControlListener.java create mode 100644 core/java/android/view/WindowInsetsAnimationController.java create mode 100644 core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java create mode 100644 packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java diff --git a/api/current.txt b/api/current.txt index 31ec76ec571c1..255a3633bb5f2 100644 --- a/api/current.txt +++ b/api/current.txt @@ -14134,8 +14134,11 @@ package android.graphics { public final class Insets { method public static android.graphics.Insets add(android.graphics.Insets, android.graphics.Insets); + method public static android.graphics.Insets max(android.graphics.Insets, android.graphics.Insets); + method public static android.graphics.Insets min(android.graphics.Insets, android.graphics.Insets); method public static android.graphics.Insets of(int, int, int, int); method public static android.graphics.Insets of(android.graphics.Rect); + method public static android.graphics.Insets subtract(android.graphics.Insets, android.graphics.Insets); field public static final android.graphics.Insets NONE; field public final int bottom; field public final int left; diff --git a/core/java/android/util/SparseSetArray.java b/core/java/android/util/SparseSetArray.java index d100f12ed0266..680e85fa2ba8b 100644 --- a/core/java/android/util/SparseSetArray.java +++ b/core/java/android/util/SparseSetArray.java @@ -54,6 +54,13 @@ public class SparseSetArray { return set.contains(value); } + /** + * @return the set of items at index n + */ + public ArraySet get(int n) { + return mData.get(n); + } + /** * Remove a value from index n. * @return TRUE when the value existed at the given index and removed, FALSE otherwise. diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java new file mode 100644 index 0000000000000..7b9f78e700500 --- /dev/null +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2018 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 android.view; + +import static android.view.InsetsState.INSET_SIDE_BOTTOM; +import static android.view.InsetsState.INSET_SIDE_LEFT; +import static android.view.InsetsState.INSET_SIDE_RIGHT; +import static android.view.InsetsState.INSET_SIDE_TOP; + +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.os.UidProto.Sync; +import android.util.ArraySet; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.util.SparseSetArray; +import android.view.InsetsState.InsetSide; +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; +import android.view.WindowInsets.Type.InsetType; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Implements {@link WindowInsetsAnimationController} + * @hide + */ +@VisibleForTesting +public class InsetsAnimationControlImpl implements WindowInsetsAnimationController { + + private final WindowInsetsAnimationControlListener mListener; + private final SparseArray mConsumers; + private final SparseIntArray mTypeSideMap = new SparseIntArray(); + private final SparseSetArray mSideSourceMap = new SparseSetArray<>(); + + /** @see WindowInsetsAnimationController#getHiddenStateInsets */ + private final Insets mHiddenInsets; + + /** @see WindowInsetsAnimationController#getShownStateInsets */ + private final Insets mShownInsets; + private final Matrix mTmpMatrix = new Matrix(); + private final InsetsState mInitialInsetsState; + private final @InsetType int mTypes; + private final Supplier mTransactionApplierSupplier; + + private Insets mCurrentInsets; + + @VisibleForTesting + public InsetsAnimationControlImpl(SparseArray consumers, Rect frame, + InsetsState state, WindowInsetsAnimationControlListener listener, + @InsetType int types, + Supplier transactionApplierSupplier) { + mConsumers = consumers; + mListener = listener; + mTypes = types; + mTransactionApplierSupplier = transactionApplierSupplier; + mInitialInsetsState = new InsetsState(state); + mCurrentInsets = getInsetsFromState(mInitialInsetsState, frame, null /* typeSideMap */); + mHiddenInsets = calculateInsets(mInitialInsetsState, frame, consumers, false /* shown */, + null /* typeSideMap */); + mShownInsets = calculateInsets(mInitialInsetsState, frame, consumers, true /* shown */, + mTypeSideMap); + buildTypeSourcesMap(mTypeSideMap, mSideSourceMap, mConsumers); + + // TODO: Check for controllability first and wait for IME if needed. + listener.onReady(this, types); + } + + @Override + public Insets getHiddenStateInsets() { + return mHiddenInsets; + } + + @Override + public Insets getShownStateInsets() { + return mShownInsets; + } + + @Override + public Insets getCurrentInsets() { + return mCurrentInsets; + } + + @Override + @InsetType + public int getTypes() { + return mTypes; + } + + @Override + public void changeInsets(Insets insets) { + insets = sanitize(insets); + final Insets offset = Insets.subtract(mShownInsets, insets); + ArrayList params = new ArrayList<>(); + if (offset.left != 0) { + updateLeashesForSide(INSET_SIDE_LEFT, offset.left, params); + } + if (offset.top != 0) { + updateLeashesForSide(INSET_SIDE_TOP, offset.top, params); + } + if (offset.right != 0) { + updateLeashesForSide(INSET_SIDE_RIGHT, offset.right, params); + } + if (offset.bottom != 0) { + updateLeashesForSide(INSET_SIDE_BOTTOM, offset.bottom, params); + } + SyncRtSurfaceTransactionApplier applier = mTransactionApplierSupplier.get(); + applier.scheduleApply(params.toArray(new SurfaceParams[params.size()])); + mCurrentInsets = insets; + } + + @Override + public void finish(int shownTypes) { + // TODO + } + + private Insets calculateInsets(InsetsState state, Rect frame, + SparseArray consumers, boolean shown, + @Nullable @InsetSide SparseIntArray typeSideMap) { + for (int i = consumers.size() - 1; i >= 0; i--) { + state.getSource(consumers.valueAt(i).getType()).setVisible(shown); + } + return getInsetsFromState(state, frame, typeSideMap); + } + + private Insets getInsetsFromState(InsetsState state, Rect frame, + @Nullable @InsetSide SparseIntArray typeSideMap) { + return state.calculateInsets(frame, false /* isScreenRound */, + false /* alwaysConsumerNavBar */, null /* displayCutout */, typeSideMap) + .getSystemWindowInsets(); + } + + private Insets sanitize(Insets insets) { + return Insets.max(Insets.min(insets, mShownInsets), mHiddenInsets); + } + + private void updateLeashesForSide(@InsetSide int side, int inset, + ArrayList surfaceParams) { + ArraySet items = mSideSourceMap.get(side); + // TODO: Implement behavior when inset spans over multiple types + for (int i = items.size() - 1; i >= 0; i--) { + final InsetsSourceConsumer consumer = items.valueAt(i); + final InsetsSource source = mInitialInsetsState.getSource(consumer.getType()); + final SurfaceControl leash = consumer.getControl().getLeash(); + mTmpMatrix.setTranslate(source.getFrame().left, source.getFrame().top); + addTranslationToMatrix(side, inset, mTmpMatrix); + surfaceParams.add(new SurfaceParams(leash, 1f, mTmpMatrix, null, 0, 0f)); + } + } + + private void addTranslationToMatrix(@InsetSide int side, int inset, Matrix m) { + switch (side) { + case INSET_SIDE_LEFT: + m.postTranslate(-inset, 0); + break; + case INSET_SIDE_TOP: + m.postTranslate(0, -inset); + break; + case INSET_SIDE_RIGHT: + m.postTranslate(inset, 0); + break; + case INSET_SIDE_BOTTOM: + m.postTranslate(0, inset); + break; + } + } + + private static void buildTypeSourcesMap(SparseIntArray typeSideMap, + SparseSetArray sideSourcesMap, + SparseArray consumers) { + for (int i = typeSideMap.size() - 1; i >= 0; i--) { + int type = typeSideMap.keyAt(i); + int side = typeSideMap.valueAt(i); + sideSourcesMap.add(side, consumers.get(type)); + } + } +} + diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java index fb4f9c03fa68d..4ab1f266cc704 100644 --- a/core/java/android/view/InsetsController.java +++ b/core/java/android/view/InsetsController.java @@ -28,6 +28,7 @@ import android.view.InsetsState.InternalInsetType; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; +import java.util.ArrayList; /** * Implements {@link WindowInsetsController} on the client. @@ -41,6 +42,7 @@ public class InsetsController implements WindowInsetsController { private final ViewRootImpl mViewRoot; private final SparseArray mTmpControlArray = new SparseArray<>(); + private final ArrayList mAnimationControls = new ArrayList<>(); public InsetsController(ViewRootImpl viewRoot) { mViewRoot = viewRoot; @@ -67,9 +69,11 @@ public class InsetsController implements WindowInsetsController { /** * @see InsetsState#calculateInsets */ - WindowInsets calculateInsets(boolean isScreenRound, + @VisibleForTesting + public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeNavBar, DisplayCutout cutout) { - return mState.calculateInsets(mFrame, isScreenRound, alwaysConsumeNavBar, cutout); + return mState.calculateInsets(mFrame, isScreenRound, alwaysConsumeNavBar, cutout, + null /* typeSideMap */); } /** @@ -116,6 +120,28 @@ public class InsetsController implements WindowInsetsController { } } + @Override + public void controlWindowInsetsAnimation(@InsetType int types, + WindowInsetsAnimationControlListener listener) { + + // TODO: Check whether we already have a controller. + final ArraySet internalTypes = mState.toInternalType(types); + final SparseArray consumers = new SparseArray<>(); + for (int i = internalTypes.size() - 1; i >= 0; i--) { + InsetsSourceConsumer consumer = getSourceConsumer(internalTypes.valueAt(i)); + if (consumer.getControl() != null) { + consumers.put(consumer.getType(), consumer); + } else { + // TODO: Let calling app know it's not possible, or wait + // TODO: Remove it from types + } + } + final InsetsAnimationControlImpl controller = new InsetsAnimationControlImpl(consumers, + mFrame, mState, listener, types, + () -> new SyncRtSurfaceTransactionApplier(mViewRoot.mView)); + mAnimationControls.add(controller); + } + private void applyLocalVisibilityOverride() { for (int i = mSourceConsumers.size() - 1; i >= 0; i--) { final InsetsSourceConsumer controller = mSourceConsumers.valueAt(i); @@ -134,7 +160,8 @@ public class InsetsController implements WindowInsetsController { return controller; } - void notifyVisibilityChanged() { + @VisibleForTesting + public void notifyVisibilityChanged() { mViewRoot.notifyInsetsChanged(); } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 0cb8ad72f1029..f8148a906bb31 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -70,7 +70,8 @@ public class InsetsSource implements Parcelable { * * @param relativeFrame The frame to calculate the insets relative to. * @param ignoreVisibility If true, always reports back insets even if source isn't visible. - * @return The resulting insets. + * @return The resulting insets. The contract is that only one side will be occupied by a + * source. */ public Insets calculateInsets(Rect relativeFrame, boolean ignoreVisibility) { if (!ignoreVisibility && !mVisible) { diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 689b14fe29c61..63025dc16f17d 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -17,12 +17,15 @@ package android.view; import android.annotation.IntDef; +import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.SparseArray; +import android.util.SparseIntArray; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetType; @@ -77,11 +80,30 @@ public class InsetsState implements Parcelable { /** A shelf is the same as the navigation bar. */ public static final int TYPE_SHELF = TYPE_NAVIGATION_BAR; + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "INSET_SIDE", value = { + INSET_SIDE_LEFT, + INSET_SIDE_TOP, + INSET_SIDE_RIGHT, + INSET_SIDE_BOTTOM, + INSET_SIDE_UNKNWON + }) + public @interface InsetSide {} + static final int INSET_SIDE_LEFT = 0; + static final int INSET_SIDE_TOP = 1; + static final int INSET_SIDE_RIGHT = 2; + static final int INSET_SIDE_BOTTOM = 3; + static final int INSET_SIDE_UNKNWON = 4; + private final ArrayMap mSources = new ArrayMap<>(); public InsetsState() { } + public InsetsState(InsetsState copy) { + set(copy); + } + /** * Calculates {@link WindowInsets} based on the current source configuration. * @@ -89,7 +111,8 @@ public class InsetsState implements Parcelable { * @return The calculated insets. */ public WindowInsets calculateInsets(Rect frame, boolean isScreenRound, - boolean alwaysConsumeNavBar, DisplayCutout cutout) { + boolean alwaysConsumeNavBar, DisplayCutout cutout, + @Nullable @InsetSide SparseIntArray typeSideMap) { Insets systemInsets = Insets.NONE; Insets maxInsets = Insets.NONE; final Rect relativeFrame = new Rect(frame); @@ -100,13 +123,13 @@ public class InsetsState implements Parcelable { continue; } systemInsets = processSource(source, systemInsets, relativeFrame, - false /* ignoreVisibility */); + false /* ignoreVisibility */, typeSideMap); // IME won't be reported in max insets as the size depends on the EditorInfo of the IME // target. if (source.getType() != TYPE_IME) { maxInsets = processSource(source, maxInsets, relativeFrameMax, - true /* ignoreVisibility */); + true /* ignoreVisibility */, null /* typeSideMap */); } } return new WindowInsets(new Rect(systemInsets), null, new Rect(maxInsets), isScreenRound, @@ -114,13 +137,39 @@ public class InsetsState implements Parcelable { } private Insets processSource(InsetsSource source, Insets insets, Rect relativeFrame, - boolean ignoreVisibility) { + boolean ignoreVisibility, @Nullable @InsetSide SparseIntArray typeSideMap) { Insets currentInsets = source.calculateInsets(relativeFrame, ignoreVisibility); insets = Insets.add(currentInsets, insets); relativeFrame.inset(insets); + if (typeSideMap != null && !Insets.NONE.equals(currentInsets)) { + @InsetSide int insetSide = getInsetSide(currentInsets); + if (insetSide != INSET_SIDE_UNKNWON) { + typeSideMap.put(source.getType(), getInsetSide(currentInsets)); + } + } return insets; } + /** + * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b + * is set in order that this method returns a meaningful result. + */ + private @InsetSide int getInsetSide(Insets insets) { + if (insets.left != 0) { + return INSET_SIDE_LEFT; + } + if (insets.top != 0) { + return INSET_SIDE_TOP; + } + if (insets.right != 0) { + return INSET_SIDE_RIGHT; + } + if (insets.bottom != 0) { + return INSET_SIDE_BOTTOM; + } + return INSET_SIDE_UNKNWON; + } + public InsetsSource getSource(@InternalInsetType int type) { return mSources.computeIfAbsent(type, InsetsSource::new); } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplier.java b/core/java/android/view/SyncRtSurfaceTransactionApplier.java similarity index 72% rename from packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplier.java rename to core/java/android/view/SyncRtSurfaceTransactionApplier.java index 807edf624db89..0270acb3aea70 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplier.java +++ b/core/java/android/view/SyncRtSurfaceTransactionApplier.java @@ -11,24 +11,22 @@ * 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 + * limitations under the License. */ -package com.android.systemui.shared.system; +package android.view; -import android.graphics.HardwareRenderer; import android.graphics.Matrix; import android.graphics.Rect; -import android.view.Surface; -import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; -import android.view.View; -import android.view.ViewRootImpl; + +import com.android.internal.annotations.VisibleForTesting; import java.util.function.Consumer; /** * Helper class to apply surface transactions in sync with RenderThread. + * @hide */ public class SyncRtSurfaceTransactionApplier { @@ -54,30 +52,32 @@ public class SyncRtSurfaceTransactionApplier { if (mTargetViewRootImpl == null) { return; } - mTargetViewRootImpl.registerRtFrameCallback(new HardwareRenderer.FrameDrawingCallback() { - @Override - public void onFrameDraw(long frame) { - if (mTargetSurface == null || !mTargetSurface.isValid()) { - return; - } - Transaction t = new Transaction(); - for (int i = params.length - 1; i >= 0; i--) { - SurfaceParams surfaceParams = params[i]; - SurfaceControl surface = surfaceParams.surface; - t.deferTransactionUntilSurface(surface, mTargetSurface, frame); - applyParams(t, surfaceParams, mTmpFloat9); - } - t.setEarlyWakeup(); - t.apply(); + mTargetViewRootImpl.registerRtFrameCallback(frame -> { + if (mTargetSurface == null || !mTargetSurface.isValid()) { + return; } + Transaction t = new Transaction(); + for (int i = params.length - 1; i >= 0; i--) { + SurfaceParams surfaceParams = params[i]; + SurfaceControl surface = surfaceParams.surface; + t.deferTransactionUntilSurface(surface, mTargetSurface, frame); + applyParams(t, surfaceParams, mTmpFloat9); + } + t.setEarlyWakeup(); + t.apply(); }); // Make sure a frame gets scheduled. mTargetViewRootImpl.getView().invalidate(); } - public static void applyParams(TransactionCompat t, SurfaceParams params) { - applyParams(t.mTransaction, params, t.mTmpValues); + public static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) { + t.setMatrix(params.surface, params.matrix, tmpFloat9); + t.setWindowCrop(params.surface, params.windowCrop); + t.setAlpha(params.surface, params.alpha); + t.setLayer(params.surface, params.layer); + t.setCornerRadius(params.surface, params.cornerRadius); + t.show(params.surface); } /** @@ -109,15 +109,6 @@ public class SyncRtSurfaceTransactionApplier { } } - private static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) { - t.setMatrix(params.surface, params.matrix, tmpFloat9); - t.setWindowCrop(params.surface, params.windowCrop); - t.setAlpha(params.surface, params.alpha); - t.setLayer(params.surface, params.layer); - t.setCornerRadius(params.surface, params.cornerRadius); - t.show(params.surface); - } - public static class SurfaceParams { /** @@ -129,9 +120,9 @@ public class SyncRtSurfaceTransactionApplier { * @param matrix Matrix to apply. * @param windowCrop Crop to apply. */ - public SurfaceParams(SurfaceControlCompat surface, float alpha, Matrix matrix, + public SurfaceParams(SurfaceControl surface, float alpha, Matrix matrix, Rect windowCrop, int layer, float cornerRadius) { - this.surface = surface.mSurfaceControl; + this.surface = surface; this.alpha = alpha; this.matrix = new Matrix(matrix); this.windowCrop = new Rect(windowCrop); @@ -139,11 +130,22 @@ public class SyncRtSurfaceTransactionApplier { this.cornerRadius = cornerRadius; } - final SurfaceControl surface; - final float alpha; - final Matrix matrix; - final Rect windowCrop; - final int layer; + @VisibleForTesting + public final SurfaceControl surface; + + @VisibleForTesting + public final float alpha; + + @VisibleForTesting final float cornerRadius; + + @VisibleForTesting + public final Matrix matrix; + + @VisibleForTesting + public final Rect windowCrop; + + @VisibleForTesting + public final int layer; } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 468d92290c13d..5a9d90f436ea5 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -10489,6 +10489,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * * @return The {@link WindowInsetsController} or {@code null} if the view isn't attached to a * a window. + * @see Window#getInsetsController() * @hide pending unhide */ public @Nullable WindowInsetsController getWindowInsetsController() { diff --git a/core/java/android/view/WindowInsetsAnimationControlListener.java b/core/java/android/view/WindowInsetsAnimationControlListener.java new file mode 100644 index 0000000000000..b27a23da61b76 --- /dev/null +++ b/core/java/android/view/WindowInsetsAnimationControlListener.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 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 android.view; + +import android.annotation.NonNull; +import android.view.WindowInsets.Type.InsetType; +import android.view.inputmethod.EditorInfo; + +/** + * Interface that informs the client about {@link WindowInsetsAnimationController} state changes. + * @hide pending unhide + */ +public interface WindowInsetsAnimationControlListener { + + /** + * Gets called as soon as the animation is ready to be controlled. This may be + * delayed when the IME needs to redraw because of an {@link EditorInfo} change, or when the + * window is starting up. + * + * @param controller The controller to control the inset animation. + * @param types The {@link InsetType}s it was able to gain control over. Note that this may be + * different than the types passed into + * {@link WindowInsetsController#controlWindowInsetsAnimation} in case the window + * wasn't able to gain the controls because it wasn't the IME target or not + * currently the window that's controlling the system bars. + */ + void onReady(@NonNull WindowInsetsAnimationController controller, @InsetType int types); + + /** + * Called when the window no longer has control over the requested types. If it loses control + * over one type, the whole control will be cancelled. If none of the requested types were + * available when requesting the control, the animation control will be cancelled immediately + * without {@link #onReady} being called. + */ + void onCancelled(); +} diff --git a/core/java/android/view/WindowInsetsAnimationController.java b/core/java/android/view/WindowInsetsAnimationController.java new file mode 100644 index 0000000000000..9de517dac5de7 --- /dev/null +++ b/core/java/android/view/WindowInsetsAnimationController.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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 android.view; + +import android.annotation.NonNull; +import android.graphics.Insets; +import android.view.WindowInsets.Type.InsetType; + +/** + * Interface to control a window inset animation frame-by-frame. + * @hide pending unhide + */ +public interface WindowInsetsAnimationController { + + /** + * Retrieves the {@link Insets} when the windows this animation is controlling are fully hidden. + * + * @return Insets when the windows this animation is controlling are fully hidden. + */ + @NonNull Insets getHiddenStateInsets(); + + /** + * Retrieves the {@link Insets} when the windows this animation is controlling are fully shown. + *

+ * In case the size of a window causing insets is changing in the middle of the animation, we + * execute that height change after this animation has finished. + * + * @return Insets when the windows this animation is controlling are fully shown. + */ + @NonNull Insets getShownStateInsets(); + + /** + * @return The current insets on the window. These will follow any animation changes. + */ + @NonNull Insets getCurrentInsets(); + + /** + * @return The {@link InsetType}s this object is currently controlling. + */ + @InsetType int getTypes(); + + /** + * Modifies the insets by indirectly moving the windows around in the system that are causing + * window insets. + *

+ * Note that this will not inform the view system of a full inset change via + * {@link View#dispatchApplyWindowInsets} in order to avoid a full layout pass during the + * animation. If you'd like to animate views during a window inset animation, use + * TODO add link to animation listeners. + *

+ * {@link View#dispatchApplyWindowInsets} will instead be called once the animation has + * finished, i.e. once {@link #finish} has been called. + * + * @param insets The new insets to apply. Based on the requested insets, the system will + * calculate the positions of the windows in the system causing insets such that + * the resulting insets of that configuration will match the passed in parameter. + * Note that these insets are being clamped to the range from + * {@link #getHiddenStateInsets} to {@link #getShownStateInsets} + */ + void changeInsets(@NonNull Insets insets); + + /** + * @param shownTypes The list of windows causing insets that should remain shown after finishing + * the animation. + */ + void finish(@InsetType int shownTypes); +} diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java index 7be5f2e7a0b0f..a35be273f3bf9 100644 --- a/core/java/android/view/WindowInsetsController.java +++ b/core/java/android/view/WindowInsetsController.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.NonNull; import android.view.WindowInsets.Type.InsetType; /** @@ -51,4 +52,15 @@ public interface WindowInsetsController { * would like to make disappear. */ void hide(@InsetType int types); + + /** + * Lets the application control window inset animations in a frame-by-frame manner by modifying + * the position of the windows in the system causing insets directly. + * + * @param types The {@link InsetType}s the application has requested to control. + * @param listener The {@link WindowInsetsAnimationControlListener} that gets called when the + * windows are ready to be controlled, among other callbacks. + */ + void controlWindowInsetsAnimation(@InsetType int types, + @NonNull WindowInsetsAnimationControlListener listener); } diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java new file mode 100644 index 0000000000000..d520f151c2fa8 --- /dev/null +++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 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 android.view; + +import static android.view.InsetsState.TYPE_NAVIGATION_BAR; +import static android.view.InsetsState.TYPE_TOP_BAR; +import static junit.framework.Assert.assertEquals; +import static org.mockito.Mockito.verify; + +import android.graphics.Insets; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.platform.test.annotations.Presubmit; +import android.support.test.filters.FlakyTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SparseArray; +import android.view.SurfaceControl.Transaction; +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.List; + +@Presubmit +@FlakyTest(detail = "Promote once confirmed non-flaky") +@RunWith(AndroidJUnit4.class) +public class InsetsAnimationControlImplTest { + + private InsetsAnimationControlImpl mController; + + private SurfaceSession mSession = new SurfaceSession(); + private SurfaceControl mTopLeash; + private SurfaceControl mNavLeash; + + @Mock Transaction mMockTransaction; + @Mock InsetsController mMockController; + @Mock WindowInsetsAnimationControlListener mMockListener; + @Mock SyncRtSurfaceTransactionApplier mMockTransactionApplier; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTopLeash = new SurfaceControl.Builder(mSession) + .setName("testSurface") + .build(); + mNavLeash = new SurfaceControl.Builder(mSession) + .setName("testSurface") + .build(); + InsetsState state = new InsetsState(); + state.getSource(TYPE_TOP_BAR).setFrame(new Rect(0, 0, 500, 100)); + state.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(400, 0, 500, 500)); + InsetsSourceConsumer topConsumer = new InsetsSourceConsumer(TYPE_TOP_BAR, state, + () -> mMockTransaction, mMockController); + topConsumer.setControl(new InsetsSourceControl(TYPE_TOP_BAR, mTopLeash)); + + InsetsSourceConsumer navConsumer = new InsetsSourceConsumer(TYPE_NAVIGATION_BAR, state, + () -> mMockTransaction, mMockController); + navConsumer.hide(); + navConsumer.setControl(new InsetsSourceControl(TYPE_NAVIGATION_BAR, mNavLeash)); + + SparseArray consumers = new SparseArray<>(); + consumers.put(TYPE_TOP_BAR, topConsumer); + consumers.put(TYPE_NAVIGATION_BAR, navConsumer); + mController = new InsetsAnimationControlImpl(consumers, + new Rect(0, 0, 500, 500), state, mMockListener, WindowInsets.Type.systemBars(), + () -> mMockTransactionApplier); + } + + @Test + public void testGetters() { + assertEquals(Insets.of(0, 100, 100, 0), mController.getShownStateInsets()); + assertEquals(Insets.of(0, 0, 0, 0), mController.getHiddenStateInsets()); + assertEquals(Insets.of(0, 100, 0, 0), mController.getCurrentInsets()); + assertEquals(WindowInsets.Type.systemBars(), mController.getTypes()); + } + + @Test + public void testChangeInsets() { + mController.changeInsets(Insets.of(0, 30, 40, 0)); + assertEquals(Insets.of(0, 30, 40, 0), mController.getCurrentInsets()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SurfaceParams.class); + verify(mMockTransactionApplier).scheduleApply(captor.capture()); + List params = captor.getAllValues(); + assertEquals(2, params.size()); + SurfaceParams first = params.get(0); + SurfaceParams second = params.get(1); + SurfaceParams topParams = first.surface == mTopLeash ? first : second; + SurfaceParams navParams = first.surface == mNavLeash ? first : second; + assertPosition(topParams.matrix, new Rect(0, 0, 500, 100), new Rect(0, -70, 500, 30)); + assertPosition(navParams.matrix, new Rect(400, 0, 500, 500), new Rect(460, 0, 560, 500)); + } + + private void assertPosition(Matrix m, Rect original, Rect transformed) { + RectF rect = new RectF(original); + rect.offsetTo(0, 0); + m.mapRect(rect); + rect.round(original); + assertEquals(original, transformed); + } +} diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java index 2ad6028960dfd..d3d274a646825 100644 --- a/core/tests/coretests/src/android/view/InsetsControllerTest.java +++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java @@ -28,6 +28,7 @@ import android.support.test.runner.AndroidJUnit4; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; @Presubmit @FlakyTest(detail = "Promote once confirmed non-flaky") diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java index 6bb9539e89bde..d41a718147f27 100644 --- a/core/tests/coretests/src/android/view/InsetsStateTest.java +++ b/core/tests/coretests/src/android/view/InsetsStateTest.java @@ -16,6 +16,8 @@ package android.view; +import static android.view.InsetsState.INSET_SIDE_BOTTOM; +import static android.view.InsetsState.INSET_SIDE_TOP; import static android.view.InsetsState.TYPE_IME; import static android.view.InsetsState.TYPE_NAVIGATION_BAR; import static android.view.InsetsState.TYPE_TOP_BAR; @@ -27,6 +29,7 @@ import android.os.Parcel; import android.platform.test.annotations.Presubmit; import android.support.test.filters.FlakyTest; import android.support.test.runner.AndroidJUnit4; +import android.util.SparseIntArray; import org.junit.Test; import org.junit.runner.RunWith; @@ -45,9 +48,12 @@ public class InsetsStateTest { mState.getSource(TYPE_TOP_BAR).setVisible(true); mState.getSource(TYPE_IME).setFrame(new Rect(0, 200, 100, 300)); mState.getSource(TYPE_IME).setVisible(true); + SparseIntArray typeSideMap = new SparseIntArray(); WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT); + DisplayCutout.NO_CUTOUT, typeSideMap); assertEquals(new Rect(0, 100, 0, 100), insets.getSystemWindowInsets()); + assertEquals(INSET_SIDE_TOP, typeSideMap.get(TYPE_TOP_BAR)); + assertEquals(INSET_SIDE_BOTTOM, typeSideMap.get(TYPE_IME)); } @Test @@ -57,7 +63,7 @@ public class InsetsStateTest { mState.getSource(TYPE_IME).setFrame(new Rect(0, 100, 100, 300)); mState.getSource(TYPE_IME).setVisible(true); WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT); + DisplayCutout.NO_CUTOUT, null); assertEquals(100, insets.getStableInsetBottom()); assertEquals(new Rect(0, 0, 0, 200), insets.getSystemWindowInsets()); } @@ -69,7 +75,7 @@ public class InsetsStateTest { mState.getSource(TYPE_NAVIGATION_BAR).setFrame(new Rect(80, 0, 100, 300)); mState.getSource(TYPE_NAVIGATION_BAR).setVisible(true); WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT); + DisplayCutout.NO_CUTOUT, null); assertEquals(new Rect(0, 100, 20, 0), insets.getSystemWindowInsets()); } @@ -81,7 +87,7 @@ public class InsetsStateTest { mState.getSource(TYPE_IME).setVisible(true); mState.removeSource(TYPE_IME); WindowInsets insets = mState.calculateInsets(new Rect(0, 0, 100, 300), false, false, - DisplayCutout.NO_CUTOUT); + DisplayCutout.NO_CUTOUT, null); assertEquals(0, insets.getSystemWindowInsetBottom()); } diff --git a/graphics/java/android/graphics/Insets.java b/graphics/java/android/graphics/Insets.java index d9da27c8b931c..8258b575d63ff 100644 --- a/graphics/java/android/graphics/Insets.java +++ b/graphics/java/android/graphics/Insets.java @@ -92,6 +92,41 @@ public final class Insets { return Insets.of(a.left + b.left, a.top + b.top, a.right + b.right, a.bottom + b.bottom); } + /** + * Subtract two Insets. + * + * @param a The minuend. + * @param b The subtrahend. + * @return a - b, i. e. all insets on every side are subtracted from each other. + */ + public static @NonNull Insets subtract(@NonNull Insets a, @NonNull Insets b) { + return Insets.of(a.left - b.left, a.top - b.top, a.right - b.right, a.bottom - b.bottom); + } + + /** + * Retrieves the maximum of two Insets. + * + * @param a The first Insets. + * @param b The second Insets. + * @return max(a, b), i. e. the larger of every inset on every side is taken for the result. + */ + public static @NonNull Insets max(@NonNull Insets a, @NonNull Insets b) { + return Insets.of(Math.max(a.left, b.left), Math.max(a.top, b.top), + Math.max(a.right, b.right), Math.max(a.bottom, b.bottom)); + } + + /** + * Retrieves the minimum of two Insets. + * + * @param a The first Insets. + * @param b The second Insets. + * @return min(a, b), i. e. the smaller of every inset on every side is taken for the result. + */ + public static @NonNull Insets min(@NonNull Insets a, @NonNull Insets b) { + return Insets.of(Math.min(a.left, b.left), Math.min(a.top, b.top), + Math.min(a.right, b.right), Math.min(a.bottom, b.bottom)); + } + /** * Two Insets instances are equal iff they belong to the same class and their fields are * pairwise equal. diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java new file mode 100644 index 0000000000000..c0a1d891cd5bf --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SyncRtSurfaceTransactionApplierCompat.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2018 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.shared.system; + +import android.graphics.Matrix; +import android.graphics.Rect; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControl.Transaction; +import android.view.SyncRtSurfaceTransactionApplier; +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; +import android.view.ThreadedRenderer; +import android.view.View; +import android.view.ViewRootImpl; + +import java.util.function.Consumer; + +/** + * Helper class to apply surface transactions in sync with RenderThread. + */ +public class SyncRtSurfaceTransactionApplierCompat { + + private final SyncRtSurfaceTransactionApplier mApplier; + + /** + * @param targetView The view in the surface that acts as synchronization anchor. + */ + public SyncRtSurfaceTransactionApplierCompat(View targetView) { + mApplier = new SyncRtSurfaceTransactionApplier(targetView); + } + + private SyncRtSurfaceTransactionApplierCompat(SyncRtSurfaceTransactionApplier applier) { + mApplier = applier; + } + + /** + * Schedules applying surface parameters on the next frame. + * + * @param params The surface parameters to apply. DO NOT MODIFY the list after passing into + * this method to avoid synchronization issues. + */ + public void scheduleApply(final SurfaceParams... params) { + mApplier.scheduleApply(convert(params)); + } + + private SyncRtSurfaceTransactionApplier.SurfaceParams[] convert(SurfaceParams[] params) { + SyncRtSurfaceTransactionApplier.SurfaceParams[] result = + new SyncRtSurfaceTransactionApplier.SurfaceParams[params.length]; + for (int i = 0; i < params.length; i++) { + result[i] = params[i].mParams; + } + return result; + } + + public static void applyParams(TransactionCompat t, SurfaceParams params) { + SyncRtSurfaceTransactionApplier.applyParams(t.mTransaction, params.mParams, t.mTmpValues); + } + + public static void create(final View targetView, + final Consumer callback) { + SyncRtSurfaceTransactionApplier.create(targetView, + new Consumer() { + @Override + public void accept(SyncRtSurfaceTransactionApplier applier) { + callback.accept(new SyncRtSurfaceTransactionApplierCompat(applier)); + } + }); + } + + public static class SurfaceParams { + + private final SyncRtSurfaceTransactionApplier.SurfaceParams mParams; + + /** + * Constructs surface parameters to be applied when the current view state gets pushed to + * RenderThread. + * + * @param surface The surface to modify. + * @param alpha Alpha to apply. + * @param matrix Matrix to apply. + * @param windowCrop Crop to apply. + */ + public SurfaceParams(SurfaceControlCompat surface, float alpha, Matrix matrix, + Rect windowCrop, int layer, float cornerRadius) { + mParams = new SyncRtSurfaceTransactionApplier.SurfaceParams(surface.mSurfaceControl, + alpha, matrix, windowCrop, layer, cornerRadius); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java index f899863dcc6b3..e1b231b96693c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ActivityLaunchAnimator.java @@ -28,12 +28,12 @@ import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationTarget; +import android.view.SyncRtSurfaceTransactionApplier; +import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.systemui.Interpolators; import com.android.systemui.shared.system.SurfaceControlCompat; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier; -import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplier.SurfaceParams; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; @@ -267,8 +267,8 @@ public class ActivityLaunchAnimator { Matrix m = new Matrix(); m.postTranslate(0, (float) (mParams.top - app.position.y)); mWindowCrop.set(mParams.left, 0, mParams.right, mParams.getHeight()); - SurfaceParams params = new SurfaceParams(new SurfaceControlCompat(app.leash), - 1f /* alpha */, m, mWindowCrop, app.prefixOrderIndex, mCornerRadius); + SurfaceParams params = new SurfaceParams(app.leash, 1f /* alpha */, m, mWindowCrop, + app.prefixOrderIndex, mCornerRadius); mSyncRtTransactionApplier.scheduleApply(params); }