使用低版本 miui(这里 miui8)手机,获取对应的代码:/system/framework/framework.jar 或 github 查找 miui 开放代码。
实现原理介绍
整体流程:查找滚动视图 → 驱动视图滚动 → 分段截图→截图内容合并
查找滚动视图

其中检查条件:
1. View visibility == View.VISIBLE
2. canScrollVertically(1) == true
3. View 在屏幕内的宽度 > 屏幕宽度/3
4. View 在屏幕内的高度 > 屏幕高度/2
触发视图滚动

1. 每次滚动前,使用 canScrollVertically(1) 判断是否向下滚动
2. 触发滚动逻辑
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));
}b. AbsListView: scrollListBy(distance);
c. 其他:view.scrollBy(0, distance);
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)之间的通信。
其中通信事件:
1. void scrollBy(View view, int x, int y)
2. boolean canScrollVertically(View view, int direction, boolean startScreenshot)
3. int getScrollY(View view)

关键实现源码如下
public static FrameLayout setupLongScreenshotSupport(FrameLayout parent,
View targetChild,
IMiuiLongScreenshotViewDelegate delegate) {
Context context = targetChild.getContext;
MiuiLongScreenshotView screenshotView = new MiuiLongScreenshotView(context);
screenshotView.setDelegate(delegate);
screenshotView.addView(targetChild, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
MiuiLongScreenshotControlView controlView = new MiuiLongScreenshotControlView(context);
controlView.bindRealScrollView(screenshotView);
if (parent == ) {
parent = new FrameLayout(context);
}
parent.addView(screenshotView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
parent.addView(controlView);
return parent;
}
public class MiuiLongScreenshotControlView extends ScrollView
implements MiuiScreenshotBroadcast.IListener {
private IMiuiLongScreenshotView mRealView;
...
public void bindRealScrollView(IMiuiLongScreenshotView v) {
mRealView = v;
removeAllViews;
Context context = getContext;
LinearLayout ll = new LinearLayout(context);
addView(ll);
View btn = new View(context);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
UIUtil.dp2px(context, 20000));
ll.addView(btn, lp);
resetScrollY(true);
}
public void resetScrollY(boolean startScreenshot) {
if (mRealView != ) {
setScrollY(0);
if (getWindowVisibility == VISIBLE) {
ThreadUtil.runOnUI(
-> mRealView.canScrollVertically(1, startScreenshot));
}
}
}
@Override
public void onReceiveScreenshot {
// 每次收到截屏广播,将 ControlView 滚动距离置 0
// 提前查找滚动 RenderObject 并缓存
// 提前计算 canScrollVertically
resetScrollY(true);
}
@Override
protected void onAttachedToWindow {
super.onAttachedToWindow;
mContext = getContext;
// 截屏广播监听
MiuiScreenshotBroadcast.register(mContext, this);
}
@Override
protected void onDetachedFromWindow {
super.onDetachedFromWindow;
MiuiScreenshotBroadcast.unregister(mContext, this);
}
@Override
public boolean canScrollVertically(int direction) {
if (mRealView != ) {
return mRealView.canScrollVertically(direction, false);
}
return super.canScrollVertically(direction);
}
@Override
public void scrollBy(int x, int y) {
super.scrollBy(x, y);
if (mRealView != ) {
mRealView.scrollBy(x, y);
}
}
// 代理获取 DrawingCache
@Override
public void setDrawingCacheEnabled(boolean enabled) {
super.setDrawingCacheEnabled(enabled);
if (mRealView != ) {
mRealView.setDrawingCacheEnabled(enabled);
}
}
@Override
public boolean isDrawingCacheEnabled {
if (mRealView != ) {
return mRealView.isDrawingCacheEnabled;
}
return super.isDrawingCacheEnabled;
}
@Override
public Bitmap getDrawingCache(boolean autoScale) {
Bitmap result = (mRealView != )
? mRealView.getDrawingCache(autoScale)
: super.getDrawingCache(autoScale);
return result;
}
@Override
public void destroyDrawingCache {
super.destroyDrawingCache;
if (mRealView != ) {
mRealView.destroyDrawingCache;
}
}
@Override
public void buildDrawingCache(boolean autoScale) {
super.buildDrawingCache(autoScale);
if (mRealView != ) {
mRealView.buildDrawingCache(autoScale);
}
}
// 不消费屏幕操作事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return false;
}
}
无侵入识别滚动区域
获取 RenderObject 根节点
使用 mixin 扩展 WidgetsFlutterBinding,进而获取 RenderView
关键实现源码如下:
mixin NativeLongScreenshotFlutterBinding on WidgetsFlutterBinding {
@override
void initInstances {
super.initInstances;
// 初始化
FlutterMiuiLongScreenshotPlugin.inst;
}
@override
void handleDrawFrame {
super.handleDrawFrame;
try {
NativeLongScreenshot.singleInstance._renderView = renderView;
} catch (error, stack) {
}
}
}
计算前台滚动 RenderObject
