Flutter EasyLoading - 讓全局Toast/Loading更簡單

flutter_easyloading: 一個簡單易用的Flutter插件,包含23種loading動畫效果、進度條展現、Toast展現。純Flutter端實現,支持iOS、Android。git

開源地址github.com/huangjianke…github

前言

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

探索

起初,我也在pub上找到了幾個比較優秀的插件:緩存

  • FlutterToast: 這個插件應該是不少剛入坑Flutter的同窗們都使用過的,它依賴於原生,但對於UI層級的問題,最好在Flutter端解決,這樣便於後期維護,也能夠減小兼容性問題;
  • flutter_oktoast: 純Flutter端實現,調用方便。但缺乏loading、進度條展現,仍可自定義實現;

試用事後,發現這些插件都或多或少不能知足咱們的產品需求,因而便結合本身產品的需求來造了這麼個輪子,也但願能夠幫到有須要的同窗們。效果預覽:markdown

flutter_easyloading

實現

showDialog 實現

先看看初期咱們實現彈窗的方式showDialog,部分源碼以下:app

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方法中:建立StatelessWidgetbuild方法
  • State對象中:建立StatefulWidgetState對象的build方法中,另外一個是State的成員變量

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

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

固然不是的,請繼續看。函數

Flutter EasyLoading 介紹

Flutter EasyLoading是一個簡單易用的Flutter插件,包含23loading動畫效果、進度條展現、Toast展現。純Flutter端實現,兼容性好,支持iOSAndroid。先簡單看下如何使用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的主要實現過程及思路:

  • OverlayOverlayEntry實現全局彈窗
  • CustomPaintCanvas實現圓形進度條繪製

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是一個StackWidget,能夠將OverlayEntry插入到Overlay中,使獨立的child窗口懸浮於其餘Widget之上。利用這個特性,咱們能夠用OverlayMaterialAppCupertinoApp包裹起來,這樣作的目的是爲了確保 loading 組件能覆蓋在其餘組件之上,由於在Flutter中只會存在一個MaterialAppCupertinoApp根節點組件。(注:這裏的作法參考於flutter_oktoast插件,感謝)。

另外,這樣作的目的還能夠解決另一個核心問題:將 context 緩存到內存中,後續全部調用均不須要提供context。實現以下:

@override
Widget 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,
  );
}
複製代碼
// 建立OverlayEntry
OverlayEntry _overlayEntry = OverlayEntry(
  builder: (BuildContext context) => LoadingContainer(
    key: _key,
    status: status,
    indicator: w,
    animation: _animation,
  ),
);

// 將OverlayEntry插入到Overlay中
// 經過Overlay.of()咱們能夠獲取到App根節點的Overlay
Overlay.of(_getInstance().context).insert(_overlayEntry);

// 調用OverlayEntry自身的remove()方法,從所在的Overlay中移除本身
_overlayEntry.remove();
複製代碼

OverlayOverlayEntry的使用及理解仍是很簡單,咱們也能夠再更多的使用場景使用他們,好比說,相似PopupWindow的彈窗效果、全局自定義Dialog彈窗等等。只要靈活運用,咱們能夠實現不少咱們想要的效果。

CustomPaintCanvas實現圓形進度條繪製

幾乎全部的UI系統都會提供一個自繪UI的接口,這個接口一般會提供一塊2D畫布CanvasCanvas內部封裝了一些基本繪製的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:當childnull時,表明默認繪製區域大小,若是有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的方法,這個方法決定了畫布何時會從新繪製,在複雜的繪製中對提高繪製性能是至關有成效的。

@override
bool shouldRepaint(_CirclePainter oldDelegate) => value != oldDelegate.value;
複製代碼

結語

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

最後,但願Flutter EasyLoading對您有所幫助。

相關文章
相關標籤/搜索