blob: 67a2595c90cd9d48d8a85c911b967f1f2b291321 [file] [log] [blame]
/*
* 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.graphics.Rect;
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.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.Recents;
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.misc.SystemServicesProxy;
import com.android.systemui.recents.misc.Utilities;
import com.android.systemui.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 = 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);
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);
}
}
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 overscrollX = Math.abs(overscrollP) / MAX_OVERSCROLL;
float interpX = OVERSCROLL_INTERP.getInterpolation(overscrollX);
curScrollP = clampedScrollP + Math.signum(overscrollP) *
(interpX * MAX_OVERSCROLL);
}
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) {
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
if (!mSv.mTouchExplorationEnabled) {
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;
}
// If tapping on the freeform workspace background, just launch the first freeform task
SystemServicesProxy ssp = Recents.getSystemServices();
if (ssp.hasFreeformWorkspaceSupport()) {
Rect freeformRect = mSv.mLayoutAlgorithm.mFreeformRect;
if (freeformRect.top <= y && y <= freeformRect.bottom) {
if (mSv.launchFreeformTasks()) {
// TODO: Animate Recents away as we launch the freeform tasks
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().indexOfStackTask(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().getStackTasks());
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) {
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);
float stackScrollOffset = (anchorTaskScroll - prevAnchorTaskScroll);
if (layoutAlgorithm.getFocusState() != TaskStackLayoutAlgorithm.STATE_FOCUSED) {
// 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)) {
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);
// 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 scroll to the final scroll position
mSv.getScroller().setStackScroll(mTargetStackScroll, null);
// 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);
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().getStackTasks();
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());
}
}