Flutter - 教你把KLineChartView移植到Flutter

背景

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

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

​ 先看看KLineChartView的效果图:

image.png
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<String, Any>?) : 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<Boolean>("isInitData") ?: false
                    val isHistoryData = call.argument<Boolean>("isHistoryData") ?: false
                    val dataListJson = call.argument<String>("data")
                    val dataList = Gson().fromJson<List<KLineEntity>>(dataListJson,
                            object : TypeToken<List<KLineEntity>>() {}.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<Int>("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<Int>("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<Boolean>("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<String, Any>)
    
    
    }
    
  • 编写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<KLineChartView> {
      MethodChannel _methodChannel;
    
      /// 添加数据
      /// + [dataList]-数据列表
      /// + [isInitData]-是否是初始数据
      /// + [isHistoryData]-是否是历史数据
      void addData(List<Map<String, dynamic>> 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<Map<String, dynamic>> 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<HomePage> {
      GlobalKey<KLineChartViewState> _klineChartViewKey;
    
      @override
      void initState() {
        _klineChartViewKey = GlobalKey<KLineChartViewState>();
        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: <Widget>[
                        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: <Widget>[
                        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: <Widget>[
                        ...['分时', '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);
                          }))
                ])));
      }
    }
    
    

    效果图如下:

QQ图片20191117142455.jpg
  1. 到此为止,我们所有的已经实现了,效果还是不错的!

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容