很是感謝 Didier Boelens 贊成我將它的一些文章翻譯爲中文發表,這是其中一篇。html
本文經過一個實例詳細講解了 Flutter 中動畫的原理。react
原文的代碼塊有行號。在這裏不支持對代碼添加行號,閱讀可能有些不方便,請諒解。git
原文 連接github
本文模仿 Facebook 的響應式按鈕,使用 響應式編程、Overlay、Animation、Streams、BLoC 設計模式 和 GestureDetector 實現。編程
難度:中等設計模式
最近,有人問我如何在 Flutter 中模仿 Facebook 的響應式按鈕。 通過一番思考以後,我意識到這是一個實踐最近我在前幾篇文章中討論的主題的機會。bash
我要解釋的解決的方案(我稱之爲「響應式按鈕」)使用如下概念:markdown
響應式編程,流,BLoCasync
Overlayide
全局定位
能夠在 GitHub 上找到本文的源代碼。 它也能夠做爲 Flutter 包使用:flutter_reactive_button。
這是一個顯示了本文結果的動畫。
在進入實施細節以前,讓咱們首先考慮 Facebook 響應式按鈕的工做原理:
當用戶按下按鈕,等待一段時間(稱爲長按)時,屏幕上會顯示一個在全部內容之上的面板,等待用戶選擇該面板中包含的其中一個圖標;
若是用戶將他/她的手指移動到圖標上,圖標的尺寸會增大;
若是用戶將他/她的手指移開該圖標,圖標恢復其原始大小;
若是用戶在仍然在圖標上方時釋放他/她的手指,則該圖標被選中;
若是用戶在沒有圖標懸停的狀況下釋放他/她的手指,則沒有選中任何一個圖標;
若是用戶只是點擊按鈕,意味着它不被視爲長按,則該動做被視爲正常的點擊;
咱們能夠在屏幕上有多個響應式按鈕實例;
圖標應顯示在視口中。
下圖顯示瞭解決方案中涉及的不一樣部分:
ReactiveButton
ReactiveButton能夠放置在屏幕上的任何位置。 當用戶對其執行長按時,它會觸發 ReactiveIconContainer 的顯示。
ReactiveIconContainer
一個簡單的容器,用於保存不一樣的 ReactiveIcons
ReactiveIcon
一個圖標,若是用戶將其懸停,可能會變大。
應用程序啓動後,Flutter 會自動建立並渲染 Overlay Widget。 這個 Overlay Widget 只是一個棧 (Stack),它容許可視組件 「浮動」 在其餘組件之上。 大多數狀況下,這個 Overlay 主要用於導航器顯示路由(=頁面或屏幕),對話框,下拉選項 ...... 下圖說明了 Overlay 的概念。各組件彼此疊加。
您插入 Overlay 的每個 Widget 都必須經過 OverlayEntry 來插入。
利用這一律念,咱們能夠經過 OverlayEntry 輕鬆地在全部內容之上顯示 ReactiveIconContainer。
其中一個要求是咱們須要在全部內容之上顯示圖標列表。
@override
Widget build(BuildContext context){
return Stack(
children: <Widget>[
_buildButton(),
_buildIconsContainer(),
...
],
);
}
Widget _buildIconsContainer(){
return !_isContainerVisible ? Container() : ...;
}
複製代碼
若是咱們使用 Stack,如上所示,這將致使一些問題:
咱們永遠不會肯定 ReactiveIconContainer 會系統地處於一切之上,由於 ReactiveButton 自己多是另外一個 Stack 的一部分(也許在另外一個 Widget 下);
咱們須要實現一些邏輯來渲染或不渲染 ReactiveIconContainer,所以必須從新構建 Stack,這不是很是有效的方式
基於這些緣由,我決定使用 Overlay 和 OverlayEntry 的概念來顯示 ReactiveIconContainer。
爲了知道咱們要作什麼(顯示圖標,增大/縮小圖標,選擇......),咱們須要使用一些手勢檢測。 換句話說,處理與用戶手指相關的事件。 在 Flutter 中,有不一樣的方式來處理與用戶手指的交互。
請注意,用戶的 手指 在 Flutter 中稱爲 Pointer。 在這個解決方案中,我選擇了 GestureDetector,它提供了咱們須要的全部便捷工具,其中包括:
onHorizontalDragDown & onVerticalDragDown
當指針觸摸屏幕時調用的回調
onHorizontalDragStart & onVerticalDragStart
當指針開始在屏幕上移動時調用的回調
onHorizontalDragEnd & onVerticalDragEnd
當先前與屏幕接觸的 Pointer 再也不觸摸屏幕時調用的回調
onHorizontalDragUpdate & onVerticalDragUpdate
當指針在屏幕上移動時調用的回調
onTap
當用戶點擊屏幕時調用的回調
onHorizontalDragCancel & onVerticalDragCancel
當剛剛使用 Pointer 完成的操做(以前觸摸過屏幕)時,調用的回調將不會致使任何點擊事件
當用戶在 ReactiveButton 上觸摸屏幕時,一切都將開始,將 ReactiveButton 用 GestureDetector 包裝彷佛很天然,以下所示:
@override
Widget build(BuildContext context){
return GestureDetector(
onHorizontalDragStart: _onDragStart,
onVerticalDragStart: _onDragStart,
onHorizontalDragCancel: _onDragCancel,
onVerticalDragCancel: _onDragCancel,
onHorizontalDragEnd: _onDragEnd,
onVerticalDragEnd: _onDragEnd,
onHorizontalDragDown: _onDragReady,
onVerticalDragDown: _onDragReady,
onHorizontalDragUpdate: _onDragMove,
onVerticalDragUpdate: _onDragMove,
onTap: _onTap,
child: _buildButton(),
);
}
複製代碼
GestureDetector 還提供了名爲 onPanStart,onPanCancel …… 的回調,也可使用,而且在沒有滾動區域時它能夠正常工做。 由於在這個例子中,咱們還須要考慮 ReactiveButton 可能位於 滾動區域中的狀況,這不會像用戶在屏幕上滑動他/她的手指那樣工做,這也會致使滾動區域滾動。
正如您所看到的,我沒有使用 onLongPress 回調,而需求表示當用戶長按按鈕時咱們須要顯示 ReactiveIconContainer。 爲何?
緣由有兩個:
捕獲手勢事件以肯定懸停/選擇哪一個圖標,使用 onLongPress 事件,不容許這樣(拖動動做將被忽略)
也許咱們須要定製「長按」持續時間
如今讓咱們弄清楚各個部分的責任……
ReactiveButton 將負責:
捕獲手勢事件
檢測到長按時顯示 ReactiveIconContainer
當用戶從屏幕上釋放他/她的手指時隱藏 ReactiveIconContainer
爲其調用者提供用戶動做的結果(onTap,onSelected)
在屏幕上正確顯示 ReactiveIconContainer
ReactiveIconContainer 僅負責:
構造容器
實例化圖標
ReactiveIcon 將負責:
根據是否懸停顯示不一樣大小的圖標
告訴 ReactiveButton 它是否在懸停
咱們剛剛看到咱們須要在組件之間發起一些通訊,以便:
ReactiveButton 能夠爲 ReactiveIcon 提供屏幕上 Pointer 的位置(用於肯定圖標是不是懸停的)
ReactiveIcon 能夠告訴 ReactiveButton 它是否懸停
爲了避免產生像意大利麪條同樣的代碼,我將使用 Stream 的概念。
這樣作,
ReactiveButton 會將 Pointer 的位置廣播給有興趣知道它的組件
ReactiveIcon 將向任何感興趣的人廣播,不管是處於懸停狀態的仍是不懸停的
下圖說明了這個想法。
因爲 ReactiveButton 可能位於頁面中的任何位置,所以咱們須要獲取其位置才能顯示 ReactiveIconContainer。
因爲頁面可能比視口大,而 ReactiveButton 可能位於頁面的任何位置,咱們須要獲取其物理座標。
如下幫助類爲咱們提供了該位置,以及與視口,窗口,可滾動...相關的其餘信息。
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
///
/// Helper class to determine the position of a widget (via its BuildContext) in the Viewport,
/// considering the case where the screen might be larger than the viewport.
/// In this case, we need to consider the scrolling offset(s).
///
class WidgetPosition {
double xPositionInViewport;
double yPositionInViewport;
double viewportWidth;
double viewportHeight;
bool isInScrollable = false;
Axis scrollableAxis;
double scrollAreaMax;
double positionInScrollArea;
Rect rect;
WidgetPosition({
this.xPositionInViewport,
this.yPositionInViewport,
this.viewportWidth,
this.viewportHeight,
this.isInScrollable : false,
this.scrollableAxis,
this.scrollAreaMax,
this.positionInScrollArea,
this.rect,
});
WidgetPosition.fromContext(BuildContext context){
// Obtain the button RenderObject
final RenderObject object = context.findRenderObject();
// Get the physical dimensions and position of the button in the Viewport
final translation = object?.getTransformTo(null)?.getTranslation();
// Get the potential Viewport (case of scroll area)
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
// Get the device Window dimensions and properties
final MediaQueryData mediaQueryData = MediaQuery.of(context);
// Get the Scroll area state (if any)
final ScrollableState scrollableState = Scrollable.of(context);
// Get the physical dimensions and dimensions on the Screen
final Size size = object?.semanticBounds?.size;
xPositionInViewport = translation.x;
yPositionInViewport = translation.y;
viewportWidth = mediaQueryData.size.width;
viewportHeight = mediaQueryData.size.height;
rect = Rect.fromLTWH(translation.x, translation.y, size.width, size.height);
// If viewport exists, this means that we are inside a Scrolling area
// Take this opportunity to get the characteristics of that Scrolling area
if (viewport != null){
final ScrollPosition position = scrollableState.position;
final RevealedOffset vpOffset = viewport.getOffsetToReveal(object, 0.0);
isInScrollable = true;
scrollAreaMax = position.maxScrollExtent;
positionInScrollArea = vpOffset.offset;
scrollableAxis = scrollableState.widget.axis;
}
}
@override
String toString(){
return 'X,Y in VP: $xPositionInViewport,$yPositionInViewport VP dimensions: $viewportWidth,$viewportHeight ScrollArea max: $scrollAreaMax X/Y in scroll: $positionInScrollArea ScrollAxis: $scrollableAxis';
}
}
複製代碼
好的,如今咱們已經有了解決方案的大塊組件規劃,讓咱們構建全部這些……
肯定用戶的意圖
這個小部件中最棘手的部分是瞭解用戶想要作什麼,換句話說,理解手勢。
如前所述,咱們不能使用 onLongPress 回調,由於咱們也要考慮拖動動做。 所以,咱們必須本身實現。
這將實現以下: 當用戶觸摸屏幕時(經過 onHorizontalDragDown 或 onVerticalDragDown),咱們啓動一個 Timer
若是用戶在 Timer 延遲以前從屏幕上鬆開手指,則表示 長按 未完成
若是用戶在 Timer 延遲以前沒有釋放他/她的手指,這意味着咱們須要考慮是 長按 而再也不是 點擊。 而後咱們顯示 ReactiveIconContainer。
若是調用 onTap 回調,咱們須要取消定時器。
如下代碼提取說明了上面解釋的實現。
import 'dart:async';
import 'package:flutter/material.dart';
class ReactiveButton extends StatefulWidget {
ReactiveButton({
Key key,
@required this.onTap,
@required this.child,
}): super(key: key);
/// Callback to be used when the user proceeds with a simple tap
final VoidCallback onTap;
/// Child
final Widget child;
@override
_ReactiveButtonState createState() => _ReactiveButtonState();
}
class _ReactiveButtonState extends State<ReactiveButton> {
// Timer to be used to determine whether a longPress completes
Timer timer;
// Flag to know whether we dispatch the onTap
bool isTap = true;
// Flag to know whether the drag has started
bool dragStarted = false;
@override
void dispose(){
_cancelTimer();
_hideIcons();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragStart: _onDragStart,
onVerticalDragStart: _onDragStart,
onHorizontalDragCancel: _onDragCancel,
onVerticalDragCancel: _onDragCancel,
onHorizontalDragEnd: _onDragEnd,
onVerticalDragEnd: _onDragEnd,
onHorizontalDragDown: _onDragReady,
onVerticalDragDown: _onDragReady,
onHorizontalDragUpdate: _onDragMove,
onVerticalDragUpdate: _onDragMove,
onTap: _onTap,
child: widget.child,
);
}
//
// The user did a simple tap.
// We need to tell the parent
//
void _onTap(){
_cancelTimer();
if (isTap && widget.onTap != null){
widget.onTap();
}
}
// The user released his/her finger
// We need to hide the icons
void _onDragEnd(DragEndDetails details){
_cancelTimer();
_hideIcons();
}
void _onDragReady(DragDownDetails details){
// Let's wait some time to make the distinction
// between a Tap and a LongTap
isTap = true;
dragStarted = false;
_startTimer();
}
// Little trick to make sure we are hiding
// the Icons container if a 'dragCancel' is
// triggered while no move has been detected
void _onDragStart(DragStartDetails details){
dragStarted = true;
}
void _onDragCancel() async {
await Future.delayed(const Duration(milliseconds: 200));
if (!dragStarted){
_hideIcons();
}
}
//
// The user is moving the pointer around the screen
// We need to pass this information to whomever
// might be interested (icons)
//
void _onDragMove(DragUpdateDetails details){
...
}
// ###### LongPress related ##########
void _startTimer(){
_cancelTimer();
timer = Timer(Duration(milliseconds: 500), _showIcons);
}
void _cancelTimer(){
if (timer != null){
timer.cancel();
timer = null;
}
}
// ###### Icons related ##########
// We have waited enough to consider that this is
// a long Press. Therefore, let's display
// the icons
void _showIcons(){
// It is no longer a Tap
isTap = false;
...
}
void _hideIcons(){
...
}
}
複製代碼
當咱們肯定是時候顯示圖標時,如前所述,咱們將使用 OverlayEntry 將它們顯示在 全部內容之上。
如下代碼說明了如何實例化 ReactiveIconContainer 並將其添加到 Overlay(以及如何從 Overlay 中刪除它)。
OverlayState _overlayState;
OverlayEntry _overlayEntry;
void _showIcons(){
// It is no longer a Tap
isTap = false;
// Retrieve the Overlay
_overlayState = Overlay.of(context);
// Generate the ReactionIconContainer that will be displayed onto the Overlay
_overlayEntry = OverlayEntry(
builder: (BuildContext context){
return ReactiveIconContainer(
...
);
},
);
// Add it to the Overlay
_overlayState.insert(_overlayEntry);
}
void _hideIcons(){
_overlayEntry?.remove();
_overlayEntry = null;
}
複製代碼
Line #9
咱們從 BuildContext 中檢索Overlay的實例
Lines #12-18
咱們建立了一個 OverlayEntry 的新實例,它包含了 ReactiveIconContainer 的新實例
Line 21
咱們將 OverlayEntry 添加到 Overlay 中
Line 25
當咱們須要從屏幕上刪除 ReactiveIconContainer 時,咱們只需刪除相應的 OverlayEntry
以前咱們說,經過使用 Streams, ReactiveButton 將用於把 Pointer 的移動廣播到 ReactiveIcon。
爲了實現這一目標,咱們須要建立用於傳遞此信息的 Stream。
在 ReactiveButton 級別,一個簡單的實現可能以下:
StreamController<Offset> _gestureStream = StreamController<Offset>.broadcast();
// then when we instantiate the OverlayEntry
...
_overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return ReactiveIconContainer(
stream: _gestureStream.stream,
);
}
);
// then when we need to broadcast the gestures
void _onDragMove(DragUpdateDetails details){
_gestureStream.sink.add(details.globalPosition);
}
複製代碼
這是能夠正常工做的,可是 從文章前面咱們還提到,第二個流將用於將信息從 ReactiveIcons 傳遞到 ReactiveButton。 所以,我決定用 BLoC 設計模式。
下面是精簡後的 BLoC,它僅用於經過使用 Stream 來傳達手勢。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
class ReactiveButtonBloc {
//
// Stream that allows broadcasting the pointer movements
//
PublishSubject<Offset> _pointerPositionController = PublishSubject<Offset>();
Sink<Offset> get inPointerPosition => _pointerPositionController.sink;
Observable<Offset> get outPointerPosition => _pointerPositionController.stream;
//
// Dispose the resources
//
void dispose() {
_pointerPositionController.close();
}
}
複製代碼
正如您將看到的,我使用了 RxDart 包,更具體地說是 PublishSubject 和 Observable (而不是 StreamController 和 Stream ),由於這些類提供了咱們稍後將使用的加強的功能。
因爲 ReactiveButton 負責廣播手勢,所以它還負責實例化 BLoC,將其提供給 ReactiveIcons 並負責銷燬其資源。
咱們將經過如下方式實現:
class _ReactiveButtonState extends State<ReactiveButton> {
ReactiveButtonBloc bloc;
...
@override
void initState() {
super.initState();
// Initialization of the ReactiveButtonBloc
bloc = ReactiveButtonBloc();
...
}
@override
void dispose() {
_cancelTimer();
_hideIcons();
bloc?.dispose();
super.dispose();
}
...
//
// The user is moving the pointer around the screen
// We need to pass this information to whomever
// might be interested (icons)
//
void _onDragMove(DragUpdateDetails details) {
bloc.inPointerPosition.add(details.globalPosition);
}
...
// We have waited enough to consider that this is
// a long Press. Therefore, let's display
// the icons
void _showIcons() {
// It is no longer a Tap
isTap = false;
// Retrieve the Overlay
_overlayState = Overlay.of(context);
// Generate the ReactionIconContainer that will be displayed onto the Overlay
_overlayEntry = OverlayEntry(
builder: (BuildContext context) {
return ReactiveIconContainer(
bloc: bloc,
);
},
);
// Add it to the Overlay
_overlayState.insert(_overlayEntry);
}
...
}
複製代碼
第10行:咱們實例化了 bloc
第19行:咱們釋放了它的資源
第29行:捕獲拖動手勢時,咱們經過 Stream 將其傳遞給圖標
第47行:咱們將 bloc 傳遞給 ReactiveIconContainer
另外一個有趣的部分是知道哪些 ReactiveIcon 被懸停以突出顯示它。
爲了得到 Pointer 的位置,每一個 ReactiveIcon 將按以下方式訂閱 Streams:
class _ReactiveIconState extends State<ReactiveIcon> with SingleTickerProviderStateMixin {
StreamSubscription _streamSubscription;
@override
void initState(){
super.initState();
// Start listening to pointer position changes
_streamSubscription = widget.bloc
.outPointerPosition
// take some time before jumping into the request (there might be several ones in a row)
.bufferTime(Duration(milliseconds: 100))
// and, do not update where this is no need
.where((batch) => batch.isNotEmpty)
.listen(_onPointerPositionChanged);
}
@override
void dispose(){
_animationController?.dispose();
_streamSubscription?.cancel();
super.dispose();
}
}
複製代碼
咱們使用 StreamSubscription 來監聽由 ReactiveButton 經過 BLoC 廣播的手勢位置。
因爲 Pointer 可能常常移動,所以每次手勢位置發生變化時檢測是否懸停圖標都不是很是有效率。 爲了減小這種檢測次數,咱們利用 Observable 來緩衝 ReactiveButton 發出的事件,而且每100毫秒只考慮一次變化。
爲了肯定 Pointer 是否懸停在一個圖標上,咱們:
經過 WidgetPosition 幫助類得到它的位置
經過 widgetPosition.rect.contains (位置)檢測指針位置是否懸停在圖標上
//
// The pointer position has changed
// We need to check whether it hovers this icon
// If yes, we need to highlight this icon (if not yet done)
// If not, we need to remove any highlight
//
bool _isHovered = false;
void _onPointerPositionChanged(List<Offset> position){
WidgetPosition widgetPosition = WidgetPosition.fromContext(context);
bool isHit = widgetPosition.rect.contains(position.last);
if (isHit){
if (!_isHovered){
_isHovered = true;
...
}
} else {
if (_isHovered){
_isHovered = false;
...
}
}
}
複製代碼
因爲緩衝 Stream 發出的事件,生成一系列位置,咱們只考慮最後一個。 這解釋了爲何在此程序中使用 position.last。
爲了突出顯示正在懸停的 ReactiveIcon,咱們將使用動畫來增長其尺寸,以下所示:
class _ReactiveIconState extends State<ReactiveIcon> with SingleTickerProviderStateMixin {
StreamSubscription _streamSubscription;
AnimationController _animationController;
// Flag to know whether this icon is currently hovered
bool _isHovered = false;
@override
void initState(){
super.initState();
// Reset
_isHovered = false;
// Initialize the animation to highlight the hovered icon
_animationController = AnimationController(
value: 0.0,
duration: const Duration(milliseconds: 200),
vsync: this,
)..addListener((){
setState((){});
}
);
// Start listening to pointer position changes
_streamSubscription = widget.bloc
.outPointerPosition
// take some time before jumping into the request (there might be several ones in a row)
.bufferTime(Duration(milliseconds: 100))
// and, do not update where this is no need
.where((batch) => batch.isNotEmpty)
.listen(_onPointerPositionChanged);
}
@override
void dispose(){
_animationController?.dispose();
_streamSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Transform.scale(
scale: 1.0 + _animationController.value * 1.2,
alignment: Alignment.center,
child: _buildIcon(),
);
}
...
//
// The pointer position has changed
// We need to check whether it hovers this icon
// If yes, we need to highlight this icon (if not yet done)
// If not, we need to remove any highlight
// Also, we need to notify whomever interested in knowning
// which icon is highlighted or lost its highlight
//
void _onPointerPositionChanged(List<Offset> position){
WidgetPosition widgetPosition = WidgetPosition.fromContext(context);
bool isHit = widgetPosition.rect.contains(position.last);
if (isHit){
if (!_isHovered){
_isHovered = true;
_animationController.forward();
}
} else {
if (_isHovered){
_isHovered = false;
_animationController.reverse();
}
}
}
}
複製代碼
說明:
第1行:咱們使用 SingleTickerProviderStateMixin 做爲動畫的 Ticker
第16-20行:咱們初始化一個 AnimationController
第20-23行:動畫運行時,咱們將從新構建 ReactiveIcon
第37行:咱們須要在刪除 ReactiveIcon 時釋放 AnimationController 引用的的資源
第44-48行:咱們將根據 AnimationController.value (範圍[0..1])按任意比例縮放 ReactiveIcon
第67行:當 ReactiveIcon 懸停時,啓動動畫(從 0 - > 1)
第72行:當 ReactiveIcon 再也不懸停時,啓動動畫(從 1 - > 0)
這個解釋的最後一部分涉及到向 ReactiveButton 傳達 ReactiveIcon 當前懸停的狀態,這樣,若是用戶從屏幕上鬆開他/她手指的那一刻,咱們須要知道哪一個 ReactiveIcon 被認爲是選擇。
如前所述,咱們將使用第二個 Stream 來傳達此信息。
爲了告訴 ReactiveButton 如今哪一個 ReactiveIcon 正處於懸停的狀態以及哪些 ReactiveIcon 再也不懸停,咱們將使用專門的消息:ReactiveIconSelectionMessage。 此消息將告知「可能選擇了哪一個圖標」和「再也不選擇哪一個圖標」。
BLoC 如今須要包含新的 Stream 來傳達此消息。
這是新的 BLoC:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:reactive_button/reactive_icon_selection_message.dart';
class ReactiveButtonBloc {
//
// Stream that allows broadcasting the pointer movements
//
PublishSubject<Offset> _pointerPositionController = PublishSubject<Offset>();
Sink<Offset> get inPointerPosition => _pointerPositionController.sink;
Observable<Offset> get outPointerPosition => _pointerPositionController.stream;
//
// Stream that allows broadcasting the icons selection
//
PublishSubject<ReactiveIconSelectionMessage> _iconSelectionController = PublishSubject<ReactiveIconSelectionMessage>();
Sink<ReactiveIconSelectionMessage> get inIconSelection => _iconSelectionController.sink;
Stream<ReactiveIconSelectionMessage> get outIconSelection => _iconSelectionController.stream;
//
// Dispose the resources
//
void dispose() {
_iconSelectionController.close();
_pointerPositionController.close();
}
}
複製代碼
爲了讓 ReactiveButton 接收 ReactiveIcons 發出的此類通知,咱們須要訂閱此消息事件,以下所示:
class _ReactiveButtonState extends State<ReactiveButton> {
...
StreamSubscription streamSubscription;
ReactiveIconDefinition _selectedButton;
@override
void initState() {
super.initState();
// Initialization of the ReactiveButtonBloc
bloc = ReactiveButtonBloc();
// Start listening to messages from icons
streamSubscription = bloc.outIconSelection.listen(_onIconSelectionChange);
}
@override
void dispose() {
_cancelTimer();
_hideIcons();
streamSubscription?.cancel();
bloc?.dispose();
super.dispose();
}
...
//
// A message has been sent by an icon to indicate whether
// it is highlighted or not
//
void _onIconSelectionChange(ReactiveIconSelectionMessage message) {
if (identical(_selectedButton, message.icon)) {
if (!message.isSelected) {
_selectedButton = null;
}
} else {
if (message.isSelected) {
_selectedButton = message.icon;
}
}
}
}
複製代碼
第14行:咱們訂閱了 Stream 發出的全部消息
第21行:當 ReactiveButton 被刪除時, 取消訂閱
第32-42行:處理 ReactiveIcons 發出的消息
當 ReactiveIcon 須要向 ReactiveButton 發送消息時,它只是按以下方式使用 Stream :
void _onPointerPositionChanged(List<Offset> position){
WidgetPosition widgetPosition = WidgetPosition.fromContext(context);
bool isHit = widgetPosition.rect.contains(position.last);
if (isHit){
if (!_isHovered){
_isHovered = true;
_animationController.forward();
_sendNotification();
}
} else {
if (_isHovered){
_isHovered = false;
_animationController.reverse();
_sendNotification();
}
}
}
//
// Send a notification to whomever is interesting
// in knowning the current status of this icon
//
void _sendNotification(){
widget.bloc
.inIconSelection
.add(ReactiveIconSelectionMessage(
icon: widget.icon,
isSelected: _isHovered,
));
}
複製代碼
第8行和第14行:當 _isHovered 變量發生改變時,調用 _sendNotification 方法
第23-29行:向 Stream 發出 ReactiveIconSelectionMessage
我的認爲,源代碼的其他部分不須要任何進一步的文檔說明,由於它只涉及 ReactiveButton Widget 的參數和外觀。
本文的目的是展現如何將多個主題( BLoC,Reactive Programming,Animation,Overlay )組合在一塊兒,並提供一個實際的使用示例。
但願你喜歡這篇文章。
請繼續關注下一篇文章,會很快發佈的。 祝編碼愉快。