“這是我參與8月更文挑戰的第30天,活動詳情檢視: 8月更文挑戰” juejin.cn/post/698796…
小菜前段時間學習了一下 Flutter 與原生 Android 之間的互動;是以 Android 為主工程,Flutter 作為 Module 方式進行互動;今天小菜嘗試一下 Flutter 中嵌入 Native View 的互動方式;Android 端採用 AndroidView iOS 端採用 UiKitView;小菜僅學習了 AndroidView 的基本用法;
原始碼分析
const AndroidView({
Key key,
@required this.viewType,
this.onPlatformViewCreated,
this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
this.layoutDirection,
this.gestureRecognizers,
this.creationParams,
this.creationParamsCodec,
})
複製程式碼
- viewType -> 與 Android 原生互動時唯一識別符號,常見形式是包名+自定義名;
- onPlatformViewCreated -> 建立檢視後的回撥;
- hitTestBehavior -> 滲透點選事件,接收範圍 opaque > translucent > transparent;
- layoutDirection -> 嵌入檢視文字方向;
- gestureRecognizers -> 可以傳遞到檢視的手勢集合;
- creationParams -> 向檢視傳遞引數,常為 PlatformViewFactory;
- creationParamsCodec -> 編解碼器型別;
基本用法
1. viewType
a. Android 端
- 自定義 PlatformView,可根據需求實現 Channel 互動介面;
public class NLayout implements PlatformView {
private LinearLayout mLinearLayout;
private BinaryMessenger messenger;
NLayout(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.messenger = messenger;
LinearLayout mLinearLayout = new LinearLayout(context);
mLinearLayout.setBackgroundColor(Color.rgb(100, 200, 100));
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(900, 900);
mLinearLayout.setLayoutParams(lp);
this.mLinearLayout = mLinearLayout;
}
@Override
public View getView() { return mLinearLayout; }
@Override
public void dispose() {}
}
複製程式碼
- 建立 PlatformViewFactory 用於生成 PlatformView;
public class NLayoutFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
public NLayoutFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
@Override
public PlatformView create(Context context, int i, Object o) {
Map<String, Object> params = (Map<String, Object>) o;
return new NLayout(context, messenger, i, params);
}
public static void registerWith(PluginRegistry registry) {
final String key = "NLayout";
if (registry.hasPlugin(key)) return;
PluginRegistry.Registrar registrar = registry.registrarFor(key);
registrar.platformViewRegistry().registerViewFactory("com.ace.ace_demo01/method_layout", new NLayoutFactory(registrar.messenger()));
}
}
複製程式碼
- 在 MainActivity 中註冊該元件;
NLayoutFactory.registerWith(this);
複製程式碼
b. Flutter 端
建立 AndroidView 並設定與原生相同的 viewType;
return ListView(children: <Widget>[
Container(child: AndroidView(viewType: "com.ace.ace_demo01/method_layout"),
color: Colors.pinkAccent, height: 400.0),
Container(child: AndroidView(viewType: "com.ace.ace_demo01/method_layout"),
color: Colors.greenAccent, height: 200.0)
]);
複製程式碼
c. 相關小結
- 小菜對比兩個 Container 高度,Container 尺寸大於 AndroidView 對應的原生 View 尺寸時,完全展示;相反小於時則會裁剪 AndroidView 對應的原生 View;
- 兩個 Container 背景色均未展示,小菜理解是 AndroidView 是填充滿 Container 的,只是 AndroidView 中展示效果跟原生 View 尺寸相關;
- AndroidView 中未填充滿的部分會展示白色或黑色背景色,與 Android 主題版本 或 裝置 相關;
2. creationParams / creationParamsCodec
creationParams 與 creationParamsCodec 一般成對使用,creationParams 為預設傳遞引數,creationParamsCodec 為編解碼器型別;
// Flutter 端 預設傳遞不同尺寸引數
return ListView(children: <Widget>[
Container(child: AndroidView(viewType: "com.ace.ace_demo01/method_layout0",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'method_layout_size': 150}),
color: Colors.pinkAccent,height: 400.0),
Container(child: AndroidView(viewType: "com.ace.ace_demo01/method_layout0",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'method_layout_size': 450}),
color: Colors.greenAccent,height: 200.0)
]);
// Android NLayout
public class NLayout implements PlatformView {
private LinearLayout mLinearLayout;
private BinaryMessenger messenger;
private int size = 0;
NLayout(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.messenger = messenger;
LinearLayout mLinearLayout = new LinearLayout(context);
mLinearLayout.setBackgroundColor(Color.rgb(100, 200, 100));
if (params != null && params.containsKey("method_layout_size")) {
size = Integer.parseInt(params.get("method_layout_size").toString());
} else {
size = 900;
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(size,size);
mLinearLayout.setLayoutParams(lp);
this.mLinearLayout = mLinearLayout;
}
@Override
public View getView() { return mLinearLayout; }
@Override
public void dispose() {}
}
複製程式碼
3. onPlatformViewCreated
Flutter 與 Android 互動一般藉助 MethodChannel / BasicMessageChannel / EventChannel 三種方式進行橋接互動;小菜以自定義 TextView 進行嘗試;PlatformViewFactory 基本一致,只是更換初始化和註冊的 N...TextView 即可;自定義 N...TextView 需實現各自的 Channel 方式;
MethodChannel 方式
// Flutter 端
return Container(height: 80.0, child: AndroidView(
onPlatformViewCreated: (id) async {
MethodChannel _channel = const MethodChannel('ace_method_text_view');
_channel..invokeMethod('method_set_text', 'Method_Channel')..setMethodCallHandler((call) {
if (call.method == 'method_click') {
_toast('Method Text FlutterToast!', context);
}
});
},
viewType: "com.ace.ace_demo01/method_text_view",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'method_text_str': 'Method Channel Params!!'}));
// Android NMethodTextView
public class NMethodTextView implements PlatformView, MethodChannel.MethodCallHandler {
private TextView mTextView;
private MethodChannel methodChannel;
private BinaryMessenger messenger;
NMethodTextView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.messenger = messenger;
TextView mTextView = new TextView(context);
mTextView.setText("我是來自Android的原生TextView");
mTextView.setBackgroundColor(Color.rgb(155, 205, 155));
mTextView.setGravity(Gravity.CENTER);
mTextView.setTextSize(16.0f);
if (params != null && params.containsKey("method_text_str")) {
mTextView.setText(params.get("method_text_str").toString());
}
this.mTextView = mTextView;
methodChannel = new MethodChannel(messenger, "ace_method_text_view");
methodChannel.setMethodCallHandler(this);
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
methodChannel.invokeMethod("method_click", "點選!");
Toast.makeText(context, "Method Click NativeToast!", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall != null && methodCall.method.toString().equals("method_set_text")) {
mTextView.setText(methodCall.arguments.toString());
result.success("method_set_text_success");
}
}
@Override
public View getView() { return mTextView; }
@Override
public void dispose() { methodChannel.setMethodCallHandler(null); }
}
複製程式碼
BasicMessageChannel 方式
// Flutter 端
return Container(height: 80.0, child: AndroidView(
hitTestBehavior: PlatformViewHitTestBehavior.translucent,
onPlatformViewCreated: (id) async {
BasicMessageChannel _channel = const BasicMessageChannel('ace_basic_text_view', StringCodec());
_channel..send("Basic_Channel")..setMessageHandler((message) {
if (message == 'basic_text_click') {
_toast('Basic Text FlutterToast!', context);
}
print('===${message.toString()}==');
});
},
viewType: "com.ace.ace_demo01/basic_text_view",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'basic_text_str': 'Basic Channel Params!!'}));
// Android NBasicTextView
public class NBasicTextView implements PlatformView, BasicMessageChannel.MessageHandler {
private TextView mTextView;
private BasicMessageChannel basicMessageChannel;
private BinaryMessenger messenger;
NBasicTextView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.messenger = messenger;
TextView mTextView = new TextView(context);
mTextView.setTextColor(Color.rgb(155, 155, 205));
mTextView.setBackgroundColor(Color.rgb(155, 105, 155));
mTextView.setGravity(Gravity.CENTER);
mTextView.setTextSize(18.0f);
if (params != null && params.containsKey("basic_text_str")) {
mTextView.setText(params.get("basic_text_str").toString());
}
this.mTextView = mTextView;
basicMessageChannel = new BasicMessageChannel(messenger, "ace_basic_text_view", StringCodec.INSTANCE);
basicMessageChannel.setMessageHandler(this);
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
basicMessageChannel.send("basic_text_click");
Toast.makeText(context, "Basic Click NativeToast!", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public View getView() { return mTextView; }
@Override
public void dispose() { basicMessageChannel.setMessageHandler(null); }
@Override
public void onMessage(Object o, BasicMessageChannel.Reply reply) {
if (o != null){
mTextView.setText(o.toString());
basicMessageChannel.send("basic_set_text_success");
}
}
}
複製程式碼
EventChannel 方式
// Flutter 端
return Container(height: 80.0, child: AndroidView(
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
onPlatformViewCreated: (id) async {
EventChannel _channel = const EventChannel('ace_event_text_view');
_channel.receiveBroadcastStream('Event_Channel').listen((message) {
if (message == 'event_text_click') {
_toast('Event Text FlutterToast!', context);
}
});
},
viewType: "com.ace.ace_demo01/event_text_view",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'event_text_str': 'Event Channel Params!!'}));
// Android EventChannel
public class NEventTextView implements PlatformView, EventChannel.StreamHandler {
private TextView mTextView;
private EventChannel eventChannel;
private BinaryMessenger messenger;
NEventTextView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.messenger = messenger;
TextView mTextView = new TextView(context);
mTextView.setTextColor(Color.rgb(250, 105, 25));
mTextView.setBackgroundColor(Color.rgb(15, 200, 155));
mTextView.setGravity(Gravity.CENTER);
mTextView.setPadding(10, 10, 10, 10);
mTextView.setTextSize(20.0f);
if (params != null && params.containsKey("event_text_str")) {
mTextView.setText(params.get("event_text_str").toString());
}
this.mTextView = mTextView;
eventChannel = new EventChannel(messenger, "ace_event_text_view");
eventChannel.setStreamHandler(this);
mTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(context, "Event Click NativeToast!", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public View getView() { return mTextView; }
@Override
public void dispose() { eventChannel.setStreamHandler(null); }
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
if (o != null) {
mTextView.setText(o.toString());
eventSink.success("event_set_text_success");
}
}
@Override
public void onCancel(Object o) {}
}
複製程式碼
4. gestureRecognizers
針對不同的 View 需要的手勢有所不同,上述 TextView 沒有設定手勢集合,預設支援點選,但對於 ListView 之類的需要滑動手勢或長按點選的話則需要新增 gestureRecognizers 手勢集合;
// Flutter 端
return Container(height: 480.0,
child: GestureDetector(
child: AndroidView(
gestureRecognizers: Set()..add(Factory<VerticalDragGestureRecognizer>(() => VerticalDragGestureRecognizer()))
..add(Factory<LongPressGestureRecognizer>(() => LongPressGestureRecognizer())),
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
onPlatformViewCreated: (id) async {
MethodChannel _channel = const MethodChannel('ace_method_list_view');
_channel..invokeMethod('method_set_list', 15)..setMethodCallHandler((call) {
if (call.method == 'method_item_click') {
_toast('List FlutterToast! position -> ${call.arguments}', context);
} else if (call.method == 'method_item_long_click') {
_toast('List FlutterToast! -> ${call.arguments}', context);
}
});
},
viewType: "com.ace.ace_demo01/method_list_view",
creationParamsCodec: const StandardMessageCodec(),
creationParams: {'method_list_size': 10})));
// Android NMethodListView
public class NMethodListView implements PlatformView, MethodChannel.MethodCallHandler, ListView.OnItemClickListener, ListView.OnItemLongClickListener {
private Context context;
private ListView mListView;
private MethodChannel methodChannel;
private List<Map<String, String>> list = new ArrayList<>();
private SimpleAdapter simpleAdapter = null;
private int listSize = 0;
NMethodListView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
this.context = context;
ListView mListView = new ListView(context);
if (params != null && params.containsKey("method_list_size")) {
listSize = Integer.parseInt(params.get("method_list_size").toString());
}
if (list != null) { list.clear(); }
for (int i = 0; i < listSize; i++) {
Map<String, String> map = new HashMap<>();
map.put("id", "current item = " + (i + 1));
list.add(map);
}
simpleAdapter = new SimpleAdapter(context, list, R.layout.list_item, new String[] { "id" }, new int[] { R.id.item_tv });
mListView.setAdapter(simpleAdapter);
mListView.setOnItemClickListener(this);
mListView.setOnItemLongClickListener(this);
this.mListView = mListView;
methodChannel = new MethodChannel(messenger, "ace_method_list_view");
methodChannel.setMethodCallHandler(this);
}
@Override
public View getView() { return mListView; }
@Override
public void dispose() { methodChannel.setMethodCallHandler(null); }
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall != null && methodCall.method.toString().equals("method_set_list")) {
if (list != null) { list.clear(); }
for (int i = 0; i < Integer.parseInt(methodCall.arguments.toString()); i++) {
Map<String, String> map = new HashMap<>();
map.put("id", "current item = " + (i + 1));
list.add(map);
}
simpleAdapter.notifyDataSetChanged();
result.success("method_set_list_success");
}
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
methodChannel.invokeMethod("method_item_click", position);
Toast.makeText(context, "ListView.onItemClick NativeToast! position -> " + position, Toast.LENGTH_SHORT).show();
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
methodChannel.invokeMethod("method_item_long_click", list.get(position).get("id"));
Toast.makeText(context, "ListView.onItemLongClick NativeToast! " + list.get(position).get("id"), Toast.LENGTH_SHORT).show();
return true;
}
}
複製程式碼
5. hitTestBehavior
小菜嘗試了資料繫結和手勢操作,但重要的一點是資料透傳,小菜在 Flutter / Android 兩端新增了 Toast 進行測試;
a. opaque
使用 PlatformViewHitTestBehavior.opaque 方式,兩端均可監聽處理,小菜理解,若有疊加 AndroidView 則不會透傳到下一層;注意 PlatformView 只可在 AndroidView 範圍內展示;
b. translucent
使用 PlatformViewHitTestBehavior.translucent 方式,兩端均可監聽處理,小菜理解,若有疊加 AndroidView 則可以透傳到下一層;
c. transparent
使用 PlatformViewHitTestBehavior.transparent 方式,兩端均不會透傳展示;
小菜在測試時,NMethodListView 設定高度超過剩餘空間高度,例 Container 高度設定 500.0 可實際螢幕剩餘高度只有 300.0,因 transparent 不會透傳,所以 Flutter 外層 ListView 可以滑動,NMethodListView 不會滑動;使用 opaque / translucent 方式,NMethodListView 可以滑動,Flutter 外層 ListView 不能滑動,故有 200.0 高度展示不出來;
小結
- 使用 AndroidView 時,Android API > 20;
- 使用 AndroidView 時均需要有界父類;
- 官網明確提醒,AndroidView 方式代價較大,由於是 GPU -> CPU -> GPU 有明顯的效能缺陷,儘量避免使用;
- 測試過程中熱過載無效,每次均需重新編譯;
小菜對兩端的互動理解還不夠深入,尤其是專有名詞的理解還不到位,如有問題請多多指導!
來源:阿策小和尚