| /* |
| * Copyright (C) 2014 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.systemui.recents.views; |
| |
| import android.animation.Animator; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Path; |
| import android.util.ArrayMap; |
| import android.util.MutableBoolean; |
| import android.view.InputDevice; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewDebug; |
| import android.view.ViewParent; |
| import android.view.animation.Interpolator; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.internal.logging.nano.MetricsProto.MetricsEvent; |
| import com.android.systemui.Interpolators; |
| import com.android.systemui.R; |
| import com.android.systemui.SwipeHelper; |
| import com.android.systemui.recents.Constants; |
| import com.android.systemui.recents.LegacyRecentsImpl; |
| import com.android.systemui.recents.events.EventBus; |
| import com.android.systemui.recents.events.activity.HideRecentsEvent; |
| import com.android.systemui.recents.events.ui.StackViewScrolledEvent; |
| import com.android.systemui.recents.events.ui.TaskViewDismissedEvent; |
| import com.android.systemui.recents.misc.FreePathInterpolator; |
| import com.android.systemui.recents.utilities.AnimationProps; |
| import com.android.systemui.recents.utilities.Utilities; |
| import com.android.systemui.shared.recents.model.Task; |
| import com.android.systemui.statusbar.FlingAnimationUtils; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * Handles touch events for a TaskStackView. |
| */ |
| class TaskStackViewTouchHandler implements SwipeHelper.Callback { |
| |
| private static final int INACTIVE_POINTER_ID = -1; |
| private static final float CHALLENGING_SWIPE_ESCAPE_VELOCITY = 800f; // dp/sec |
| // The min overscroll is the amount of task progress overscroll we want / the max overscroll |
| // curve value below |
| private static final float MAX_OVERSCROLL = 0.7f / 0.3f; |
| private static final Interpolator OVERSCROLL_INTERP; |
| static { |
| Path OVERSCROLL_PATH = new Path(); |
| OVERSCROLL_PATH.moveTo(0, 0); |
| OVERSCROLL_PATH.cubicTo(0.2f, 0.175f, 0.25f, 0.3f, 1f, 0.3f); |
| OVERSCROLL_INTERP = new FreePathInterpolator(OVERSCROLL_PATH); |
| } |
| |
| Context mContext; |
| TaskStackView mSv; |
| TaskStackViewScroller mScroller; |
| VelocityTracker mVelocityTracker; |
| FlingAnimationUtils mFlingAnimUtils; |
| ValueAnimator mScrollFlingAnimator; |
| |
| @ViewDebug.ExportedProperty(category="recents") |
| boolean mIsScrolling; |
| float mDownScrollP; |
| int mDownX, mDownY; |
| int mLastY; |
| int mActivePointerId = INACTIVE_POINTER_ID; |
| int mOverscrollSize; |
| TaskView mActiveTaskView = null; |
| |
| int mMinimumVelocity; |
| int mMaximumVelocity; |
| // The scroll touch slop is used to calculate when we start scrolling |
| int mScrollTouchSlop; |
| // Used to calculate when a tap is outside a task view rectangle. |
| final int mWindowTouchSlop; |
| |
| private final StackViewScrolledEvent mStackViewScrolledEvent = new StackViewScrolledEvent(); |
| |
| // The current and final set of task transforms, sized to match the list of tasks in the stack |
| private ArrayList<Task> mCurrentTasks = new ArrayList<>(); |
| private ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>(); |
| private ArrayList<TaskViewTransform> mFinalTaskTransforms = new ArrayList<>(); |
| private ArrayMap<View, Animator> mSwipeHelperAnimations = new ArrayMap<>(); |
| private TaskViewTransform mTmpTransform = new TaskViewTransform(); |
| private float mTargetStackScroll; |
| |
| SwipeHelper mSwipeHelper; |
| boolean mInterceptedBySwipeHelper; |
| |
| public TaskStackViewTouchHandler(Context context, TaskStackView sv, |
| TaskStackViewScroller scroller) { |
| Resources res = context.getResources(); |
| ViewConfiguration configuration = ViewConfiguration.get(context); |
| mContext = context; |
| mSv = sv; |
| mScroller = scroller; |
| mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); |
| mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); |
| mScrollTouchSlop = configuration.getScaledTouchSlop(); |
| mWindowTouchSlop = configuration.getScaledWindowTouchSlop(); |
| mFlingAnimUtils = new FlingAnimationUtils(context, 0.2f); |
| mOverscrollSize = res.getDimensionPixelSize(R.dimen.recents_fling_overscroll_distance); |
| mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, context) { |
| @Override |
| protected float getSize(View v) { |
| return getScaledDismissSize(); |
| } |
| |
| @Override |
| protected void prepareDismissAnimation(View v, Animator anim) { |
| mSwipeHelperAnimations.put(v, anim); |
| } |
| |
| @Override |
| protected void prepareSnapBackAnimation(View v, Animator anim) { |
| anim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); |
| mSwipeHelperAnimations.put(v, anim); |
| } |
| |
| @Override |
| protected float getUnscaledEscapeVelocity() { |
| return CHALLENGING_SWIPE_ESCAPE_VELOCITY; |
| } |
| |
| @Override |
| protected long getMaxEscapeAnimDuration() { |
| return 700; |
| } |
| }; |
| mSwipeHelper.setDisableHardwareLayers(true); |
| } |
| |
| /** Velocity tracker helpers */ |
| void initOrResetVelocityTracker() { |
| if (mVelocityTracker == null) { |
| mVelocityTracker = VelocityTracker.obtain(); |
| } else { |
| mVelocityTracker.clear(); |
| } |
| } |
| void recycleVelocityTracker() { |
| if (mVelocityTracker != null) { |
| mVelocityTracker.recycle(); |
| mVelocityTracker = null; |
| } |
| } |
| |
| /** Touch preprocessing for handling below */ |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| // Pass through to swipe helper if we are swiping |
| mInterceptedBySwipeHelper = isSwipingEnabled() && mSwipeHelper.onInterceptTouchEvent(ev); |
| if (mInterceptedBySwipeHelper) { |
| return true; |
| } |
| |
| return handleTouchEvent(ev); |
| } |
| |
| /** Handles touch events once we have intercepted them */ |
| public boolean onTouchEvent(MotionEvent ev) { |
| // Pass through to swipe helper if we are swiping |
| if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { |
| return true; |
| } |
| |
| handleTouchEvent(ev); |
| return true; |
| } |
| |
| /** |
| * Finishes all scroll-fling and non-dismissing animations currently running. |
| */ |
| public void cancelNonDismissTaskAnimations() { |
| Utilities.cancelAnimationWithoutCallbacks(mScrollFlingAnimator); |
| if (!mSwipeHelperAnimations.isEmpty()) { |
| // For the non-dismissing tasks, freeze the position into the task overrides |
| List<TaskView> taskViews = mSv.getTaskViews(); |
| for (int i = taskViews.size() - 1; i >= 0; i--) { |
| TaskView tv = taskViews.get(i); |
| |
| if (mSv.isIgnoredTask(tv.getTask())) { |
| continue; |
| } |
| |
| tv.cancelTransformAnimation(); |
| mSv.getStackAlgorithm().addUnfocusedTaskOverride(tv, mTargetStackScroll); |
| } |
| mSv.getStackAlgorithm().setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); |
| // Update the scroll to the final scroll position from onBeginDrag() |
| mSv.getScroller().setStackScroll(mTargetStackScroll, null); |
| |
| mSwipeHelperAnimations.clear(); |
| } |
| mActiveTaskView = null; |
| } |
| |
| private boolean handleTouchEvent(MotionEvent ev) { |
| // Short circuit if we have no children |
| if (mSv.getTaskViews().size() == 0) { |
| return false; |
| } |
| |
| final TaskStackLayoutAlgorithm layoutAlgorithm = mSv.mLayoutAlgorithm; |
| int action = ev.getAction(); |
| switch (action & MotionEvent.ACTION_MASK) { |
| case MotionEvent.ACTION_DOWN: { |
| // Stop the current scroll if it is still flinging |
| mScroller.stopScroller(); |
| mScroller.stopBoundScrollAnimation(); |
| mScroller.resetDeltaScroll(); |
| cancelNonDismissTaskAnimations(); |
| mSv.cancelDeferredTaskViewLayoutAnimation(); |
| |
| // Save the touch down info |
| mDownX = (int) ev.getX(); |
| mDownY = (int) ev.getY(); |
| mLastY = mDownY; |
| mDownScrollP = mScroller.getStackScroll(); |
| mActivePointerId = ev.getPointerId(0); |
| mActiveTaskView = findViewAtPoint(mDownX, mDownY); |
| |
| // Initialize the velocity tracker |
| initOrResetVelocityTracker(); |
| mVelocityTracker.addMovement(ev); |
| break; |
| } |
| case MotionEvent.ACTION_POINTER_DOWN: { |
| final int index = ev.getActionIndex(); |
| mActivePointerId = ev.getPointerId(index); |
| mDownX = (int) ev.getX(index); |
| mDownY = (int) ev.getY(index); |
| mLastY = mDownY; |
| mDownScrollP = mScroller.getStackScroll(); |
| mScroller.resetDeltaScroll(); |
| mVelocityTracker.addMovement(ev); |
| break; |
| } |
| case MotionEvent.ACTION_MOVE: { |
| int activePointerIndex = ev.findPointerIndex(mActivePointerId); |
| if (activePointerIndex == -1) { |
| break; |
| } |
| int y = (int) ev.getY(activePointerIndex); |
| int x = (int) ev.getX(activePointerIndex); |
| if (!mIsScrolling) { |
| int yDiff = Math.abs(y - mDownY); |
| int xDiff = Math.abs(x - mDownX); |
| if (Math.abs(y - mDownY) > mScrollTouchSlop && yDiff > xDiff) { |
| mIsScrolling = true; |
| float stackScroll = mScroller.getStackScroll(); |
| List<TaskView> taskViews = mSv.getTaskViews(); |
| for (int i = taskViews.size() - 1; i >= 0; i--) { |
| layoutAlgorithm.addUnfocusedTaskOverride(taskViews.get(i).getTask(), |
| stackScroll); |
| } |
| layoutAlgorithm.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); |
| |
| // Disallow parents from intercepting touch events |
| final ViewParent parent = mSv.getParent(); |
| if (parent != null) { |
| parent.requestDisallowInterceptTouchEvent(true); |
| } |
| |
| MetricsLogger.action(mSv.getContext(), MetricsEvent.OVERVIEW_SCROLL); |
| mLastY = mDownY = y; |
| } |
| } |
| if (mIsScrolling) { |
| // If we just move linearly on the screen, then that would map to 1/arclength |
| // of the curve, so just move the scroll proportional to that |
| float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y); |
| |
| // Modulate the overscroll to prevent users from pulling the stack too far |
| float minScrollP = layoutAlgorithm.mMinScrollP; |
| float maxScrollP = layoutAlgorithm.mMaxScrollP; |
| float curScrollP = mDownScrollP + deltaP; |
| if (curScrollP < minScrollP || curScrollP > maxScrollP) { |
| float clampedScrollP = Utilities.clamp(curScrollP, minScrollP, maxScrollP); |
| float overscrollP = (curScrollP - clampedScrollP); |
| float maxOverscroll = LegacyRecentsImpl.getConfiguration().isLowRamDevice |
| ? layoutAlgorithm.mTaskStackLowRamLayoutAlgorithm.getMaxOverscroll() |
| : MAX_OVERSCROLL; |
| float overscrollX = Math.abs(overscrollP) / maxOverscroll; |
| float interpX = OVERSCROLL_INTERP.getInterpolation(overscrollX); |
| curScrollP = clampedScrollP + Math.signum(overscrollP) * |
| (interpX * maxOverscroll); |
| } |
| mDownScrollP += mScroller.setDeltaStackScroll(mDownScrollP, |
| curScrollP - mDownScrollP); |
| mStackViewScrolledEvent.updateY(y - mLastY); |
| EventBus.getDefault().send(mStackViewScrolledEvent); |
| } |
| |
| mLastY = y; |
| mVelocityTracker.addMovement(ev); |
| break; |
| } |
| case MotionEvent.ACTION_POINTER_UP: { |
| int pointerIndex = ev.getActionIndex(); |
| int pointerId = ev.getPointerId(pointerIndex); |
| if (pointerId == mActivePointerId) { |
| // Select a new active pointer id and reset the motion state |
| final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; |
| mActivePointerId = ev.getPointerId(newPointerIndex); |
| mDownX = (int) ev.getX(pointerIndex); |
| mDownY = (int) ev.getY(pointerIndex); |
| mLastY = mDownY; |
| mDownScrollP = mScroller.getStackScroll(); |
| } |
| mVelocityTracker.addMovement(ev); |
| break; |
| } |
| case MotionEvent.ACTION_UP: { |
| mVelocityTracker.addMovement(ev); |
| mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); |
| int activePointerIndex = ev.findPointerIndex(mActivePointerId); |
| int y = (int) ev.getY(activePointerIndex); |
| int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); |
| if (mIsScrolling) { |
| if (mScroller.isScrollOutOfBounds()) { |
| mScroller.animateBoundScroll(); |
| } else if (Math.abs(velocity) > mMinimumVelocity && |
| !LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| float minY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, |
| layoutAlgorithm.mMaxScrollP); |
| float maxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP, |
| layoutAlgorithm.mMinScrollP); |
| mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY, |
| mOverscrollSize); |
| mSv.invalidate(); |
| } |
| |
| // Reset the focused task after the user has scrolled, but we have no scrolling |
| // in grid layout and therefore we don't want to reset the focus there. |
| if (!mSv.mTouchExplorationEnabled && !mSv.useGridLayout()) { |
| if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| mScroller.scrollToClosestTask(velocity); |
| } else { |
| mSv.resetFocusedTask(mSv.getFocusedTask()); |
| } |
| } |
| } else if (mActiveTaskView == null) { |
| // This tap didn't start on a task. |
| maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY()); |
| } |
| |
| mActivePointerId = INACTIVE_POINTER_ID; |
| mIsScrolling = false; |
| recycleVelocityTracker(); |
| break; |
| } |
| case MotionEvent.ACTION_CANCEL: { |
| mActivePointerId = INACTIVE_POINTER_ID; |
| mIsScrolling = false; |
| recycleVelocityTracker(); |
| break; |
| } |
| } |
| return mIsScrolling; |
| } |
| |
| /** Hides recents if the up event at (x, y) is a tap on the background area. */ |
| void maybeHideRecentsFromBackgroundTap(int x, int y) { |
| // Ignore the up event if it's too far from its start position. The user might have been |
| // trying to scroll or swipe. |
| int dx = Math.abs(mDownX - x); |
| int dy = Math.abs(mDownY - y); |
| if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) { |
| return; |
| } |
| |
| // Shift the tap position toward the center of the task stack and check to see if it would |
| // have hit a view. The user might have tried to tap on a task and missed slightly. |
| int shiftedX = x; |
| if (x > (mSv.getRight() - mSv.getLeft()) / 2) { |
| shiftedX -= mWindowTouchSlop; |
| } else { |
| shiftedX += mWindowTouchSlop; |
| } |
| if (findViewAtPoint(shiftedX, y) != null) { |
| return; |
| } |
| |
| // Disallow tapping above and below the stack to dismiss recents |
| if (x > mSv.mLayoutAlgorithm.mStackRect.left && x < mSv.mLayoutAlgorithm.mStackRect.right) { |
| return; |
| } |
| |
| // The user intentionally tapped on the background, which is like a tap on the "desktop". |
| // Hide recents and transition to the launcher. |
| EventBus.getDefault().send(new HideRecentsEvent(false, true)); |
| } |
| |
| /** Handles generic motion events */ |
| public boolean onGenericMotionEvent(MotionEvent ev) { |
| if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == |
| InputDevice.SOURCE_CLASS_POINTER) { |
| int action = ev.getAction(); |
| switch (action & MotionEvent.ACTION_MASK) { |
| case MotionEvent.ACTION_SCROLL: |
| // Find the front most task and scroll the next task to the front |
| float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); |
| if (vScroll > 0) { |
| mSv.setRelativeFocusedTask(true, true /* stackTasksOnly */, |
| false /* animated */); |
| } else { |
| mSv.setRelativeFocusedTask(false, true /* stackTasksOnly */, |
| false /* animated */); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /**** SwipeHelper Implementation ****/ |
| |
| @Override |
| public View getChildAtPosition(MotionEvent ev) { |
| TaskView tv = findViewAtPoint((int) ev.getX(), (int) ev.getY()); |
| if (tv != null && canChildBeDismissed(tv)) { |
| return tv; |
| } |
| return null; |
| } |
| |
| @Override |
| public boolean canChildBeDismissed(View v) { |
| // Disallow dismissing an already dismissed task |
| TaskView tv = (TaskView) v; |
| Task task = tv.getTask(); |
| return !mSwipeHelperAnimations.containsKey(v) && |
| (mSv.getStack().indexOfTask(task) != -1); |
| } |
| |
| /** |
| * Starts a manual drag that goes through the same swipe helper path. |
| */ |
| public void onBeginManualDrag(TaskView v) { |
| mActiveTaskView = v; |
| mSwipeHelperAnimations.put(v, null); |
| onBeginDrag(v); |
| } |
| |
| @Override |
| public void onBeginDrag(View v) { |
| TaskView tv = (TaskView) v; |
| |
| // Disable clipping with the stack while we are swiping |
| tv.setClipViewInStack(false); |
| // Disallow touch events from this task view |
| tv.setTouchEnabled(false); |
| // Disallow parents from intercepting touch events |
| final ViewParent parent = mSv.getParent(); |
| if (parent != null) { |
| parent.requestDisallowInterceptTouchEvent(true); |
| } |
| |
| // Add this task to the set of tasks we are deleting |
| mSv.addIgnoreTask(tv.getTask()); |
| |
| // Determine if we are animating the other tasks while dismissing this task |
| mCurrentTasks = new ArrayList<Task>(mSv.getStack().getTasks()); |
| MutableBoolean isFrontMostTask = new MutableBoolean(false); |
| Task anchorTask = mSv.findAnchorTask(mCurrentTasks, isFrontMostTask); |
| TaskStackLayoutAlgorithm layoutAlgorithm = mSv.getStackAlgorithm(); |
| TaskStackViewScroller stackScroller = mSv.getScroller(); |
| if (anchorTask != null) { |
| // Get the current set of task transforms |
| mSv.getCurrentTaskTransforms(mCurrentTasks, mCurrentTaskTransforms); |
| |
| // Get the stack scroll of the task to anchor to (since we are removing something, the |
| // front most task will be our anchor task) |
| float prevAnchorTaskScroll = 0; |
| boolean pullStackForward = mCurrentTasks.size() > 0; |
| if (pullStackForward) { |
| if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| float index = layoutAlgorithm.getStackScrollForTask(anchorTask); |
| prevAnchorTaskScroll = mSv.getStackAlgorithm().mTaskStackLowRamLayoutAlgorithm |
| .getScrollPForTask((int) index); |
| } else { |
| prevAnchorTaskScroll = layoutAlgorithm.getStackScrollForTask(anchorTask); |
| } |
| } |
| |
| // Calculate where the views would be without the deleting tasks |
| mSv.updateLayoutAlgorithm(false /* boundScroll */); |
| |
| float newStackScroll = stackScroller.getStackScroll(); |
| if (isFrontMostTask.value) { |
| // Bound the stack scroll to pull tasks forward if necessary |
| newStackScroll = stackScroller.getBoundedStackScroll(newStackScroll); |
| } else if (pullStackForward) { |
| // Otherwise, offset the scroll by the movement of the anchor task |
| float anchorTaskScroll = |
| layoutAlgorithm.getStackScrollForTaskIgnoreOverrides(anchorTask); |
| if (LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| float index = layoutAlgorithm.getStackScrollForTask(anchorTask); |
| anchorTaskScroll = mSv.getStackAlgorithm().mTaskStackLowRamLayoutAlgorithm |
| .getScrollPForTask((int) index); |
| } |
| float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll); |
| if (layoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED |
| && !LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| // If we are focused, we don't want the front task to move, but otherwise, we |
| // allow the back task to move up, and the front task to move back |
| stackScrollOffset *= 0.75f; |
| } |
| newStackScroll = stackScroller.getBoundedStackScroll(stackScroller.getStackScroll() |
| + stackScrollOffset); |
| } |
| |
| // Pick up the newly visible views, not including the deleting tasks |
| mSv.bindVisibleTaskViews(newStackScroll, true /* ignoreTaskOverrides */); |
| |
| // Get the final set of task transforms (with task removed) |
| mSv.getLayoutTaskTransforms(newStackScroll, TaskStackLayoutAlgorithm.STATE_UNFOCUSED, |
| mCurrentTasks, true /* ignoreTaskOverrides */, mFinalTaskTransforms); |
| |
| // Set the target to scroll towards upon dismissal |
| mTargetStackScroll = newStackScroll; |
| |
| /* |
| * Post condition: All views that will be visible as a part of the gesture are retrieved |
| * and at their initial positions. The stack is still at the current |
| * scroll, but the layout is updated without the task currently being |
| * dismissed. The final layout is in the unfocused stack state, which |
| * will be applied when the current task is dismissed. |
| */ |
| } |
| } |
| |
| @Override |
| public boolean updateSwipeProgress(View v, boolean dismissable, float swipeProgress) { |
| // Only update the swipe progress for the surrounding tasks if the dismiss animation was not |
| // preempted from a call to cancelNonDismissTaskAnimations |
| if ((mActiveTaskView == v || mSwipeHelperAnimations.containsKey(v)) && |
| !LegacyRecentsImpl.getConfiguration().isLowRamDevice) { |
| updateTaskViewTransforms( |
| Interpolators.FAST_OUT_SLOW_IN.getInterpolation(swipeProgress)); |
| } |
| return true; |
| } |
| |
| /** |
| * Called after the {@link TaskView} is finished animating away. |
| */ |
| @Override |
| public void onChildDismissed(View v) { |
| TaskView tv = (TaskView) v; |
| |
| // Re-enable clipping with the stack (we will reuse this view) |
| tv.setClipViewInStack(true); |
| // Re-enable touch events from this task view |
| tv.setTouchEnabled(true); |
| // Update the scroll to the final scroll position before laying out the tasks during dismiss |
| if (mSwipeHelperAnimations.containsKey(v)) { |
| mSv.getScroller().setStackScroll(mTargetStackScroll, null); |
| } |
| // Remove the task view from the stack, ignoring the animation if we've started dragging |
| // again |
| EventBus.getDefault().send(new TaskViewDismissedEvent(tv.getTask(), tv, |
| mSwipeHelperAnimations.containsKey(v) |
| ? new AnimationProps(TaskStackView.DEFAULT_SYNC_STACK_DURATION, |
| Interpolators.FAST_OUT_SLOW_IN) |
| : null)); |
| // Only update the final scroll and layout state (set in onBeginDrag()) if the dismiss |
| // animation was not preempted from a call to cancelNonDismissTaskAnimations |
| if (mSwipeHelperAnimations.containsKey(v)) { |
| // Update the focus state to the final focus state |
| mSv.getStackAlgorithm().setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED); |
| mSv.getStackAlgorithm().clearUnfocusedTaskOverrides(); |
| // Stop tracking this deletion animation |
| mSwipeHelperAnimations.remove(v); |
| } |
| // Keep track of deletions by keyboard |
| MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source", |
| Constants.Metrics.DismissSourceSwipeGesture); |
| } |
| |
| /** |
| * Called after the {@link TaskView} is finished animating back into the list. |
| * onChildDismissed() calls. |
| */ |
| @Override |
| public void onChildSnappedBack(View v, float targetLeft) { |
| TaskView tv = (TaskView) v; |
| |
| // Re-enable clipping with the stack |
| tv.setClipViewInStack(true); |
| // Re-enable touch events from this task view |
| tv.setTouchEnabled(true); |
| |
| // Stop tracking this deleting task, and update the layout to include this task again. The |
| // stack scroll does not need to be reset, since the scroll has not actually changed in |
| // onBeginDrag(). |
| mSv.removeIgnoreTask(tv.getTask()); |
| mSv.updateLayoutAlgorithm(false /* boundScroll */); |
| mSv.relayoutTaskViews(AnimationProps.IMMEDIATE); |
| mSwipeHelperAnimations.remove(v); |
| } |
| |
| @Override |
| public void onDragCancelled(View v) { |
| // Do nothing |
| } |
| |
| @Override |
| public boolean isAntiFalsingNeeded() { |
| return false; |
| } |
| |
| @Override |
| public float getFalsingThresholdFactor() { |
| return 0; |
| } |
| |
| /** |
| * Interpolates the non-deleting tasks to their final transforms from their current transforms. |
| */ |
| private void updateTaskViewTransforms(float dismissFraction) { |
| List<TaskView> taskViews = mSv.getTaskViews(); |
| int taskViewCount = taskViews.size(); |
| for (int i = 0; i < taskViewCount; i++) { |
| TaskView tv = taskViews.get(i); |
| Task task = tv.getTask(); |
| |
| if (mSv.isIgnoredTask(task)) { |
| continue; |
| } |
| |
| int taskIndex = mCurrentTasks.indexOf(task); |
| if (taskIndex == -1) { |
| // If a task was added to the stack view after the start of the dismiss gesture, |
| // just ignore it |
| continue; |
| } |
| |
| TaskViewTransform fromTransform = mCurrentTaskTransforms.get(taskIndex); |
| TaskViewTransform toTransform = mFinalTaskTransforms.get(taskIndex); |
| |
| mTmpTransform.copyFrom(fromTransform); |
| // We only really need to interpolate the bounds, progress and translation |
| mTmpTransform.rect.set(Utilities.RECTF_EVALUATOR.evaluate(dismissFraction, |
| fromTransform.rect, toTransform.rect)); |
| mTmpTransform.dimAlpha = fromTransform.dimAlpha + (toTransform.dimAlpha - |
| fromTransform.dimAlpha) * dismissFraction; |
| mTmpTransform.viewOutlineAlpha = fromTransform.viewOutlineAlpha + |
| (toTransform.viewOutlineAlpha - fromTransform.viewOutlineAlpha) * |
| dismissFraction; |
| mTmpTransform.translationZ = fromTransform.translationZ + |
| (toTransform.translationZ - fromTransform.translationZ) * dismissFraction; |
| |
| mSv.updateTaskViewToTransform(tv, mTmpTransform, AnimationProps.IMMEDIATE); |
| } |
| } |
| |
| /** Returns the view at the specified coordinates */ |
| private TaskView findViewAtPoint(int x, int y) { |
| List<Task> tasks = mSv.getStack().getTasks(); |
| int taskCount = tasks.size(); |
| for (int i = taskCount - 1; i >= 0; i--) { |
| TaskView tv = mSv.getChildViewForTask(tasks.get(i)); |
| if (tv != null && tv.getVisibility() == View.VISIBLE) { |
| if (mSv.isTouchPointInView(x, y, tv)) { |
| return tv; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the scaled size used to calculate the dismiss fraction. |
| */ |
| public float getScaledDismissSize() { |
| return 1.5f * Math.max(mSv.getWidth(), mSv.getHeight()); |
| } |
| |
| /** |
| * Returns whether swiping is enabled. |
| */ |
| private boolean isSwipingEnabled() { |
| return !mSv.useGridLayout(); |
| } |
| } |