Flutter - 手寫體widgets之wired_elements

QuickPai發表於2021-07-06

介紹

今天帶大家一起看看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個資料夾roughsrc,其中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自己的進度條直線和當前位置的實心圓,通過設定activeColorinactiveColor的顏色為透明色即可,這樣我們就可以用手寫體直線和圓來覆蓋當前位置,達到了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佈局並指定alignmentcenter,達到覆蓋原有的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方法強制讓widgetbuild一次,從而拿到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,謝謝!!!

相關文章