✨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 : 這個插件應該是很多剛入坑
的同學們都使用過的,它依賴于原生,但對于UI層級的問題,最好在Flutter端解決,這樣便于後期維護,也可以減少相容性問題;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
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
對您有所幫助。