AOSP14 Launcher3——手势模式下底部上滑的两种场景
这里强调的是手势模式下,三按钮模式不在本文讨论范围内。
手势模式下,我们可以在Launcher桌面的底部使用手势上滑停顿进最近任务,或者在第三方应用底部上滑进最近任务。
这两种场景是手势模式底部上滑的两种常见场景,本文来讨论一下这两种场景的流程和原理。
第三方应用(非Launcher)底部上滑
Launcher底部上滑
TouchInteractionService
Launcher中提到底部上滑,绕不开TouchInteractionService这个类。
这个类是Launcher中的一个核心类,是一个Service,在Launcher3启动的时候会加载的服务。
里面会对输入事件进行监听,最终在onInputEvent回调方法中对事件进行分发处理。
private void onInputEvent(InputEvent ev) {
if (!(ev instanceof MotionEvent)) {
ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
.append("Cannot process input event, received unknown event ")
.append(ev.toString()));
return;
}
MotionEvent event = (MotionEvent) ev;
}
这里首先对事件进行判断,确保是触摸事件。
接着在ACTION_DOWN的时候会确认事件的消费者。
if (action == ACTION_DOWN || isHoverActionWithoutConsumer) {
mRotationTouchHelper.setOrientationTransformIfNeeded(event);
boolean isOneHandedModeActive = mDeviceState.isOneHandedModeActive();
boolean isInSwipeUpTouchRegion = mRotationTouchHelper.isInSwipeUpTouchRegion(event);
if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
|| isHoverActionWithoutConsumer) {
reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion
? "one handed mode is not active and event is in swipe up region"
: "isHoverActionWithoutConsumer == true")
.append(", creating new input consumer");
// Clone the previous gesture state since onConsumerAboutToBeSwitched might trigger
// onConsumerInactive and wipe the previous gesture state
GestureState prevGestureState = new GestureState(mGestureState);
GestureState newGestureState = createGestureState(mGestureState,
getTrackpadGestureType(event));
newGestureState.setSwipeUpStartTimeMs(SystemClock.uptimeMillis());
mConsumer.onConsumerAboutToBeSwitched();
mGestureState = newGestureState;
mConsumer = newConsumer(prevGestureState, mGestureState, event);
mUncheckedConsumer = mConsumer;
}
这里有一句非常重要的代码
mConsumer = newConsumer(prevGestureState, mGestureState, event);
这里根据系统的整体情况,会创建不同的consumer。
这里的consumer就是对触摸事件最终处理的消费者。
在Launcher3中,有多种Consumer,针对不同的情况。本文提到的两种场景,就涉及到其中的两种,分别是OtherActivityConsumer和OverviewInputConsumer
整个Launcher3中还有多种consumer,根据不同的场景,可以找到对应的consumer,这里不再赘述。
总之,可以明确的一点是,前面提到的两个场景分别对应的是OtherActivityConsumer和OverviewInputConsumer。
下面分别来介绍这两个重要的类。
OtherActivityConsumer
- 核心作用: 负责处理起始于非 Launcher 活动窗口(即“Other Activity”)的上滑手势。这是实现从应用返回桌面、进入概览(最近任务)或进行快速切换(左右滑动切换任务)的主要入口。
- 关键职责:
- 启动/继续 Recents 动画: 当手势开始时,它负责通过
TaskAnimationManager
启动或继续一个RecentsAnimation
。这个动画允许 Launcher 控制应用窗口的 Surface。 - 创建和管理
AbsSwipeUpHandler
: 它是实际处理手势逻辑和动画细节的核心。OtherActivityInputConsumer
会创建一个AbsSwipeUpHandler
(的具体子类实例,如LauncherSwipeHandlerV2
),并将触摸事件传递给它。 - 手势追踪与状态判断: 使用
VelocityTracker
追踪手指速度,结合MotionPauseDetector
检测手势是否暂停,判断用户的意图(去 Home、去 Recents、切换任务等)。 - 事件拦截 (Pilfering): 一旦确认用户正在进行手势导航(超过
TouchSlop
),它会调用mInputMonitorCompat.pilferPointers()
来拦截后续的触摸事件,防止这些事件传递给下方的应用窗口。 - 协调
AbsSwipeUpHandler
: 将计算出的位移、速度传递给AbsSwipeUpHandler
,由后者驱动窗口和 Launcher UI 的动画。 - 生命周期管理: 管理自身和
AbsSwipeUpHandler
的状态,并在手势完成或被取消时通知系统(通过mOnCompleteCallback
)。
- 启动/继续 Recents 动画: 当手势开始时,它负责通过
- 使用场景:
- 用户在任何第三方应用或系统应用(非 Launcher)界面,从屏幕底部边缘向上滑动时。 这是最常见的场景,涵盖了:
- 上滑返回桌面 (Go Home)。
- 上滑并暂停进入概览/最近任务 (Enter Overview/Recents)。
- 上滑并快速左右滑动切换到上一个/下一个应用 (Quick Switch)。
- 用户在任何第三方应用或系统应用(非 Launcher)界面,从屏幕底部边缘向上滑动时。 这是最常见的场景,涵盖了:
OverviewInputConsumer
- 核心作用: 负责处理发生在 Launcher/概览活动界面内的输入事件。当用户已经在与 Launcher 的 UI(如 RecentsView 中的任务卡片、主屏幕背景等)交互时,这个消费者会被激活。
- 关键职责:
- 事件代理/转发: 它的主要工作是将接收到的触摸事件 (
onMotionEvent
) 转发给 Launcher 的DragLayer
(mTarget.proxyTouchEvent
)。它本质上是一个事件传递者。 - 坐标转换: 在转发事件前,会调整事件坐标,使其相对于
DragLayer
。 - 处理其他输入: 可以处理悬停事件 (
onHoverEvent
) 和按键事件 (onKeyEvent
),并将它们分发给 Launcher Activity。对于按键,它可能会处理特定的按键(如音量键、方向键的焦点处理)。 - 选择性拦截: 如果触摸事件起始于屏幕边缘但在活动范围内被
DragLayer
处理了 (mTargetHandledTouch
变为 true),它也会调用mInputMonitor.pilferPointers()
,确保 Launcher 完全接管事件处理。
- 事件代理/转发: 它的主要工作是将接收到的触摸事件 (
- 使用场景:
- 用户已经在概览/最近任务界面时:
- 点击某个任务卡片来启动应用。
- 在任务卡片上进行滑动操作(例如,上滑清除任务)。
- 左右滑动浏览不同的任务卡片。
- 点击概览界面上的其他按钮或空白区域。
- 在返回桌面的过渡动画中与 Launcher 交互: 当用户从应用返回桌面,动画正在进行时,如果用户触摸到 Launcher 的某个元素(如图标),
OverviewInputConsumer
会接管事件处理。 - 键盘导航: 处理在概览界面中的键盘焦点和导航事件。
- 用户已经在概览/最近任务界面时:
两个Consumer的主要区别总结:
特性 | OtherActivityInputConsumer | OverviewInputConsumer |
---|---|---|
事件起始位置 | 非 Launcher/概览 活动窗口 | Launcher/概览 活动窗口内部 |
核心功能 | 启动和管理手势导航动画与逻辑 | 代理和转发输入事件给 Launcher UI |
动画控制 | 主动驱动窗口和 Launcher UI 动画 (通过 Handler) | 不直接控制窗口动画,依赖 Launcher 自身处理 |
事件处理方式 | 解释事件用于手势状态判断和动画进度控制 | 转发事件给 DragLayer 或 Activity |
交互对象 | AbsSwipeUpHandler , RecentsAnimationController | DragLayer , Activity |
主要目的 | 实现从 外部应用 进入 Launcher/概览/切换任务 | 处理在 Launcher/概览内部 的用户交互 |
两个场景的区别
这两个场景看上去都是进入了最近任务,但是区别还是很大的。
通过第三方应用进入最近任务,如果这个第三方应用是一个视频应用,例如爱奇艺在播放视频,你会发现,在进入最近任务后,爱奇艺仍然在播放视频,中间的任务是动态实时展示的。
而通过Launcher底部进入最近任务,中间的任务是静态的,展示的是应用的截图。
为什么出现这两种情况?这是由于这两种场景完全不同的实现方式决定的。
前一种情况,简单的说,第三方应用底部缩放的时候是将应用本身进行缩放,然后通过RemoteAnimationTarget将应用的画面模拟成task的样子显示在最近任务中,实际上,显示的画面是应用画面且实时更新的。
这一部分可以从AbsSwipeUpHandler的核心方法apply中看出来,代码如下:
//aosp/packages/apps/Launcher3/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
protected void applyScrollAndTransform() {
// No need to apply any transform if there is ongoing swipe-to-home animator
// swipe-to-pip handles the leash solely
// swipe-to-icon animation is handled by RectFSpringAnim anim
boolean notSwipingToHome = mRecentsAnimationTargets != null
&& mGestureState.getEndTarget() != HOME;
boolean setRecentsScroll = mRecentsViewScrollLinked && mRecentsView != null;
float progress = Math.max(mCurrentShift.value, getScaleProgressDueToScroll());
int scrollOffset = setRecentsScroll ? mRecentsView.getScrollOffset() : 0;
Log.i("yy_log", "AbsSwipeUpHandler#applyScrollAndTransform, notSwipingToHome: " + notSwipingToHome
+ ", setRecentsScroll: " + setRecentsScroll + ", progress: " + progress + ", scrollOffset: " + scrollOffset, new Exception());
if (!mStartMovingTasks && (progress > 0 || scrollOffset != 0)) {
mStartMovingTasks = true;
startInterceptingTouchesForGesture();
}
for (RemoteTargetHandle remoteHandle : mRemoteTargetHandles) {
AnimatorControllerWithResistance playbackController =
remoteHandle.getPlaybackController();
if (playbackController != null) {
playbackController.setProgress(progress, mDragLengthFactor);
}
if (notSwipingToHome) {
// 进入最近任务
TaskViewSimulator taskViewSimulator = remoteHandle.getTaskViewSimulator();
if (setRecentsScroll) {
taskViewSimulator.setScroll(scrollOffset);
}
//将第三方应用的画面模拟成taskview
taskViewSimulator.apply(remoteHandle.getTransformParams());
}
}
}
后一种情况,前面我们看到的是走的OverviewInputConsumer,这种情况下,最终会将触摸事件代理到BaseDragLayer,proxyTouchEvent里面会去寻找touchController,如下代码:
mProxyTouchController = findControllerToHandleTouch(ev);
07-04 22:01:05.140 3310 3310 I yy_log : BaseDragLayer#proxyTouchEvent, mProxyTouchController: com.android.launcher3.uioverrides.touchcontrollers.NoButtonNavbarToOverviewTouchController@ae19028
这里打个日志,可以看到最终找到的Controller是NoButtonNavbarToOverviewTouchController
最终在手势停止的时候回调onMotionPauseDetected方法,进入OVERVIEW状态。因此,这个场景与上面的是完全不同的两个场景,虽然最终看上去都是进入了最近任务页面。
private void onMotionPauseDetected() {
if (mCurrentAnimation == null) {
return;
}
mNormalToHintOverviewScrimAnimator = null;
mCurrentAnimation.getTarget().addListener(newCancelListener(() ->
mLauncher.getStateManager().goToState(OVERVIEW, true, forSuccessCallback(() -> {
mOverviewResistYAnim = AnimatorControllerWithResistance
.createRecentsResistanceFromOverviewAnim(mLauncher, null)
.createPlaybackController();
mReachedOverview = true;
maybeSwipeInteractionToOverviewComplete();
}))));
mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener);
mCurrentAnimation.dispatchOnCancel();
mStartedOverview = true;
VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC);
}