/* * 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; private float mDisplacementShift; 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 case 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 LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive); protected abstract float initCurrentAnimation(); private boolean reinitCurrentAnimation(boolean reachedToState, boolean isDragTowardPositive) { LauncherState newFromState = mFromState == null ? mLauncher.getStateManager().getState() : reachedToState ? mToState : mFromState; LauncherState newToState = getTargetState(newFromState, isDragTowardPositive); if (newFromState == mFromState && newToState == mToState || (newFromState == newToState)) { return false; } mFromState = newFromState; mToState = newToState; mStartProgress = 0; mProgressMultiplier = initCurrentAnimation(); mCurrentAnimation.getTarget().addListener(this); mCurrentAnimation.dispatchOnStart(); return true; } @Override public void onDragStart(boolean start) { if (mCurrentAnimation == null) { mFromState = mToState = null; reinitCurrentAnimation(false, mDetector.wasInitialTouchPositive()); mDisplacementShift = 0; } else { mCurrentAnimation.pause(); mStartProgress = mCurrentAnimation.getProgressFraction(); } } @Override public boolean onDrag(float displacement, float velocity) { float deltaProgress = mProgressMultiplier * (displacement - mDisplacementShift); float progress = deltaProgress + mStartProgress; updateProgress(progress); boolean isDragTowardPositive = (displacement - mDisplacementShift) < 0; if (progress <= 0) { if (reinitCurrentAnimation(false, isDragTowardPositive)) { mDisplacementShift = displacement; } } else if (progress >= 1) { if (reinitCurrentAnimation(true, isDragTowardPositive)) { mDisplacementShift = displacement; } } return true; } protected void updateProgress(float fraction) { mCurrentAnimation.setPlayFraction(fraction); } @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 * mProgressMultiplier, 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 * mProgressMultiplier, 0f, 1f); duration = SwipeDetector.calculateDuration(velocity, Math.min(progress, 1) - endProgress); } } mCurrentAnimation.setEndAction(() -> onSwipeInteractionCompleted(targetState, logAction)); ValueAnimator anim = mCurrentAnimation.getAnimationPlayer(); anim.setFloatValues(startProgress, endProgress); updateSwipeCompleteAnimation(anim, duration, targetState, velocity, fling); anim.start(); } protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling) { animator.setDuration(expectedDuration) .setInterpolator(scrollInterpolatorForVelocity(velocity)); } 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(); } } }