天天看點

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

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

✨開源位址:

https://github.com/huangjianke/flutter_easyloading ,歡迎star

前言

Flutter

Google

在2017年推出的一套開源跨平台

UI

架構,可以快速地在

iOS

Android

Web

平台上建構高品質的原生使用者界面。

Flutter

釋出至今,不可謂不說是大受追捧,吸引了大批

App

原生開發者、

Web

開發者前赴後繼的投入其懷抱,也正由于

Flutter

是跨平台領域的新星,總的來說,其生态目前還不是十分完善,我相信對于習慣了原生開發的同學們來說,找輪子肯定沒有了那種章手就萊的感覺。比如說這篇文章即将講到的,如何在

Flutter

應用内簡單、友善的展示

Toast

或者

Loading

框呢?

探索

起初,我也在

pub

上找到了幾個比較優秀的插件:

  • FlutterToast : 這個插件應該是很多剛入坑

    Flutter

    的同學們都使用過的,它依賴于原生,但對于UI層級的問題,最好在Flutter端解決,這樣便于後期維護,也可以減少相容性問題;
  • flutter_oktoast : 純

    Flutter

    端實作,調用友善。但缺少

    loading

    、進度條展示,仍可自定義實作;

試用過後,發現這些插件都或多或少不能滿足我們的産品需求,于是便結合自己産品的需求來造了這麼個輪子,也希望可以幫到有需要的同學們。效果預覽:

實作

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

根節點元件。(注:這裡的做法參考于

插件,感謝)。

另外,這樣做的目的還可以解決另外一個核心問題:将

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();           

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

的方法,這個方法決定了畫布什麼時候會重新繪制,在複雜的繪制中對提升繪制性能是相當有成效的。

@override
bool shouldRepaint(_CirclePainter oldDelegate) => value != oldDelegate.value;           

結語

毫無疑問,

Flutter

的前景是一片光明的,也許現在還存在諸多問題,但我相信更多的人會願意陪着

Flutter

一起成長。期待着

Flutter

的生态圈的完善。後期我也會逐漸完善

Flutter EasyLoading

,期待您的寶貴意見。

最後,希望

Flutter EasyLoading

對您有所幫助。

繼續閱讀