当前位置: 首页 > news >正文

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)。
  • 使用场景:
    • 用户在任何第三方应用或系统应用(非 Launcher)界面,从屏幕底部边缘向上滑动时。 这是最常见的场景,涵盖了:
      • 上滑返回桌面 (Go Home)。
      • 上滑并暂停进入概览/最近任务 (Enter Overview/Recents)。
      • 上滑并快速左右滑动切换到上一个/下一个应用 (Quick Switch)。

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的主要区别总结:

特性OtherActivityInputConsumerOverviewInputConsumer
事件起始位置非 Launcher/概览 活动窗口Launcher/概览 活动窗口内部
核心功能启动和管理手势导航动画与逻辑代理和转发输入事件给 Launcher UI
动画控制主动驱动窗口和 Launcher UI 动画 (通过 Handler)不直接控制窗口动画,依赖 Launcher 自身处理
事件处理方式解释事件用于手势状态判断和动画进度控制转发事件DragLayerActivity
交互对象AbsSwipeUpHandler, RecentsAnimationControllerDragLayer, 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);
    }

相关文章:

  • Zabbix 简介+部署+对接Grafana(详细部署!!)
  • Redis-集群
  • 迷你世界脚本之容器接口:WorldContainer
  • 实例3.5.2 数字签名
  • ubuntu2204安装显卡驱动+多版本的cuda+cudnn+多版本tensorRT
  • Series和 DataFrame是 Pandas 库中的两种核心数据结构
  • 设计模式——建造者模式(生成器模式)总结
  • 【从0到1学Elasticsearch】Elasticsearch从入门到精通(上)
  • MySQL 5.7.43 二进制安装指南:从零开始的高效快速实现安装部署
  • 【C++】C与C++、C++内存空间、堆与栈
  • SAX解析XML:Java程序员的“刑侦破案式“数据处理
  • JS的大数运算(注意:原生的只支持整数计算!!!)
  • 四、Appium Inspector
  • 【小沐学GIS】基于C++绘制三维数字地球Earth(QT5、OpenGL、GIS、卫星)第五期
  • 【AI大模型】大模型RAG技术Langchain4j 核心组件深入详解
  • C++项目 —— 基于多设计模式下的同步异步日志系统(2)(工厂模式)
  • CAD 像素点显示图片——CAD二次开发 OpenCV实现
  • c语言 open函数
  • 「OC」小白书读书笔记——Block的相关知识(下)
  • 实现vlan间的通信
  • 中国—美国经贸合作对接交流会在华盛顿成功举行
  • 线下哪些商家支持无理由退货?查询方法公布
  • 言短意长|西湖大学首次“走出西湖”
  • 《淮水竹亭》:一手好牌,为何打成这样
  • 行知读书会|换一个角度看见社会
  • 浙江省机电集团党委书记、董事长廉俊接受审查调查