FlutterView 技术分享
Android 平台上 Flutter UI 的承载核心
一、FlutterView 概述
FlutterView 是 Android 平台上承载 Flutter UI 的核心组件,继承自 FrameLayout
,负责连接 Flutter 引擎与 Android 视图系统。
关键特性:
多渲染模式支持
Surface
模式:使用SurfaceView
,性能最佳Texture
模式:使用TextureView
,支持动画和视图层级混合ImageView
模式:后台静态渲染,节省资源
全生命周期管理
引擎绑定/解绑
状态保存/恢复
跨 Activity/Fragment 复用
平台集成桥梁
触摸事件处理
键盘输入管理
无障碍服务支持
折叠屏/刘海屏适配
场景 | 推荐模式 | 优势 |
---|---|---|
常规页面 | Surface | 60fps 满帧率,功耗最低 |
包含动画的页面 | Texture | 支持变换/透明度/视图混合 |
后台态 | ImageView | 内存占用降低 40%,避免空渲染 |
二、核心实现机制
1. 引擎绑定机制
// Activity/Fragment 中
override fun onStart() {
super.onStart()
flutterView.attachToFlutterEngine(flutterEngine)
}
override fun onStop() {
super.onStop()
if (isChangingConfigurations.not()) {
flutterView.detachFromFlutterEngine()
}
}
初始化各种插件,绑定渲染引擎
// 绑定引擎(完整流程)
public void attachToFlutterEngine(@NonNull FlutterEngine engine) {
// 1. 渲染器连接
renderSurface.attachToRenderer(engine.getRenderer());
engine.getRenderer().addIsDisplayingFlutterUiListener(uiListener);
// 2. 初始化事件处理器
textInputPlugin = new TextInputPlugin(this, engine.getTextInputChannel());
accessibilityBridge = new AccessibilityBridge(...);
// 3. 平台视图控制器连接
engine.getPlatformViewsController().attachToView(this);
// 4. 状态同步
sendUserSettingsToFlutter();
sendViewportMetricsToFlutter();
}
// 解绑时的资源清理
public void detachFromFlutterEngine() {
// 1. 释放无障碍服务
accessibilityBridge.release();
// 2. 断开渲染连接
flutterEngine.getRenderer().removeIsDisplayingFlutterUiListener(uiListener);
renderSurface.detachFromRenderer();
// 3. 销毁插件实例
textInputPlugin.destroy();
keyboardManager.destroy();
}
三、关键技术点
1.渲染模式切换
// 切换到静态图像渲染(用于后台优化)
public void convertToImageView() {
renderSurface.pause();
if (flutterImageView == null) {
flutterImageView = new FlutterImageView(...);
addView(flutterImageView);
}
previousRenderSurface = renderSurface;
renderSurface = flutterImageView;
renderSurface.attachToRenderer(flutterEngine.getRenderer());
}
// 视口数据同步(关键性能点)
private void sendViewportMetricsToFlutter() {
viewportMetrics.devicePixelRatio = getResources().getDisplayMetrics().density;
viewportMetrics.physicalTouchSlop = ViewConfiguration.getScaledTouchSlop();
flutterEngine.getRenderer().setViewportMetrics(viewportMetrics);
}
2. 刘海屏兼容处理
// 刘海屏适配(API 28+)
@TargetApi(API_LEVELS.API_28)
protected void handleDisplayCutout(WindowInsets insets) {
if (Build.VERSION.SDK_INT >= API_LEVELS.API_28) {
DisplayCutout cutout = insets.getDisplayCutout();
for (Rect bounds : cutout.getBoundingRects()) {
viewportMetrics.displayFeatures.add(
new DisplayFeature(bounds, DisplayFeatureType.CUTOUT)
);
}
sendViewportMetricsToFlutter();
}
}
// 折叠屏状态同步
private void handleFoldingFeature(FoldingFeature feature) {
DisplayFeatureType type = (feature.getOcclusionType() == FULL) ?
DisplayFeatureType.HINGE : DisplayFeatureType.FOLD;
viewportMetrics.displayFeatures.add(
new DisplayFeature(feature.getBounds(), type, getFeatureState(feature))
);
}
3. 性能选择性重绘
// 选择性重绘(减少不必要的 GPU 调用)
private void resetWillNotDraw(boolean isAccessibilityEnabled,
boolean isTouchExplorationEnabled) {
// 无障碍模式下需要强制重绘
setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
}
// 触摸事件优化(减少事件传递层级)
@Override
public boolean onTouchEvent(MotionEvent event) {
requestUnbufferedDispatch(event); // 跳过缓冲直接处理
return androidTouchProcessor.onTouchEvent(event);
}
4. 无障碍深度支持
// 复杂视图树的无障碍节点查找
@SuppressLint("DiscouragedPrivateApi")
private View findViewByAccessibilityIdRootedAtCurrentView(int id, View view) {
// 深度优先搜索遍历视图树
if (view.getAccessibilityViewId() == id) return view;
if (view instanceof ViewGroup) {
for (int i = 0; i < ((ViewGroup)view).getChildCount(); i++) {
View result = findViewByAccessibilityId(...);
if (result != null) return result;
}
}
return null;
}
// 悬停事件转无障碍事件
@Override
public boolean onHoverEvent(MotionEvent event) {
return accessibilityBridge.onAccessibilityHoverEvent(event);
}
5. 键盘问题解决方案
// 键盘高度精确计算
private int guessBottomKeyboardInset(WindowInsets insets) {
int screenHeight = getRootView().getHeight();
return (insets.getSystemWindowInsetBottom() > screenHeight * 0.18) ?
insets.getSystemWindowInsetBottom() : 0; // 过滤掉导航栏
}
// 在 onApplyWindowInsets 中同步
viewportMetrics.viewInsetBottom = guessBottomKeyboardInset(insets);
sendViewportMetricsToFlutter();
6.触摸事件处理
/**
* Invoked by Android when a user touch event occurs.
*
* <p>Flutter handles all of its own gesture detection and processing, therefore this method
* forwards all {@link MotionEvent} data from Android to Flutter.
*/
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) {
return super.onTouchEvent(event);
}
requestUnbufferedDispatch(event);
return androidTouchProcessor.onTouchEvent(event);
}
将触摸各种参数封装成一个数据包,发送到引擎
/**
* Sends the given {@link MotionEvent} data to Flutter in a format that Flutter understands.
*
* @param event The motion event from the view.
* @param transformMatrix Applies to the view that originated the event. It's used to transform
* the gesture pointers into screen coordinates.
* @return True if the event was handled.
*/
public boolean onTouchEvent(@NonNull MotionEvent event, @NonNull Matrix transformMatrix) {
int pointerCount = event.getPointerCount();
// The following packing code must match the struct in pointer_data.h.
// Prepare a data packet of the appropriate size and order.
ByteBuffer packet =
ByteBuffer.allocateDirect(pointerCount * POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD);
packet.order(ByteOrder.LITTLE_ENDIAN);
int maskedAction = event.getActionMasked();
int pointerChange = getPointerChangeForAction(event.getActionMasked());
boolean updateForSinglePointer =
maskedAction == MotionEvent.ACTION_DOWN || maskedAction == MotionEvent.ACTION_POINTER_DOWN;
boolean updateForMultiplePointers =
!updateForSinglePointer
&& (maskedAction == MotionEvent.ACTION_UP
|| maskedAction == MotionEvent.ACTION_POINTER_UP);
if (updateForSinglePointer) {
// ACTION_DOWN and ACTION_POINTER_DOWN always apply to a single pointer only.
addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, transformMatrix, packet);
} else if (updateForMultiplePointers) {
// ACTION_UP and ACTION_POINTER_UP may contain position updates for other pointers.
// We are converting these updates to move events here in order to preserve this data.
// We also mark these events with a flag in order to help the framework reassemble
// the original Android event later, should it need to forward it to a PlatformView.
for (int p = 0; p < pointerCount; p++) {
if (p != event.getActionIndex() && event.getToolType(p) == MotionEvent.TOOL_TYPE_FINGER) {
addPointerForIndex(
event, p, PointerChange.MOVE, POINTER_DATA_FLAG_BATCHED, transformMatrix, packet);
}
}
// It's important that we're sending the UP event last. This allows PlatformView
// to correctly batch everything back into the original Android event if needed.
addPointerForIndex(event, event.getActionIndex(), pointerChange, 0, transformMatrix, packet);
} else {
// ACTION_MOVE may not actually mean all pointers have moved
// but it's the responsibility of a later part of the system to
// ignore 0-deltas if desired.
for (int p = 0; p < pointerCount; p++) {
addPointerForIndex(event, p, pointerChange, 0, transformMatrix, packet);
}
}
// Verify that the packet is the expected size.
if (packet.position() % (POINTER_DATA_FIELD_COUNT * BYTES_PER_FIELD) != 0) {
throw new AssertionError("Packet position is not on field boundary");
}
// Send the packet to Flutter.
renderer.dispatchPointerDataPacket(packet, packet.position());
return true;
}
public void dispatchPointerDataPacket(@NonNull ByteBuffer buffer, int position) {
flutterJNI.dispatchPointerDataPacket(buffer, position);
}