Merge "Fix bug where two items could occupy same grid cell in hotseat." into ub-launcher3-master
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
index 11bc883..2f0cd78 100644
--- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
+++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
@@ -47,6 +47,10 @@
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.DrawableFactory;
+import com.android.quickstep.RecentsAnimationInterpolator;
+import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
+import com.android.quickstep.RecentsView;
+import com.android.quickstep.TaskView;
import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -70,6 +74,7 @@
private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
"android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
+ private static final int RECENTS_LAUNCH_DURATION = 336;
private static final int LAUNCHER_RESUME_START_DELAY = 150;
private static final int CLOSING_TRANSITION_DURATION_MS = 350;
@@ -139,8 +144,18 @@
// Post at front of queue ignoring sync barriers to make sure it gets
// processed before the next frame.
postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
- LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
- getLauncherAnimators(v), getWindowAnimators(v, targets));
+ final boolean removeTrackingView;
+ LauncherTransitionAnimator animator =
+ composeRecentsLaunchAnimator(v, targets);
+ if (animator != null) {
+ // We are animating the task view directly, do not remove it after
+ removeTrackingView = false;
+ } else {
+ animator = composeAppLaunchAnimator(v, targets);
+ // A new floating view is created for the animation, remove it after
+ removeTrackingView = true;
+ }
+
setCurrentAnimator(animator);
mAnimator = animator.getAnimatorSet();
mAnimator.addListener(new AnimatorListenerAdapter() {
@@ -148,7 +163,10 @@
public void onAnimationEnd(Animator animation) {
// Reset launcher to normal state
v.setVisibility(View.VISIBLE);
- ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
+ if (removeTrackingView) {
+ ((ViewGroup) mDragLayer.getParent()).removeView(
+ mFloatingView);
+ }
mDragLayer.setAlpha(1f);
mDragLayer.setTranslationY(0f);
@@ -179,6 +197,131 @@
}
/**
+ * Composes the animations for a launch from the recents list if possible.
+ */
+ private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
+ RemoteAnimationTargetCompat[] targets) {
+ // Ensure recents is actually visible
+ if (!mLauncher.isInState(LauncherState.OVERVIEW)) {
+ return null;
+ }
+
+ // Resolve the opening task id
+ int openingTaskId = -1;
+ for (RemoteAnimationTargetCompat target : targets) {
+ if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+ openingTaskId = target.taskId;
+ break;
+ }
+ }
+
+ // If there is no opening task id, fall back to the normal app icon launch animation
+ if (openingTaskId == -1) {
+ return null;
+ }
+
+ // If the opening task id is not currently visible in overview, then fall back to normal app
+ // icon launch animation
+ RecentsView recentsView = mLauncher.getOverviewPanel();
+ TaskView taskView = recentsView.getTaskView(openingTaskId);
+ if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
+ return null;
+ }
+
+ // Found a visible recents task that matches the opening app, lets launch the app from there
+ return new LauncherTransitionAnimator(null, getRecentsWindowAnimator(taskView, targets));
+ }
+
+ /**
+ * @return Animator that controls the window of the opening targets for the recents launch
+ * animation.
+ */
+ private ValueAnimator getRecentsWindowAnimator(TaskView v,
+ RemoteAnimationTargetCompat[] targets) {
+ Rect taskViewBounds = new Rect();
+ mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
+
+ // TODO: Use the actual target insets instead of the current thumbnail insets in case the
+ // device state has changed
+ RecentsAnimationInterpolator recentsInterpolator = new RecentsAnimationInterpolator(
+ new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx),
+ v.getThumbnail().getInsets(),
+ taskViewBounds, new Rect(0, v.getThumbnail().getTop(), 0, 0));
+
+ Rect crop = new Rect();
+ Matrix matrix = new Matrix();
+
+ ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
+ appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
+ appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
+ appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ boolean isFirstFrame = true;
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final Surface surface = getSurface(v);
+ final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
+ if (frameNumber == -1) {
+ // Booo, not cool! Our surface got destroyed, so no reason to animate anything.
+ Log.w(TAG, "Failed to animate, surface got destroyed.");
+ return;
+ }
+ final float percent = animation.getAnimatedFraction();
+ TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
+
+ v.setScaleX(tw.taskScale);
+ v.setScaleY(tw.taskScale);
+ v.setTranslationX(tw.taskX);
+ v.setTranslationY(tw.taskY);
+ // Defer fading out the view until after the app window gets faded in
+ v.setAlpha(getValue(1f, 0f, 75, 75,
+ appAnimator.getDuration() * percent, Interpolators.LINEAR));
+
+ matrix.setScale(tw.winScale, tw.winScale);
+ matrix.postTranslate(tw.winX, tw.winY);
+ crop.set(tw.winCrop);
+
+ // Fade in the app window.
+ float alphaDelay = 0;
+ float alphaDuration = 75;
+ float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
+ appAnimator.getDuration() * percent, Interpolators.LINEAR);
+
+ TransactionCompat t = new TransactionCompat();
+ for (RemoteAnimationTargetCompat target : targets) {
+ if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
+ t.setAlpha(target.leash, alpha);
+
+ // TODO: This isn't correct at the beginning of the animation, but better
+ // than nothing.
+ matrix.postTranslate(target.position.x, target.position.y);
+ t.setMatrix(target.leash, matrix);
+ t.setWindowCrop(target.leash, crop);
+ t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
+ }
+ if (isFirstFrame) {
+ t.show(target.leash);
+ }
+ }
+ t.apply();
+
+ matrix.reset();
+ isFirstFrame = false;
+ }
+ });
+ return appAnimator;
+ }
+
+ /**
+ * Composes the animations for a launch from an app icon.
+ */
+ private LauncherTransitionAnimator composeAppLaunchAnimator(View v,
+ RemoteAnimationTargetCompat[] targets) {
+ return new LauncherTransitionAnimator(getLauncherAnimators(v),
+ getWindowAnimators(v, targets));
+ }
+
+ /**
* @return Animators that control the movements of the Launcher and icon of the opening target.
*/
private AnimatorSet getLauncherAnimators(View v) {
diff --git a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
index 80eaef7..aec2869 100644
--- a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
+++ b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java
@@ -32,11 +32,15 @@
private Animator mWindowAnimator;
LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) {
- mLauncherAnimator = launcherAnimator;
+ if (launcherAnimator != null) {
+ mLauncherAnimator = launcherAnimator;
+ }
mWindowAnimator = windowAnimator;
mAnimatorSet = new AnimatorSet();
- mAnimatorSet.play(launcherAnimator);
+ if (launcherAnimator != null) {
+ mAnimatorSet.play(launcherAnimator);
+ }
mAnimatorSet.play(windowAnimator);
}
@@ -53,6 +57,8 @@
}
public void finishLauncherAnimation() {
- mLauncherAnimator.end();
+ if (mLauncherAnimator != null) {
+ mLauncherAnimator.end();
+ }
}
}
diff --git a/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java b/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
index 21b032b..b3ebd77 100644
--- a/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
+++ b/quickstep/src/com/android/quickstep/BaseSwipeInteractionHandler.java
@@ -38,8 +38,10 @@
public abstract void updateInteractionType(@InteractionType int interactionType);
+ @WorkerThread
public abstract void onQuickScrubEnd();
+ @WorkerThread
public abstract void onQuickScrubProgress(float progress);
@WorkerThread
diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
index 61d4790..d8f7aaf 100644
--- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
+++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java
@@ -157,13 +157,7 @@
startTouchTrackingForScreenshotAnimation();
}
- // Notify the handler that the gesture has actually started
- mInteractionHandler.onGestureStarted();
-
- // Notify the system that we have started tracking the event
- if (mISystemUiProxy != null) {
- executeSafely(mISystemUiProxy::onRecentsAnimationStarted);
- }
+ notifyGestureStarted();
}
} else {
// Move
@@ -182,6 +176,16 @@
}
}
+ private void notifyGestureStarted() {
+ // Notify the handler that the gesture has actually started
+ mInteractionHandler.onGestureStarted();
+
+ // Notify the system that we have started tracking the event
+ if (mISystemUiProxy != null) {
+ executeSafely(mISystemUiProxy::onRecentsAnimationStarted);
+ }
+ }
+
private boolean isNavBarOnRight() {
return mDisplayRotation == Surface.ROTATION_90 && mStableInsets.right > 0;
}
@@ -263,7 +267,7 @@
handler.setLauncherOnDrawCallback(() -> {
drawWaitLock.countDown();
if (handler == mInteractionHandler) {
- switchToMainConsumer();
+ switchToMainChoreographer();
}
});
handler.initWhenReady(mMainThreadExecutor);
@@ -346,6 +350,8 @@
@Override
public void updateTouchTracking(int interactionType) {
+ notifyGestureStarted();
+
mMainThreadExecutor.execute(() -> {
if (mInteractionHandler != null) {
mInteractionHandler.updateInteractionType(interactionType);
@@ -378,7 +384,7 @@
public void onTouchTrackingComplete() { }
- public void switchToMainConsumer() { }
+ public void switchToMainChoreographer() { }
@Override
public void preProcessMotionEvent(MotionEvent ev) {
diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java
index 3e65ffe..7f9d3a1 100644
--- a/quickstep/src/com/android/quickstep/QuickScrubController.java
+++ b/quickstep/src/com/android/quickstep/QuickScrubController.java
@@ -32,6 +32,7 @@
private static final int NUM_QUICK_SCRUB_SECTIONS = 5;
private static final long AUTO_ADVANCE_DELAY = 500;
+ private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325;
private static final int QUICKSCRUB_END_SNAP_DURATION_PER_PAGE = 60;
private Launcher mLauncher;
@@ -58,17 +59,22 @@
if (mRecentsView == null) {
} else {
int page = mRecentsView.getNextPage();
- // Settle on the page then launch it.
- int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
- * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
- mRecentsView.snapToPage(page, snapDuration);
- mRecentsView.postDelayed(() -> {
+ Runnable launchTaskRunnable = () -> {
if (page < mRecentsView.getFirstTaskIndex()) {
mRecentsView.getPageAt(page).performClick();
} else {
((TaskView) mRecentsView.getPageAt(page)).launchTask(true);
}
- }, snapDuration);
+ };
+ int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen())
+ * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE;
+ if (mRecentsView.snapToPage(page, snapDuration)) {
+ // Settle on the page then launch it
+ mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+ } else {
+ // No page move needed, just launch it
+ launchTaskRunnable.run();
+ }
}
}
@@ -92,7 +98,9 @@
private void goToPageWithHaptic(int pageToGoTo) {
if (pageToGoTo != mRecentsView.getNextPage()) {
- mRecentsView.snapToPage(pageToGoTo);
+ int duration = Math.abs(pageToGoTo - mRecentsView.getNextPage())
+ * QUICKSCRUB_SNAP_DURATION_PER_PAGE;
+ mRecentsView.snapToPage(pageToGoTo, duration);
mRecentsView.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
new file mode 100644
index 0000000..9cc038f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java
@@ -0,0 +1,111 @@
+/*
+ * 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.graphics.Rect;
+
+import com.android.launcher3.Utilities;
+
+/**
+ * Helper class to interpolate the animation between a task view representation and an actual
+ * window.
+ */
+public class RecentsAnimationInterpolator {
+
+ public static class TaskWindowBounds {
+ public float taskScale = 1f;
+ public float taskX = 0f;
+ public float taskY = 0f;
+
+ public float winScale = 1f;
+ public float winX = 0f;
+ public float winY = 0f;
+ public Rect winCrop = new Rect();
+
+ @Override
+ public String toString() {
+ return "taskScale=" + taskScale + " taskX=" + taskX + " taskY=" + taskY
+ + " winScale=" + winScale + " winX=" + winX + " winY=" + winY
+ + " winCrop=" + winCrop;
+ }
+ }
+
+ private TaskWindowBounds mTmpTaskWindowBounds = new TaskWindowBounds();
+ private Rect mTmpInsets = new Rect();
+
+ private Rect mWindow;
+ private Rect mInsetWindow;
+ private Rect mInsets;
+ private Rect mTask;
+ private Rect mTaskInsets;
+ private Rect mThumbnail;
+
+ private float mTaskScale;
+ private Rect mScaledTask;
+ private Rect mTargetTask;
+ private Rect mSrcWindow;
+
+ public RecentsAnimationInterpolator(Rect window, Rect insets, Rect task, Rect taskInsets) {
+ mWindow = window;
+ mInsets = insets;
+ mTask = task;
+ mTaskInsets = taskInsets;
+ mInsetWindow = new Rect(window);
+ Utilities.insetRect(mInsetWindow, insets);
+
+ mThumbnail = new Rect(task);
+ Utilities.insetRect(mThumbnail, taskInsets);
+ mTaskScale = (float) mInsetWindow.width() / mThumbnail.width();
+ mScaledTask = new Rect(task);
+ Utilities.scaleRectAboutCenter(mScaledTask, mTaskScale);
+ Rect finalScaledTaskInsets = new Rect(taskInsets);
+ Utilities.scaleRect(finalScaledTaskInsets, mTaskScale);
+ mTargetTask = new Rect(mInsetWindow);
+ mTargetTask.offsetTo(window.top + insets.top - finalScaledTaskInsets.top,
+ window.left + insets.left - finalScaledTaskInsets.left);
+
+ float initialWinScale = 1f / mTaskScale;
+ Rect scaledWindow = new Rect(mInsetWindow);
+ Utilities.scaleRectAboutCenter(scaledWindow, initialWinScale);
+ Rect scaledInsets = new Rect(insets);
+ Utilities.scaleRect(scaledInsets, initialWinScale);
+ mSrcWindow = new Rect(scaledWindow);
+ mSrcWindow.offsetTo(mThumbnail.left - scaledInsets.left,
+ mThumbnail.top - scaledInsets.top);
+ }
+
+ public TaskWindowBounds interpolate(float t) {
+ mTmpTaskWindowBounds.taskScale = Utilities.mapRange(t,
+ 1, (float) mInsetWindow.width() / mThumbnail.width());
+ mTmpTaskWindowBounds.taskX = Utilities.mapRange(t,
+ 0, mTargetTask.left - mScaledTask.left);
+ mTmpTaskWindowBounds.taskY = Utilities.mapRange(t,
+ 0, mTargetTask.top - mScaledTask.top);
+
+ mTmpTaskWindowBounds.winScale = mTmpTaskWindowBounds.taskScale / mTaskScale;
+ mTmpTaskWindowBounds.winX = Utilities.mapRange(t,
+ mSrcWindow.left, 0);
+ mTmpTaskWindowBounds.winY = Utilities.mapRange(t,
+ mSrcWindow.top, 0);
+
+ mTmpInsets.set(mInsets);
+ Utilities.scaleRect(mTmpInsets, (1f - t));
+ mTmpTaskWindowBounds.winCrop.set(mWindow);
+ Utilities.insetRect(mTmpTaskWindowBounds.winCrop, mTmpInsets);
+
+ return mTmpTaskWindowBounds;
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java
index 8e03f37..6b1f3d3 100644
--- a/quickstep/src/com/android/quickstep/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/RecentsView.java
@@ -75,6 +75,7 @@
private boolean mOverviewStateEnabled;
private boolean mTaskStackListenerRegistered;
private LayoutTransition mLayoutTransition;
+ private Runnable mNextPageSwitchRunnable;
/**
* TODO: Call reloadIdNeeded in onTaskStackChanged.
@@ -215,6 +216,21 @@
return mFirstTaskIndex;
}
+ public boolean isTaskViewVisible(TaskView tv) {
+ // For now, just check if it's the active task
+ return indexOfChild(tv) == getNextPage();
+ }
+
+ public TaskView getTaskView(int taskId) {
+ for (int i = getFirstTaskIndex(); i < getChildCount(); i++) {
+ TaskView tv = (TaskView) getChildAt(i);
+ if (tv.getTask().key.id == taskId) {
+ return tv;
+ }
+ }
+ return null;
+ }
+
public void setStateController(RecentsViewStateController stateController) {
mStateController = stateController;
}
@@ -228,6 +244,19 @@
updateTaskStackListenerState();
}
+ public void setNextPageSwitchRunnable(Runnable r) {
+ mNextPageSwitchRunnable = r;
+ }
+
+ @Override
+ protected void onPageEndTransition() {
+ super.onPageEndTransition();
+ if (mNextPageSwitchRunnable != null) {
+ mNextPageSwitchRunnable.run();
+ mNextPageSwitchRunnable = null;
+ }
+ }
+
private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
final RecentsTaskLoader loader = mModel.getRecentsTaskLoader();
TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
@@ -254,11 +283,16 @@
}
setLayoutTransition(mLayoutTransition);
- // Rebind all task views
+ // Rebind and reset all task views
for (int i = tasks.size() - 1; i >= 0; i--) {
final Task task = tasks.get(i);
final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1 + mFirstTaskIndex);
taskView.bind(task);
+ taskView.setScaleX(1f);
+ taskView.setScaleY(1f);
+ taskView.setTranslationX(0f);
+ taskView.setTranslationY(0f);
+ taskView.setAlpha(1f);
loader.loadTaskData(task);
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
index 36a0601..4f93b1c 100644
--- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java
@@ -28,6 +28,7 @@
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
+import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
@@ -108,6 +109,13 @@
updateThumbnailPaintFilter();
}
+ public Rect getInsets() {
+ if (mThumbnailData != null) {
+ return mThumbnailData.insets;
+ }
+ return new Rect();
+ }
+
@Override
protected void onDraw(Canvas canvas) {
canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 6f3a8ff..5e89644 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -190,7 +190,7 @@
mHomeIntent, mISystemUiProxy, mMainThreadExecutor) {
@Override
- public void switchToMainConsumer() {
+ public void switchToMainChoreographer() {
if (mCurrentConsumer == this) {
mEventQueue.setInterimChoreographer(null);
}
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 6082aea..dd0892b 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -41,6 +41,7 @@
import android.os.Looper;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
+import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver.OnDrawListener;
@@ -65,8 +66,12 @@
import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
+import java.util.StringJoiner;
+
@TargetApi(Build.VERSION_CODES.O)
public class WindowTransformSwipeHandler extends BaseSwipeInteractionHandler {
+ private static final String TAG = WindowTransformSwipeHandler.class.getSimpleName();
+ private static final boolean DEBUG_STATES = false;
// Launcher UI related states
private static final int STATE_LAUNCHER_PRESENT = 1 << 0;
@@ -86,8 +91,21 @@
private static final int LAUNCHER_UI_STATES =
STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE;
+ // For debugging, keep in sync with above states
+ private static final String[] STATES = new String[] {
+ "STATE_LAUNCHER_PRESENT",
+ "STATE_LAUNCHER_DRAWN",
+ "STATE_ACTIVITY_MULTIPLIER_COMPLETE",
+ "STATE_APP_CONTROLLER_RECEIVED",
+ "STATE_SCALED_CONTROLLER_RECENTS",
+ "STATE_SCALED_CONTROLLER_APP",
+ "STATE_HANDLER_INVALIDATED",
+ "STATE_GESTURE_STARTED"
+ };
+
private static final long MAX_SWIPE_DURATION = 200;
private static final long MIN_SWIPE_DURATION = 80;
+ private static final int QUICK_SWITCH_START_DURATION = 133;
private static final int QUICK_SWITCH_SNAP_DURATION = 120;
private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f;
@@ -142,6 +160,7 @@
private @InteractionType int mInteractionType = INTERACTION_NORMAL;
private boolean mStartedQuickScrubFromHome;
+ private boolean mDeferredQuickScrubEnd;
private final RecentsAnimationWrapper mRecentsAnimationWrapper = new RecentsAnimationWrapper();
private Matrix mTmpMatrix = new Matrix();
@@ -153,7 +172,13 @@
}
private void initStateCallbacks() {
- mStateCallback = new MultiStateCallback();
+ mStateCallback = new MultiStateCallback() {
+ @Override
+ public void setState(int stateFlag) {
+ debugNewState(stateFlag);
+ super.setState(stateFlag);
+ }
+ };
mStateCallback.addCallback(STATE_LAUNCHER_DRAWN | STATE_GESTURE_STARTED,
this::initializeLauncherAnimationController);
mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN,
@@ -352,9 +377,10 @@
private void updateUiForQuickScrub() {
mStartedQuickScrubFromHome = mWasLauncherAlreadyVisible;
+ mDeferredQuickScrubEnd = false;
mQuickScrubController = mRecentsView.getQuickScrubController();
mQuickScrubController.onQuickScrubStart(mStartedQuickScrubFromHome);
- animateToProgress(1f, MAX_SWIPE_DURATION);
+ animateToProgress(1f, QUICK_SWITCH_START_DURATION);
if (mStartedQuickScrubFromHome) {
mLauncherLayoutListener.setVisibility(View.INVISIBLE);
}
@@ -547,7 +573,11 @@
}
public void reset() {
- setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+ if (mInteractionType != INTERACTION_QUICK_SCRUB) {
+ // Only invalidate the handler if we are not quick scrubbing, otherwise, it will be
+ // invalidated after the quick scrub ends
+ setStateOnUiThread(STATE_HANDLER_INVALIDATED);
+ }
}
private void invalidateHandler() {
@@ -573,36 +603,45 @@
}
private void switchToScreenshot() {
+ synchronized (mRecentsAnimationWrapper) {
+ if (mRecentsAnimationWrapper.controller != null) {
+ TransactionCompat transaction = new TransactionCompat();
+ for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
+ if (app.mode == MODE_CLOSING) {
+ // Update the screenshot of the task
+ final ThumbnailData thumbnail =
+ mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
+ mRecentsView.updateThumbnail(app.taskId, thumbnail);
+ }
+ }
+ transaction.apply();
+ }
+ }
+ mRecentsAnimationWrapper.finish(true /* toHome */);
+
if (mInteractionType == INTERACTION_QUICK_SWITCH) {
for (int i = mRecentsView.getFirstTaskIndex(); i < mRecentsView.getPageCount(); i++) {
TaskView taskView = (TaskView) mRecentsView.getPageAt(i);
if (taskView.getTask().key.id != mRunningTaskId) {
- mRecentsView.snapToPage(i, QUICK_SWITCH_SNAP_DURATION);
- taskView.postDelayed(() -> {taskView.launchTask(true);},
- QUICK_SWITCH_SNAP_DURATION);
+ Runnable launchTaskRunnable = () -> taskView.launchTask(true);
+ if (mRecentsView.snapToPage(i, QUICK_SWITCH_SNAP_DURATION)) {
+ // Snap to the new page then launch it
+ mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable);
+ } else {
+ // No need to move page, just launch task directly
+ launchTaskRunnable.run();
+ }
break;
}
}
} else if (mInteractionType == INTERACTION_QUICK_SCRUB) {
if (mQuickScrubController != null) {
- mQuickScrubController.snapToPageForCurrentQuickScrubSection();
- }
- } else {
- synchronized (mRecentsAnimationWrapper) {
- if (mRecentsAnimationWrapper.controller != null) {
- TransactionCompat transaction = new TransactionCompat();
- for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) {
- if (app.mode == MODE_CLOSING) {
- // Update the screenshot of the task
- final ThumbnailData thumbnail =
- mRecentsAnimationWrapper.controller.screenshotTask(app.taskId);
- mRecentsView.updateThumbnail(app.taskId, thumbnail);
- }
- }
- transaction.apply();
+ if (mDeferredQuickScrubEnd) {
+ onQuickScrubEnd();
+ } else {
+ mQuickScrubController.snapToPageForCurrentQuickScrubSection();
}
}
- mRecentsAnimationWrapper.finish(true /* toHome */);
}
}
@@ -610,7 +649,6 @@
// Re apply state in case we did something funky during the transition.
mLauncher.getStateManager().reapplyState();
-
// Animate ui the first icon.
View currentRecentsPage = mRecentsView.getPageAt(mRecentsView.getCurrentPage());
if (currentRecentsPage instanceof TaskView) {
@@ -619,11 +657,24 @@
}
public void onQuickScrubEnd() {
+ if ((mStateCallback.getState() & STATE_SCALED_CONTROLLER_RECENTS) == 0) {
+ // If we are still animating into recents, then defer until that has run to end
+ // quick scrub since we need to finish the window animation before launching the next
+ // task
+ mDeferredQuickScrubEnd = true;
+ return;
+ }
+
if (mQuickScrubController != null) {
mQuickScrubController.onQuickScrubEnd();
} else {
// TODO:
}
+
+ // Normally this is handled in reset(), but since we are still scrubbing after the
+ // transition into recents, we need to defer the handler invalidation for quick scrub until
+ // after the gesture ends
+ setStateOnUiThread(STATE_HANDLER_INVALIDATED);
}
public void onQuickScrubProgress(float progress) {
@@ -633,4 +684,24 @@
// TODO:
}
}
+
+ private synchronized void debugNewState(int stateFlag) {
+ if (!DEBUG_STATES) {
+ return;
+ }
+
+ int state = mStateCallback.getState();
+ StringJoiner currentStateStr = new StringJoiner(", ", "[", "]");
+ String stateFlagStr = "Unknown-" + stateFlag;
+ for (int i = 0; i < STATES.length; i++) {
+ if ((state & (i << i)) != 0) {
+ currentStateStr.add(STATES[i]);
+ }
+ if (stateFlag == (1 << i)) {
+ stateFlagStr = STATES[i] + " (" + stateFlag + ")";
+ }
+ }
+ Log.d(TAG, "[" + System.identityHashCode(this) + "] Adding " + stateFlagStr + " to "
+ + currentStateStr);
+ }
}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 0ebae81..bb137b0 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -1622,7 +1622,7 @@
return (float) Math.sin(f);
}
- protected void snapToPageWithVelocity(int whichPage, int velocity) {
+ protected boolean snapToPageWithVelocity(int whichPage, int velocity) {
whichPage = validateNewPage(whichPage);
int halfScreenSize = getMeasuredWidth() / 2;
@@ -1633,8 +1633,7 @@
if (Math.abs(velocity) < mMinFlingVelocity) {
// If the velocity is low enough, then treat this more as an automatic page advance
// as opposed to an apparent physical response to flinging
- snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
- return;
+ return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
}
// Here we compute a "distance" that will be used in the computation of the overall
@@ -1653,39 +1652,39 @@
// interpolator at zero, ie. 5. We use 4 to make it a little slower.
duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
- snapToPage(whichPage, delta, duration);
+ return snapToPage(whichPage, delta, duration);
}
- public void snapToPage(int whichPage) {
- snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
+ public boolean snapToPage(int whichPage) {
+ return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION);
}
- public void snapToPageImmediately(int whichPage) {
- snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null);
+ public boolean snapToPageImmediately(int whichPage) {
+ return snapToPage(whichPage, PAGE_SNAP_ANIMATION_DURATION, true, null);
}
- public void snapToPage(int whichPage, int duration) {
- snapToPage(whichPage, duration, false, null);
+ public boolean snapToPage(int whichPage, int duration) {
+ return snapToPage(whichPage, duration, false, null);
}
- protected void snapToPage(int whichPage, int duration, TimeInterpolator interpolator) {
- snapToPage(whichPage, duration, false, interpolator);
+ protected boolean snapToPage(int whichPage, int duration, TimeInterpolator interpolator) {
+ return snapToPage(whichPage, duration, false, interpolator);
}
- protected void snapToPage(int whichPage, int duration, boolean immediate,
+ protected boolean snapToPage(int whichPage, int duration, boolean immediate,
TimeInterpolator interpolator) {
whichPage = validateNewPage(whichPage);
int newX = getScrollForPage(whichPage);
final int delta = newX - getUnboundedScrollX();
- snapToPage(whichPage, delta, duration, immediate, interpolator);
+ return snapToPage(whichPage, delta, duration, immediate, interpolator);
}
- protected void snapToPage(int whichPage, int delta, int duration) {
- snapToPage(whichPage, delta, duration, false, null);
+ protected boolean snapToPage(int whichPage, int delta, int duration) {
+ return snapToPage(whichPage, delta, duration, false, null);
}
- protected void snapToPage(int whichPage, int delta, int duration, boolean immediate,
+ protected boolean snapToPage(int whichPage, int delta, int duration, boolean immediate,
TimeInterpolator interpolator) {
whichPage = validateNewPage(whichPage);
@@ -1723,6 +1722,7 @@
}
invalidate();
+ return Math.abs(delta) > 0;
}
public void scrollLeft() {
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 158c540..d559b44 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -237,16 +237,27 @@
int cx = r.centerX();
int cy = r.centerY();
r.offset(-cx, -cy);
+ scaleRect(r, scale);
+ r.offset(cx, cy);
+ }
+ }
+ public static void scaleRect(Rect r, float scale) {
+ if (scale != 1.0f) {
r.left = (int) (r.left * scale + 0.5f);
r.top = (int) (r.top * scale + 0.5f);
r.right = (int) (r.right * scale + 0.5f);
r.bottom = (int) (r.bottom * scale + 0.5f);
-
- r.offset(cx, cy);
}
}
+ public static void insetRect(Rect r, Rect insets) {
+ r.left = Math.min(r.right, r.left + insets.left);
+ r.top = Math.min(r.bottom, r.top + insets.top);
+ r.right = Math.max(r.left, r.right - insets.right);
+ r.bottom = Math.max(r.top, r.bottom - insets.bottom);
+ }
+
public static float shrinkRect(Rect r, float scaleX, float scaleY) {
float scale = Math.min(Math.min(scaleX, scaleY), 1.0f);
if (scale < 1.0f) {
@@ -261,6 +272,10 @@
return scale;
}
+ public static float mapRange(float value, float min, float max) {
+ return min + (value * (max - min));
+ }
+
public static boolean isSystemApp(Context context, Intent intent) {
PackageManager pm = context.getPackageManager();
ComponentName cn = intent.getComponent();
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 2343654..0dcebe3 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -50,6 +50,9 @@
public static final Interpolator OVERSHOOT_0 = new OvershootInterpolator(0);
+ public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR =
+ new PathInterpolator(0.3f, 0f, 0.1f, 1f);
+
/**
* Inversion of zInterpolate, compounded with an ease-out.
*/