summaryrefslogtreecommitdiffstats
path: root/quickstep/src/com
diff options
context:
space:
mode:
authorSunny Goyal <sunnygoyal@google.com>2017-12-08 13:39:39 -0800
committerSunny Goyal <sunnygoyal@google.com>2017-12-11 09:56:17 -0800
commitde5535a1ecb38c6251010583b70c70cee4aedab4 (patch)
treea4b277bd4da5394565499d53e99947f6533e5ad6 /quickstep/src/com
parent76e81da4e0783dbf0a6cd1026f6e664ae73ba901 (diff)
downloadandroid_packages_apps_Trebuchet-de5535a1ecb38c6251010583b70c70cee4aedab4.tar.gz
android_packages_apps_Trebuchet-de5535a1ecb38c6251010583b70c70cee4aedab4.tar.bz2
android_packages_apps_Trebuchet-de5535a1ecb38c6251010583b70c70cee4aedab4.zip
Initial interaction for two state swipe to overview
> Currently swipe only works from NORMAL and ALL_APPS state > All interpolation is spread linearly On pausing the drag for some time, the workspace moves to overview state, and all other transitions interpolate linearly from there over the remaining swipe range Change-Id: Ic79f9d0f446c9bfff11e4af4d31ddc1c86c45ab2
Diffstat (limited to 'quickstep/src/com')
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java80
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java51
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java249
3 files changed, 358 insertions, 22 deletions
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java b/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java
new file mode 100644
index 000000000..1977e93a7
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/DragPauseDetector.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.launcher3.uioverrides;
+
+import com.android.launcher3.Alarm;
+import com.android.launcher3.OnAlarmListener;
+
+/**
+ * Utility class to detect a pause during a drag.
+ */
+public class DragPauseDetector implements OnAlarmListener {
+
+ private static final float MAX_VELOCITY_TO_PAUSE = 0.2f;
+ private static final long PAUSE_DURATION = 100;
+
+ private final Alarm mAlarm;
+ private final Runnable mOnPauseCallback;
+
+ private boolean mEnabled = true;
+ private boolean mTriggered = false;
+
+ public DragPauseDetector(Runnable onPauseCallback) {
+ mOnPauseCallback = onPauseCallback;
+
+ mAlarm = new Alarm();
+ mAlarm.setOnAlarmListener(this);
+ mAlarm.setAlarm(PAUSE_DURATION);
+ }
+
+ public void onDrag(float displacement, float velocity) {
+ if (mTriggered || !mEnabled) {
+ return;
+ }
+
+ if (Math.abs(velocity) > MAX_VELOCITY_TO_PAUSE) {
+ // Cancel any previous alarm and set a new alarm
+ mAlarm.setAlarm(PAUSE_DURATION);
+ }
+ }
+
+ @Override
+ public void onAlarm(Alarm alarm) {
+ if (!mTriggered && mEnabled) {
+ mTriggered = true;
+ mOnPauseCallback.run();
+ }
+ }
+
+ public boolean isTriggered () {
+ return mTriggered;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ if (mEnabled != isEnabled) {
+ mEnabled = isEnabled;
+ if (isEnabled && !mTriggered) {
+ mAlarm.setAlarm(PAUSE_DURATION);
+ } else if (!isEnabled) {
+ mAlarm.cancelAlarm();
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java b/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java
new file mode 100644
index 000000000..651a75354
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/uioverrides/TaggedAnimatorSetBuilder.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 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.launcher3.uioverrides;
+
+import android.animation.Animator;
+import android.util.SparseArray;
+
+import com.android.launcher3.anim.AnimatorSetBuilder;
+
+import java.util.Collections;
+import java.util.List;
+
+public class TaggedAnimatorSetBuilder extends AnimatorSetBuilder {
+
+ /**
+ * Map of the index in {@link #mAnims} to tag. All the animations in {@link #mAnims} starting
+ * from this index correspond to the tag (until a new tag is specified for an index)
+ */
+ private final SparseArray<Object> mTags = new SparseArray<>();
+
+ @Override
+ public void startTag(Object obj) {
+ mTags.put(mAnims.size(), obj);
+ }
+
+ public List<Animator> getAnimationsForTag(Object tag) {
+ int startIndex = mTags.indexOfValue(tag);
+ if (startIndex < 0) {
+ return Collections.emptyList();
+ }
+ int startPos = mTags.keyAt(startIndex);
+
+ int endIndex = startIndex + 1;
+ int endPos = endIndex >= mTags.size() ? mAnims.size() : mTags.keyAt(endIndex);
+
+ return mAnims.subList(startPos, endPos);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
index 9081865ee..299db4768 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/TwoStepSwipeController.java
@@ -17,22 +17,32 @@ 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.scrollInterpolatorForVelocity;
import static com.android.launcher3.anim.SpringAnimationHandler.Y_DIRECTION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.support.animation.SpringAnimation;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.animation.Interpolator;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.LauncherStateManager;
+import com.android.launcher3.LauncherStateManager.AnimationConfig;
+import com.android.launcher3.LauncherStateManager.StateHandler;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AllAppsContainerView;
+import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.AnimatorSetBuilder;
+import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.SpringAnimationHandler;
import com.android.launcher3.touch.SwipeDetector;
import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
@@ -52,16 +62,34 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
private static final float RECATCH_REJECTION_FRACTION = .0875f;
private static final int SINGLE_FRAME_MS = 16;
+ private static final long QUICK_SNAP_TO_OVERVIEW_DURATION = 250;
// Progress after which the transition is assumed to be a success in case user does not fling
private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f;
+ /**
+ * Index of the vertical swipe handles in {@link LauncherStateManager#getStateHandlers()}.
+ */
+ private static final int SWIPE_HANDLER_INDEX = 0;
+
+ /**
+ * Index of various UI handlers in {@link LauncherStateManager#getStateHandlers()} not related
+ * to vertical swipe.
+ */
+ private static final int OTHER_HANDLERS_START_INDEX = SWIPE_HANDLER_INDEX + 1;
+
private final Launcher mLauncher;
private final SwipeDetector mDetector;
private boolean mNoIntercept;
private int mStartContainerType;
+ private DragPauseDetector mDragPauseDetector;
+ private TaggedAnimatorSetBuilder mTaggedAnimatorSetBuilder;
+ private AnimatorSet mQuickOverviewAnimation;
+ private boolean mAnimatingToOverview;
+ private TwoStateAnimationController mTwoStateAnimationController;
+
private AnimatorPlaybackController mCurrentAnimation;
private LauncherState mToState;
@@ -81,6 +109,9 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
// Don't listen for the swipe gesture if we are already in some other state.
return false;
}
+ if (mAnimatingToOverview) {
+ return false;
+ }
if (mCurrentAnimation != null) {
// If we are already animating from a previous state, we can intercept.
return true;
@@ -97,10 +128,9 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
@Override
public void onAnimationCancel(Animator animation) {
- if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
+ if (mCurrentAnimation != null && animation == mCurrentAnimation.getOriginalTarget()) {
Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception());
- mDetector.finishedScrolling();
- mCurrentAnimation = null;
+ clearState();
}
}
@@ -190,10 +220,14 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
float range = getShiftRange();
long maxAccuracy = (long) (2 * range);
+ mDragPauseDetector = new DragPauseDetector(this::onDragPauseDetected);
+ mTaggedAnimatorSetBuilder = new TaggedAnimatorSetBuilder();
+
// Build current animation
mToState = mLauncher.isInState(ALL_APPS) ? NORMAL : ALL_APPS;
- mCurrentAnimation = mLauncher.getStateManager()
- .createAnimationToNewWorkspace(mToState, maxAccuracy);
+ mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(
+ mToState, mTaggedAnimatorSetBuilder, maxAccuracy);
+
mCurrentAnimation.getTarget().addListener(this);
mStartProgress = 0;
mProgressMultiplier = (mLauncher.isInState(ALL_APPS) ? 1 : -1) / range;
@@ -214,6 +248,8 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
@Override
public boolean onDrag(float displacement, float velocity) {
+ mDragPauseDetector.onDrag(displacement, velocity);
+
float deltaProgress = mProgressMultiplier * displacement;
mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress);
return true;
@@ -221,6 +257,11 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
@Override
public void onDragEnd(float velocity, boolean fling) {
+ if (!fling && mDragPauseDetector.isEnabled() && mDragPauseDetector.isTriggered()) {
+ snapToOverview(velocity);
+ return;
+ }
+
final long animationDuration;
final int logAction;
final LauncherState targetState;
@@ -255,23 +296,7 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
h.animateToFinalPosition(0 /* pos */, 1 /* startValue */);
}
}
-
- mCurrentAnimation.setEndAction(new Runnable() {
- @Override
- public void run() {
- if (targetState == mToState) {
- // Transition complete. log the action
- mLauncher.getUserEventDispatcher().logActionOnContainer(logAction,
- mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
- mStartContainerType, mLauncher.getWorkspace().getCurrentPage());
- } else {
- mLauncher.getStateManager().goToState(
- mToState == ALL_APPS ? NORMAL : ALL_APPS, false);
- }
- mDetector.finishedScrolling();
- mCurrentAnimation = null;
- }
- });
+ mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction));
float nextFrameProgress = Utilities.boundToRange(
progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
@@ -281,5 +306,185 @@ public class TwoStepSwipeController extends AnimatorListenerAdapter
anim.setDuration(animationDuration);
anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
anim.start();
+
+ // TODO: Re-enable later
+ mDragPauseDetector.setEnabled(false);
+ }
+
+ private void onSwipeInteractionCompleted(LauncherState targetState, int logAction) {
+ if (targetState == mToState) {
+ // Transition complete. log the action
+ mLauncher.getUserEventDispatcher().logActionOnContainer(logAction,
+ mToState == ALL_APPS ? Direction.UP : Direction.DOWN,
+ mStartContainerType, mLauncher.getWorkspace().getCurrentPage());
+ }
+ clearState();
+
+ // TODO: mQuickOverviewAnimation might still be running in which changing a state instantly
+ // may cause a jump. Animate the state change with a short duration in this case?
+ mLauncher.getStateManager().goToState(targetState, false /* animated */);
+ }
+
+ private void snapToOverview(float velocity) {
+ mAnimatingToOverview = true;
+
+ final float progress = mCurrentAnimation.getProgressFraction();
+ float endProgress = mToState == NORMAL ? 1f : 0f;
+ long animationDuration = SwipeDetector.calculateDuration(
+ velocity, Math.abs(endProgress - progress));
+ float nextFrameProgress = Utilities.boundToRange(
+ progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f);
+
+ mCurrentAnimation.setEndAction(() -> {
+ // TODO: Add logging
+ clearState();
+ mLauncher.getStateManager().goToState(OVERVIEW, false /* animated */);
+ });
+
+ if (mTwoStateAnimationController != null) {
+ mTwoStateAnimationController.goBackToStart(endProgress);
+ }
+
+ ValueAnimator anim = mCurrentAnimation.getAnimationPlayer();
+ anim.setFloatValues(nextFrameProgress, endProgress);
+ anim.setDuration(animationDuration);
+ anim.setInterpolator(scrollInterpolatorForVelocity(velocity));
+ anim.start();
+ }
+
+ private void onDragPauseDetected() {
+ final ValueAnimator twoStepAnimator = ValueAnimator.ofFloat(0, 1);
+ twoStepAnimator.setDuration(mCurrentAnimation.getDuration());
+ StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
+
+ // Change the current animation to only play the vertical handle
+ AnimatorSet anim = new AnimatorSet();
+ anim.playTogether(mTaggedAnimatorSetBuilder.getAnimationsForTag(
+ handlers[SWIPE_HANDLER_INDEX]));
+ anim.play(twoStepAnimator);
+ mCurrentAnimation = mCurrentAnimation.cloneFor(anim);
+
+ AnimatorSetBuilder builder = new AnimatorSetBuilder();
+ AnimationConfig config = new AnimationConfig();
+ config.duration = QUICK_SNAP_TO_OVERVIEW_DURATION;
+ for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
+ handlers[i].setStateWithAnimation(OVERVIEW, builder, config);
+ }
+ mQuickOverviewAnimation = builder.build();
+ mQuickOverviewAnimation.addListener(new AnimationSuccessListener() {
+ @Override
+ public void onAnimationSuccess(Animator animator) {
+ onQuickOverviewAnimationComplete(twoStepAnimator);
+ }
+ });
+ mQuickOverviewAnimation.start();
+ }
+
+ private void onQuickOverviewAnimationComplete(ValueAnimator twoStepAnimator) {
+ if (mAnimatingToOverview) {
+ return;
+ }
+
+ // The remaining state handlers are on the OVERVIEW state. Create two animations, one
+ // towards the NORMAL state and one towards ALL_APPS state and control them based on the
+ // swipe progress.
+ AnimationConfig config = new AnimationConfig();
+ config.duration = (long) (2 * getShiftRange());
+ config.userControlled = true;
+
+ LauncherState fromState = mToState == ALL_APPS ? NORMAL : ALL_APPS;
+ AnimatorSetBuilder builderToTargetState = new AnimatorSetBuilder();
+ AnimatorSetBuilder builderToSourceState = new AnimatorSetBuilder();
+
+ StateHandler[] handlers = mLauncher.getStateManager().getStateHandlers();
+ for (int i = OTHER_HANDLERS_START_INDEX; i < handlers.length; i++) {
+ handlers[i].setStateWithAnimation(mToState, builderToTargetState, config);
+ handlers[i].setStateWithAnimation(fromState, builderToSourceState, config);
+ }
+
+ mTwoStateAnimationController = new TwoStateAnimationController(
+ AnimatorPlaybackController.wrap(builderToSourceState.build(), config.duration),
+ AnimatorPlaybackController.wrap(builderToTargetState.build(), config.duration),
+ twoStepAnimator.getAnimatedFraction());
+ twoStepAnimator.addUpdateListener(mTwoStateAnimationController);
+ }
+
+ private void clearState() {
+ mCurrentAnimation = null;
+ mTaggedAnimatorSetBuilder = null;
+ if (mDragPauseDetector != null) {
+ mDragPauseDetector.setEnabled(false);
+ }
+ mDragPauseDetector = null;
+
+ if (mQuickOverviewAnimation != null) {
+ mQuickOverviewAnimation.cancel();
+ mQuickOverviewAnimation = null;
+ }
+ mTwoStateAnimationController = null;
+ mAnimatingToOverview = false;
+
+ mDetector.finishedScrolling();
+ }
+
+ /**
+ * {@link AnimatorUpdateListener} which interpolates two animations based the progress
+ */
+ private static class TwoStateAnimationController implements AnimatorUpdateListener {
+
+ private final AnimatorPlaybackController mControllerTowardsStart;
+ private final AnimatorPlaybackController mControllerTowardsEnd;
+
+ private Interpolator mInterpolator = Interpolators.LINEAR;
+ private float mStartFraction;
+ private float mLastFraction;
+
+ TwoStateAnimationController(AnimatorPlaybackController controllerTowardsStart,
+ AnimatorPlaybackController controllerTowardsEnd, float startFraction) {
+ mControllerTowardsStart = controllerTowardsStart;
+ mControllerTowardsEnd = controllerTowardsEnd;
+ mLastFraction = mStartFraction = startFraction;
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ mLastFraction = mInterpolator.getInterpolation(valueAnimator.getAnimatedFraction());
+ if (mLastFraction > mStartFraction) {
+ if (mStartFraction >= 1) {
+ mControllerTowardsEnd.setPlayFraction(0);
+ } else {
+ mControllerTowardsEnd.setPlayFraction(
+ (mLastFraction - mStartFraction) / (1 - mStartFraction));
+ }
+ } else {
+ if (mStartFraction <= 0) {
+ mControllerTowardsStart.setPlayFraction(0);
+ } else {
+ mControllerTowardsStart.setPlayFraction(
+ (mStartFraction - mLastFraction) / mStartFraction);
+ }
+ }
+ }
+
+ /**
+ * Changes the interpolator such that from this point ({@link #mLastFraction}), the
+ * animation run towards {@link #mStartFraction}. This allows us to animate the UI back
+ * to the original point.
+ * @param endFraction expected end point for this animation. Should either be 0 or 1.
+ */
+ public void goBackToStart(float endFraction) {
+ if (mLastFraction == mStartFraction || mLastFraction == endFraction) {
+ mInterpolator = (v) -> mStartFraction;
+ } else if (mLastFraction > mStartFraction && endFraction < mStartFraction) {
+ mInterpolator = (v) -> Math.max(v, mStartFraction);
+ } else if (mLastFraction < mStartFraction && endFraction > mStartFraction) {
+ mInterpolator = (v) -> Math.min(mStartFraction, v);
+ } else {
+ final float start = mLastFraction;
+ final float range = endFraction - mLastFraction;
+ mInterpolator = (v) ->
+ SwipeDetector.interpolate(start, mStartFraction, (v - start) / range);
+ }
+ }
}
}