介紹
今天帶大家一起看看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,謝謝!!!