专栏名称: saka
目录
相关文章推荐
51好读  ›  专栏  ›  saka

Flutter自定义折线图并添加点击事件

saka  · 掘金  ·  · 2018-11-21 03:02

正文

阅读 91

Flutter自定义折线图并添加点击事件

前言

最近用Flutter做了一个天气类的app,我也是新手,对flutter理解还不是很深入,但是开发过程中的编程思想给了我很大的启发。Dart语言特性很优秀,单线程模型,异步io,初始化列表,函数也是对象,链式调用等等,flutter的设计思想很前卫。好了,马屁只拍到这里,下面讲一下在开发过程中我碰到的一个关于自定义view和触摸事件处理的经验。看一下效果图:

主要有两个功能,一是绘制折线图添加文字和图片,二十点击事件,点击不同的时间点弹出的对话框显示的时间也不同。

绘制流程

fluttert提供的自定义控件API与安卓中的极为相似,同样是canvas和paint,细节上有一些改动,不过上手应该很容易。这里我们应该使用到三个相关类:

StatefulWidget CustomPaint Custompainter

StatefulWidget 类是flutter中必知必会的基础类,用来将我们的自定义view封装成为一个单独的有状态的控件,并可以传入一些参数,来刷新UI,这里不做详细说明了。

CustomPaint 类是自定义view必须要掌握的类,它继承自 SingleChildRenderObjectWidget ,官方对他的定义就是提供一个canvas,当被要求绘制时,它首先会调用painter来绘制自身的内容,然后再绘制子view,最后调用foregroundPainter来绘制前景,这个和recyclerview绘制流程很相似。

Custompainter 类是一个画笔工具,这里我们只介绍这一个工具类。必须重写 void paint(Canvas canvas, Size size) 方法来绘制我们预期的效果。这里的两个参数比较简单,一个就是画布,size表示位置和大小。

Canvas的坐标系同android中一样,左上角是原点,向右为x轴正方向,向下为y轴正方向,掌握了这点绘制容易很多。

废话不多说了,直接开干。

构建StatefulWidget

首先建好一个类,继承 StatefulWidget ,并传入一下变量作为构建的参数:

 final List<HourlyForecast> hourlyList;//天气数据列表
 final String imagePath;//图片路径
 final EdgeInsetsGeometry padding;//padding
 final Size size;//大小
 final void Function(int index) onTapUp;//点击事件的回调方法

复制代码

因为要在初始化列表中使用这些变量,所以做成了final,表示我也不想修改他们,注意最后一个变量是一个函数,参数为点击的位置索引,这也是dart的语言特性,可以把函数作为对象。

HourlyForecast是从和风天气的接口中返回的实体类,主要数据如下:

class HourlyForecast {
  String time; //	预报时间,格式yyyy-MM-dd hh:mm	2013-12-30 13:00
  String tmp; //	温度	2
  String cond_code; //	天气状况代码	101
  String cond_txt; //天气状况代码	多云
  String wind_deg; //风向360角度	290
  String wind_dir; //风向	西北
  String wind_sc; //风力	3-4
  String wind_spd; //风速,公里/小时	15
  String hum; //	相对湿度	30
  String pres; //大气压强	1030
  String dew; //露点温度	12
  String cloud; //云量	23
  bool isDay;

  HourlyForecast.formJson(Map<String, dynamic> json)
      : time = json['time'],
        tmp = json['tmp'],
        cond_code = json['cond_code'],
        cond_txt = json['cond_txt'],
        wind_deg = json['wind_deg'],
        wind_dir = json['wind_dir'],
        wind_sc = json['wind_sc'],
        wind_spd = json['wind_spd'],
        hum = json['hum'],
        pres = json['pres'],
        dew = json['dew'],
        cloud = json['cloud'] {
    isDay = DateTime.parse(time).hour > 6 && DateTime.parse(time).hour < 18;
  }

  String getHourTime() {
    return time.split(' ')[1];
  }
}
复制代码

其中 HourlyForecast.formJson(Map<String, dynamic> json) 方法是dart中常用的简单json解析方式,可以直接从convert包中的map数据导出为实体类。

定义好了Widget,我们还需要定义一个State来管理Widget的状态。看一下build方法:

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTapUp: (TapUpDetails detail) {
        print('onTapUp');
        onTap(context, detail);
      },
      child: CustomSingleChildLayout(
        delegate: _SakaLayoutDelegate(widget.size, widget.padding),
        child: CustomPaint(
          painter: _HourlyForecastPaint(context, widget.hourlyList,
              widget.padding.deflateSize(widget.size), areaListCallback,
              imagePath: widget.imagePath,
              iconDay: iconDay,
              iconDayRect: iconDayRect,
              iconNight: iconNight,
              iconNightRect: iconNightRect),
        ),
      ),
    );
  }
复制代码

最外层是一个 GestureDectecor ,flutter中使用这种方式处理点击事件是最简单的一种方式,但是要注意一点OnTapUp事件中只能获取点击的全局位置,我们需要将他转换为控件的相对坐标系位置,后边会详细讲解这里的坑。

构建CustomPaint

有实质内容的就是这个 GestureDectector 中的 CustomSingleChildLayout 控件,这个控件是一个非常简但是非常实用的类,它只能装载一个控件,并且将自己和子控件委托给 SingleChildLayoutDelegate 来定位子控件在父控件中的位置。

class _SakaLayoutDelegate extends SingleChildLayoutDelegate {
  final Size size;
  final EdgeInsetsGeometry padding;

  _SakaLayoutDelegate(this.size, this.padding)
      : assert(size != null),
        assert(padding != null);

  @override
  Size getSize(BoxConstraints constraints) {
    return size;
  }

  @override
  bool shouldRelayout(_SakaLayoutDelegate oldDelegate) {
    return this.size != oldDelegate.size;
  }

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    return BoxConstraints.tight(padding.deflateSize(size));
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    return Offset((size.width - childSize.width) / 2,
        (size.height - childSize.height) / 2);
  }
}
复制代码

这是类中的主要代码, getSize 返回父控件的大小,这里我直接使用的从StatefulWidget中传入的参数作为父控件的大小。







请到「今天看啥」查看全文