欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SystemUI RecentTask 流程分析

程序员文章站 2022-04-15 22:14:54
...

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);
	}
}