blob: 5830e37edca848573da5da16b3c405362ea49ac9 [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.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.OverScroller;
import com.android.systemui.R;
import com.android.systemui.recents.Console;
import com.android.systemui.recents.Constants;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.RecentsPackageMonitor;
import com.android.systemui.recents.RecentsTaskLoader;
import com.android.systemui.recents.Utilities;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
/* The visual representation of a task stack view */
public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCallbacks,
TaskView.TaskViewCallbacks, ViewPool.ViewPoolConsumer<TaskView, Task>,
View.OnClickListener, RecentsPackageMonitor.PackageCallbacks {
/** The TaskView callbacks */
interface TaskStackViewCallbacks {
public void onTaskLaunched(TaskStackView stackView, TaskView tv, TaskStack stack, Task t);
public void onTaskAppInfoLaunched(Task t);
public void onTaskRemoved(Task t);
}
TaskStack mStack;
TaskStackViewTouchHandler mTouchHandler;
TaskStackViewCallbacks mCb;
ViewPool<TaskView, Task> mViewPool;
// The various rects that define the stack view
Rect mRect = new Rect();
Rect mStackRect = new Rect();
Rect mStackRectSansPeek = new Rect();
Rect mTaskRect = new Rect();
// The virtual stack scroll that we use for the card layout
int mStackScroll;
int mMinScroll;
int mMaxScroll;
int mStashedScroll;
int mFocusedTaskIndex = -1;
OverScroller mScroller;
ObjectAnimator mScrollAnimator;
// Optimizations
int mHwLayersRefCount;
int mStackViewsAnimationDuration;
boolean mStackViewsDirty = true;
boolean mAwaitingFirstLayout = true;
boolean mStartEnterAnimationRequestedAfterLayout;
int[] mTmpVisibleRange = new int[2];
Rect mTmpRect = new Rect();
Rect mTmpRect2 = new Rect();
LayoutInflater mInflater;
public TaskStackView(Context context, TaskStack stack) {
super(context);
mStack = stack;
mStack.setCallbacks(this);
mScroller = new OverScroller(context);
mTouchHandler = new TaskStackViewTouchHandler(context, this);
mViewPool = new ViewPool<TaskView, Task>(context, this);
mInflater = LayoutInflater.from(context);
}
/** Sets the callbacks */
void setCallbacks(TaskStackViewCallbacks cb) {
mCb = cb;
}
/** Requests that the views be synchronized with the model */
void requestSynchronizeStackViewsWithModel() {
requestSynchronizeStackViewsWithModel(0);
}
void requestSynchronizeStackViewsWithModel(int duration) {
if (Console.Enabled) {
Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
"[TaskStackView|requestSynchronize]", "" + duration + "ms", Console.AnsiYellow);
}
if (!mStackViewsDirty) {
invalidate();
}
if (mAwaitingFirstLayout) {
// Skip the animation if we are awaiting first layout
mStackViewsAnimationDuration = 0;
} else {
mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
}
mStackViewsDirty = true;
}
// XXX: Optimization: Use a mapping of Task -> View
private TaskView getChildViewForTask(Task t) {
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView tv = (TaskView) getChildAt(i);
if (tv.getTask() == t) {
return tv;
}
}
return null;
}
/** Update/get the transform */
public TaskViewTransform getStackTransform(int indexInStack, int stackScroll) {
TaskViewTransform transform = new TaskViewTransform();
// Return early if we have an invalid index
if (indexInStack < 0) return transform;
// Map the items to an continuous position relative to the specified scroll
int numPeekCards = Constants.Values.TaskStackView.StackPeekNumCards;
float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
float peekHeight = Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
float t = ((indexInStack * overlapHeight) - stackScroll) / overlapHeight;
float boundedT = Math.max(t, -(numPeekCards + 1));
// Set the scale relative to its position
float minScale = Constants.Values.TaskStackView.StackPeekMinScale;
float scaleRange = 1f - minScale;
float scaleInc = scaleRange / numPeekCards;
float scale = Math.max(minScale, Math.min(1f, 1f + (boundedT * scaleInc)));
float scaleYOffset = ((1f - scale) * mTaskRect.height()) / 2;
transform.scale = scale;
// Set the translation
if (boundedT < 0f) {
transform.translationY = (int) ((Math.max(-numPeekCards, boundedT) /
numPeekCards) * peekHeight - scaleYOffset);
} else {
transform.translationY = (int) (boundedT * overlapHeight - scaleYOffset);
}
// Set the alphas
transform.dismissAlpha = Math.max(-1f, Math.min(0f, t + 1)) + 1f;
// Update the rect and visibility
transform.rect.set(mTaskRect);
if (t < -(numPeekCards + 1)) {
transform.visible = false;
} else {
transform.rect.offset(0, transform.translationY);
Utilities.scaleRectAboutCenter(transform.rect, transform.scale);
transform.visible = Rect.intersects(mRect, transform.rect);
}
transform.t = t;
return transform;
}
/**
* Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
*/
private ArrayList<TaskViewTransform> getStackTransforms(ArrayList<Task> tasks,
int stackScroll,
int[] visibleRangeOut,
boolean boundTranslationsToRect) {
// XXX: Optimization: Use binary search to find the visible range
ArrayList<TaskViewTransform> taskTransforms = new ArrayList<TaskViewTransform>();
int taskCount = tasks.size();
int firstVisibleIndex = -1;
int lastVisibleIndex = -1;
for (int i = 0; i < taskCount; i++) {
TaskViewTransform transform = getStackTransform(i, stackScroll);
taskTransforms.add(transform);
if (transform.visible) {
if (firstVisibleIndex < 0) {
firstVisibleIndex = i;
}
lastVisibleIndex = i;
}
if (boundTranslationsToRect) {
transform.translationY = Math.min(transform.translationY, mRect.bottom);
}
}
if (visibleRangeOut != null) {
visibleRangeOut[0] = firstVisibleIndex;
visibleRangeOut[1] = lastVisibleIndex;
}
return taskTransforms;
}
/** Synchronizes the views with the model */
void synchronizeStackViewsWithModel() {
if (Console.Enabled) {
Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
"[TaskStackView|synchronizeViewsWithModel]",
"mStackViewsDirty: " + mStackViewsDirty, Console.AnsiYellow);
}
if (mStackViewsDirty) {
// XXX: Consider using TaskViewTransform pool to prevent allocations
// XXX: Iterate children views, update transforms and remove all that are not visible
// For all remaining tasks, update transforms and if visible add the view
// Get all the task transforms
int[] visibleRange = mTmpVisibleRange;
int stackScroll = getStackScroll();
ArrayList<Task> tasks = mStack.getTasks();
ArrayList<TaskViewTransform> taskTransforms = getStackTransforms(tasks, stackScroll,
visibleRange, false);
// Update the visible state of all the tasks
int taskCount = tasks.size();
for (int i = 0; i < taskCount; i++) {
Task task = tasks.get(i);
TaskViewTransform transform = taskTransforms.get(i);
TaskView tv = getChildViewForTask(task);
if (transform.visible) {
if (tv == null) {
tv = mViewPool.pickUpViewFromPool(task, task);
// When we are picking up a new view from the view pool, prepare it for any
// following animation by putting it in a reasonable place
if (mStackViewsAnimationDuration > 0 && i != 0) {
int fromIndex = (transform.t < 0) ? (visibleRange[0] - 1) :
(visibleRange[1] + 1);
tv.updateViewPropertiesToTaskTransform(null,
getStackTransform(fromIndex, stackScroll), 0);
}
}
} else {
if (tv != null) {
mViewPool.returnViewToPool(tv);
}
}
}
// Update all the remaining view children
// NOTE: We have to iterate in reverse where because we are removing views directly
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
TaskView tv = (TaskView) getChildAt(i);
Task task = tv.getTask();
int taskIndex = mStack.indexOfTask(task);
if (taskIndex < 0 || !taskTransforms.get(taskIndex).visible) {
mViewPool.returnViewToPool(tv);
} else {
tv.updateViewPropertiesToTaskTransform(null, taskTransforms.get(taskIndex),
mStackViewsAnimationDuration);
}
}
if (Console.Enabled) {
Console.log(Constants.Log.TaskStack.SynchronizeViewsWithModel,
" [TaskStackView|viewChildren]", "" + getChildCount());
}
mStackViewsAnimationDuration = 0;
mStackViewsDirty = false;
}
}
/** Sets the current stack scroll */
public void setStackScroll(int value) {
mStackScroll = value;
requestSynchronizeStackViewsWithModel();
}
/** Sets the current stack scroll without synchronizing the stack view with the model */
public void setStackScrollRaw(int value) {
mStackScroll = value;
}
/** Sets the current stack scroll to the initial state when you first enter recents */
public void setStackScrollToInitialState() {
if (mStack.getTaskCount() > 2) {
int initialScroll = mMaxScroll - mTaskRect.height() / 2;
setStackScroll(initialScroll);
} else {
setStackScroll(mMaxScroll);
}
}
/**
* Returns the scroll to such that the task transform at that index will have t=0. (If the scroll
* is not bounded)
*/
int getStackScrollForTaskIndex(int i) {
int taskHeight = mTaskRect.height();
return (int) (i * Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
}
/** Gets the current stack scroll */
public int getStackScroll() {
return mStackScroll;
}
/** Animates the stack scroll into bounds */
ObjectAnimator animateBoundScroll() {
int curScroll = getStackScroll();
int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
if (newScroll != curScroll) {
// Enable hw layers on the stack
addHwLayersRefCount("animateBoundScroll");
// Start a new scroll animation
animateScroll(curScroll, newScroll, new Runnable() {
@Override
public void run() {
// Disable hw layers on the stack
decHwLayersRefCount("animateBoundScroll");
}
});
}
return mScrollAnimator;
}
/** Animates the stack scroll */
void animateScroll(int curScroll, int newScroll, final Runnable postRunnable) {
// Abort any current animations
abortScroller();
abortBoundScrollAnimation();
mScrollAnimator = ObjectAnimator.ofInt(this, "stackScroll", curScroll, newScroll);
mScrollAnimator.setDuration(Utilities.calculateTranslationAnimationDuration(newScroll -
curScroll, 250));
mScrollAnimator.setInterpolator(RecentsConfiguration.getInstance().fastOutSlowInInterpolator);
mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setStackScroll((Integer) animation.getAnimatedValue());
}
});
mScrollAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (postRunnable != null) {
postRunnable.run();
}
mScrollAnimator.removeAllListeners();
}
});
mScrollAnimator.start();
}
/** Aborts any current stack scrolls */
void abortBoundScrollAnimation() {
if (mScrollAnimator != null) {
mScrollAnimator.cancel();
}
}
/** Aborts the scroller and any current fling */
void abortScroller() {
if (!mScroller.isFinished()) {
// Abort the scroller
mScroller.abortAnimation();
// And disable hw layers on the stack
decHwLayersRefCount("flingScroll");
}
}
/** Bounds the current scroll if necessary */
public boolean boundScroll() {
int curScroll = getStackScroll();
int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
if (newScroll != curScroll) {
setStackScroll(newScroll);
return true;
}
return false;
}
/**
* Bounds the current scroll if necessary, but does not synchronize the stack view with the
* model.
*/
public boolean boundScrollRaw() {
int curScroll = getStackScroll();
int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll, curScroll));
if (newScroll != curScroll) {
setStackScrollRaw(newScroll);
return true;
}
return false;
}
/** Returns the amount that the scroll is out of bounds */
int getScrollAmountOutOfBounds(int scroll) {
if (scroll < mMinScroll) {
return mMinScroll - scroll;
} else if (scroll > mMaxScroll) {
return scroll - mMaxScroll;
}
return 0;
}
/** Returns whether the specified scroll is out of bounds */
boolean isScrollOutOfBounds() {
return getScrollAmountOutOfBounds(getStackScroll()) != 0;
}
/** Returns whether the task view is in the stack bounds or not */
boolean isTaskInStackBounds(TaskView tv) {
Rect r = new Rect();
tv.getHitRect(r);
return r.bottom <= mRect.bottom;
}
/** Updates the min and max virtual scroll bounds */
void updateMinMaxScroll(boolean boundScrollToNewMinMax) {
// Compute the min and max scroll values
int numTasks = Math.max(1, mStack.getTaskCount());
int taskHeight = mTaskRect.height();
int stackHeight = mStackRectSansPeek.height();
int maxScrollHeight = taskHeight + (int) ((numTasks - 1) *
Constants.Values.TaskStackView.StackOverlapPct * taskHeight);
if (numTasks <= 1) {
// If there is only one task, then center the task in the stack rect (sans peek)
mMinScroll = mMaxScroll = -(stackHeight - taskHeight) / 2;
} else {
mMinScroll = Math.min(stackHeight, maxScrollHeight) - stackHeight;
mMaxScroll = maxScrollHeight - stackHeight;
}
// Debug logging
if (Constants.Log.UI.MeasureAndLayout) {
Console.log(" [TaskStack|minScroll] " + mMinScroll);
Console.log(" [TaskStack|maxScroll] " + mMaxScroll);
}
if (boundScrollToNewMinMax) {
boundScroll();
}
}
/** Focuses the task at the specified index in the stack */
void focusTask(int taskIndex, boolean scrollToNewPosition) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "" + taskIndex);
}
if (0 <= taskIndex && taskIndex < mStack.getTaskCount()) {
mFocusedTaskIndex = taskIndex;
// Focus the view if possible, otherwise, focus the view after we scroll into position
Task t = mStack.getTasks().get(taskIndex);
TaskView tv = getChildViewForTask(t);
Runnable postScrollRunnable = null;
if (tv != null) {
tv.setFocusedTask();
if (Console.Enabled) {
Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]", "Requesting focus");
}
} else {
postScrollRunnable = new Runnable() {
@Override
public void run() {
Task t = mStack.getTasks().get(mFocusedTaskIndex);
TaskView tv = getChildViewForTask(t);
if (tv != null) {
tv.setFocusedTask();
if (Console.Enabled) {
Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusTask]",
"Requesting focus after scroll animation");
}
}
}
};
}
if (scrollToNewPosition) {
// Scroll the view into position
int newScroll = Math.max(mMinScroll, Math.min(mMaxScroll,
getStackScrollForTaskIndex(taskIndex)));
animateScroll(getStackScroll(), newScroll, postScrollRunnable);
} else {
if (postScrollRunnable != null) {
postScrollRunnable.run();
}
}
}
}
/** Focuses the next task in the stack */
void focusNextTask(boolean forward) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.Focus, "[TaskStackView|focusNextTask]", "" +
mFocusedTaskIndex);
}
// Find the next index to focus
int numTasks = mStack.getTaskCount();
if (mFocusedTaskIndex < 0) {
mFocusedTaskIndex = numTasks - 1;
}
if (0 <= mFocusedTaskIndex && mFocusedTaskIndex < numTasks) {
mFocusedTaskIndex = Math.max(0, Math.min(numTasks - 1,
mFocusedTaskIndex + (forward ? -1 : 1)));
}
focusTask(mFocusedTaskIndex, true);
}
/** Enables the hw layers and increments the hw layer requirement ref count */
void addHwLayersRefCount(String reason) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.HwLayers,
"[TaskStackView|addHwLayersRefCount] refCount: " +
mHwLayersRefCount + "->" + (mHwLayersRefCount + 1) + " " + reason);
}
if (mHwLayersRefCount == 0) {
// Enable hw layers on each of the children
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView tv = (TaskView) getChildAt(i);
tv.enableHwLayers();
}
}
mHwLayersRefCount++;
}
/** Decrements the hw layer requirement ref count and disables the hw layers when we don't
need them anymore. */
void decHwLayersRefCount(String reason) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.HwLayers,
"[TaskStackView|decHwLayersRefCount] refCount: " +
mHwLayersRefCount + "->" + (mHwLayersRefCount - 1) + " " + reason);
}
mHwLayersRefCount--;
if (mHwLayersRefCount == 0) {
// Disable hw layers on each of the children
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView tv = (TaskView) getChildAt(i);
tv.disableHwLayers();
}
} else if (mHwLayersRefCount < 0) {
new Throwable("Invalid hw layers ref count").printStackTrace();
Console.logError(getContext(), "Invalid HW layers ref count");
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
setStackScroll(mScroller.getCurrY());
invalidate();
// If we just finished scrolling, then disable the hw layers
if (mScroller.isFinished()) {
decHwLayersRefCount("finishedFlingScroll");
}
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mTouchHandler.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return mTouchHandler.onTouchEvent(ev);
}
@Override
public void dispatchDraw(Canvas canvas) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.Draw, "[TaskStackView|dispatchDraw]", "",
Console.AnsiPurple);
}
synchronizeStackViewsWithModel();
super.dispatchDraw(canvas);
}
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
if (Constants.DebugFlags.App.EnableTaskStackClipping) {
RecentsConfiguration config = RecentsConfiguration.getInstance();
TaskView tv = (TaskView) child;
TaskView nextTv = null;
TaskView tmpTv = null;
if (tv.shouldClipViewInStack()) {
int curIndex = indexOfChild(tv);
// Find the next view to clip against
while (nextTv == null && curIndex < getChildCount()) {
tmpTv = (TaskView) getChildAt(++curIndex);
if (tmpTv != null && tmpTv.shouldClipViewInStack()) {
nextTv = tmpTv;
}
}
// Clip against the next view (if we aren't animating its alpha)
if (nextTv != null && nextTv.getAlpha() == 1f) {
Rect curRect = tv.getClippingRect(mTmpRect);
Rect nextRect = nextTv.getClippingRect(mTmpRect2);
// The hit rects are relative to the task view, which needs to be offset by
// the system bar height
curRect.offset(0, config.systemInsets.top);
nextRect.offset(0, config.systemInsets.top);
// Compute the clip region
Region clipRegion = new Region();
clipRegion.op(curRect, Region.Op.UNION);
clipRegion.op(nextRect, Region.Op.DIFFERENCE);
// Clip the canvas
int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.clipRegion(clipRegion);
boolean invalidate = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(saveCount);
return invalidate;
}
}
}
return super.drawChild(canvas, child, drawingTime);
}
/** Computes the stack and task rects */
public void computeRects(int width, int height, int insetLeft, int insetBottom) {
// Note: We let the stack view be the full height because we want the cards to go under the
// navigation bar if possible. However, the stack rects which we use to calculate
// max scroll, etc. need to take the nav bar into account
RecentsConfiguration config = RecentsConfiguration.getInstance();
// Compute the stack rects
mRect.set(0, 0, width, height);
mStackRect.set(mRect);
mStackRect.left += insetLeft;
mStackRect.bottom -= insetBottom;
int widthPadding = (int) (config.taskStackWidthPaddingPct * mStackRect.width());
int heightPadding = config.taskStackTopPaddingPx;
if (Constants.DebugFlags.App.EnableSearchLayout) {
mStackRect.top += heightPadding;
mStackRect.left += widthPadding;
mStackRect.right -= widthPadding;
mStackRect.bottom -= heightPadding;
} else {
mStackRect.inset(widthPadding, heightPadding);
}
mStackRectSansPeek.set(mStackRect);
mStackRectSansPeek.top += Constants.Values.TaskStackView.StackPeekHeightPct * mStackRect.height();
// Compute the task rect
int size = mStackRect.width();
int left = mStackRect.left + (mStackRect.width() - size) / 2;
mTaskRect.set(left, mStackRectSansPeek.top,
left + size, mStackRectSansPeek.top + size);
// Update the scroll bounds
updateMinMaxScroll(false);
}
/**
* This is called with the size of the space not including the top or right insets, or the
* search bar height in portrait (but including the search bar width in landscape, since we want
* to draw under it.
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
if (Console.Enabled) {
Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|measure]",
"width: " + width + " height: " + height +
" awaitingFirstLayout: " + mAwaitingFirstLayout, Console.AnsiGreen);
}
// Compute our stack/task rects
RecentsConfiguration config = RecentsConfiguration.getInstance();
Rect taskStackBounds = new Rect();
config.getTaskStackBounds(width, height, taskStackBounds);
computeRects(width, height, taskStackBounds.left, config.systemInsets.bottom);
// Debug logging
if (Constants.Log.UI.MeasureAndLayout) {
Console.log(" [TaskStack|fullRect] " + mRect);
Console.log(" [TaskStack|stackRect] " + mStackRect);
Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
Console.log(" [TaskStack|taskRect] " + mTaskRect);
}
// If this is the first layout, then scroll to the front of the stack and synchronize the
// stack views immediately
if (mAwaitingFirstLayout) {
setStackScrollToInitialState();
requestSynchronizeStackViewsWithModel();
synchronizeStackViewsWithModel();
}
// Measure each of the children
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView t = (TaskView) getChildAt(i);
t.measure(MeasureSpec.makeMeasureSpec(mTaskRect.width(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mTaskRect.height(), MeasureSpec.EXACTLY));
}
setMeasuredDimension(width, height);
}
/**
* This is called with the size of the space not including the top or right insets, or the
* search bar height in portrait (but including the search bar width in landscape, since we want
* to draw under it.
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.MeasureAndLayout, "[TaskStackView|layout]",
"" + new Rect(left, top, right, bottom), Console.AnsiGreen);
}
// Debug logging
if (Constants.Log.UI.MeasureAndLayout) {
Console.log(" [TaskStack|fullRect] " + mRect);
Console.log(" [TaskStack|stackRect] " + mStackRect);
Console.log(" [TaskStack|stackRectSansPeek] " + mStackRectSansPeek);
Console.log(" [TaskStack|taskRect] " + mTaskRect);
}
// Layout each of the children
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView t = (TaskView) getChildAt(i);
t.layout(mTaskRect.left, mStackRectSansPeek.top,
mTaskRect.right, mStackRectSansPeek.top + mTaskRect.height());
}
if (mAwaitingFirstLayout) {
RecentsConfiguration config = RecentsConfiguration.getInstance();
// Update the focused task index to be the next item to the top task
if (config.launchedFromAltTab) {
focusTask(Math.max(0, mStack.getTaskCount() - 2), false);
}
// Prepare the first view for its enter animation
if (config.launchedWithThumbnailAnimation) {
TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
if (tv != null) {
tv.prepareAnimateOnEnterRecents();
}
}
// Mark that we have completely the first layout
mAwaitingFirstLayout = false;
// If the enter animation started already and we haven't completed a layout yet, do the
// enter animation now
if (mStartEnterAnimationRequestedAfterLayout) {
startOnEnterAnimation();
}
}
}
/** Requests this task stacks to start it's enter-recents animation */
public void startOnEnterAnimation() {
RecentsConfiguration config = RecentsConfiguration.getInstance();
if (!config.launchedWithThumbnailAnimation) return;
// If we are still waiting to layout, then just defer until then
if (mAwaitingFirstLayout) {
mStartEnterAnimationRequestedAfterLayout = true;
return;
}
// Animate the task bar of the first task view
TaskView tv = (TaskView) getChildAt(getChildCount() - 1);
if (tv != null) {
tv.animateOnEnterRecents();
}
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
requestSynchronizeStackViewsWithModel();
}
public boolean isTransformedTouchPointInView(float x, float y, View child) {
return isTransformedTouchPointInView(x, y, child, null);
}
/**** TaskStackCallbacks Implementation ****/
@Override
public void onStackTaskAdded(TaskStack stack, Task t) {
requestSynchronizeStackViewsWithModel();
}
@Override
public void onStackTaskRemoved(TaskStack stack, Task t) {
// Remove the view associated with this task, we can't rely on updateTransforms
// to work here because the task is no longer in the list
int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
TaskView tv = (TaskView) getChildAt(i);
if (tv.getTask() == t) {
mViewPool.returnViewToPool(tv);
break;
}
}
// Update the min/max scroll and animate other task views into their new positions
updateMinMaxScroll(true);
int movement = (int) (Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height());
requestSynchronizeStackViewsWithModel(Utilities.calculateTranslationAnimationDuration(movement));
// If there are no remaining tasks, then either unfilter the current stack, or just close
// the activity if there are no filtered stacks
if (mStack.getTaskCount() == 0) {
boolean shouldFinishActivity = true;
if (mStack.hasFilteredTasks()) {
mStack.unfilterTasks();
shouldFinishActivity = (mStack.getTaskCount() == 0);
}
if (shouldFinishActivity) {
Activity activity = (Activity) getContext();
activity.finish();
}
}
}
/**
* Creates the animations for all the children views that need to be removed or to move views
* to their un/filtered position when we are un/filtering a stack, and returns the duration
* for these animations.
*/
int getExitTransformsForFilterAnimation(ArrayList<Task> curTasks,
ArrayList<TaskViewTransform> curTaskTransforms,
ArrayList<Task> tasks, ArrayList<TaskViewTransform> taskTransforms,
HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut,
ArrayList<TaskView> childrenToRemoveOut,
RecentsConfiguration config) {
// Animate all of the existing views out of view (if they are not in the visible range in
// the new stack) or to their final positions in the new stack
int movement = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
TaskView tv = (TaskView) getChildAt(i);
Task task = tv.getTask();
int taskIndex = tasks.indexOf(task);
TaskViewTransform toTransform;
// If the view is no longer visible, then we should just animate it out
boolean willBeInvisible = taskIndex < 0 || !taskTransforms.get(taskIndex).visible;
if (willBeInvisible) {
if (taskIndex < 0) {
toTransform = curTaskTransforms.get(curTasks.indexOf(task));
} else {
toTransform = new TaskViewTransform(taskTransforms.get(taskIndex));
}
tv.prepareTaskTransformForFilterTaskVisible(toTransform);
childrenToRemoveOut.add(tv);
} else {
toTransform = taskTransforms.get(taskIndex);
// Use the movement of the visible views to calculate the duration of the animation
movement = Math.max(movement, Math.abs(toTransform.translationY -
(int) tv.getTranslationY()));
}
childViewTransformsOut.put(tv, new Pair(0, toTransform));
}
return Utilities.calculateTranslationAnimationDuration(movement,
config.filteringCurrentViewsMinAnimDuration);
}
/**
* Creates the animations for all the children views that need to be animated in when we are
* un/filtering a stack, and returns the duration for these animations.
*/
int getEnterTransformsForFilterAnimation(ArrayList<Task> tasks,
ArrayList<TaskViewTransform> taskTransforms,
HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransformsOut,
RecentsConfiguration config) {
int offset = 0;
int movement = 0;
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 0; i--) {
Task task = tasks.get(i);
TaskViewTransform toTransform = taskTransforms.get(i);
if (toTransform.visible) {
TaskView tv = getChildViewForTask(task);
if (tv == null) {
// For views that are not already visible, animate them in
tv = mViewPool.pickUpViewFromPool(task, task);
// Compose a new transform to fade and slide the new task in
TaskViewTransform fromTransform = new TaskViewTransform(toTransform);
tv.prepareTaskTransformForFilterTaskHidden(fromTransform);
tv.updateViewPropertiesToTaskTransform(null, fromTransform, 0);
int startDelay = offset *
Constants.Values.TaskStackView.FilterStartDelay;
childViewTransformsOut.put(tv, new Pair(startDelay, toTransform));
// Use the movement of the new views to calculate the duration of the animation
movement = Math.max(movement,
Math.abs(toTransform.translationY - fromTransform.translationY));
offset++;
}
}
}
return Utilities.calculateTranslationAnimationDuration(movement,
config.filteringNewViewsMinAnimDuration);
}
/** Orchestrates the animations of the current child views and any new views. */
void doFilteringAnimation(ArrayList<Task> curTasks,
ArrayList<TaskViewTransform> curTaskTransforms,
final ArrayList<Task> tasks,
final ArrayList<TaskViewTransform> taskTransforms) {
final RecentsConfiguration config = RecentsConfiguration.getInstance();
// Calculate the transforms to animate out all the existing views if they are not in the
// new visible range (or to their final positions in the stack if they are)
final ArrayList<TaskView> childrenToRemove = new ArrayList<TaskView>();
final HashMap<TaskView, Pair<Integer, TaskViewTransform>> childViewTransforms =
new HashMap<TaskView, Pair<Integer, TaskViewTransform>>();
int duration = getExitTransformsForFilterAnimation(curTasks, curTaskTransforms, tasks,
taskTransforms, childViewTransforms, childrenToRemove, config);
// If all the current views are in the visible range of the new stack, then don't wait for
// views to animate out and animate all the new views into their place
final boolean unifyNewViewAnimation = childrenToRemove.isEmpty();
if (unifyNewViewAnimation) {
int inDuration = getEnterTransformsForFilterAnimation(tasks, taskTransforms,
childViewTransforms, config);
duration = Math.max(duration, inDuration);
}
// Animate all the views to their final transforms
for (final TaskView tv : childViewTransforms.keySet()) {
Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv);
tv.animate().cancel();
tv.animate()
.setStartDelay(t.first)
.withEndAction(new Runnable() {
@Override
public void run() {
childViewTransforms.remove(tv);
if (childViewTransforms.isEmpty()) {
// Return all the removed children to the view pool
for (TaskView tv : childrenToRemove) {
mViewPool.returnViewToPool(tv);
}
if (!unifyNewViewAnimation) {
// For views that are not already visible, animate them in
childViewTransforms.clear();
int duration = getEnterTransformsForFilterAnimation(tasks,
taskTransforms, childViewTransforms, config);
for (final TaskView tv : childViewTransforms.keySet()) {
Pair<Integer, TaskViewTransform> t = childViewTransforms.get(tv);
tv.animate().setStartDelay(t.first);
tv.updateViewPropertiesToTaskTransform(null, t.second, duration);
}
}
}
}
});
tv.updateViewPropertiesToTaskTransform(null, t.second, duration);
}
}
@Override
public void onStackFiltered(TaskStack newStack, final ArrayList<Task> curTasks,
Task filteredTask) {
// Stash the scroll and filtered task for us to restore to when we unfilter
mStashedScroll = getStackScroll();
// Calculate the current task transforms
ArrayList<TaskViewTransform> curTaskTransforms =
getStackTransforms(curTasks, getStackScroll(), null, true);
// Scroll the item to the top of the stack (sans-peek) rect so that we can see it better
updateMinMaxScroll(false);
float overlapHeight = Constants.Values.TaskStackView.StackOverlapPct * mTaskRect.height();
setStackScrollRaw((int) (newStack.indexOfTask(filteredTask) * overlapHeight));
boundScrollRaw();
// Compute the transforms of the items in the new stack after setting the new scroll
final ArrayList<Task> tasks = mStack.getTasks();
final ArrayList<TaskViewTransform> taskTransforms =
getStackTransforms(mStack.getTasks(), getStackScroll(), null, true);
// Animate
doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms);
}
@Override
public void onStackUnfiltered(TaskStack newStack, final ArrayList<Task> curTasks) {
// Calculate the current task transforms
final ArrayList<TaskViewTransform> curTaskTransforms =
getStackTransforms(curTasks, getStackScroll(), null, true);
// Restore the stashed scroll
updateMinMaxScroll(false);
setStackScrollRaw(mStashedScroll);
boundScrollRaw();
// Compute the transforms of the items in the new stack after restoring the stashed scroll
final ArrayList<Task> tasks = mStack.getTasks();
final ArrayList<TaskViewTransform> taskTransforms =
getStackTransforms(tasks, getStackScroll(), null, true);
// Animate
doFilteringAnimation(curTasks, curTaskTransforms, tasks, taskTransforms);
// Clear the saved vars
mStashedScroll = 0;
}
/**** ViewPoolConsumer Implementation ****/
@Override
public TaskView createView(Context context) {
if (Console.Enabled) {
Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|createPoolView]");
}
return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false);
}
@Override
public void prepareViewToEnterPool(TaskView tv) {
Task task = tv.getTask();
tv.resetViewProperties();
if (Console.Enabled) {
Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|returnToPool]",
tv.getTask() + " tv: " + tv);
}
// Report that this tasks's data is no longer being used
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
loader.unloadTaskData(task);
// Detach the view from the hierarchy
detachViewFromParent(tv);
// Disable hw layers on this view
tv.disableHwLayers();
}
@Override
public void prepareViewToLeavePool(TaskView tv, Task prepareData, boolean isNewView) {
if (Console.Enabled) {
Console.log(Constants.Log.ViewPool.PoolCallbacks, "[TaskStackView|leavePool]",
"isNewView: " + isNewView);
}
// Setup and attach the view to the window
Task task = prepareData;
// We try and rebind the task (this MUST be done before the task filled)
tv.onTaskBound(task);
// Request that this tasks's data be filled
RecentsTaskLoader loader = RecentsTaskLoader.getInstance();
loader.loadTaskData(task);
// Find the index where this task should be placed in the children
int insertIndex = -1;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
Task tvTask = ((TaskView) getChildAt(i)).getTask();
if (mStack.containsTask(task) && (mStack.indexOfTask(task) < mStack.indexOfTask(tvTask))) {
insertIndex = i;
break;
}
}
// Sanity check, the task view should always be clipping against the stack at this point,
// but just in case, re-enable it here
tv.setClipViewInStack(true);
// Add/attach the view to the hierarchy
if (Console.Enabled) {
Console.log(Constants.Log.ViewPool.PoolCallbacks, " [TaskStackView|insertIndex]",
"" + insertIndex);
}
if (isNewView) {
addView(tv, insertIndex);
// Set the callbacks and listeners for this new view
tv.setOnClickListener(this);
tv.setCallbacks(this);
} else {
attachViewToParent(tv, insertIndex, tv.getLayoutParams());
}
// Enable hw layers on this view if hw layers are enabled on the stack
if (mHwLayersRefCount > 0) {
tv.enableHwLayers();
}
}
@Override
public boolean hasPreferredData(TaskView tv, Task preferredData) {
return (tv.getTask() == preferredData);
}
/**** TaskViewCallbacks Implementation ****/
@Override
public void onTaskIconClicked(TaskView tv) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Icon]",
tv.getTask() + " is currently filtered: " + mStack.hasFilteredTasks(),
Console.AnsiCyan);
}
if (Constants.DebugFlags.App.EnableTaskFiltering) {
if (mStack.hasFilteredTasks()) {
mStack.unfilterTasks();
} else {
mStack.filterTasks(tv.getTask());
}
}
}
@Override
public void onTaskAppInfoClicked(TaskView tv) {
if (mCb != null) {
mCb.onTaskAppInfoLaunched(tv.getTask());
}
}
@Override
public void onTaskFocused(TaskView tv) {
// Do nothing
}
@Override
public void onTaskDismissed(TaskView tv) {
Task task = tv.getTask();
// Remove the task from the view
mStack.removeTask(task);
// Notify the callback that we've removed the task and it can clean up after it
mCb.onTaskRemoved(task);
}
/**** View.OnClickListener Implementation ****/
@Override
public void onClick(View v) {
TaskView tv = (TaskView) v;
Task task = tv.getTask();
if (Console.Enabled) {
Console.log(Constants.Log.UI.ClickEvents, "[TaskStack|Clicked|Thumbnail]",
task + " cb: " + mCb);
}
if (mCb != null) {
mCb.onTaskLaunched(this, tv, mStack, task);
}
}
/**** RecentsPackageMonitor.PackageCallbacks Implementation ****/
@Override
public void onComponentRemoved(Set<ComponentName> cns) {
RecentsConfiguration config = RecentsConfiguration.getInstance();
// For other tasks, just remove them directly if they no longer exist
ArrayList<Task> tasks = mStack.getTasks();
for (int i = tasks.size() - 1; i >= 0; i--) {
final Task t = tasks.get(i);
if (cns.contains(t.key.baseIntent.getComponent())) {
TaskView tv = getChildViewForTask(t);
if (tv != null) {
// For visible children, defer removing the task until after the animation
tv.animateRemoval(new Runnable() {
@Override
public void run() {
mStack.removeTask(t);
}
});
} else {
// Otherwise, remove the task from the stack immediately
mStack.removeTask(t);
}
}
}
}
}
/* Handles touch events */
class TaskStackViewTouchHandler implements SwipeHelper.Callback {
static int INACTIVE_POINTER_ID = -1;
TaskStackView mSv;
VelocityTracker mVelocityTracker;
boolean mIsScrolling;
int mInitialMotionX, mInitialMotionY;
int mLastMotionX, mLastMotionY;
int mActivePointerId = INACTIVE_POINTER_ID;
TaskView mActiveTaskView = null;
int mTotalScrollMotion;
int mMinimumVelocity;
int mMaximumVelocity;
// The scroll touch slop is used to calculate when we start scrolling
int mScrollTouchSlop;
// The page touch slop is used to calculate when we start swiping
float mPagingTouchSlop;
SwipeHelper mSwipeHelper;
boolean mInterceptedBySwipeHelper;
public TaskStackViewTouchHandler(Context context, TaskStackView sv) {
ViewConfiguration configuration = ViewConfiguration.get(context);
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
mScrollTouchSlop = configuration.getScaledTouchSlop();
mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
mSv = sv;
float densityScale = context.getResources().getDisplayMetrics().density;
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
mSwipeHelper.setMinAlpha(1f);
}
/** Velocity tracker helpers */
void initOrResetVelocityTracker() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
} else {
mVelocityTracker.clear();
}
}
void initVelocityTrackerIfNotExists() {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
}
void recycleVelocityTracker() {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
/** Returns the view at the specified coordinates */
TaskView findViewAtPoint(int x, int y) {
int childCount = mSv.getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
TaskView tv = (TaskView) mSv.getChildAt(i);
if (tv.getVisibility() == View.VISIBLE) {
if (mSv.isTransformedTouchPointInView(x, y, tv)) {
return tv;
}
}
}
return null;
}
/** Touch preprocessing for handling below */
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.TouchEvents,
"[TaskStackViewTouchHandler|interceptTouchEvent]",
Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
}
// Return early if we have no children
boolean hasChildren = (mSv.getChildCount() > 0);
if (!hasChildren) {
return false;
}
// Pass through to swipe helper if we are swiping
mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
if (mInterceptedBySwipeHelper) {
return true;
}
boolean wasScrolling = !mSv.mScroller.isFinished() ||
(mSv.mScrollAnimator != null && mSv.mScrollAnimator.isRunning());
int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
// Save the touch down info
mInitialMotionX = mLastMotionX = (int) ev.getX();
mInitialMotionY = mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
// Stop the current scroll if it is still flinging
mSv.abortScroller();
mSv.abortBoundScrollAnimation();
// Initialize the velocity tracker
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
// Check if the scroller is finished yet
mIsScrolling = !mSv.mScroller.isFinished();
break;
}
case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == INACTIVE_POINTER_ID) break;
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
int y = (int) ev.getY(activePointerIndex);
int x = (int) ev.getX(activePointerIndex);
if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
// Save the touch move info
mIsScrolling = true;
// Initialize the velocity tracker if necessary
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
// Enable HW layers
mSv.addHwLayersRefCount("stackScroll");
}
mLastMotionX = x;
mLastMotionY = y;
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
// Animate the scroll back if we've cancelled
mSv.animateBoundScroll();
// Disable HW layers
if (mIsScrolling) {
mSv.decHwLayersRefCount("stackScroll");
}
// Reset the drag state and the velocity tracker
mIsScrolling = false;
mActivePointerId = INACTIVE_POINTER_ID;
mActiveTaskView = null;
mTotalScrollMotion = 0;
recycleVelocityTracker();
break;
}
}
return wasScrolling || mIsScrolling;
}
/** Handles touch events once we have intercepted them */
public boolean onTouchEvent(MotionEvent ev) {
if (Console.Enabled) {
Console.log(Constants.Log.UI.TouchEvents,
"[TaskStackViewTouchHandler|touchEvent]",
Console.motionEventActionToString(ev.getAction()), Console.AnsiBlue);
}
// Short circuit if we have no children
boolean hasChildren = (mSv.getChildCount() > 0);
if (!hasChildren) {
return false;
}
// Pass through to swipe helper if we are swiping
if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
return true;
}
// Update the velocity tracker
initVelocityTrackerIfNotExists();
mVelocityTracker.addMovement(ev);
int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
// Save the touch down info
mInitialMotionX = mLastMotionX = (int) ev.getX();
mInitialMotionY = mLastMotionY = (int) ev.getY();
mActivePointerId = ev.getPointerId(0);
mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
// Stop the current scroll if it is still flinging
mSv.abortScroller();
mSv.abortBoundScrollAnimation();
// Initialize the velocity tracker
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
final int index = ev.getActionIndex();
mActivePointerId = ev.getPointerId(index);
mLastMotionX = (int) ev.getX(index);
mLastMotionY = (int) ev.getY(index);
break;
}
case MotionEvent.ACTION_MOVE: {
if (mActivePointerId == INACTIVE_POINTER_ID) break;
int activePointerIndex = ev.findPointerIndex(mActivePointerId);
int x = (int) ev.getX(activePointerIndex);
int y = (int) ev.getY(activePointerIndex);
int yTotal = Math.abs(y - mInitialMotionY);
int deltaY = mLastMotionY - y;
if (!mIsScrolling) {
if (yTotal > mScrollTouchSlop) {
mIsScrolling = true;
// Initialize the velocity tracker
initOrResetVelocityTracker();
mVelocityTracker.addMovement(ev);
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
// Enable HW layers
mSv.addHwLayersRefCount("stackScroll");
}
}
if (mIsScrolling) {
int curStackScroll = mSv.getStackScroll();
int overScrollAmount = mSv.getScrollAmountOutOfBounds(curStackScroll + deltaY);
if (overScrollAmount != 0) {
// Bound the overscroll to a fixed amount, and inversely scale the y-movement
// relative to how close we are to the max overscroll
float maxOverScroll = mSv.mTaskRect.height() / 3f;
deltaY = Math.round(deltaY * (1f - (Math.min(maxOverScroll, overScrollAmount)
/ maxOverScroll)));
}
mSv.setStackScroll(curStackScroll + deltaY);
if (mSv.isScrollOutOfBounds()) {
mVelocityTracker.clear();
}
}
mLastMotionX = x;
mLastMotionY = y;
mTotalScrollMotion += Math.abs(deltaY);
break;
}
case MotionEvent.ACTION_UP: {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocity = (int) velocityTracker.getYVelocity(mActivePointerId);
if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
// Enable HW layers on the stack
mSv.addHwLayersRefCount("flingScroll");
// XXX: Make this animation a function of the velocity AND distance
int overscrollRange = (int) (Math.min(1f,
Math.abs((float) velocity / mMaximumVelocity)) *
Constants.Values.TaskStackView.TaskStackOverscrollRange);
if (Console.Enabled) {
Console.log(Constants.Log.UI.TouchEvents,
"[TaskStackViewTouchHandler|fling]",
"scroll: " + mSv.getStackScroll() + " velocity: " + velocity +
" maxVelocity: " + mMaximumVelocity +
" overscrollRange: " + overscrollRange,
Console.AnsiGreen);
}
// Fling scroll
mSv.mScroller.fling(0, mSv.getStackScroll(),
0, -velocity,
0, 0,
mSv.mMinScroll, mSv.mMaxScroll,
0, overscrollRange);
// Invalidate to kick off computeScroll
mSv.invalidate();
} else if (mSv.isScrollOutOfBounds()) {
// Animate the scroll back into bounds
// XXX: Make this animation a function of the velocity OR distance
mSv.animateBoundScroll();
}
if (mIsScrolling) {
// Disable HW layers
mSv.decHwLayersRefCount("stackScroll");
}
mActivePointerId = INACTIVE_POINTER_ID;
mIsScrolling = false;
mTotalScrollMotion = 0;
recycleVelocityTracker();
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);
mLastMotionX = (int) ev.getX(newPointerIndex);
mLastMotionY = (int) ev.getY(newPointerIndex);
mVelocityTracker.clear();
}
break;
}
case MotionEvent.ACTION_CANCEL: {
if (mIsScrolling) {
// Disable HW layers
mSv.decHwLayersRefCount("stackScroll");
}
if (mSv.isScrollOutOfBounds()) {
// Animate the scroll back into bounds
// XXX: Make this animation a function of the velocity OR distance
mSv.animateBoundScroll();
}
mActivePointerId = INACTIVE_POINTER_ID;
mIsScrolling = false;
mTotalScrollMotion = 0;
recycleVelocityTracker();
break;
}
}
return true;
}
/**** SwipeHelper Implementation ****/
@Override
public View getChildAtPosition(MotionEvent ev) {
return findViewAtPoint((int) ev.getX(), (int) ev.getY());
}
@Override
public boolean canChildBeDismissed(View v) {
return true;
}
@Override
public void onBeginDrag(View v) {
// Enable HW layers
mSv.addHwLayersRefCount("swipeBegin");
// Disable clipping with the stack while we are swiping
TaskView tv = (TaskView) v;
tv.setClipViewInStack(false);
// Disallow parents from intercepting touch events
final ViewParent parent = mSv.getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(true);
}
}
@Override
public void onSwipeChanged(View v, float delta) {
// Do nothing
}
@Override
public void onChildDismissed(View v) {
TaskView tv = (TaskView) v;
mSv.onTaskDismissed(tv);
// Re-enable clipping with the stack (we will reuse this view)
tv.setClipViewInStack(true);
// Disable HW layers
mSv.decHwLayersRefCount("swipeComplete");
}
@Override
public void onSnapBackCompleted(View v) {
// Re-enable clipping with the stack
TaskView tv = (TaskView) v;
tv.setClipViewInStack(true);
}
@Override
public void onDragCancelled(View v) {
// Disable HW layers
mSv.decHwLayersRefCount("swipeCancelled");
}
}