diff options
17 files changed, 752 insertions, 56 deletions
diff --git a/api/current.txt b/api/current.txt index 17ea06fecb77..328bd028bb2c 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 d100f12ed026..680e85fa2ba8 100644 --- a/core/java/android/util/SparseSetArray.java +++ b/core/java/android/util/SparseSetArray.java @@ -55,6 +55,13 @@ public class SparseSetArray<T> { } /** + * @return the set of items at index n + */ + public ArraySet<T> 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 000000000000..7b9f78e70050 --- /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<InsetsSourceConsumer> mConsumers; + private final SparseIntArray mTypeSideMap = new SparseIntArray(); + private final SparseSetArray<InsetsSourceConsumer> 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<SyncRtSurfaceTransactionApplier> mTransactionApplierSupplier; + + private Insets mCurrentInsets; + + @VisibleForTesting + public InsetsAnimationControlImpl(SparseArray<InsetsSourceConsumer> consumers, Rect frame, + InsetsState state, WindowInsetsAnimationControlListener listener, + @InsetType int types, + Supplier<SyncRtSurfaceTransactionApplier> 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<SurfaceParams> 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<InsetsSourceConsumer> 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> surfaceParams) { + ArraySet<InsetsSourceConsumer> 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<InsetsSourceConsumer> sideSourcesMap, + SparseArray<InsetsSourceConsumer> 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 fb4f9c03fa68..4ab1f266cc70 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<InsetsSourceControl> mTmpControlArray = new SparseArray<>(); + private final ArrayList<InsetsAnimationControlImpl> 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<Integer> internalTypes = mState.toInternalType(types); + final SparseArray<InsetsSourceConsumer> 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 0cb8ad72f102..f8148a906bb3 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 689b14fe29c6..63025dc16f17 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<Integer, InsetsSource> 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 index 807edf624db8..0270acb3aea7 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 69cd3e6487c9..57635efff72e 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -10488,6 +10488,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 000000000000..b27a23da61b7 --- /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 000000000000..9de517dac5de --- /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. + * <p> + * 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. + * <p> + * Note that this will <b>not</b> 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. + * <p> + * {@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 7be5f2e7a0b0..a35be273f3bf 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 000000000000..d520f151c2fa --- /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<InsetsSourceConsumer> 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<SurfaceParams> captor = ArgumentCaptor.forClass(SurfaceParams.class); + verify(mMockTransactionApplier).scheduleApply(captor.capture()); + List<SurfaceParams> 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 2ad6028960df..d3d274a64682 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 6bb9539e89bd..d41a718147f2 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 d9da27c8b931..8258b575d63f 100644 --- a/graphics/java/android/graphics/Insets.java +++ b/graphics/java/android/graphics/Insets.java @@ -93,6 +93,41 @@ public final class Insets { } /** + * 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 000000000000..c0a1d891cd5b --- /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<SyncRtSurfaceTransactionApplierCompat> callback) { + SyncRtSurfaceTransactionApplier.create(targetView, + new Consumer<SyncRtSurfaceTransactionApplier>() { + @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 f899863dcc6b..e1b231b96693 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); } |