Flutter - 教你把KLineChartView移植到Fl
背景
由于需要,最近需要在Flutter中应用股票走势线,寻找了很久Flutter相关第三方库,无果,没有一个是能达到要求的。于是脑子萌发了一个想法,那么我把原生的组件移植过来不也可以吗,只要达到组件的效果即可。 转载请注明出处,谢谢!
那么现在我需要做的是找一个合适的K线的原生代码写的第三方库,原生项目地址:点击查看,感谢KLineChartView的作者。
先看看KLineChartView的效果图:
image.png image.png万事具备,只欠东风,现在我们把KLineChartView装入Flutter中并使用!
-
第一步,新建一个Flutter项目,这里暂且就不叙述了!
-
第二步,打开Flutter项目中的android文件夹,并导入KLineChartViewLib!
-
第三步,使用原生代码把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) //注册组件 } }
-
第四步,开始编写我的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)}); // }
-
使用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); })) ]))); } }
效果图如下:
- 到此为止,我们所有的已经实现了,效果还是不错的!
总结
通过这次组件的嵌入化,对PlatformView的了解更加深入了解了。更重点的是,我们要学会使用MethodChannel这个重要的类,如何使用它与Dart代码沟通,如何相互调用。其次是如何创建一个具有Native性质的Flutter组件,玩的愉快!其实我最开始也只是抱着试一试的心态,没想到还可以,也支持视图缩放。