Flutter 长截屏适配 Miui 系统,一点都不难

本文详细介绍了Flutter应用在小米系统中长截屏的适配方案,包括小米长截屏的实现原理、闲鱼适配过程中遇到的问题及解决方案,如通过创建ControlView实现系统指令传递,以及使用异步Channel和同步FFI通信方案确保流畅性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景

现有 App 大部分业务场景都是以长列表呈现,为更好满足用户内容分享的诉求,Android 各大厂商都在系统层面提供十分便捷的长截屏能力。然而我们发现 Flutter 长列表页面在部分 Android 手机上无法截长屏,Flutter 官方和社区也没有提供框架层面的长截屏能力。
闲鱼作为 Flutter 在国内业务落地的代表作,大部分页面都以 Flutter 承接。为了闲鱼用户也能享受厂商系统的长截屏能力,更好的满足商品、社区内容分享的诉求,闲鱼技术团队主动做了分析和适配。

针对线上舆情做了统计分析,发现小米用户舆情反馈量占比最多,其次少量是华为用户。为此我们针对 Miui 长截屏功能做了适配。

这里华为、OPPO、VIVO 基于无障碍服务实现,长截屏功能已经适配 Flutter 页面。这里少量用户反馈,是因为截屏反馈小把手 PopupWindow 有可能出现遮挡,导致系统无法驱动长列表滚动。通过重写 isImportantForAccessibility 便能解决。

小米长截屏解读

操作和表现

35277be3a6843d6d296571f26aa4132d.png小米手机可通过音量 键+电源键、或顶部下拉功能菜单“截屏”,触发截屏。经过简单尝试,可以发现,原生长列表页面支持截长屏,原生页面无长列表不支持,闲鱼 Flutter 长列表页面(如详情页、搜索结果页)不支持。 277cadf4fed9c36e28ce15bd6146e254.png 点击“截长屏”后,能看到长列表页面会自动滚动,点击结束或者触底的时候,自动打开图片编辑页面,能看到生成的长截图。那小米系统是如何无侵入的实现以下关键点:
  1. 1. 当前页面是否支持滚动截屏(长截屏 按钮是否置灰)

  2. 2. 如何触发 App 长列表页面滚动

  3. 3. 如何判断是否已经滚动触底

  4. 4. 如何合成长截图

系统源码获取

小米厂商能判断前台 App 页面能否滚动,必然需要调用前台 App 视图的关键接口来获取信息。编写一个自定义 RecyclerView 列表页面,日志输出 RecycleView 方法调用:698644a35aa8fad48f6710083b1afce4.png已知长截屏需要调用的方法,再查看堆栈,可以看到调用方是系统类:miui.util.LongScreenshotUtils&ContentPort

20ea3eb007e829f9d6dcca02513fd3e3.png

使用低版本 miui(这里 miui8)手机,获取对应的代码:/system/framework/framework.jar 或 github 查找 miui 开放代码。

实现原理介绍

整体流程:查找滚动视图 → 驱动视图滚动 → 分段截图→截图内容合并

查找滚动视图

fe10e6f65ed221a4d3b9088b7cf6a779.png

其中检查条件:

  1. 1. View visibility == View.VISIBLE

  2. 2. canScrollVertically(1) == true

  3. 3. View 在屏幕内的宽度 > 屏幕宽度/3

  4. 4. View 在屏幕内的高度 > 屏幕高度/2

触发视图滚动

78f47e6deef6bc7cf784d12c696830e7.png
  1. 1. 每次滚动前,使用 canScrollVertically(1) 判断是否向下滚动

  2. 2. 触发滚动逻辑

    1. a. 特殊视图: dispatchFakeTouchEvent(2);private boolean checkNeedFakeTouchForScroll() {
       if ((this.mMainScrollView instanceof AbsListView) || 
        (this.mMainScrollView instanceof ScrollView) || 
        isRecyclerView(this.mMainScrollView.getClass()) || 
        isNestedScrollView(this.mMainScrollView.getClass())) { 
        return false;
       }
       return !(this.mMainScrollView instanceof AbsoluteLayout) || 
        (Build.VERSION.SDK_INT > 19 &&
         !"com.ucmobile".equalsIgnoreCase(this.mMainScrollView.getContext().getPackageName()) &&
         !"com.eg.android.AlipayGphone".equalsIgnoreCase(this.mMainScrollView.getContext().getPackageName()));
      }

    2. b. AbsListView: scrollListBy(distance);

    3. c. 其他:view.scrollBy(0, distance);

  3. 3. 滚动结束,对比 scrollY 和 mPrevScrolledY 是否相同,相同则认为触底,停止滚动流程

生成长截图

每次滚动后广播,触发 mMainScrollView 局部截图,最后生成多个 Bitmap,最后合成 File 文件。在适配 Flutter 页面,这里并没有差异,所以这里就不做源码解读(不同 Miui 版本实现也有所不同)。

闲鱼适配方案

Flutter 长截屏不适配原因

通过分析源码可知,Flutter 容器(SurfaceView/TextureView) canScrollVertically 方法并未被重写,为此无法被找到作为 mMainScrollView。假如我们重写 Flutter 容器,我们需要真实实现 getScrollY 才能保证触发滚动后 scrollY 和 mPrevScrolledY 不相等。不幸的是,getScrollY 是 final 类型,无法被继承类重写,为此我们无法在 Flutter 容器上做处理。

@InspectableProperty
public final int getScrollY() {
    return mScrollY;
}

系统事件代理

转变思路,我们并不需要让 Flutter 容器被 Miui 系统识为可滚动视图,而是让 Flutter 接收到 Miui 系统指令。为此,我们构建一个不可见、不影响交互的滚动视图 ControlView 被 Miui 系统识别,并接收系统指令。ControlView 最后把指令传递给 Flutter,最终建立了 Miui 系统(ContentPort)和闲鱼 Flutter(可滚动 RenderObject)之间的通信。

其中通信事件:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值