本文主要是介绍Flutter - 手写体widgets之wired_elements,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
介绍
今天带大家一起看看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,谢谢!!!
这篇关于Flutter - 手写体widgets之wired_elements的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!