前言
最近用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中传入的参数作为父控件的大小。