介绍
今天带大家一起看看wired_elements,Wired Elements 是一系列具有手绘外观的基本 UI 元素。
其实这种外观的UI元素在web端已经有非常成熟的组件库,请看这里。他是基于rough.js实现的一系列组件,可用于快速建立交互型产品设计稿,已经有基于此设计的可拖拽的网页端项目软件,大家可以搜一搜看看,我之前搜到过,不过当时没有收藏。。。也可用于自己blog的UI,也可以just for fun。总之web端是有了,但是Flutter我是没有看到,只有一个flutter_rough项目,他是rough.js的一个Flutter实现。
所以今天我就站在居然的肩膀上,写了一个Wired Elements的Flutter实现,先上图,后面会挑选1,2个widget说一下是如何实现的。
巨人flutter_rough
上面说到了是站在巨人的肩膀上,就是说的flutter_rough,但是此组件库的作者没看到在维护了,似乎对这种样式的需求不太高?但是我们不管,just for fun!因为最新的flutter插件都需要null safety,但是flutter_rough并不是null safety的,已经有人建了一个issue提了这个问题,作者迟迟没有回复,所以这里我们没有办法,不能通过dependency的方式引入,只得把代码拷贝到自己的项目中自己做了null safety(这里已经在wired_elements的readme中说明标注了引入flutter_rough)。好了,基本工作已经完成,下面我们就可以开始切入我们wired_elements的组件库了。
初始化wired_elements组件库
首先,我们需要开发的是flutter&dart的package,所以我们按照官方的步骤一步步创建项目,做好初始化工作。
1. 创建项目:
flutter create --template=package wired_elements
2. 实现package wired_elements:如下图,lib文件夹下面有一个导出文件wired_elements.dart
,2个文件夹rough
和src
,其中rough
是拷贝过来并做了null safety
的flutter_rough组件库,src文件夹下面是我们需要实现的手写体widgets,可以看到目前为止一些基本的widgets都有了,但是还有很多没有比如日历、进度条等等,后面会继续迭代。src文件夹下面还有个canvas
文件夹,里面包含的主要是一些通用canvas
操作,方便开发我们的widgets,具体就不细说了,大家可以看源码,我们的主要任务是介绍一下具体的手写体widgets。
手写体按钮 - wired_button
大家可以参考web端的按钮,主要是边框和文字,文字手写体我们直接引入google的hand writing字体即可,比较简单,但是这种手写体的边框如何实现?
其实很简单,我们隐藏Flutter按钮的边框,然后外部包一层Container
,实现自定义的decoration
即可,这里的自定义decoration
已经被flutter_rough实现好了,所以我们拿来主义即可。
@override
Widget buildWiredElement() {
return Container(
padding: EdgeInsets.zero,
height: 42.0,
decoration: RoughBoxDecoration(
shape: RoughBoxShape.rectangle,
borderStyle: RoughDrawingStyle(
width: 1,
color: borderColor,
),
),
child: SizedBox(
height: double.infinity,
child: TextButton(
style: TextButton.styleFrom(
primary: textColor,
),
child: child,
onPressed: onPressed,
),
),
);
}
上面的代码片段我们使用了RoughBoxDecotation
,它提供了具体的边框形状和样式供我们选择,我们这里使用了长方形并且指定了粗细和颜色,这样一个简单的wired_button
就实现了,剩下的就是添加一些Flutter button本身的参数暴露出来即可。
如果大家看了wired_button的源码就知道,我们没有直接继承StatefulWidget
或者StatelessWidget
,我们自己写了WiredBaseWidget
继承了StatelessWidget
,然后wired_button
继承WiredBaseWidget
,并实现WiredBaseWidget
的方法buildWiredElement()。
为什么要这么做呢?可以看到在WiredBaseWidget
类中,我们包裹了一层RepaintBoundary
,它是用来隔离屏幕canvas
的重绘,因为我们使用了自定义的decoration
,继承了BoxPainter
,使用的是同一个canvas
,这样屏幕上只要是用了这个自定义dcoration
的就是使用了同一个canvas
实例来绘制屏幕,如果屏幕上面有多个wired_button
,那么当我们点击某一个按钮,他会触发重绘,如果我们不隔离重绘,其它的按钮也会跟着重绘,这并不是我们期望的,所以我们使用了RepaintBoundary
来避免这个问题。
手写体滑件 - wired_slider
滑件在调节屏幕亮度,调节视频播放进度、调节音量等等地方都可以用到。可以参考Flutter material的Slider。
那么手写体滑件该怎么实现呢,在这里我们的思路是:隐藏Flutter Slider自己的进度条直线和当前位置的实心圆,通过设置activeColor
和inactiveColor
的颜色为透明色即可,这样我们就可以用手写体直线和圆来覆盖当前位置,达到了UI的手写体样式。如何覆盖?用Stack布局。
@override
Widget build(BuildContext context) {
return Stack(
alignment: Alignment.center,
children: [
// 覆盖Slider的直线
SizedBox(
height: 1,
width: double.infinity,
child: WiredCanvas(
painter: WiredLineBase(
x1: 0,
y1: 0,
x2: double.infinity,
y2: 0,
strokeWidth: 2,
),
fillerType: RoughFilter.HatchFiller,
),
),
// 覆盖Slider的的位置圆圈
Positioned(
left: _getSliderWidth() * _currentSliderValue / widget.max - 12,
child: SizedBox(
height: 24.0,
width: 24.0,
child: WiredCanvas(
painter: WiredCircleBase(
diameterRatio: .7,
fillColor: textColor,
),
fillerType: RoughFilter.HachureFiller,
fillerConfig: FillerConfig.build(hachureGap: 1.0),
),
),
),
SliderTheme(
data: SliderThemeData(
trackShape: CustomTrackShape(),
),
child: Slider(
value: _currentSliderValue,
min: widget.min,
max: widget.max,
activeColor: Colors.transparent,
inactiveColor: Colors.transparent,
divisions: widget.divisions,
label: widget.label,
onChanged: (value) {
bool result = false;
if (widget.onChanged != null) {
result = widget.onChanged!(value);
}
if (result) {
setState(() {
_currentSliderValue = value;
});
}
},
),
),
],
);
}
以上代码,我们使用了Stack
布局并指定alignment
为center
,达到覆盖原有的Slider
的效果。
因为Flutter Slider有一个默认的margins
,但是我们并不想这样,我们覆盖的直线需要从头覆盖到尾部,如果有了margins就会导致覆盖的直线长于Flutter Slider
,所以使用了SliderTheme
包裹了Slider
,然后自定义实现data
,为什么这么做可以去掉margins
?请参考此处。
我们绘制了手写体UI,但是Slider
是可以改变他的value
的,换句话说,Slider
的那个实心圆是可以改变位置的,那么我们绘制的圆圈也要在拖动的时候跟着改变到正确的位置。从源码中看到可以按照比例来改变,我们知道当前Slider的值_currentSliderValue
,我们也知道Slider的滑动最大值widget.max
,这个时候如果知道Slider
的物理像素值sliderWidth
,我们就可以按照比例来计算出Slider的实心圆的位置x = sliderWidth * _currentSliderValue / widget.max
,幸运的是我们可以通过下面的方法拿到sliderWidth
:
double _getSliderWidth() {
double width = 0;
try {
var box = context.findRenderObject() as RenderBox;
width = box.size.width;
} catch (e) {}
return width;
}
有的同学会问了,看源码为什么还要减去12?因为实心球的直径是24!
等等!源码里面在initState
方法为甚还有这么一段代码?如注释描述,第一次进入build
方法拿不到sliderWidth
,所以我们需要在第一次build
完毕在调用setState
方法强制让widget
再build
一次,从而拿到sliderWidth
让初始化时实心球的位置也能正确展示。
// Delay for calculate the slider's width `_getSliderWidth()` during the next frame
Future.delayed(Duration(milliseconds: 0), () {
setState(() {});
});
结尾
我们在这里主要挑选了具有代表性的2个widgets - wired_button和wired_slider介绍了一下,其他的目前已实现的widgets基本差不多,主要思路是去除已有的边框或者线条,用手写体来覆盖已有的UI,从而代码UI样式的改变。源码在此,pub.dev在此,欢迎大家提PR或者issues,如果对你有帮助希望能够给个star,谢谢!!!