diff options
Diffstat (limited to 'src/com/android/launcher3/touch/AbstractStateChangeTouchController.java')
-rw-r--r-- | src/com/android/launcher3/touch/AbstractStateChangeTouchController.java | 230 |
1 files changed, 230 insertions, 0 deletions
diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java new file mode 100644 index 000000000..db5363436 --- /dev/null +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2019 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.touch; + +import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.util.Log; +import android.view.MotionEvent; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherState; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.TouchController; + +/** + * TouchController for handling state changes + */ +public abstract class AbstractStateChangeTouchController extends AnimatorListenerAdapter + implements TouchController, SwipeDetector.Listener { + + private static final String TAG = "ASCTouchController"; + public static final float RECATCH_REJECTION_FRACTION = .0875f; + public static final int SINGLE_FRAME_MS = 16; + + // Progress after which the transition is assumed to be a success in case user does not fling + public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; + + protected final Launcher mLauncher; + protected final SwipeDetector mDetector; + + private boolean mNoIntercept; + protected int mStartContainerType; + + protected LauncherState mFromState; + protected LauncherState mToState; + protected AnimatorPlaybackController mCurrentAnimation; + + private float mStartProgress; + // Ratio of transition process [0, 1] to drag displacement (px) + private float mProgressMultiplier; + + public AbstractStateChangeTouchController(Launcher l, SwipeDetector.Direction dir) { + mLauncher = l; + mDetector = new SwipeDetector(l, this, dir); + } + + protected abstract boolean canInterceptTouch(MotionEvent ev); + + /** + * Initializes the {@code mFromState} and {@code mToState} and swipe direction to use for + * the detector. In can of disabling swipe, return 0. + */ + protected abstract int getSwipeDirection(MotionEvent ev); + + @Override + public final boolean onControllerInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mNoIntercept = !canInterceptTouch(ev); + if (mNoIntercept) { + return false; + } + + // Now figure out which direction scroll events the controller will start + // calling the callbacks. + final int directionsToDetectScroll; + boolean ignoreSlopWhenSettling = false; + + if (mCurrentAnimation != null) { + if (mCurrentAnimation.getProgressFraction() > 1 - RECATCH_REJECTION_FRACTION) { + directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + } else if (mCurrentAnimation.getProgressFraction() < RECATCH_REJECTION_FRACTION ) { + directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE; + } else { + directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; + ignoreSlopWhenSettling = true; + } + } else { + directionsToDetectScroll = getSwipeDirection(ev); + if (directionsToDetectScroll == 0) { + mNoIntercept = true; + return false; + } + } + mDetector.setDetectableScrollConditions( + directionsToDetectScroll, ignoreSlopWhenSettling); + } + + if (mNoIntercept) { + return false; + } + + onControllerTouchEvent(ev); + return mDetector.isDraggingOrSettling(); + } + + @Override + public final boolean onControllerTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + protected float getShiftRange() { + return mLauncher.getAllAppsController().getShiftRange(); + } + + protected abstract float initCurrentAnimation(); + + @Override + public void onDragStart(boolean start) { + if (mCurrentAnimation == null) { + mStartProgress = 0; + mProgressMultiplier = initCurrentAnimation(); + + mCurrentAnimation.getTarget().addListener(this); + mCurrentAnimation.dispatchOnStart(); + } else { + mCurrentAnimation.pause(); + mStartProgress = mCurrentAnimation.getProgressFraction(); + } + } + + @Override + public boolean onDrag(float displacement, float velocity) { + float deltaProgress = mProgressMultiplier * displacement; + mCurrentAnimation.setPlayFraction(deltaProgress + mStartProgress); + return true; + } + + @Override + public void onDragEnd(float velocity, boolean fling) { + final int logAction; + final LauncherState targetState; + final float progress = mCurrentAnimation.getProgressFraction(); + + if (fling) { + logAction = Touch.FLING; + targetState = + Float.compare(Math.signum(velocity), Math.signum(mProgressMultiplier)) == 0 + ? mToState : mFromState; + // snap to top or bottom using the release velocity + } else { + logAction = Touch.SWIPE; + targetState = (progress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState; + } + + + final float endProgress; + final float startProgress; + final long duration; + + if (targetState == mToState) { + endProgress = 1; + if (progress >= 1) { + duration = 0; + startProgress = 1; + } else { + startProgress = Utilities.boundToRange( + progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f); + duration = SwipeDetector.calculateDuration(velocity, + endProgress - Math.max(progress, 0)); + } + } else { + endProgress = 0; + if (progress <= 0) { + duration = 0; + startProgress = 0; + } else { + startProgress = Utilities.boundToRange( + progress + velocity * SINGLE_FRAME_MS / getShiftRange(), 0f, 1f); + duration = SwipeDetector.calculateDuration(velocity, + Math.min(progress, 1) - endProgress); + } + } + + mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction)); + ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); + anim.setFloatValues(startProgress, endProgress); + anim.setDuration(duration).setInterpolator(scrollInterpolatorForVelocity(velocity)); + anim.start(); + } + + protected int getDirectionForLog() { + return mToState.ordinal > mFromState.ordinal ? Direction.UP : Direction.DOWN; + } + + protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { + if (targetState != mFromState) { + // Transition complete. log the action + mLauncher.getUserEventDispatcher().logStateChangeAction(logAction, + getDirectionForLog(), + mStartContainerType, + mFromState.containerType, + mToState.containerType, + mLauncher.getWorkspace().getCurrentPage()); + } + clearState(); + mLauncher.getStateManager().goToState(targetState, false /* animated */); + } + + protected void clearState() { + mCurrentAnimation = null; + mDetector.finishedScrolling(); + } + + @Override + public void onAnimationCancel(Animator animation) { + if (mCurrentAnimation != null && animation == mCurrentAnimation.getOriginalTarget()) { + Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception()); + clearState(); + } + } +} |