0f5a1e3661
Test: manual Fix: 200813011 Change-Id: I74ce053023935a037074db25552f730d30f8ef52
385 lines
17 KiB
Java
385 lines
17 KiB
Java
/*
|
|
* 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.launcher3.uioverrides.touchcontrollers;
|
|
|
|
import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
|
|
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
|
|
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.os.SystemClock;
|
|
import android.os.VibrationEffect;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.animation.Interpolator;
|
|
|
|
import com.android.launcher3.AbstractFloatingView;
|
|
import com.android.launcher3.BaseDraggingActivity;
|
|
import com.android.launcher3.LauncherAnimUtils;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
|
import com.android.launcher3.anim.Interpolators;
|
|
import com.android.launcher3.anim.PendingAnimation;
|
|
import com.android.launcher3.touch.BaseSwipeDetector;
|
|
import com.android.launcher3.touch.PagedOrientationHandler;
|
|
import com.android.launcher3.touch.SingleAxisSwipeDetector;
|
|
import com.android.launcher3.util.FlingBlockCheck;
|
|
import com.android.launcher3.util.TouchController;
|
|
import com.android.launcher3.views.BaseDragLayer;
|
|
import com.android.quickstep.SysUINavigationMode;
|
|
import com.android.quickstep.util.VibratorWrapper;
|
|
import com.android.quickstep.views.RecentsView;
|
|
import com.android.quickstep.views.TaskView;
|
|
|
|
/**
|
|
* Touch controller for handling task view card swipes
|
|
*/
|
|
public abstract class TaskViewTouchController<T extends BaseDraggingActivity>
|
|
extends AnimatorListenerAdapter implements TouchController,
|
|
SingleAxisSwipeDetector.Listener {
|
|
|
|
private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
|
|
private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
|
|
private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;
|
|
|
|
public static final int TASK_DISMISS_VIBRATION_PRIMITIVE =
|
|
Utilities.ATLEAST_R ? VibrationEffect.Composition.PRIMITIVE_TICK : -1;
|
|
public static final float TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE = 1f;
|
|
public static final VibrationEffect TASK_DISMISS_VIBRATION_FALLBACK =
|
|
VibratorWrapper.EFFECT_TEXTURE_TICK;
|
|
|
|
protected final T mActivity;
|
|
private final SingleAxisSwipeDetector mDetector;
|
|
private final RecentsView mRecentsView;
|
|
private final int[] mTempCords = new int[2];
|
|
private final boolean mIsRtl;
|
|
|
|
private AnimatorPlaybackController mCurrentAnimation;
|
|
private boolean mCurrentAnimationIsGoingUp;
|
|
private boolean mAllowGoingUp;
|
|
private boolean mAllowGoingDown;
|
|
|
|
private boolean mNoIntercept;
|
|
|
|
private float mDisplacementShift;
|
|
private float mProgressMultiplier;
|
|
private float mEndDisplacement;
|
|
private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck();
|
|
private Float mOverrideVelocity = null;
|
|
|
|
private TaskView mTaskBeingDragged;
|
|
|
|
private boolean mIsDismissHapticRunning = false;
|
|
|
|
public TaskViewTouchController(T activity) {
|
|
mActivity = activity;
|
|
mRecentsView = activity.getOverviewPanel();
|
|
mIsRtl = Utilities.isRtl(activity.getResources());
|
|
SingleAxisSwipeDetector.Direction dir =
|
|
mRecentsView.getPagedOrientationHandler().getUpDownSwipeDirection();
|
|
mDetector = new SingleAxisSwipeDetector(activity, this, dir);
|
|
}
|
|
|
|
private boolean canInterceptTouch(MotionEvent ev) {
|
|
if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) != 0) {
|
|
// Don't intercept swipes on the nav bar, as user might be trying to go home
|
|
// during a task dismiss animation.
|
|
if (mCurrentAnimation != null) {
|
|
mCurrentAnimation.getAnimationPlayer().end();
|
|
}
|
|
return false;
|
|
}
|
|
if (mCurrentAnimation != null) {
|
|
mCurrentAnimation.forceFinishIfCloseToEnd();
|
|
}
|
|
if (mCurrentAnimation != null) {
|
|
// If we are already animating from a previous state, we can intercept.
|
|
return true;
|
|
}
|
|
if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
|
|
return false;
|
|
}
|
|
return isRecentsInteractive();
|
|
}
|
|
|
|
protected abstract boolean isRecentsInteractive();
|
|
|
|
/** Is recents view showing a single task in a modal way. */
|
|
protected abstract boolean isRecentsModal();
|
|
|
|
protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) {
|
|
}
|
|
|
|
@Override
|
|
public void onAnimationCancel(Animator animation) {
|
|
if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) {
|
|
clearState();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
|
|
if ((ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL)
|
|
&& mCurrentAnimation == null) {
|
|
clearState();
|
|
}
|
|
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.
|
|
int directionsToDetectScroll = 0;
|
|
boolean ignoreSlopWhenSettling = false;
|
|
if (mCurrentAnimation != null) {
|
|
directionsToDetectScroll = DIRECTION_BOTH;
|
|
ignoreSlopWhenSettling = true;
|
|
} else {
|
|
mTaskBeingDragged = null;
|
|
|
|
for (int i = 0; i < mRecentsView.getTaskViewCount(); i++) {
|
|
TaskView view = mRecentsView.getTaskViewAt(i);
|
|
|
|
if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer()
|
|
.isEventOverView(view, ev)) {
|
|
// Disable swiping up and down if the task overlay is modal.
|
|
if (isRecentsModal()) {
|
|
mTaskBeingDragged = null;
|
|
break;
|
|
}
|
|
mTaskBeingDragged = view;
|
|
int upDirection = mRecentsView.getPagedOrientationHandler()
|
|
.getUpDirection(mIsRtl);
|
|
|
|
// The task can be dragged up to dismiss it
|
|
mAllowGoingUp = true;
|
|
|
|
// The task can be dragged down to open it if:
|
|
// - It's the current page
|
|
// - We support gestures to enter overview
|
|
// - It's the focused task if in grid view
|
|
// - The task is snapped
|
|
mAllowGoingDown = i == mRecentsView.getCurrentPage()
|
|
&& SysUINavigationMode.getMode(mActivity).hasGestures
|
|
&& (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
|
|
&& mRecentsView.isTaskSnapped(i);
|
|
|
|
directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
|
|
break;
|
|
}
|
|
}
|
|
if (mTaskBeingDragged == null) {
|
|
mNoIntercept = true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
mDetector.setDetectableScrollConditions(
|
|
directionsToDetectScroll, ignoreSlopWhenSettling);
|
|
}
|
|
|
|
if (mNoIntercept) {
|
|
return false;
|
|
}
|
|
|
|
onControllerTouchEvent(ev);
|
|
return mDetector.isDraggingOrSettling();
|
|
}
|
|
|
|
@Override
|
|
public boolean onControllerTouchEvent(MotionEvent ev) {
|
|
return mDetector.onTouchEvent(ev);
|
|
}
|
|
|
|
private void reInitAnimationController(boolean goingUp) {
|
|
if (mCurrentAnimation != null && mCurrentAnimationIsGoingUp == goingUp) {
|
|
// No need to init
|
|
return;
|
|
}
|
|
if ((goingUp && !mAllowGoingUp) || (!goingUp && !mAllowGoingDown)) {
|
|
// Trying to re-init in an unsupported direction.
|
|
return;
|
|
}
|
|
if (mCurrentAnimation != null) {
|
|
mCurrentAnimation.setPlayFraction(0);
|
|
mCurrentAnimation.getTarget().removeListener(this);
|
|
mCurrentAnimation.dispatchOnCancel();
|
|
}
|
|
|
|
PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
|
|
mCurrentAnimationIsGoingUp = goingUp;
|
|
BaseDragLayer dl = mActivity.getDragLayer();
|
|
final int secondaryLayerDimension = orientationHandler.getSecondaryDimension(dl);
|
|
long maxDuration = 2 * secondaryLayerDimension;
|
|
int verticalFactor = orientationHandler.getTaskDragDisplacementFactor(mIsRtl);
|
|
int secondaryTaskDimension = orientationHandler.getSecondaryDimension(mTaskBeingDragged);
|
|
// The interpolator controlling the most prominent visual movement. We use this to determine
|
|
// whether we passed SUCCESS_TRANSITION_PROGRESS.
|
|
final Interpolator currentInterpolator;
|
|
PendingAnimation pa;
|
|
if (goingUp) {
|
|
currentInterpolator = Interpolators.LINEAR;
|
|
pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged,
|
|
true /* animateTaskView */, true /* removeTask */, maxDuration,
|
|
false /* dismissingForSplitSelection*/);
|
|
|
|
mEndDisplacement = -secondaryTaskDimension;
|
|
} else {
|
|
currentInterpolator = Interpolators.ZOOM_IN;
|
|
pa = mRecentsView.createTaskLaunchAnimation(
|
|
mTaskBeingDragged, maxDuration, currentInterpolator);
|
|
|
|
// Since the thumbnail is what is filling the screen, based the end displacement on it.
|
|
View thumbnailView = mTaskBeingDragged.getThumbnail();
|
|
mTempCords[1] = orientationHandler.getSecondaryDimension(thumbnailView);
|
|
dl.getDescendantCoordRelativeToSelf(thumbnailView, mTempCords);
|
|
mEndDisplacement = secondaryLayerDimension - mTempCords[1];
|
|
}
|
|
mEndDisplacement *= verticalFactor;
|
|
mCurrentAnimation = pa.createPlaybackController();
|
|
|
|
// Setting this interpolator doesn't affect the visual motion, but is used to determine
|
|
// whether we successfully reached the target state in onDragEnd().
|
|
mCurrentAnimation.getTarget().setInterpolator(currentInterpolator);
|
|
onUserControlledAnimationCreated(mCurrentAnimation);
|
|
mCurrentAnimation.getTarget().addListener(this);
|
|
mCurrentAnimation.dispatchOnStart();
|
|
mProgressMultiplier = 1 / mEndDisplacement;
|
|
}
|
|
|
|
@Override
|
|
public void onDragStart(boolean start, float startDisplacement) {
|
|
PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
|
|
if (mCurrentAnimation == null) {
|
|
reInitAnimationController(orientationHandler.isGoingUp(startDisplacement, mIsRtl));
|
|
mDisplacementShift = 0;
|
|
} else {
|
|
mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier;
|
|
mCurrentAnimation.pause();
|
|
}
|
|
mFlingBlockCheck.unblockFling();
|
|
mOverrideVelocity = null;
|
|
}
|
|
|
|
@Override
|
|
public boolean onDrag(float displacement) {
|
|
PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
|
|
float totalDisplacement = displacement + mDisplacementShift;
|
|
boolean isGoingUp = totalDisplacement == 0 ? mCurrentAnimationIsGoingUp :
|
|
orientationHandler.isGoingUp(totalDisplacement, mIsRtl);
|
|
if (isGoingUp != mCurrentAnimationIsGoingUp) {
|
|
reInitAnimationController(isGoingUp);
|
|
mFlingBlockCheck.blockFling();
|
|
} else {
|
|
mFlingBlockCheck.onEvent();
|
|
}
|
|
|
|
if (isGoingUp) {
|
|
if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
|
|
// Halve the value when dismissing, as we are animating the drag across the full
|
|
// length for only the first half of the progress
|
|
mCurrentAnimation.setPlayFraction(
|
|
Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
|
|
} else {
|
|
// Set mOverrideVelocity to control task dismiss velocity in onDragEnd
|
|
int velocityDimenId = R.dimen.default_task_dismiss_drag_velocity;
|
|
if (mRecentsView.showAsGrid()) {
|
|
if (mTaskBeingDragged.isFocusedTask()) {
|
|
velocityDimenId =
|
|
R.dimen.default_task_dismiss_drag_velocity_grid_focus_task;
|
|
} else {
|
|
velocityDimenId = R.dimen.default_task_dismiss_drag_velocity_grid;
|
|
}
|
|
}
|
|
mOverrideVelocity = -mTaskBeingDragged.getResources().getDimension(velocityDimenId);
|
|
|
|
// Once halfway through task dismissal interpolation, switch from reversible
|
|
// dragging-task animation to playing the remaining task translation animations
|
|
final long now = SystemClock.uptimeMillis();
|
|
MotionEvent upAction = MotionEvent.obtain(now, now,
|
|
MotionEvent.ACTION_UP, 0.0f, 0.0f, 0);
|
|
mDetector.onTouchEvent(upAction);
|
|
upAction.recycle();
|
|
}
|
|
} else {
|
|
mCurrentAnimation.setPlayFraction(
|
|
Utilities.boundToRange(totalDisplacement * mProgressMultiplier, 0, 1));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onDragEnd(float velocity) {
|
|
if (mOverrideVelocity != null) {
|
|
velocity = mOverrideVelocity;
|
|
mOverrideVelocity = null;
|
|
}
|
|
// Limit velocity, as very large scalar values make animations play too quickly
|
|
float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
|
|
R.dimen.max_task_dismiss_drag_velocity);
|
|
velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
|
|
maxTaskDismissDragVelocity);
|
|
boolean fling = mDetector.isFling(velocity);
|
|
final boolean goingToEnd;
|
|
boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
|
|
if (blockedFling) {
|
|
fling = false;
|
|
}
|
|
PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler();
|
|
boolean goingUp = orientationHandler.isGoingUp(velocity, mIsRtl);
|
|
float progress = mCurrentAnimation.getProgressFraction();
|
|
float interpolatedProgress = mCurrentAnimation.getInterpolatedProgress();
|
|
if (fling) {
|
|
goingToEnd = goingUp == mCurrentAnimationIsGoingUp;
|
|
} else {
|
|
goingToEnd = interpolatedProgress > SUCCESS_TRANSITION_PROGRESS;
|
|
}
|
|
long animationDuration = BaseSwipeDetector.calculateDuration(
|
|
velocity, goingToEnd ? (1 - progress) : progress);
|
|
if (blockedFling && !goingToEnd) {
|
|
animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
|
|
}
|
|
// Due to very high or low velocity dismissals, animation durations can be inconsistently
|
|
// long or short. Bound the duration for animation of task translations for a more
|
|
// standardized feel.
|
|
animationDuration = Utilities.boundToRange(animationDuration,
|
|
MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);
|
|
|
|
mCurrentAnimation.setEndAction(this::clearState);
|
|
mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
|
|
velocity * orientationHandler.getSecondaryTranslationDirectionFactor(),
|
|
mEndDisplacement, animationDuration);
|
|
if (goingUp && goingToEnd && !mIsDismissHapticRunning) {
|
|
VibratorWrapper.INSTANCE.get(mActivity).vibrate(TASK_DISMISS_VIBRATION_PRIMITIVE,
|
|
TASK_DISMISS_VIBRATION_PRIMITIVE_SCALE, TASK_DISMISS_VIBRATION_FALLBACK);
|
|
mIsDismissHapticRunning = true;
|
|
}
|
|
}
|
|
|
|
private void clearState() {
|
|
mDetector.finishedScrolling();
|
|
mDetector.setDetectableScrollConditions(0, false);
|
|
mTaskBeingDragged = null;
|
|
mCurrentAnimation = null;
|
|
mIsDismissHapticRunning = false;
|
|
}
|
|
}
|