Flutter 58: Graphical Flutter Embedded in Native Android View Trial

Some time ago, I learned how Flutter interacts with native Android. It's an Android-based project and Flutter interacts as a Module. Today, I try to embed Native View in Flutter, and Android uses it. AndroidView iOS End Adoption UiKitView The side dish only learns the basic usage of Android View.

Source code analysis

const AndroidView({
    Key key,
    @required this.viewType,
    this.onPlatformViewCreated,
    this.hitTestBehavior = PlatformViewHitTestBehavior.opaque,
    this.layoutDirection,
    this.gestureRecognizers,
    this.creationParams,
    this.creationParamsCodec,
})
  1. ViewType - > Unique identifier when interacting with Android native, the common form is package name + custom name;
  2. On Platform View Created - > Callback after view creation;
  3. HitTestBehavior - > Penetration Click Event, receiving range opaque > translucent > transparent;
  4. Layout Direction - > Embedded View Text Direction;
  5. Gesture Recognizers - > A set of gestures that can be passed to the view;
  6. Creation Params - > passes parameters to the view, often Platform ViewFactory;
  7. Creation Params Codec - > codec type;

Basic Usage

1. viewType

a. Android side
  1. Customize Platform View, and implement Channel interface according to requirement.
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() {}
}
  1. Create Platform ViewFactory to generate Platform View;
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()));
    }
}
  1. Register the component in MainActivity;
NLayoutFactory.registerWith(this);
b. Flutter side

Create Android View and set the same viewType as the original.

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. Relevant Summary
  1. Comparing the two Container heights, when the Container size is larger than the original View size corresponding to Android View, the dish will be fully displayed; otherwise, when the Container size is smaller, the original View corresponding to Android View will be tailored.
  2. Both Container background colors are not displayed. The side dish understands that Android View is filled with Container, but the display effect in Android View is related to the size of native View.
  3. The unfilled part of Android View displays white or black background colors, which are related to Android theme versions or devices.

2. creationParams / creationParamsCodec

CreationParams and creationParamsCodec are usually used in pairs, creationParams is the default transfer parameter and creationParamsCodec is the codec type.

// Flutter transmits parameters of different sizes by default
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 interacts with Android in three ways: Method Channel/Basic Message Channel/Event Channel; the side dish tries to customize TextView; the Platform ViewFactory is basically the same, just replacing the initialized and registered N...TextView; and customizing N...TextView needs to be real. Now they have their own Channel mode.

Method Channel Approach
// Flutter end
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("I come from Android Native 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", "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); }
}
Basic Message Channel approach
// Flutter end
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");
        }
    }
}
Event Channel approach
// Flutter end
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

TextView has no set of gesture sets and supports clicks by default, but it needs to add gesture Recognizers gesture sets for sliding gestures or long clicks like ListView.

// Flutter end
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

The dish tried data binding and gesture manipulation, but the important thing was data transmission. The dish was tested with Toast at both ends of Flutter / Android.

a. opaque

Platform View HitTest Behavior. opaque mode, both ends can be monitored and processed, the side dish understands, if there is overlay Android View will not be transmitted to the next layer; note that Platform View can only be displayed in the Android View scope;

b. translucent

Platform View HitTestBehavior. translucent mode, both ends can be monitored and processed, the side dish understanding, if there is overlay Android View can be transmitted to the next layer;

c. transparent

Platform ViewHitTestBehavior. transparent mode, both ends will not be transmitted to display;

During the test, the height of NMethodListView is higher than the height of the remaining space. For example, the height of Container is 500.0, but the real screen is only 300.0. Because transparent can not be transmitted through, Flutter's outer ListView can slide, and NMethodListView can not slide; opaque / translucent is used. In this way, NMethodListView can be sliding, while Flutter's outer ListView can't be sliding, so it can't be displayed at 200.0 altitude.

Summary

  1. When using Android View, Android API > 20;
  2. A bounded parent class is required when using AndroidView.
  3. The official website clearly reminds us that AndroidView mode is expensive, because it is GPU - > CPU - > GPU with obvious performance defects, try to avoid using it.
  4. The hot overload is invalid during the testing process and needs to be recompiled every time.

The reciprocal understanding of the two ends of the dish is not deep enough, especially the understanding of proper nouns is not in place. If you have any questions, please give more guidance.

Source: Ace Junior Monk

Keywords: Android codec iOS

Added by r-it on Mon, 26 Aug 2019 14:35:20 +0300