背景

由于需要,最近需要在Flutter中应用股票走势线,寻找了很久Flutter相关第三方库,无果,没有一个是能达到要求的。于是脑子萌发了一个想法,那么我把原生的组件移植过来不也可以吗,只要达到组件的效果即可。 转载请注明出处,谢谢!

那么现在我需要做的是找一个合适的K线的原生代码写的第三方库,原生项目地址:点击查看,感谢KLineChartView的作者。

​ 先看看KLineChartView的效果图:

frc 207919a49959430bcc4161e80e252af0 - Flutter - 教你把KLineChartView移植到Flutter
image.png
frc b5942a5e4895a334963fc2e5b29c5ae2 - Flutter - 教你把KLineChartView移植到Flutter
image.png

万事具备,只欠东风,现在我们把KLineChartView装入Flutter中并使用!

  1. 第一步,新建一个Flutter项目,这里暂且就不叙述了!

  2. 第二步,打开Flutter项目中的android文件夹,并导入KLineChartViewLib!

  3. 第三步,使用原生代码把KLineChartView封装为Flutter的组件,Follow Me!

  • 封装KLineView,要求类实现PlatformView接口,如果需要处理Dart调用控件的事件,则需要实现MethodChannel.MethodCallHandler接口

    方法:getView则是需要传入的你要显示的组件对象

    onMethodCall 则是需要如何处理由Flutter传回的调用

    dispose 则是如何处理组件的资源释放

    注意:请查看构造函数,其中有个参数叫 params,该参数则是描述该组件的初始化参数。

    class KLineView(context: Context?, messenger: BinaryMessenger, id: Int, params: Map?) : PlatformView, MethodChannel.MethodCallHandler {
    
        private var klineChartView: KLineChartView = KLineChartView(context)
        private val klineAdapter by lazy { KLineChartAdapter() }
        private var mainDrawType: Status = Status.MA
        private var childDrawPosition = -1
        private var isShowMainDrawLine = false
    
        init {
            val gridRowCount: Int = when {
                params?.containsKey("gridRowCount") != true || params["gridRowCount"] == null -> 4
                else -> params["gridRowCount"].toString().toInt()
            }
            val gridColumnCount: Int = when {
                params?.containsKey("gridColumnCount") != true || params["gridColumnCount"] == null -> 4
                else -> params["gridColumnCount"].toString().toInt()
            }
            klineChartView.apply {
                adapter = klineAdapter
                dateTimeFormatter = DateFormatter()
                setGridRows(gridRowCount)
                setGridColumns(gridColumnCount)
            }
            MethodChannel(messenger, "${KLineViewFlutterPlugin.ViewTypeID}_$id").setMethodCallHandler(this) ///这里初始化一个MethodChanel对象,为了使Dart能够正常调用这边的方法及相关属性的更改,id则是每个原生组件创建时会由系统生成一个ID号,用于识别该组件,在Dart代码中也需要用到该ID。
        }
    
        override fun getView(): View = klineChartView
    
        override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
            when (call.method) {
                "addData" -> {
                    val isInitData = call.argument("isInitData") ?: false
                    val isHistoryData = call.argument("isHistoryData") ?: false
                    val dataListJson = call.argument("data")
                    val dataList = Gson().fromJson>(dataListJson,
                            object : TypeToken>() {}.type)
                    if (isInitData) klineChartView.justShowLoading()
                    DataHelper.calculate(dataList)
                    when (isHistoryData) {
                        true -> klineAdapter.addHeaderData(dataList)
                        else -> klineAdapter.addFooterData(dataList)
                    }
                    klineAdapter.notifyDataSetChanged()
                    klineChartView.startAnimation()
                    if (isInitData) klineChartView.refreshEnd()
                    result.success(null)
                }
                "changeMainDrawType" -> {
                    val status = when (call.argument("type")) {
                        1 -> Status.MA
                        2 -> Status.BOLL
                        else -> Status.NONE
                    }
                    if (status != mainDrawType) {
                        mainDrawType = status
                        klineChartView.hideSelectData()
                        klineChartView.changeMainDrawType(status)
                    }
                    result.success(null)
                }
                "changeChildDraw" -> {
                    val position = call.argument("position") ?: -1
                    if (childDrawPosition != position) {
                        childDrawPosition = position
                        klineChartView.hideSelectData()
                        when (position) {
                            -1 -> klineChartView.hideChildDraw()
                            else -> klineChartView.setChildDraw(position)
                        }
                    }
                    result.success(null)
                }
                "setMainDrawLine" -> {
                    val showMainDrawLine = call.argument("isShowMainDrawLine") ?: false
                    if (isShowMainDrawLine != showMainDrawLine) {
                        isShowMainDrawLine = showMainDrawLine
                        klineChartView.setMainDrawLine(showMainDrawLine)
                    }
                    result.success(null)
                }
            }
        }
    
        override fun dispose() {
    
        }
    }
    
  • 封装KLineViewFactory, 该类比较简单,继承于PlatformViewFactory,需要注意PlatformViewFactory的构造方法的参数,其值是指 解析Flutter传入参数的方式,这里为一个固定实例对象StandardMessageCodec.INSTANCE

    方法:create 则是需要返回一个上面写下的一个View的对象。

    
    class KLineViewFactory(private var messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    
        @Suppress("unchecked_cast")
        override fun create(context: Context, viewId: Int, args: Any?): PlatformView = KLineView(context, messenger, viewId, args as? Map)
    
    
    }
    
  • 编写KLineViewFlutterPlugin,并注入Flutter中

    编写registerWith方法,检测控件是否已经注册及控件的注册

    object KLineViewFlutterPlugin {
    
        const val ViewTypeID = "plugins.mrper.andrid-view/kline-view"
    
        @JvmStatic
        fun registerWith(registry: PluginRegistry) {
            val key = KLineViewFlutterPlugin::class.java.canonicalName
            if (registry.hasPlugin(key)) return
            val registrar = registry.registrarFor(key)
            registrar.platformViewRegistry().registerViewFactory(ViewTypeID, KLineViewFactory(registrar.messenger()))
        }
    
    }
    

    MainActivity注册该组件:

    class MainActivity : FlutterActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            GeneratedPluginRegistrant.registerWith(this)
            KLineViewFlutterPlugin.registerWith(this) //注册组件
        }
    }
    

  1. 第四步,开始编写我的Flutter的KLineChartView,在此我就不再解释下面代码内容,请具体查看代码。

    import 'dart:convert';
    
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/services.dart';
    
    typedef void OnKLineChartViewCreated();
    
    /// 主图类型,默认为MA
    enum KLineMainDrawType { None, MA, BOLL }
    
    /// 副图类型,默认为None
    enum KLineChildDrawPosition { None, MACD, KDJ, RSI, WR }
    
    /// 股票走势图组件
    class KLineChartView extends StatefulWidget {
      KLineChartView({
        Key key,
        this.gridColumnCount = 4,
        this.gridRowCount = 4,
        double width,
        double height,
        BoxConstraints constraints,
        this.backgroundColor = const Color(0xff333333),
        this.padding,
        this.margin,
        this.alignment = Alignment.center,
        @required this.onViewCreated,
      })  : assert(constraints == null || constraints.debugAssertIsValid()),
            assert(onViewCreated != null),
            constraints = (width != null || height != null)
                ? (constraints ??= BoxConstraints())
                        ?.tighten(width: width, height: height) ??
                    BoxConstraints.tightFor(width: width, height: height)
                : (constraints ??= BoxConstraints()),
            super(key: key);
    
      final int gridColumnCount;
      final int gridRowCount;
      final BoxConstraints constraints;
      final Color backgroundColor;
      final EdgeInsets padding;
      final EdgeInsets margin;
      final Alignment alignment;
      final OnKLineChartViewCreated onViewCreated;
    
      @override
      KLineChartViewState createState() => KLineChartViewState();
    }
    
    class KLineChartViewState extends State {
      MethodChannel _methodChannel;
    
      /// 添加数据
      /// + [dataList]-数据列表
      /// + [isInitData]-是否是初始数据
      /// + [isHistoryData]-是否是历史数据
      void addData(List> dataList,
              {bool isInitData = false, bool isHistoryData = false}) =>
          _methodChannel.invokeMethod('addData', {
            'isInitData': isInitData,
            'isHistoryData': isHistoryData,
            'data': json.encode(dataList)
          });
    
      /// 切换主图
      void changeMainDrawType([KLineMainDrawType type = KLineMainDrawType.MA]) {
        int typeValue = 1;
        if (type == KLineMainDrawType.MA)
          typeValue = 1;
        else if (type == KLineMainDrawType.BOLL)
          typeValue = 2;
        else
          typeValue = 0;
        _methodChannel.invokeMethod('changeMainDrawType', {'type': typeValue});
      }
    
      /// 设置副图
      /// + [position]-副图类型
      void changeChildDraw(
          [KLineChildDrawPosition position = KLineChildDrawPosition.None]) {
        int positionValue = -1;
        if (position == KLineChildDrawPosition.MACD)
          positionValue = 0;
        else if (position == KLineChildDrawPosition.KDJ)
          positionValue = 1;
        else if (position == KLineChildDrawPosition.RSI)
          positionValue = 2;
        else if (position == KLineChildDrawPosition.WR)
          positionValue = 3;
        else
          positionValue = -1;
        _methodChannel.invokeMethod('changeChildDraw', {'position': positionValue});
      }
    
      /// 设置分时线[TRUE]或者K线图[FALSE]
      /// + [isShowMainDrawLine]-TRUE OR FALSE
      void setMainDrawLine([bool isShowMainDrawLine = false]) =>
          _methodChannel.invokeMethod('setMainDrawLine',
              {'isShowMainDrawLine': (isShowMainDrawLine ?? false)});
    
      @override
      Widget build(BuildContext context) => Container(
          constraints: widget.constraints,
          alignment: widget.alignment,
          padding: widget.padding,
          margin: widget.margin,
          color: widget.backgroundColor,
          child: AndroidView(
              viewType: 'plugins.mrper.andrid-view/kline-view',
              creationParams: {
                'gridRowCount': widget.gridColumnCount,
                'gridColumnCount': widget.gridRowCount
              },
              creationParamsCodec: const StandardMessageCodec(),
              onPlatformViewCreated: (int id) {
                _methodChannel =
                    MethodChannel('plugins.mrper.andrid-view/kline-view_$id');
                widget.onViewCreated();
              }));
    }
    
    // class KLineChartViewController {
    //   MethodChannel _methodChannel;
    
    //   KLineChartViewController(int id) {
    //     _methodChannel = MethodChannel('plugins.mrper.andrid-view/kline-view_$id');
    //   }
    
    //   /// 添加数据
    //   /// + [dataList]-数据列表
    //   /// + [isInitData]-是否是初始数据
    //   /// + [isHistoryData]-是否是历史数据
    //   void addData(List> dataList,
    //           {bool isInitData = false, bool isHistoryData = false}) =>
    //       _methodChannel.invokeMethod('addData', {
    //         'isInitData': isInitData,
    //         'isHistoryData': isHistoryData,
    //         'data': json.encode(dataList)
    //       });
    
    //   /// 切换主图
    //   void changeMainDrawType([KLineMainDrawType type = KLineMainDrawType.MA]) {
    //     int typeValue = 1;
    //     if (type == KLineMainDrawType.MA)
    //       typeValue = 1;
    //     else if (type == KLineMainDrawType.BOLL)
    //       typeValue = 2;
    //     else
    //       typeValue = 0;
    //     _methodChannel.invokeMethod('changeMainDrawType', {'type': typeValue});
    //   }
    
    //   /// 设置副图
    //   /// + [position]-副图类型
    //   void changeChildDraw(
    //       [KLineChildDrawPosition position = KLineChildDrawPosition.None]) {
    //     int positionValue = -1;
    //     if (position == KLineChildDrawPosition.MACD)
    //       positionValue = 0;
    //     else if (position == KLineChildDrawPosition.KDJ)
    //       positionValue = 1;
    //     else if (position == KLineChildDrawPosition.RSI)
    //       positionValue = 2;
    //     else if (position == KLineChildDrawPosition.WR)
    //       positionValue = 3;
    //     else
    //       positionValue = -1;
    //     _methodChannel.invokeMethod('changeChildDraw', {'position': positionValue});
    //   }
    
    //   /// 设置分时线[TRUE]或者K线图[FALSE]
    //   /// + [isShowMainDrawLine]-TRUE OR FALSE
    //   void setMainDrawLine([bool isShowMainDrawLine = false]) =>
    //       _methodChannel.invokeMethod('setMainDrawLine',
    //           {'isShowMainDrawLine': (isShowMainDrawLine ?? false)});
    // }
    
    
  2. 使用KlineChartView

    class HomePage extends StatefulWidget {
      HomePage({Key key}) : super(key: key);
      @override
      _HomePageState createState() => _HomePageState();
    }
    
    class _HomePageState extends State {
      GlobalKey _klineChartViewKey;
    
      @override
      void initState() {
        _klineChartViewKey = GlobalKey();
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(title: Text('首页')),
            body: LoaderContainer(
                state: LoaderState.NoAction,
                onReload: () {},
                emptyView: ClassicalNoDataView(
                    spacingFromImageToText: 20,
                    spacingFromTextToButton: 65,
                    onRefresh: () =>
                        showDialog(context: context, child: LoadingDialog()),
                    buttonBackgroundColor: Colors.yellow,
                    buttonBorderRadius: BorderRadius.all(Radius.circular(3))),
                errorView: ClassicalErrorView(
                    spacingFromImageToText: 20,
                    imageWidth: 120,
                    imageHeight: 120,
                    onReload: () {}),
                contentView: Column(children: [
                  Row(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: [
                        Padding(
                            padding: const EdgeInsets.symmetric(horizontal: 5),
                            child: Text('主图')),
                        ...['MA', 'BOLL', '隐藏']
                            .map((item) => InkWell(
                                child: Container(
                                    height: 35,
                                    padding:
                                        const EdgeInsets.symmetric(horizontal: 5),
                                    alignment: Alignment.center,
                                    child: Text(item)),
                                onTap: () {
                                  KLineMainDrawType type = KLineMainDrawType.None;
                                  if (item == '隐藏')
                                    type = KLineMainDrawType.None;
                                  else if (item == 'MA')
                                    type = KLineMainDrawType.MA;
                                  else
                                    type = KLineMainDrawType.BOLL;
                                  _klineChartViewKey.currentState
                                      .changeMainDrawType(type);
                                }))
                            .toList()
                      ]),
                  Row(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: [
                        Padding(
                            padding: const EdgeInsets.symmetric(horizontal: 5),
                            child: Text('副图')),
                        ...['MACD', 'KDJ', 'RSI', 'WR', '隐藏']
                            .map((item) => InkWell(
                                child: Container(
                                    height: 35,
                                    padding:
                                        const EdgeInsets.symmetric(horizontal: 5),
                                    alignment: Alignment.center,
                                    child: Text(item)),
                                onTap: () {
                                  KLineChildDrawPosition position =
                                      KLineChildDrawPosition.None;
                                  if (item == '隐藏')
                                    position = KLineChildDrawPosition.None;
                                  else if (item == 'MACD')
                                    position = KLineChildDrawPosition.MACD;
                                  else if (item == 'KDJ')
                                    position = KLineChildDrawPosition.KDJ;
                                  else if (item == 'RSI')
                                    position = KLineChildDrawPosition.RSI;
                                  else
                                    position = KLineChildDrawPosition.WR;
                                  _klineChartViewKey.currentState
                                      .changeChildDraw(position);
                                }))
                            .toList()
                      ]),
                  Row(
                      mainAxisSize: MainAxisSize.max,
                      mainAxisAlignment: MainAxisAlignment.start,
                      children: [
                        ...['分时', 'K线图']
                            .map((item) => InkWell(
                                child: Container(
                                    height: 35,
                                    padding:
                                        const EdgeInsets.symmetric(horizontal: 5),
                                    alignment: Alignment.center,
                                    child: Text(item)),
                                onTap: () {
                                  _klineChartViewKey.currentState
                                      .setMainDrawLine(item == '分时');
                                }))
                            .toList()
                      ]),
                  Expanded(
                      child: KLineChartView(
                          key: _klineChartViewKey,
                          height: MediaQuery.of(context).size.height * 2 / 3,
                          onViewCreated: () {
                            _klineChartViewKey.currentState
                                .addData(TEST_DATA, isInitData: true);
                          }))
                ])));
      }
    }
    
    

    效果图如下:

frc fbcc7e4d0cd4d938869d0e08694e1e31 - Flutter - 教你把KLineChartView移植到Flutter
QQ图片20191117142455.jpg
  1. 到此为止,我们所有的已经实现了,效果还是不错的!

总结

通过这次组件的嵌入化,对PlatformView的了解更加深入了解了。更重点的是,我们要学会使用MethodChannel这个重要的类,如何使用它与Dart代码沟通,如何相互调用。其次是如何创建一个具有Native性质的Flutter组件,玩的愉快!其实我最开始也只是抱着试一试的心态,没想到还可以,也支持视图缩放。

Flutter – 教你把KLineChartView移植到Flutter