首頁>技術>

前言

Flutter是Google在2017年推出的一套開源跨平臺UI框架,可以快速地在iOS、Android和Web平臺上構建高品質的原生使用者介面。Flutter釋出至今,不可謂不說是大受追捧,吸引了大批App原生開發者、Web開發者前赴後繼的投入其懷抱,也正由於Flutter是跨平臺領域的新星,總的來說,其生態目前還不是十分完善,我相信對於習慣了原生開發的同學們來說,找輪子肯定沒有了那種章手就萊的感覺。比如說這篇文章即將講到的,如何在Flutter應用內簡單、方便的展示Toast或者Loading框呢?

探索

起初,我也在pub上找到了幾個比較優秀的外掛:

FlutterToast: 這個外掛應該是很多剛入坑Flutter的同學們都使用過的,它依賴於原生,但對於UI層級的問題,最好在Flutter端解決,這樣便於後期維護,也可以減少相容性問題;

flutter_oktoast: 純Flutter端實現,呼叫方便。但缺少loading、進度條展示,仍可自定義實現;試用過後,發現這些外掛都或多或少不能滿足我們的產品需求,於是便結合自己產品的需求來造了這麼個輪子,也希望可以幫到有需要的同學們。效果預覽:

flutter_easyloading

image

實現showDialog 實現

先看看初期我們實現彈窗的方式showDialog,部分原始碼如下:

Future<T> showDialog<T>({  @required BuildContext context,  bool barrierDismissible = true,  @Deprecated(    'Instead of using the "child" argument, return the child from a closure '    'provided to the "builder" argument. This will ensure that the BuildContext '    'is appropriate for widgets built in the dialog. '    'This feature was deprecated after v0.2.3.'  )  Widget child,  WidgetBuilder builder,  bool useRootNavigator = true,})

這裡有個必傳引數context,想必接觸過Flutter開發一段時間的同學,都會對BuildContext有所了解。簡單來說BuildContext就是構建Widget中的應用上下文,是Flutter的重要組成部分。BuildContext只出現在兩個地方:

StatelessWidget.build方法中:建立StatelessWidget的build方法State物件中:建立StatefulWidget的State物件的build方法中,另一個是State的成員變數

有關BuildContext更深入的探討不在此文的探討範圍內,如果使用showDialog實現彈窗操作,那麼我們所考慮的問題便是,如何方便快捷的在任意地方去獲取BuildContext,從而實現彈窗。如果有同學恰巧也用了showDialog這種方式的話,我相信,你也會發現,在任意地方獲取BuildContext並不是那麼簡單,而且會產生很多不必要的程式碼量。

那麼,我們就只能使用這種體驗極其不友好的方法麼?

當然不是的,請繼續看。

Flutter EasyLoading 介紹

Flutter EasyLoading是一個簡單易用的Flutter外掛,包含23種loading動畫效果、進度條展示、Toast展示。純Flutter端實現,相容性好,支援iOS、Android。先簡單看下如何使用Flutter EasyLoading。

安裝

將以下程式碼新增到您專案中的 pubspec.yaml 檔案:

dependencies:  flutter_easyloading: ^1.1.0 // 請使用最新版
匯入
import 'package:flutter_easyloading/flutter_easyloading.dart';
如何使用

首先, 使用 FlutterEasyLoading 元件包裹您的App元件:

class MyApp extends StatelessWidget {  @override  Widget build(BuildContext context) {    /// 子元件通常為 [MaterialApp] 或者 [CupertinoApp].    /// 這樣做是為了確保 loading 元件能覆蓋在其他元件之上.    return FlutterEasyLoading(      child: MaterialApp(        title: 'Flutter EasyLoading',        theme: ThemeData(          primarySwatch: Colors.blue,        ),        home: MyHomePage(title: 'Flutter EasyLoading'),      ),    );  }}

然後, 請盡情使用吧:

EasyLoading.show(status: 'loading...'); EasyLoading.showProgress(0.3, status: 'downloading...');EasyLoading.showSuccess('Great Success!');EasyLoading.showError('Failed with Error');EasyLoading.showInfo('Useful Information.');EasyLoading.dismiss();
自定義樣式

首先,我們看下Flutter EasyLoading目前支援的自定義屬性:

/// loading的樣式, 預設[EasyLoadingStyle.dark].EasyLoadingStyle loadingStyle;/// loading的指示器型別, 預設[EasyLoadingIndicatorType.fadingCircle].EasyLoadingIndicatorType indicatorType;/// loading的遮罩型別, 預設[EasyLoadingMaskType.none].EasyLoadingMaskType maskType;/// 文字的對齊方式 , 預設[TextAlign.center].TextAlign textAlign;/// loading內容區域的內邊距.EdgeInsets contentPadding;/// 文字的內邊距.EdgeInsets textPadding;/// 指示器的大小, 預設40.0.double indicatorSize;/// loading的圓角大小, 預設5.0.double radius;/// 文字大小, 預設15.0.double fontSize;/// 進度條指示器的寬度, 預設2.0.double progressWidth;/// [showSuccess] [showError] [showInfo]的展示時間, 預設2000ms.Duration displayDuration;/// 文字的顏色, 僅對[EasyLoadingStyle.custom]有效.Color textColor;/// 指示器的顏色, 僅對[EasyLoadingStyle.custom]有效.Color indicatorColor;/// 進度條指示器的顏色, 僅對[EasyLoadingStyle.custom]有效.Color progressColor;/// loading的背景色, 僅對[EasyLoadingStyle.custom]有效.Color backgroundColor;/// 遮罩的背景色, 僅對[EasyLoadingMaskType.custom]有效.Color maskColor;/// 當loading展示的時候,是否允許使用者操作.bool userInteractions;/// 展示成功狀態的自定義元件Widget successWidget;/// 展示失敗狀態的自定義元件Widget errorWidget;/// 展示資訊狀態的自定義元件Widget infoWidget;

因為 EasyLoading 是一個全域性單例, 所以我們可以在任意一個地方自定義它的樣式:

EasyLoading.instance  ..displayDuration = const Duration(milliseconds: 2000)  ..indicatorType = EasyLoadingIndicatorType.fadingCircle  ..loadingStyle = EasyLoadingStyle.dark  ..indicatorSize = 45.0  ..radius = 10.0  ..backgroundColor = Colors.green  ..indicatorColor = Colors.yellow  ..textColor = Colors.yellow  ..maskColor = Colors.blue.withOpacity(0.5);

更多的指示器動畫型別可檢視 flutter_spinkit showcase

可以看到,Flutter EasyLoading的整合以及使用相當的簡單,而且有豐富的自定義樣式,總會有你滿意的。

接下來,我們來看看Flutter EasyLoading的程式碼實現。

Flutter EasyLoading 的實現

本文將通過以下兩個知識點來介紹Flutter EasyLoading的主要實現過程及思路:

Overlay、OverlayEntry實現全域性彈窗CustomPaint與Canvas實現圓形進度條繪製Overlay、OverlayEntry 實現全域性彈窗

先看看官方關於Overlay的描述:

/// A [Stack] of entries that can be managed independently.////// Overlays let independent child widgets "float" visual elements on top of/// other widgets by inserting them into the overlay's [Stack]. The overlay lets/// each of these widgets manage their participation in the overlay using/// [OverlayEntry] objects.////// Although you can create an [Overlay] directly, it's most common to use the/// overlay created by the [Navigator] in a [WidgetsApp] or a [MaterialApp]. The/// navigator uses its overlay to manage the visual appearance of its routes.////// See also://////  * [OverlayEntry].///  * [OverlayState].///  * [WidgetsApp].///  * [MaterialApp].class Overlay extends StatefulWidget {}

也就是說,Overlay是一個Stack的Widget,可以將OverlayEntry插入到Overlay中,使獨立的child視窗懸浮於其他Widget之上。利用這個特性,我們可以用Overlay將 MaterialApp或CupertinoApp包裹起來,這樣做的目的是為了確保 loading 元件能覆蓋在其他元件之上,因為在Flutter中只會存在一個MaterialApp或CupertinoApp根節點元件。(注:這裡的做法參考於flutter_oktoast外掛,感謝)。

另外,這樣做的目的還可以解決另外一個核心問題:將 context 快取到記憶體中,後續所有呼叫均不需要提供context。實現如下:

@overrideWidget build(BuildContext context) {  return Directionality(    child: Overlay(      initialEntries: [        OverlayEntry(          builder: (BuildContext _context) {            // 快取 context            EasyLoading.instance.context = _context;            // 這裡的child必須是MaterialApp或CupertinoApp            return widget.child;          },        ),      ],    ),    textDirection: widget.textDirection,  );}// 建立OverlayEntryOverlayEntry _overlayEntry = OverlayEntry(  builder: (BuildContext context) => LoadingContainer(    key: _key,    status: status,    indicator: w,    animation: _animation,  ),);// 將OverlayEntry插入到Overlay中// 通過Overlay.of()我們可以獲取到App根節點的OverlayOverlay.of(_getInstance().context).insert(_overlayEntry);// 呼叫OverlayEntry自身的remove()方法,從所在的Overlay中移除自己_overlayEntry.remove();

Overlay、OverlayEntry的使用及理解還是很簡單,我們也可以再更多的使用場景使用他們,比如說,類似PopupWindow的彈窗效果、全域性自定義Dialog彈窗等等。只要靈活運用,我們可以實現很多我們想要的效果。

CustomPaint與Canvas實現圓形進度條繪製

幾乎所有的UI系統都會提供一個自繪UI的介面,這個介面通常會提供一塊2D畫布Canvas,Canvas內部封裝了一些基本繪製的API,我們可以通過Canvas繪製各種自定義圖形。在Flutter中,提供了一個CustomPaint元件,它可以結合一個畫筆CustomPainter來實現繪製自定義圖形。接下來我將簡單介紹下圓形進度條的實現。

我們先來看看CustomPaint建構函式:

const CustomPaint({  Key key,  this.painter,  this.foregroundPainter,  this.size = Size.zero,  this.isComplex = false,  this.willChange = false,  Widget child,})
painter: 背景畫筆,會顯示在子節點後面;foregroundPainter: 前景畫筆,會顯示在子節點前面size:當child為null時,代表預設繪製區域大小,如果有child則忽略此引數,畫布尺寸則為child尺寸。如果有child但是想指定畫布為特定大小,可以使用SizeBox包裹CustomPaint實現。isComplex:是否複雜的繪製,如果是,Flutter會應用一些快取策略來減少重複渲染的開銷。willChange:和isComplex配合使用,當啟用快取時,該屬性代表在下一幀中繪製是否會改變。

可以看到,繪製時我們需要提供前景或背景畫筆,兩者也可以同時提供。我們的畫筆需要繼承CustomPainter類,我們在畫筆類中實現真正的繪製邏輯。

接下來,我們看下怎麼通過CustomPainter繪製圓形進度條:

class _CirclePainter extends CustomPainter {  final Color color;  final double value;  final double width;  _CirclePainter({    @required this.color,    @required this.value,    @required this.width,  });  @override  void paint(Canvas canvas, Size size) {    final paint = Paint()      ..color = color      ..strokeWidth = width      ..style = PaintingStyle.stroke      ..strokeCap = StrokeCap.round;    canvas.drawArc(      Offset.zero & size,      -math.pi / 2,      math.pi * 2 * value,      false,      paint,    );  }  @override  bool shouldRepaint(_CirclePainter oldDelegate) => value != oldDelegate.value;}

從上面我們可以看到,CustomPainter中定義了一個虛擬函式paint:

void paint(Canvas canvas, Size size);

這個函式是繪製的核心所在,它包含了以下兩個引數:

canvas: 畫布,包括各種繪製方法, 如 drawLine(畫線)、drawRect(畫矩形)、drawCircle(畫圓)等size: 當前繪製區域大小

畫布現在有了,那麼接下來我們就需要一支畫筆了。Flutter提供了Paint類來實現畫筆。而且可以配置畫筆的各種屬性如粗細、顏色、樣式等,比如:

final paint = Paint()  ..color = color // 顏色  ..strokeWidth = width // 寬度  ..style = PaintingStyle.stroke  ..strokeCap = StrokeCap.round;   

最後,我們就是需要使用drawArc方法進行圓弧的繪製了:

canvas.drawArc(  Offset.zero & size,  -math.pi / 2,  math.pi * 2 * value,  false,  paint,);

到此,我們就完成了進度條的繪製。另外我們也需要注意下繪製效能問題。好在類中提供了重寫shouldRepaint的方法,這個方法決定了畫布什麼時候會重新繪製,在複雜的繪製中對提升繪製效能是相當有成效的。

@overridebool shouldRepaint(_CirclePainter oldDelegate) => value != oldDelegate.value;
結語

毫無疑問,Flutter的前景是一片光明的,也許現在還存在諸多問題,但我相信更多的人會願意陪著Flutter一起成長。期待著Flutter的生態圈的完善。後期我也會逐步完善Flutter EasyLoading,期待您的寶貴意見。

相信它會給大家帶來很多收穫:

image

image

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • (進階篇)簡析一個比Flask和Tornado更高效能的API 框架FastAPI