✨flutter_easyloading: 一個簡單易用的Flutter外掛,包含23種loading動畫效果、進度條展示、Toast展示。純Flutter端實現,支援iOS、Android。
✨開源地址:github.com/huangjianke…
前言
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
根節點元件。(注:這裡的做法參考於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();
複製程式碼
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
對您有所幫助。