diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java index 66f3450759..c8b54adaf0 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java @@ -18,14 +18,11 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; -import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5; -import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.util.Log; import android.view.MotionEvent; @@ -43,6 +40,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.launcher3.util.TouchController; +import com.android.quickstep.PendingAnimation; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -65,6 +63,7 @@ public class OverviewSwipeController extends AnimatorListenerAdapter private final RecentsView mRecentsView; private final int[] mTempCords = new int[2]; + private PendingAnimation mPendingAnimation; private AnimatorPlaybackController mCurrentAnimation; private boolean mCurrentAnimationIsGoingUp; @@ -178,6 +177,11 @@ public class OverviewSwipeController extends AnimatorListenerAdapter if (mCurrentAnimation != null) { mCurrentAnimation.setPlayFraction(0); } + if (mPendingAnimation != null) { + mPendingAnimation.finish(false); + mPendingAnimation = null; + } + mCurrentAnimationIsGoingUp = goingUp; float range = mLauncher.getAllAppsController().getShiftRange(); long maxDuration = (long) (2 * range); @@ -194,19 +198,11 @@ public class OverviewSwipeController extends AnimatorListenerAdapter } } else { if (goingUp) { - AnimatorSet anim = new AnimatorSet(); - ObjectAnimator translate = ObjectAnimator.ofFloat( - mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom()); - translate.setInterpolator(LINEAR); - translate.setDuration(maxDuration); - anim.play(translate); - - ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0); - alpha.setInterpolator(DEACCEL_1_5); - alpha.setDuration(maxDuration); - anim.play(alpha); - mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration); - mEndDisplacement = -mTaskBeingDragged.getBottom(); + mPendingAnimation = mRecentsView + .createTaskDismissAnimation(mTaskBeingDragged, maxDuration); + mCurrentAnimation = AnimatorPlaybackController + .wrap(mPendingAnimation.anim, maxDuration); + mEndDisplacement = -mTaskBeingDragged.getHeight(); } else { AnimatorSet anim = new AnimatorSet(); // TODO: Setup a zoom animation @@ -292,15 +288,17 @@ public class OverviewSwipeController extends AnimatorListenerAdapter } private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) { + if (mPendingAnimation != null) { + mPendingAnimation.finish(wasSuccess); + mPendingAnimation = null; + } if (mTaskBeingDragged == null) { LauncherState state = wasSuccess ? (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW; mLauncher.getStateManager().goToState(state, false); } else if (wasSuccess) { - if (mCurrentAnimationIsGoingUp) { - mRecentsView.onTaskDismissed(mTaskBeingDragged); - } else { + if (!mCurrentAnimationIsGoingUp) { mTaskBeingDragged.launchTask(false); mLauncher.getUserEventDispatcher().logTaskLaunch(logAction, Direction.DOWN, mTaskBeingDragged.getTask().getTopComponent()); diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java index 8b0793451b..b7f79b321c 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java @@ -57,10 +57,7 @@ public class RecentsViewStateController implements StateHandler { setVisibility(state.overviewUi); setTransitionProgress(state.overviewUi ? 1 : 0); if (state.overviewUi) { - for (int i = 0; i < mRecentsView.getPageCount(); i++) { - ((TaskView) mRecentsView.getPageAt(i)).resetVisualProperties(); - } - mRecentsView.updateCurveProperties(); + mRecentsView.resetTaskVisuals(); } } diff --git a/quickstep/src/com/android/quickstep/PendingAnimation.java b/quickstep/src/com/android/quickstep/PendingAnimation.java new file mode 100644 index 0000000000..d22ef61053 --- /dev/null +++ b/quickstep/src/com/android/quickstep/PendingAnimation.java @@ -0,0 +1,53 @@ +/* + * 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.quickstep; + +import android.animation.AnimatorSet; +import android.annotation.TargetApi; +import android.os.Build; + +import java.util.ArrayList; +import java.util.function.Consumer; + +/** + * Utility class to keep track of a running animation. + * + * This class allows attaching end callbacks to an animation is intended to be used with + * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case + * AnimationListeners are not properly dispatched. + */ +@TargetApi(Build.VERSION_CODES.O) +public class PendingAnimation { + + private final ArrayList> mEndListeners = new ArrayList<>(); + + public final AnimatorSet anim; + + public PendingAnimation(AnimatorSet anim) { + this.anim = anim; + } + + public void finish(boolean isSuccess) { + for (Consumer listeners : mEndListeners) { + listeners.accept(isSuccess); + } + mEndListeners.clear(); + } + + public void addEndListener(Consumer listener) { + mEndListeners.add(listener); + } +} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index fb0a757f66..23e6e5bb20 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -16,23 +16,32 @@ package com.android.quickstep.views; -import android.animation.LayoutTransition; -import android.animation.LayoutTransition.TransitionListener; +import static com.android.launcher3.anim.Interpolators.ACCEL; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.anim.Interpolators.LINEAR; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.annotation.TargetApi; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.graphics.Rect; +import android.os.Build; import android.util.AttributeSet; import android.util.SparseBooleanArray; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.PagedView; import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; +import com.android.quickstep.PendingAnimation; import com.android.quickstep.QuickScrubController; import com.android.quickstep.RecentsModel; import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; @@ -49,6 +58,7 @@ import java.util.ArrayList; /** * A list of recent tasks. */ +@TargetApi(Build.VERSION_CODES.P) public abstract class RecentsView extends PagedView implements OnSharedPreferenceChangeListener { @@ -90,14 +100,14 @@ public abstract class RecentsView private boolean mOverviewStateEnabled; private boolean mTaskStackListenerRegistered; - private LayoutTransition mLayoutTransition; private Runnable mNextPageSwitchRunnable; + private PendingAnimation mPendingAnimation; + public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); enableFreeScroll(true); - setupLayoutTransition(); setClipToOutline(true); mFastFlingVelocity = getResources() @@ -136,33 +146,6 @@ public abstract class RecentsView return null; } - private void setupLayoutTransition() { - // We want to show layout transitions when pages are deleted, to close the gap. - // TODO: We should this manually so we can control the animation (fill in the gap as the - // dismissing task is being tracked, and also so we can update the visible task data during - // the transition. For now, the workaround is to expand the visible tasks to load. - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING); - mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - - mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); - mLayoutTransition.addTransitionListener(new TransitionListener() { - @Override - public void startTransition(LayoutTransition layoutTransition, ViewGroup viewGroup, - View view, int i) { - loadVisibleTaskData(); - } - - @Override - public void endTransition(LayoutTransition layoutTransition, ViewGroup viewGroup, - View view, int i) { - loadVisibleTaskData(); - } - }); - setLayoutTransition(mLayoutTransition); - } - @Override protected void onWindowVisibilityChanged(int visibility) { super.onWindowVisibilityChanged(visibility); @@ -231,6 +214,10 @@ public abstract class RecentsView } private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { + if (mPendingAnimation != null) { + mPendingAnimation.addEndListener((b) -> applyLoadPlan(loadPlan)); + return; + } TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null; if (stack == null) { removeAllViews(); @@ -243,7 +230,6 @@ public abstract class RecentsView // necessary) final LayoutInflater inflater = LayoutInflater.from(getContext()); final ArrayList tasks = new ArrayList<>(stack.getTasks()); - setLayoutTransition(null); final int requiredChildCount = tasks.size(); for (int i = getChildCount(); i < requiredChildCount; i++) { @@ -254,7 +240,6 @@ public abstract class RecentsView final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1); removeView(taskView); } - setLayoutTransition(mLayoutTransition); // Unload existing visible task data unloadVisibleTaskData(); @@ -265,12 +250,8 @@ public abstract class RecentsView final Task task = tasks.get(i); final TaskView taskView = (TaskView) getChildAt(pageIndex); taskView.bind(task); - taskView.resetVisualProperties(); } - updateCurveProperties(); - - // Update the set of visible task's data - loadVisibleTaskData(); + resetTaskVisuals(); applyIconScale(false /* animate */); if (oldChildCount != getChildCount()) { @@ -278,6 +259,16 @@ public abstract class RecentsView } } + public void resetTaskVisuals() { + for (int i = getChildCount() - 1; i >= 0; i--) { + ((TaskView) getChildAt(i)).resetVisualProperties(); + } + + updateCurveProperties(); + // Update the set of visible task's data + loadVisibleTaskData(); + } + private void updateTaskStackListenerState() { boolean registerStackListener = mOverviewStateEnabled && isAttachedToWindow() && getWindowVisibility() == VISIBLE; @@ -375,7 +366,7 @@ public abstract class RecentsView final int pageCount = getPageCount(); for (int i = 0; i < pageCount; i++) { View page = getPageAt(i); - int pageCenter = page.getLeft() + halfPageWidth; + float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth; float distanceFromScreenCenter = screenCenter - pageCenter; float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing; mScrollState.linearInterpolation = Math.min(1, @@ -432,13 +423,6 @@ public abstract class RecentsView mHasVisibleTaskData.clear(); } - public void onTaskDismissed(TaskView taskView) { - ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id); - removeView(taskView); - if (getChildCount() == 0) { - onAllTasksRemoved(); - } - } protected abstract void onAllTasksRemoved(); @@ -470,11 +454,9 @@ public abstract class RecentsView if (getChildCount() == 0) { needsReload = true; // Add an empty view for now - setLayoutTransition(null); final TaskView taskView = (TaskView) LayoutInflater.from(getContext()) .inflate(R.layout.task, this, false); addView(taskView, 0); - setLayoutTransition(mLayoutTransition); } mRunningTaskId = runningTaskId; setCurrentPage(0); @@ -529,4 +511,78 @@ public abstract class RecentsView */ public float linearInterpolation; } + + public PendingAnimation createTaskDismissAnimation(TaskView taskView, long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getChildCount(); + if (count == 0) { + return pendingAnimation; + } + + int[] oldScroll = new int[count]; + getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC); + + int[] newScroll = new int[count]; + getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); + + int maxScrollDiff = 0; + int lastPage = mIsRtl ? 0 : count - 1; + if (getChildAt(lastPage) == taskView) { + if (count > 1) { + int secondLastPage = mIsRtl ? 1 : count - 2; + maxScrollDiff = oldScroll[lastPage] - newScroll[secondLastPage]; + } + } + + boolean needsCurveUpdates = false; + for (int i = 0; i < count; i++) { + View child = getChildAt(i); + if (child == taskView) { + addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); + addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), + duration, LINEAR, anim); + } else { + int scrollDiff = newScroll[i] - oldScroll[i] + maxScrollDiff; + if (scrollDiff != 0) { + addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), + duration, ACCEL, anim); + needsCurveUpdates = true; + } + } + } + + if (needsCurveUpdates) { + ValueAnimator va = ValueAnimator.ofFloat(0, 1); + va.addUpdateListener((a) -> updateCurveProperties()); + anim.play(va); + } + + // Add a tiny bit of translation Z, so that it draws on top of other views + taskView.setTranslationZ(0.1f); + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((isSuccess) -> { + if (isSuccess) { + ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id); + removeView(taskView); + if (getChildCount() == 0) { + onAllTasksRemoved(); + } + } + resetTaskVisuals(); + mPendingAnimation = null; + }); + return pendingAnimation; + } + + private static void addAnim(ObjectAnimator anim, long duration, + TimeInterpolator interpolator, AnimatorSet set) { + anim.setDuration(duration).setInterpolator(interpolator); + set.play(anim); + } } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index 4d734a2b6f..7a575ad253 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -154,6 +154,7 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback setScaleY(1f); setTranslationX(0f); setTranslationY(0f); + setTranslationZ(0); setAlpha(1f); } diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java index 30f9c8e160..6e8cacd9c6 100644 --- a/src/com/android/launcher3/PagedView.java +++ b/src/com/android/launcher3/PagedView.java @@ -56,7 +56,9 @@ import java.util.ArrayList; public abstract class PagedView extends ViewGroup { private static final String TAG = "PagedView"; private static final boolean DEBUG = false; + protected static final int INVALID_PAGE = -1; + protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE; public static final int PAGE_SNAP_ANIMATION_DURATION = 750; public static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950; @@ -540,43 +542,13 @@ public abstract class PagedView extends ViewGrou if (DEBUG) Log.d(TAG, "PagedView.onLayout()"); final int childCount = getChildCount(); - final int startIndex = mIsRtl ? childCount - 1 : 0; - final int endIndex = mIsRtl ? -1 : childCount; - final int delta = mIsRtl ? -1 : 1; - - int verticalPadding = getPaddingTop() + getPaddingBottom(); - - int scrollOffsetLeft = mInsets.left + getPaddingLeft(); - int childLeft = scrollOffsetLeft; - boolean pageScrollChanged = false; if (mPageScrolls == null || childCount != mChildCountOnLastLayout) { mPageScrolls = new int[childCount]; pageScrollChanged = true; } - - for (int i = startIndex; i != endIndex; i += delta) { - final View child = getPageAt(i); - if (child.getVisibility() != View.GONE) { - int childTop = getPaddingTop() + mInsets.top; - childTop += (getMeasuredHeight() - mInsets.top - mInsets.bottom - verticalPadding - - child.getMeasuredHeight()) / 2; - - final int childWidth = child.getMeasuredWidth(); - final int childHeight = child.getMeasuredHeight(); - - if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop); - child.layout(childLeft, childTop, - childLeft + child.getMeasuredWidth(), childTop + childHeight); - - final int pageScroll = childLeft - scrollOffsetLeft; - if (mPageScrolls[i] != pageScroll) { - pageScrollChanged = true; - mPageScrolls[i] = pageScroll; - } - - childLeft += childWidth + mPageSpacing + getChildGap(); - } + if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) { + pageScrollChanged = true; } final LayoutTransition transition = getLayoutTransition(); @@ -614,6 +586,51 @@ public abstract class PagedView extends ViewGrou mChildCountOnLastLayout = childCount; } + /** + * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length + * of {@code outPageScrolls} should be same as the the childCount + * + */ + protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren, + ComputePageScrollsLogic scrollLogic) { + final int childCount = getChildCount(); + + final int startIndex = mIsRtl ? childCount - 1 : 0; + final int endIndex = mIsRtl ? -1 : childCount; + final int delta = mIsRtl ? -1 : 1; + + int verticalPadding = getPaddingTop() + getPaddingBottom(); + + int scrollOffsetLeft = mInsets.left + getPaddingLeft(); + int childLeft = scrollOffsetLeft; + boolean pageScrollChanged = false; + + for (int i = startIndex; i != endIndex; i += delta) { + final View child = getPageAt(i); + if (scrollLogic.shouldIncludeView(child)) { + int childTop = getPaddingTop() + mInsets.top; + childTop += (getMeasuredHeight() - mInsets.top - mInsets.bottom - verticalPadding + - child.getMeasuredHeight()) / 2; + final int childWidth = child.getMeasuredWidth(); + + if (layoutChildren) { + final int childHeight = child.getMeasuredHeight(); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), childTop + childHeight); + } + + final int pageScroll = childLeft - scrollOffsetLeft; + if (outPageScrolls[i] != pageScroll) { + pageScrollChanged = true; + outPageScrolls[i] = pageScroll; + } + + childLeft += childWidth + mPageSpacing + getChildGap(); + } + } + return pageScrollChanged; + } + protected int getChildGap() { return 0; } @@ -1525,4 +1542,9 @@ public abstract class PagedView extends ViewGrou public boolean onHoverEvent(android.view.MotionEvent event) { return true; } + + protected interface ComputePageScrollsLogic { + + boolean shouldIncludeView(View view); + } }