SystemUI RecentTask 流程分析
Android
SystemUI
Recent
文章目錄
在默認的設計中,最近任務卡片在界面佈局中的排列和滾動都是以摺疊方式由上到下方式顯示的,本文中所有分析都基於此設計前提。
1. 啓動流程
1.1 RecentsActivity
SystemUI/src/com/android/systemui/recents/RecentsActivity.java
從入口開始,RecentsActivity.onCreate(),加載R.layout.recetns 佈局,並調用reloadStackView();:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Set the Recents layout
setContentView(R.layout.recents);
mRecentsView = (RecentsView) findViewById(R.id.recents_view);
...
// Reload the stack view
reloadStackView();
}
RecentsActivity.reloadStackView(),異步獲取最近任務列表,然後刷新任務棧:
private void reloadStackView() {
RecentsTaskLoader loader = Recents.getTaskLoader();
...
// 異步獲取最近任務列表
loader.loadTasks(this, loadPlan, loadOpts);
TaskStack stack = loadPlan.getTaskStack();
mRecentsView.onReload(mIsVisible, stack.getTaskCount() == 0);
mRecentsView.updateStack(stack, true /* setStackViewTasks */);
...
}
RecentsView.onReload(),初始化並更新任務棧界面TaskStackView:
public void onReload(boolean isResumingFromVisible, boolean isTaskStackEmpty) {
...
if (mTaskStackView == null) {
isResumingFromVisible = false;
// 初始化TaskStackView
mTaskStackView = new TaskStackView(getContext());
mTaskStackView.setSystemInsets(mSystemInsets);
addView(mTaskStackView);
}
...
// 更新
mTaskStackView.onReload(isResumingFromVisible);
...
}
1.2 TaskStackView
SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
接下來進入到TaskStackView,初始化時調用構造方法,在這裏初始化了幾個重要的類成員:
public TaskStackView(Context context) {
...
// 幾個重要的類成員初始化
mViewPool = new ViewPool<>(context, this); // View池,負責創建、回收任務卡片TaskView
mLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, this); // 佈局算法封裝,與佈局有關的計算接口都放在這裏
mStableLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, null); // 相對於mLayoutAlgorithm的一個穩定成員
mStackScroller = new TaskStackViewScroller(context, this, mLayoutAlgorithm); // 滾動算法封裝,內部封裝了OverScroller,用於處理滾動邏輯及數據
mTouchHandler = new TaskStackViewTouchHandler(context, this, mStackScroller); // 觸摸動作處理,處理觸摸動作相關邏輯和數據並將結果傳遞給滾動類
mAnimationHelper = new TaskStackAnimationHelper(context, this);
....
}
在createView()方法中加載任務卡片TaskView的佈局文件recents_task_view.xml,該方法繼承自ViewPool.ViewPoolConsumer< TaskView, Task >,在Viewpool中的pickUpViewFromPool()方法裏,當View池初次爲空時調用:
@Override
public TaskView createView(Context context) {
if (Recents.getConfiguration().isGridEnabled) {
return (GridTaskView) mInflater.inflate(R.layout.recents_grid_task_view, this, false);
} else {
return (TaskView) mInflater.inflate(R.layout.recents_task_view, this, false);
}
}
在onMeasure()方法中獲取並設置每個任務卡片TaskView的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
// 通過mLayoutAlgorithm的getTaskStackBounds()方法獲取TaskView的尺寸,結果保存在mTmpRect中
mLayoutAlgorithm.getTaskStackBounds(mDisplayRect,
new Rect(0, 0, width, height),
mLayoutAlgorithm.mSystemInsets.top,
mLayoutAlgorithm.mSystemInsets.left,
mLayoutAlgorithm.mSystemInsets.right, mTmpRect);
...
// 調用mStableLayoutAlgorithm.mLayoutAlgorithm,計算棧和任務相關的矩形
mStableLayoutAlgorithm.initialize(mDisplayRect, mStableWindowRect, mStableStackBounds,
TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
mLayoutAlgorithm.initialize(mDisplayRect, mWindowRect, mStackBounds,
TaskStackLayoutAlgorithm.StackState.getStackStateForStack(mStack));
...
// 綁定和更新任務卡片TaskView的屬性值
bindVisibleTaskViews(mStackScroller.getStackScroll(), false /* ignoreTaskOverrides */);
...
// 逐個設置每個任務卡片TaskView的尺寸
for (int i = 0; i < taskViewCount; i++) {
measureTaskView(mTmpTaskViews.get(i));
}
} ...
}
onLayout()方法中調用了relayoutTaskViews()方法,用於重新佈局任務卡片TaskView的顯示:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
...
// Relayout all of the task views including the ignored ones
relayoutTaskViews(AnimationProps.IMMEDIATE);
...
if (mAwaitingFirstLayout || !mEnterAnimationComplete) {
mAwaitingFirstLayout = false;
mInitialState = INITIAL_STATE_UPDATE_NONE;
onFirstLayout();
}
}
接着調用onFirstLayout()方法,調用mAnimationHelper.prepareForEnterAnimation()爲每個TaskView創建一個進入動畫前的屬性值transform,調用updateTaskViewToTransform(AnimationProps.IMMEDIATE),將其立刻應用到TaskView上:
void onFirstLayout() {
// Setup the view for the enter animation
mAnimationHelper.prepareForEnterAnimation();
...
}
relayoutTaskViews()方法中首先調用bindVisibleTaskViews()方法更新任務卡片TaskView的屬性值transform,然後調用updateTaskViewToTransform()對每個TaskView應用對應的屬性值
public void relayoutTaskViews(AnimationProps animation) {
relayoutTaskViews(animation, null /* animationOverrides */,
false /* ignoreTaskOverrides */);
}
private void relayoutTaskViews(AnimationProps animation,
ArrayMap<Task, AnimationProps> animationOverrides,
boolean ignoreTaskOverrides) {
// If we had a deferred animation, cancel that
cancelDeferredTaskViewLayoutAnimation();
// Synchronize the current set of TaskViews
bindVisibleTaskViews(mStackScroller.getStackScroll(),
ignoreTaskOverrides /* ignoreTaskOverrides */);
// 更新TaskView的屬性
// Animate them to their final transforms with the given animation
List<TaskView> taskViews = getTaskViews();
int taskViewCount = taskViews.size();
for (int i = 0; i < taskViewCount; i++) {
TaskView tv = taskViews.get(i);
Task task = tv.getTask();
int taskIndex = mStack.indexOfStackTask(task);
TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex);
...
updateTaskViewToTransform(tv, transform, animation);
}
}
bindVisibleTaskViews()方法中,調用computeVisibleTaskTransforms()方法初始化或重新計算每個任務卡片的屬性值Transform,並通過保存在mCurrentTaskTransforms數組中與每個任務卡片對應起來
void bindVisibleTaskViews(float targetStackScroll, boolean ignoreTaskOverrides) {
// Get all the task transforms
ArrayList<Task> tasks = mStack.getStackTasks();
// 初始化或重新計算每個任務卡片的屬性值Transform
int[] visibleTaskRange = computeVisibleTaskTransforms(mCurrentTaskTransforms, tasks, mStackScroller.getStackScroll(), targetStackScroll, mIgnoreTasks, gnoreTaskOverrides);
...
}
computeVisibleTaskTransforms()方法中初始化或更新taskTransforms數組,即mCurrentTaskTransforms數組
int[] computeVisibleTaskTransforms(ArrayList<TaskViewTransform> taskTransforms,
ArrayList<Task> tasks, float curStackScroll, float targetStackScroll,
ArraySet<Task.TaskKey> ignoreTasksSet, boolean ignoreTaskOverrides) {
...
// 修正Transform數組大小與當前任務卡片TaskView數量保持一致,以便可以對應起來
Utilities.matchTaskListSize(tasks, taskTransforms);
for (int i = taskCount - 1; i >= 0; i--) {
Task task = tasks.get(i);
// 計算並獲取task對應的transform,結果保存在taskTransforms數組中
transform = mLayoutAlgorithm.getStackTransform(task, curStackScroll,
taskTransforms.get(i), frontTransform, ignoreTaskOverrides);
...
}
}
updateTaskViewToTransform()方法中對接收的任務卡片TaskView應用屬性值transform,animation是應用過程中採用的動畫屬性,當animation的時長設置爲0時,應用到當前TaskView的屬性值transform會立即生效:
public void updateTaskViewToTransform(TaskView taskView, TaskViewTransform transform,
AnimationProps animation) {
if (taskView.isAnimatingTo(transform)) {
return;
}
Task t = taskView.getTask();
taskView.cancelTransformAnimation();
taskView.updateViewPropertiesToTaskTransform(transform, animation,
mRequestUpdateClippingListener);
}
2. 滾動邏輯
滾動相關邏輯和數據主要在類TaskStackViewScroller中實現,觸摸滑動邏輯和數據主要在類TaskStackViewTouchHandler中實現,這兩個類的實例都在TaskView的構造函數中進行了初始化(參見1.2);
2.1 TaskStackViewTouchHandle
SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
進入到類TaskStackViewTouchHandler,其中實際處理Touch事件的是handleTouchEvent()方法:
private boolean handleTouchEvent(MotionEvent ev) {
switch (action & MotionEvent.ACTION_MASK) {
...
case MotionEvent.ACTION_POINTER_UP: {
...
// 觸摸開始前記錄Scroller的滾動變量
mDownScrollP = mScroller.getStackScroll();
...
}
case MotionEvent.ACTION_MOVE: {
...
if (mIsScrolling) {
// 根據向下滑動的座標差,計算獲取滑動變量deltaP
// layoutAlgorithm.getDeltaPForY()方法將座標差映射到變量deltaP
float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y);
...
// 計算獲取當前Scroller的滾動變量curScrollP
float curScrollP = mDownScrollP + deltaP;
// 將滾動變量curScrollP傳遞給TaskStackViewScroller
mEndScrollP += mScroller.setDeltaStackScroll(mDownScrollP, curScrollP - mDownScrollP);
mStackViewScrolledEvent.updateY(y - mLastY);
EventBus.getDefault().send(mStackViewScrolledEvent);
}
}
case MotionEvent.ACTION_UP: {
...
// 設置速度追蹤,以1000ms(1s)滾動的像素值作爲速度描述返回
mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
if (mIsScrolling) {
if (mScroller.isScrollOutOfBounds()) {
// 滾動到邊界,阻尼動畫
mScroller.animateBoundScroll();
} else if (Math.abs(velocity) > mMinimumVelocity) {
// 滾動時curY允許的最小值:mDownScrollP越大,scroller的CurY越小(向上滑動)
float minY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
layoutAlgorithm.mMaxScrollP);
// 滾動時curY允許的最大值:mDownScrollP越小,scroller的CurY越大(向下滑動)
float maxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
layoutAlgorithm.mMinScrollP);
mOverscrollSize);
// 調用fling計算慣性滾動座標,傳遞參數到TaskStackViewScroller
mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY,
mOverscrollSize);
// 刷新棧佈局TaskStackView,這會觸發TaskStackView的computeScroll([**具體見2.2**](#2.2))
mSv.invalidate();
}
...
}
...
}
return mIsScrolling;
}
2.2 TaskStackViewTouchHandler
SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
類TaskStackViewTouchHandler繼承了SwipeHelper.Callback藉口,並聲明瞭一個SwipeHelper揮動輔助處理類實例來實現任務卡TaskView揮動監聽:
// 聲明一個橫向(X軸)的揮動處理實例,傳入this作爲回調實例
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, context) {
...
}
// 調用mSwipeHelper.onInterceptTouchEvent()優先處理任務卡TaskView在橫向的揮動動作
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);
}
// 調用mSwipeHelper.onTouchEvent(),優先處理任務卡TaskView在橫向的揮動動作
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;
}
2.3 TaskStackViewScroller
SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
類TaskStackViewScroller內部聲明瞭一個OverScroller實例處理真正滾動計算, 同時包含一個TaskStackViewScrollerCallbacks的接口實例,該實例即在構造時傳入的TaskStackView實例本身(參見1.2),當Scroller計算出新的結果,則回調該接口實例的onStackScrollChanged():
public void setStackScroll(float newScroll, AnimationProps animation) {
float prevScroll = mStackScrollP;
mStackScrollP = newScroll;
if (mCb != null) {
mCb.onStackScrollChanged(prevScroll, mStackScrollP, animation);
}
}
TaskStackViewTouchHandler實例在滑動後調用TaskStackViewScroller的setDeltaStackScroll()方法調用,該方法最終通過setStackScroll()方法回調到TaskStackView的onStackScrollChanged()方法:
public float setDeltaStackScroll(float downP, float deltaP) {
float targetScroll = downP + deltaP;
float newScroll = mLayoutAlgorithm.updateFocusStateOnScroll(downP + mLastDeltaP, targetScroll,
mStackScrollP);
setStackScroll(newScroll, AnimationProps.IMMEDIATE);
mLastDeltaP = deltaP;
return newScroll - targetScroll;
}
TaskStackView的onStackScrollChanged()方法中,調用了relayoutTaskViewsOnNextFrame()方法:
@Override
public void onStackScrollChanged(float prevScroll, float curScroll, AnimationProps animation) {
...
if (animation != null) {
relayoutTaskViewsOnNextFrame(animation);
}
...
relayoutTaskViewsOnNextFrame()方法內部調用了invalidate()方法觸發重繪:
void relayoutTaskViewsOnNextFrame(AnimationProps animation) {
mDeferredTaskViewLayoutAnimation = animation;
invalidate();
}
TaskStackViewTouchHandle中處理揮動時調用fling計算慣性滾動座標後也主動調用了TaskStackView的invalidate()方法:
private boolean handleTouchEvent(MotionEvent ev) {
switch (action & MotionEvent.ACTION_MASK) {
...
case MotionEvent.ACTION_UP: {
...
if (mIsScrolling) {
...
mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY,
mOverscrollSize);
mSv.invalidate();
...
}
...
}
...
}
return mIsScrolling;
}
invalidate()方法會觸發computeScroll()方法, TaskStackView覆寫了此方法,其內部調用了relayoutTaskViews()方法,mDeferredTaskViewLayoutAnimation的值來自於TaskViewScroller的setDeltaStackScroll()方法,爲AnimationProps.IMMEDIATE:
@Override
public void computeScroll() {
// 調用TaskViewScroller的computeScroll()計算新的滾動變量
if (mStackScroller.computeScroll()) {
// Notify accessibility
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
}
// 調用relayoutTaskViews()方法刷新TaskView佈局
if (mDeferredTaskViewLayoutAnimation != null) {
relayoutTaskViews(mDeferredTaskViewLayoutAnimation);
mTaskViewsClipDirty = true;
mDeferredTaskViewLayoutAnimation = null;
}
if (mTaskViewsClipDirty) {
clipTaskViews();
}
}
relayoutTaskViews()方法在1.2中已介紹,最終前面的回調會通過調用updateTaskViewToTransform()方法將屬性值transform立刻應用到每個任務卡片TaskView上,從而實現任務卡片TaskView的滑動或滾動動畫效果;
3. 位置計算
在**“啓動流程”和“滾動邏輯”**中可以瞭解到,任務卡片TaskView的顯示狀態主要由其對應的屬性值transfrom決定,獲取屬性值transform的地方是mLayoutAlgorithm.getStackTransform;
3.1 TaskStackLayoutAlgorithm
SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
進入到TaskStackLayoutAlgorithm,查看getStackTransform()方法,這個方法存在幾個重載方法,最終會調用到如下兩個重要的重載方法:
/** 重載一,重要的幾個參數:
* @task:Task實例
* @stackScroll:mScroller當前的滾動變量
* @transformOut:當前任務卡片TaskView對應的屬性值
@frontTransform:蓋在當前任務卡片之上的任務卡片對應的屬性值
*/
public TaskViewTransform getStackTransform(Task task, float stackScroll, int focusState,
TaskViewTransform transformOut, TaskViewTransform frontTransform, boolean forceUpdate,
boolean ignoreTaskOverrides) {
// 可以根據傳入的Task實例得到一些重要的參數用作進一步的計算或判斷依據,例如任務索引、任務總數等
if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
...
return transformOut;
} else if (useGridLayout()) {
...
return transformOut;
} else {
int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1);
if (task == null || nonOverrideTaskProgress == -1) {
transformOut.reset();
return transformOut;
}
// 此處獲取了一個重要的參數:描述任務進度的值taskProgress
float taskProgress = ignoreTaskOverrides
? nonOverrideTaskProgress
: getStackScrollForTask(task);
getStackTransform(taskProgress, nonOverrideTaskProgress, stackScroll, focusState,
transformOut, frontTransform, false /* ignoreSingleTaskCase */, forceUpdate);
return transformOut;
}
前面的重載方法最終會調用如下第二個重載方法:
/** 重載二,重要的幾個參數:
* @taskProgress:描述Task進度的值
* @stackScroll:mScroller當前的滾動變量
* @transformOut:當前任務卡片TaskView對應的屬性值
* @frontTransform:蓋在當前任務卡片之上的任務卡片對應的屬性值
*/
public void getStackTransform(float taskProgress, float nonOverrideTaskProgress,
float stackScroll, int focusState, TaskViewTransform transformOut,
TaskViewTransform frontTransform, boolean ignoreSingleTaskCase, boolean forceUpdate) {
...
// 將位置區間起點偏移stackScroll
mUnfocusedRange.offset(stackScroll);
mFocusedRange.offset(stackScroll);
// 根據taskProgress計算得出歸一化的範圍值unfocusedRangeX和focusedRangeX
float unfocusedRangeX = mUnfocusedRange.getNormalizedX(taskProgress);
float focusedRangeX = mFocusedRange.getNormalizedX(taskProgress);
...
// 當前任務卡片的水平方向偏移量X等於佈局中心點
int x = (mStackRect.width() - mTaskRect.width()) / 2;
if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1 && !ignoreSingleTaskCase) {
float tmpP = (mMinScrollP - stackScroll) / mNumStackTasks;
int centerYOffset = (mStackRect.top - mTaskRect.top) +
(mStackRect.height() - mSystemInsets.bottom - mTaskRect.height()) / 2;
// 只有一個任務卡片時,垂直方向偏移量Y等於中心點+向上滾動變量
// getYForDeltaP(tmpP, 0) = getYForDeltaP(0, -tmpP) = getYForDeltaP(0, stackScroll - mMinScrollP)
// mNumStackTasks = 1,因此getYForDeltaP(tmpP, 0)是向上滾動變量
y = centerYOffset + getYForDeltaP(tmpP, 0);
z = mMaxTranslationZ;
...
} else {
// 根據歸一化的範圍值和路徑差值器計算獲取偏移量unfocusedY和focusedY
int unfocusedY = (int) ((1f - mUnfocusedCurveInterpolator.getInterpolation(
unfocusedRangeX)) * mStackRect.height());
int focusedY = (int) ((1f - mFocusedCurveInterpolator.getInterpolation(
focusedRangeX)) * mStackRect.height());
// 根據unfocusedY、focusedY以及focusState計算垂直方向偏移量Y
y = (mStackRect.top - mTaskRect.top) +
(int) Utilities.mapRange(focusState, unfocusedY, focusedY);
}
}
Utilities.mapRange()方法如下:
// 當focusState爲STATE_UNFOCUSED:0,y = unfocusedY
// 當focusState爲STATE_FOCUSED:1,y = focusedY - unfocusedY
public static float mapRange(@FloatRange(from=0.0,to=1.0) float value, float min, float max) {
return min + (value * (max - min));
}
可以看出,數量爲1時,任務卡片的Y方向位置僅由statckScroll決定;數量大於1時,任務卡片的Y方向位置由statckScroll和taskProgress共同決定;
statckScroll的值由TaskStackViewScroller滾動時回調傳入;
在前面提到的第一個getStackTransform重載方法中,taskProgress的值等於nonOverrideTaskProgress或getStackScrollForTask():
int nonOverrideTaskProgress = mTaskIndexMap.get(task.key.id, -1);
float taskProgress = ignoreTaskOverrides
? nonOverrideTaskProgress
: getStackScrollForTask(task);
**“啓動流程”和“滾動邏輯”**調用getStackTransform()方法時參數ignoreTaskOverrides都是false,因此taskProgress的值實際等於getStackScrollForTask(task)返回值,即taskProgress的值等於當前任務的真實索引或覆蓋索引:
float getStackScrollForTask(Task t) {
// 獲取覆蓋索引
Float overrideP = mTaskIndexOverrideMap.get(t.key.id, null);
if (overrideP == null) {
// 覆蓋索引爲空,獲取真實索引
return (float) mTaskIndexMap.get(t.key.id, 0);
}
return overrideP;
}
當前任務的覆蓋索引不爲空時,getStackTransform()方法中的垂直偏移量Y值優先由覆蓋索引決定,mTaskIndexOverrideMap中覆蓋索引的來源主要有四個地方:
(1) TaskStackLayoutAlgorithm的setTaskOverridesForInitialState()方法,該方法會初始化任務卡片TaskView的覆蓋索引:
public void setTaskOverridesForInitialState(TaskStack stack, boolean ignoreScrollToFront) {
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
mTaskIndexOverrideMap.clear();
boolean scrollToFront = launchState.launchedFromHome ||
launchState.launchedFromBlacklistedApp ||
launchState.launchedViaDockGesture;
if (getInitialFocusState() == STATE_UNFOCUSED && mNumStackTasks > 1) {
if (ignoreScrollToFront || (!launchState.launchedWithAltTab && !scrollToFront)) {
// Set the initial scroll to the predefined state (which differs from the stack)
float [] initialNormX = null;
float minBottomTaskNormX = getNormalizedXFromUnfocusedY(mSystemInsets.bottom +
mInitialBottomOffset, FROM_BOTTOM);
float maxBottomTaskNormX = getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight +
mTaskRect.height() - mMinMargin, FROM_TOP);
if (mNumStackTasks <= 2) {
// For small stacks, position the tasks so that they are top aligned to under
// the action button, but ensure that it is at least a certain offset from the
// bottom of the stack
initialNormX = new float[] {
Math.min(maxBottomTaskNormX, minBottomTaskNormX),
getNormalizedXFromUnfocusedY(mFocusedTopPeekHeight, FROM_TOP)
};
} else {
initialNormX = new float[] {
minBottomTaskNormX,
getNormalizedXFromUnfocusedY(mInitialTopOffset, FROM_TOP)
};
}
mUnfocusedRange.offset(0f);
List<Task> tasks = stack.getStackTasks();
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 0; i--) {
int indexFromFront = taskCount - i - 1;
if (indexFromFront >= initialNormX.length) {
break;
}
float newTaskProgress = mInitialScrollP +
mUnfocusedRange.getAbsoluteX(initialNormX[indexFromFront]);
mTaskIndexOverrideMap.put(tasks.get(i).key.id, newTaskProgress);
}
}
}
}
該方法在棧佈局TaskStackView初始化時updateToInitialState()方法中被調用;另一處是在棧佈局TaskStackView響應MultiWindowStateChangedEvent時,在調用的startNewStackScrollAnimation()方法中被調用(暫不關注);
public void updateToInitialState() {
mStackScroller.setStackScrollToInitialState();
// 傳入setTaskOverridesForInitialState()方法的ignoreScrollToFront參數爲false
mLayoutAlgorithm.setTaskOverridesForInitialState(mStack, false /* ignoreScrollToFront */);
}
當從HOME界面點擊Recent按鍵進入界面時,傳入的ignoreScrollToFront參數值爲false,scrollToFront爲true,因此mTaskIndexOverrideMap沒有被更新,任務卡片TaskView的Y偏移量由其真實索引計算得到;
當從應用界面點擊Recent按鍵進入界面時,傳入的ignoreScrollToFront參數值爲false,但scrollToFront爲false,此時mTaskIndexOverrideMap被更新,按照Task索引順序存入最後兩個Task的覆蓋索引值,因此末尾最上層兩個任務卡片TaskView的Y偏移量由覆蓋索引值決定;表現在界面顯示上,即從應用界面進入Recent界面時,倒數第一個應用縮略圖處在屏幕邊緣,着重顯示倒數第二個應用的縮略圖方便用戶切換;
(2) TaskStackLayoutAlgorithm的addUnfocusedTaskOverride(Task, float)方法,該方法在任務從焦點過度爲非焦點時向mTaskIndexOverrideMap數組中添加數據:
public void addUnfocusedTaskOverride(Task task, float stackScroll) {
if (mFocusState != STATE_UNFOCUSED) {
mFocusedRange.offset(stackScroll);
mUnfocusedRange.offset(stackScroll);
float focusedRangeX = mFocusedRange.getNormalizedX(mTaskIndexMap.get(task.key.id));
float focusedY = mFocusedCurveInterpolator.getInterpolation(focusedRangeX);
float unfocusedRangeX = mUnfocusedCurveInterpolator.getX(focusedY);
float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX);
if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) {
mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress);
}
}
}
該方法在TaskStackViewTouchHandler的handleTouchEvent()方法中在處理MotionEvent.ACTION_MOVE事件時被調用;
在最近任務卡片界面滑動首次觸發場景中,此時mFocusState等於STATE_UNFOCUSED,此時mTaskIndexOverrideMap沒有被更新,任務卡片TaskView的Y偏移量仍由其真實索引計算得到:
private boolean handleTouchEvent(MotionEvent ev) {
...
switch (action & MotionEvent.ACTION_MASK) {
...
case MotionEvent.ACTION_MOVE: {
...
if (!mIsScrolling) {
...
mIsScrolling = true;
for (int i = taskViews.size() - 1; i >= 0; i--) {
layoutAlgorithm.addUnfocusedTaskOverride(taskViews.get(i).getTask(), stackScroll);
}
layoutAlgorithm.setFocusState(TaskStackLayoutAlgorithm.STATE_UNFOCUSED);
...
}
...
}
}
...
}
(3) TaskStackLayoutAlgorithm的addUnfocusedTaskOverride(TaskView, float)方法,該方法在任務從焦點過度爲非焦點時向mTaskIndexOverrideMap數組中添加數據:
public void addUnfocusedTaskOverride(TaskView taskView, float stackScroll) {
mFocusedRange.offset(stackScroll);
mUnfocusedRange.offset(stackScroll);
Task task = taskView.getTask();
int top = taskView.getTop() - mTaskRect.top;
float focusedRangeX = getNormalizedXFromFocusedY(top, FROM_TOP);
float unfocusedRangeX = getNormalizedXFromUnfocusedY(top, FROM_TOP);
float unfocusedTaskProgress = stackScroll + mUnfocusedRange.getAbsoluteX(unfocusedRangeX);
if (Float.compare(focusedRangeX, unfocusedRangeX) != 0) {
mTaskIndexOverrideMap.put(task.key.id, unfocusedTaskProgress);
}
}
該方法在TaskStackViewTouchHandler的cancelNonDismissTaskAnimations()方法中觸發,與取消非DismissTask動畫有關,暫時不關注;
(4)TaskStackLayoutAlgorithm的updateFocusStateOnScroll()方法,該方法在滑動事件結果傳入Scroller後,在Scroller中處理滾動參數時,根據當前滾動參數向mTaskIndexOverrideMap數組中添加或移除無效數據:
public float updateFocusStateOnScroll(float lastTargetStackScroll, float targetStackScroll,
float lastStackScroll) {
if (targetStackScroll == lastStackScroll) {
return targetStackScroll;
}
// 當前滾動增量
float deltaScroll = targetStackScroll - lastStackScroll;
// 當前目標滾動變量 與 上一次目標滾動變量 相比的增量
float deltaTargetScroll = targetStackScroll - lastTargetStackScroll;
// 默認返回 當前目標滾動變量 作爲 新的滾動變量值 來更新棧佈局
float newScroll = targetStackScroll;
mUnfocusedRange.offset(targetStackScroll);
for (int i = mTaskIndexOverrideMap.size() - 1; i >= 0; i--) {
int taskId = mTaskIndexOverrideMap.keyAt(i);
// 當前任務Task的真實索引值
float x = mTaskIndexMap.get(taskId);
// 當前任務Task的覆蓋索引值
float overrideX = mTaskIndexOverrideMap.get(taskId, 0f);
// 默認新的覆蓋索引值 = 更新前覆蓋索引值 + 當前滾動增量
float newOverrideX = overrideX + deltaScroll;
// 判斷當前覆蓋索引值是否無效
// 當前覆蓋索引值:必須在mUnfocusedRange範圍區間之內,且值在正確的偏移方向內
if (isInvalidOverrideX(x, overrideX, newOverrideX)) {
// 移除無效的當前覆蓋索引值
// Remove the override once we reach the original task index
mTaskIndexOverrideMap.removeAt(i);
// 判斷當前覆蓋索引值偏移方向與滾動方向否一致
} else if ((overrideX >= x && deltaScroll <= 0f) ||
(overrideX <= x && deltaScroll >= 0f)) {
// Scrolling from override x towards x, then lock the task in place
// 覆蓋索引值偏移方向與滾動方向不一致,更新當前覆蓋索引值到mTaskIndexOverrideMap數組
mTaskIndexOverrideMap.put(taskId, newOverrideX);
} else {
// 覆蓋索引值有效,但偏移方向與滾動方向一致
// 重新計算並更新當前覆蓋索引值
// Scrolling override x away from x, we should still move the scroll towards x
// 返回 上一次滾動變量,表現爲棧佈局不更新滾動
newScroll = lastStackScroll;
// 新的覆蓋索引值 = 當前覆蓋索引值 - 目標滾動變量增量
newOverrideX = overrideX - deltaTargetScroll;
// 判斷新的覆蓋索引值有效性並更新到mTaskIndexOverrideMap數組
if (isInvalidOverrideX(x, overrideX, newOverrideX)) {
mTaskIndexOverrideMap.removeAt(i);
} else{
mTaskIndexOverrideMap.put(taskId, newOverrideX);
}
}
}
return newScroll;
}
private boolean isInvalidOverrideX(float x, float overrideX, float newOverrideX) {
// 新的覆蓋索引值時否在範圍區間之內
boolean outOfBounds = mUnfocusedRange.getNormalizedX(newOverrideX) < 0f ||
mUnfocusedRange.getNormalizedX(newOverrideX) > 1f;
// [不在範圍] 或 [偏移方向爲正,偏移量爲負] 或 [偏移方向爲負,偏移量爲正]
return outOfBounds || (overrideX >= x && x >= newOverrideX) ||
(overrideX <= x && x <= newOverrideX);
}
偏移方向和偏移量解釋:
overrideX > x
: 即覆蓋索引值相對真實索引值偏移方向爲正,以此得出的任務卡片位置相對覆蓋前真實索引值得出的位置靠下;- **若
newOverrideX > x
:**則由新覆蓋索引值得出的任務卡片位置更靠下,即向下滑動,任務卡片向下移動;- **若
x >= newOverrideX
:**則新覆蓋索引得出的任務卡片位置高於覆蓋前真實索引值的出位置了,即向上滑動,任務卡片向上移動並最終超過了原本正常應該的位置;此時的覆蓋索引值已經無效,因爲它的作用是令倒數第一個任務卡片向屏幕邊緣偏移,着重顯示倒數第二個任務卡片方便用戶切換。
該方法在TaskStackViewScroll的setDeltaStackScroll()方法中被調用,並將返回值作爲最新的滾動變量:
public float setDeltaStackScroll(float downP, float deltaP) {
float targetScroll = downP + deltaP;
// 更新TaskStackLayoutAlgorithm類中mTaskIndexOverrideMap數組
// 獲取返回值作爲最新的滾動變量
float newScroll = mLayoutAlgorithm.updateFocusStateOnScroll(downP + mLastDeltaP, targetScroll,
mStackScrollP);
setStackScroll(newScroll, AnimationProps.IMMEDIATE);
mLastDeltaP = deltaP;
return newScroll - targetScroll;
}
參考方法註釋,當覆蓋索有效且趨勢與滾動方向一致時,同時更新Scroller滾動變量和覆蓋索引值,當覆蓋索有效但趨勢與滾動方向不一致時,僅更新覆蓋索引值而Scroller滾動變量不變;表現在界面顯示上,即從應用界面進入Recent界面並上下滑動時(此時最後一個任務卡片在屏幕最底側邊緣),向上滑動時只更新最後一個任務卡片位置,而整個列表不滾動,向下滑動時,整個列表滾動同時更新最後一個任務卡片位置。
4. 縮略圖顯示
4.1 TaskViewThumbnail
SystemUI/src/com/android/systemui/recents/views/TaskViewThumbnail.java
TaskViewThumbnail在onTaskDataLoaded()方法中獲取任務縮略圖信息:
void onTaskDataLoaded(ActivityManager.TaskThumbnailInfo thumbnailInfo) {
if (mTask.thumbnail != null) {
setThumbnail(mTask.thumbnail, thumbnailInfo);
} else {
setThumbnail(null, null);
}
}
調用setThumbnail()方法將縮略圖片設置到畫筆,並記錄圖片尺寸到mThumbnailRec用於後面的縮放計算:
void setThumbnail(Bitmap bm, ActivityManager.TaskThumbnailInfo thumbnailInfo) {
if (bm != null) {
bm.prepareToDraw();
// 將圖片設置給渲染器
mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
// 將渲染器設置給畫筆
mDrawPaint.setShader(mBitmapShader);
// 保存圖片尺寸到mThumbnailRect
mThumbnailRect.set(0, 0, bm.getWidth(), bm.getHeight());
mThumbnailInfo = thumbnailInfo;
// 縮放計算
updateThumbnailScale();
} else {
mBitmapShader = null;
mDrawPaint.setShader(null);
mThumbnailRect.setEmpty();
mThumbnailInfo = null;
}
}
縮放計算存在兩種計算方式,默認模式和適應模式:
public void updateThumbnailScale() {
mThumbnailScale = 1f;
if (mBitmapShader != null) {
...
if (mTaskViewRect.isEmpty() || mThumbnailInfo == null ||
mThumbnailInfo.taskWidth == 0 || mThumbnailInfo.taskHeight == 0) {
// If we haven't measured or the thumbnail is invalid, skip the thumbnail drawing
// and only draw the background color
mThumbnailScale = 0f;
} else if (mSizeToFit) {
// 適應模式
// Make sure we fill the entire space regardless of the orientation.
float viewAspectRatio = (float) mTaskViewRect.width() /
(float) (mTaskViewRect.height() - mTitleBarHeight);
float thumbnailAspectRatio =
(float) mThumbnailRect.width() / (float) mThumbnailRect.height();
// View的寬高比大於圖片寬高比時,按照寬度比例縮放,否則按照高度比例縮放
// 圖片會填滿View,多餘的部分裁減
if (viewAspectRatio > thumbnailAspectRatio) {
mThumbnailScale =
(float) mTaskViewRect.width() / (float) mThumbnailRect.width();
} else {
mThumbnailScale = (float) (mTaskViewRect.height() - mTitleBarHeight)
/ (float) mThumbnailRect.height();
}
} else if (isStackTask) {
// 默認模式
float invThumbnailScale = 1f / mFullscreenThumbnailScale;
if (mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT) {
if (mThumbnailInfo.screenOrientation == Configuration.ORIENTATION_PORTRAIT) {
// 橫屏下,按照寬度比例縮放
// If we are in the same orientation as the screenshot, just scale it to the
// width of the task view
mThumbnailScale = (float) mTaskViewRect.width() / mThumbnailRect.width();
} else {
// 豎屏下,先按照全屏縮略圖縮放比例反向縮放,然後按照View與屏幕的寬度比例縮小
// Scale the landscape thumbnail up to app size, then scale that to the task
// view size to match other portrait screenshots
mThumbnailScale = invThumbnailScale *
((float) mTaskViewRect.width() / mDisplayRect.width());
}
} else {
// Otherwise, scale the screenshot to fit 1:1 in the current orientation
mThumbnailScale = invThumbnailScale;
}
} else {
...
}
}
在onDraw()函數中使用Canvas繪製:
@Override
protected void onDraw(Canvas canvas) {
if (mInvisible) {
return;
}
int viewWidth = mTaskViewRect.width();
int viewHeight = mTaskViewRect.height();
int thumbnailWidth = Math.min(viewWidth,
(int) (mThumbnailRect.width() * mThumbnailScale));
int thumbnailHeight = Math.min(viewHeight,
(int) (mThumbnailRect.height() * mThumbnailScale));
if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) {
int topOffset = 0;
if (mTaskBar != null && mOverlayHeaderOnThumbnailActionBar) {
topOffset = mTaskBar.getHeight() - mCornerRadius;
}
// Draw the background, there will be some small overdraw with the thumbnail
if (thumbnailWidth < viewWidth) {
// Portrait thumbnail on a landscape task view
canvas.drawRoundRect(Math.max(0, thumbnailWidth - mCornerRadius), topOffset,
viewWidth, viewHeight,
mCornerRadius, mCornerRadius, mBgFillPaint);
}
if (thumbnailHeight < viewHeight) {
// Landscape thumbnail on a portrait task view
canvas.drawRoundRect(0, Math.max(topOffset, thumbnailHeight - mCornerRadius),
viewWidth, viewHeight,
mCornerRadius, mCornerRadius, mBgFillPaint);
}
// Draw the thumbnail
canvas.drawRoundRect(0, topOffset, thumbnailWidth, thumbnailHeight,
mCornerRadius, mCornerRadius, mDrawPaint);
} else {
canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius,
mBgFillPaint);
}
}