flutter 图解_Flutter自绘组件:微信悬浮窗(三)
前期指路:
????Flutter自繪組件:微信懸浮窗(一)
????Flutter自繪組件:微信懸浮窗(二)
上兩講中講解了微信懸浮窗按鈕形態(tài)的實現(xiàn),在本章中講解如何實現(xiàn)懸浮窗列表形態(tài)。廢話不多說,先上效果對比圖。
效果對比
實現(xiàn)難點
這部分的難點主要有以下:
列表的每一項均是不規(guī)則的圖形。
該項存在多個動畫,如關(guān)閉時從屏幕中間返回至屏幕邊緣的動畫,關(guān)閉某項后該項往下的所有項向上平移的動畫,以及出現(xiàn)時由屏幕邊緣伸展至屏幕中間的動畫。
列表中存在動畫的銜接,如某列表項關(guān)閉是會有從中間返回至屏幕邊緣的消失動畫,且在消失之后,該列表項下面的列表項會產(chǎn)生一個往上移動的動畫效果,如何做到這兩個動畫的無縫鏈接?
實現(xiàn)思路
列表項非規(guī)則圖形,依舊按照按鈕形態(tài)的方法,使用CustomPainter和CustomPaint進(jìn)行自定義圖形的繪制。多個動畫,根據(jù)觸發(fā)的條件和環(huán)境不同,選擇直接使用AnimationController進(jìn)行管理或編寫一個AnimatedWidget的子類,在父組件中進(jìn)行管理。至于動畫銜接部分,核心是狀態(tài)管理。不同的列表項同屬一個Widget,當(dāng)其中一個列表項關(guān)閉完成后通知父組件列表,然后父組件再控制該列表項下的所有列表項進(jìn)行一個自下而上的平移動畫,直至到達(dá)關(guān)閉的列表項原位置。
這個組件的關(guān)鍵詞列表和動畫,可能很多人已經(jīng)想到了十分簡單的實現(xiàn)方法,就是使用AnimatedList組件,它其內(nèi)包含了增、刪、插入時動畫的接口,實現(xiàn)起來十分方便,但在本次中為了更深入了解狀態(tài)管理和培養(yǎng)邏輯思維,并沒有使用到這個組件,而是通過InheritedWidget和Notification的方法,完成了狀態(tài)的傳遞,從而實現(xiàn)動畫的銜接。在下一篇文章中會使用AnimatedList重寫,讀者可以把兩種實現(xiàn)進(jìn)行一個對比,加深理解。
使用到的新類
AnimationWidget:鏈接 :《Flutter實戰(zhàn)》--動畫結(jié)構(gòu)
Notification和NotificationListener:鏈接:《Flutter實戰(zhàn)》--Notification
InheritedWidget?:?鏈接:《Flutter實戰(zhàn) 》--數(shù)據(jù)共享
列表項圖解及繪制代碼
圖解對比如下:
image
在設(shè)計的時候我把列表項的寬度設(shè)為屏幕的寬度的一般再加上50.0,左右列表項在中間的內(nèi)容部分的布局是完全一樣的,只是在外層部分有所不同,在繪制的時候,我分別把列表項的背景部分(背景陰影,外邊緣,以及內(nèi)層)、Logo部分、文字部分、交叉部分分別封裝成了一個函數(shù),避免了重復(fù)代碼的編寫,需要注意的是繪制Logo的Image對象的獲取,在上一章中有講到,此處不再詳述。其他詳情看代碼及注釋:
/// [FloatingItemPainter]:畫筆類,繪制列表項class FloatingItemPainter extends CustomPainter{
FloatingItemPainter({
@required this.title,
@required this.isLeft,
@required this.isPress,
@required this.image
});
/// [isLeft] 列表項在左側(cè)/右側(cè)
bool isLeft = true;
/// [isPress] 列表項是否被選中,選中則繪制陰影
bool isPress;
/// [title] 繪制列表項內(nèi)容
String title;
/// [image] 列表項圖標(biāo)
ui.Image image;
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
if(size.width < 50.0){
return ;
}
else{
if(isLeft){
paintLeftItem(canvas, size);
if(image != null)//防止傳入null引起崩潰
paintLogo(canvas, size);
paintParagraph(canvas, size);
paintCross(canvas, size);
}else{
paintRightItem(canvas, size);
paintParagraph(canvas, size);
paintCross(canvas, size);
if(image != null)
paintLogo(canvas, size);
}
}
}
/// 通過傳入[Canvas]對象和[Size]對象繪制左側(cè)列表項外邊緣,陰影以及內(nèi)層
void paintLeftItem(Canvas canvas,Size size){
/// 外邊緣路徑
Path edgePath = new Path() ..moveTo(size.width - 25.0, 0.0);
edgePath.lineTo(0.0, 0.0);
edgePath.lineTo(0.0, size.height);
edgePath.lineTo(size.width - 25.0, size.height);
edgePath.arcTo(Rect.fromCircle(center: Offset(size.width - 25.0,size.height / 2),radius: 25), pi * 1.5, pi, true);
/// 繪制背景陰影
canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 3, true);
var paint = new Paint()
..style = PaintingStyle.fill
..color = Colors.white;
/// 通過填充去除列表項內(nèi)部多余的陰影
canvas.drawPath(edgePath, paint);
paint = new Paint()
..isAntiAlias = true // 抗鋸齒
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1)
..strokeWidth = 0.75
..maskFilter = MaskFilter.blur(BlurStyle.solid, 0.25); //邊緣模糊
/// 繪制列表項外邊緣
canvas.drawPath(edgePath, paint);
/// [innerPath] 內(nèi)層路徑
Path innerPath = new Path() ..moveTo(size.width - 25.0, 1.5);
innerPath.lineTo(0.0, 1.5);
innerPath.lineTo(0.0, size.height - 1.5);
innerPath.lineTo(size.width - 25.0, size.height - 1.5);
innerPath.arcTo(Rect.fromCircle(center: Offset(size.width - 25.0,size.height / 2),radius: 23.5), pi * 1.5, pi, true);
paint = new Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 1);
/// 繪制列表項內(nèi)層
canvas.drawPath(innerPath, paint);
/// 繪制選中陰影
if(isPress)
canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, true);
}
/// 通過傳入[Canvas]對象和[Size]對象繪制左側(cè)列表項外邊緣,陰影以及內(nèi)層
void paintRightItem(Canvas canvas,Size size){
/// 外邊緣路徑
Path edgePath = new Path() ..moveTo(25.0, 0.0);
edgePath.lineTo(size.width, 0.0);
edgePath.lineTo(size.width, size.height);
edgePath.lineTo(25.0, size.height);
edgePath.arcTo(Rect.fromCircle(center: Offset(25.0,size.height / 2),radius: 25), pi * 0.5, pi, true);
/// 繪制列表項背景陰影
canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 3, true);
var paint = new Paint()
..style = PaintingStyle.fill
..color = Colors.white;
/// 通過填充白色去除列表項內(nèi)部多余陰影
canvas.drawPath(edgePath, paint);
paint = new Paint()
..isAntiAlias = true
..style = PaintingStyle.stroke
..color = Color.fromRGBO(0xCF, 0xCF, 0xCF, 1)
..strokeWidth = 0.75
..maskFilter = MaskFilter.blur(BlurStyle.solid, 0.25); //邊緣模糊
/// 繪制列表項外邊緣
canvas.drawPath(edgePath, paint);
/// 列表項內(nèi)層路徑
Path innerPath = new Path() ..moveTo(25.0, 1.5);
innerPath.lineTo(size.width, 1.5);
innerPath.lineTo(size.width, size.height - 1.5);
innerPath.lineTo(25.0, size.height - 1.5);
innerPath.arcTo(Rect.fromCircle(center: Offset(25.0,25.0),radius: 23.5), pi * 0.5, pi, true);
paint = new Paint()
..isAntiAlias = false
..style = PaintingStyle.fill
..color = Color.fromRGBO(0xF3, 0xF3, 0xF3, 1);
/// 繪制列表項內(nèi)層
canvas.drawPath(innerPath, paint);
/// 條件繪制選中陰影
if(isPress)
canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, false);
}
/// 通過傳入[Canvas]對象和[Size]對象以及[image]繪制列表項Logo
void paintLogo(Canvas canvas,Size size){
//繪制中間圖標(biāo)
var paint = new Paint();
canvas.save(); //剪裁前保存圖層
RRect imageRRect = RRect.fromRectAndRadius(Rect.fromLTWH(25.0 - 17.5,25.0- 17.5, 35, 35),Radius.circular(17.5));
canvas.clipRRect(imageRRect);//圖片為圓形,圓形剪裁
canvas.drawColor(Colors.white, BlendMode.srcOver); //設(shè)置填充顏色為白色
Rect srcRect = Rect.fromLTWH(0.0, 0.0, image.width.toDouble(), image.height.toDouble());
Rect dstRect = Rect.fromLTWH(25.0 - 17.5, 25.0 - 17.5, 35, 35);
canvas.drawImageRect(image, srcRect, dstRect, paint);
canvas.restore();//圖片繪制完畢恢復(fù)圖層
}
/// 通過傳入[Canvas]對象和[Size]對象以及[title]繪制列表項的文字說明部分
void paintParagraph(Canvas canvas,Size size){
ui.ParagraphBuilder pb = ui.ParagraphBuilder(ui.ParagraphStyle(
textAlign: TextAlign.left,//左對齊
fontWeight: FontWeight.w500,
fontSize: 14.0, //字體大小
fontStyle: FontStyle.normal,
maxLines: 1, //行數(shù)限制
ellipsis: "…" //省略顯示
));
pb.pushStyle(ui.TextStyle(color: Color.fromRGBO(61, 61, 61, 1),)); //字體顏色
double pcLength = size.width - 100.0; //限制繪制字符串寬度
ui.ParagraphConstraints pc = ui.ParagraphConstraints(width: pcLength);
pb.addText(title);
ui.Paragraph paragraph = pb.build() ..layout(pc);
Offset startOffset = Offset(50.0,18.0); // 字符串顯示位置
/// 繪制字符串
canvas.drawParagraph(paragraph, startOffset);
}
/// 通過傳入[Canvas]對象和[Size]對象繪制列表項末尾的交叉部分,
void paintCross(Canvas canvas,Size size){
/// ‘x’ 路徑
Path crossPath = new Path()
..moveTo(size.width - 28.5, 21.5);
crossPath.lineTo(size.width - 21.5,28.5);
crossPath.moveTo(size.width - 28.5, 28.5);
crossPath.lineTo(size.width - 21.5, 21.5);
var paint = new Paint()
..isAntiAlias = true
..color = Color.fromRGBO(61, 61, 61, 1)
..style = PaintingStyle.stroke
..strokeWidth = 0.75
..maskFilter = MaskFilter.blur(BlurStyle.normal, 0.25); // 線段模糊
/// 繪制交叉路徑
canvas.drawPath(crossPath, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return (true && image != null);
}
}
列表項的實現(xiàn)代碼
實現(xiàn)完列表項的繪制代碼FloatingItemPainter類,你還需要一個畫布CustomPaint和事件邏輯。一個完整列表項類除了繪制代碼外還需要補(bǔ)充繪制區(qū)域的定位,列表項手勢方法的捕捉(關(guān)閉和點擊事件,關(guān)閉動畫的邏輯處理。對于定位,縱坐標(biāo)是根據(jù)傳進(jìn)來的top值決定的,對于列表項的Letf值則是根據(jù)列表項位于左側(cè) / 右側(cè)的,左側(cè)很好理解就為0。而右側(cè)的坐標(biāo),由于列表項的長度為width + 50.0,因此列表項位于右側(cè)時,橫坐標(biāo)為width - 50.0,如下圖:
對于關(guān)閉動畫,則是對橫坐標(biāo)Left取動畫值來實現(xiàn)由中間收縮回邊緣的動畫效果。
對于事件的捕捉,需要確定當(dāng)前列表項的點擊區(qū)域和關(guān)閉區(qū)域。在事件處理的時候需要考慮較為極端的情況,就是把UI使用者不當(dāng)正常人來看。正常的點擊包括按下和抬起兩個事件,但如果存在按下后拖拽出區(qū)域的情況呢?這時即使抬起后列表項還是處于選中的狀態(tài),還需要監(jiān)聽一個onTapCancel的事件,當(dāng)拖拽離開列表項監(jiān)聽區(qū)域時將列表項設(shè)為未選中狀態(tài)。
FloatingItem類的具體代碼及解析如下:
/// [FloatingItem]一個單獨功能完善的列表項類class FloatingItem extends StatefulWidget {
FloatingItem({
@required this.top,
@required this.isLeft,
@required this.title,
@required this.imageProvider,
@required this.index,
this.left,
Key key
});
/// [index] 列表項的索引值
int index;
/// [top]列表項的y坐標(biāo)值
double top;
/// [left]列表項的x坐標(biāo)值
double left;
///[isLeft] 列表項是否在左側(cè),否則是右側(cè)
bool isLeft;
/// [title] 列表項的文字說明
String title;
///[imageProvider] 列表項Logo的imageProvider
ImageProvider imageProvider;
@override
_FloatingItemState createState() => _FloatingItemState();
}
class _FloatingItemState extends State<FloatingItem> with TickerProviderStateMixin{
/// [isPress] 列表項是否被按下
bool isPress = false;
///[image] 列表項Logo的[ui.Image]對象,用于繪制Logo
ui.Image image;
/// [animationController] 列表關(guān)閉動畫的控制器
AnimationController animationController;
/// [animation] 列表項的關(guān)閉動畫
Animation animation;
/// [width] 屏幕寬度的一半,用于確定列表項的寬度
double width;
@override
void initState() {
// TODO: implement initState
isPress = false;
/// 獲取Logo的ui.Image對象
loadImageByProvider(widget.imageProvider).then((value) {
setState(() {
image = value;
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
if(width == null)
width = MediaQuery.of(context).size.width / 2 ;
if(widget.left == null)
widget.left = widget.isLeft ? 0.0 : width - 50.0;
return Positioned(
left: widget.left,
top: widget.top,
child: GestureDetector(
/// 監(jiān)聽按下事件,在點擊區(qū)域內(nèi)則將[isPress]設(shè)為true,若在關(guān)閉區(qū)域內(nèi)則不做任何操作
onPanDown: (details) {
if (widget.isLeft) {
/// 點擊區(qū)域內(nèi)
if (details.globalPosition.dx < width) {
setState(() {
isPress = true;
});
}
}
else{
/// 點擊區(qū)域內(nèi)
if(details.globalPosition.dx < width * 2 - 50){
setState(() {
isPress = true;
});
}
}
},
/// 監(jiān)聽抬起事件
onTapUp: (details) async {
/// 通過左右列表項來決定關(guān)閉的區(qū)域,以及選中區(qū)域,觸發(fā)相應(yīng)的關(guān)閉或選中事件
if(widget.isLeft){
/// 位于關(guān)閉區(qū)域
if(details.globalPosition.dx >= width && !isPress){
/// 設(shè)置從中間返回至邊緣的關(guān)閉動畫
animationController = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));
animation = new Tween<double>(begin: 0.0,end: -(width + 50.0)).animate(animationController)
..addListener(() {
setState(() {
widget.left = animation.value;
});
});
/// 等待關(guān)閉動畫結(jié)束后通知父級已關(guān)閉
await animationController.forward();
/// 銷毀動畫資源
animationController.dispose();
/// 通知父級觸發(fā)關(guān)閉事件
ClickNotification(deletedIndex: widget.index).dispatch(context);
}
else{
/// 通知父級觸發(fā)相應(yīng)的點擊事件
ClickNotification(clickIndex: widget.index).dispatch(context);
}
}
else{
/// 位于關(guān)閉區(qū)域
if(details.globalPosition.dx >= width * 2 - 50.0 && !isPress){
/// 設(shè)置從中間返回至邊緣的關(guān)閉動畫
animationController = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));
animation = new Tween<double>(begin: width - 50.0,end: width * 2).animate(animationController)
..addListener(() {
setState(() {
widget.left = animation.value;
});
});
/// 等待執(zhí)行完畢
await animationController.forward();
/// 銷毀動畫資源
animationController.dispose();
/// 通知父級觸發(fā)關(guān)閉事件
ClickNotification(deletedIndex: widget.index).dispatch(context);
}
else{
/// 通知父級觸發(fā)選中事件
ClickNotification(clickIndex: widget.index).dispatch(context);
}
}
/// 抬起后取消選中
setState(() {
isPress = false;
});
},
onTapCancel: (){
/// 超出范圍取消選中
setState(() {
isPress = false;
});
},
child:
CustomPaint(
size: new Size(width + 50.0,50.0),
painter: FloatingItemPainter(
title: widget.title,
isLeft: widget.isLeft,
isPress: isPress,
image: image,
)
)
)
);
}
/// 通過ImageProvider獲取ui.image
Future<ui.Image> loadImageByProvider(
ImageProvider provider, {
ImageConfiguration config = ImageConfiguration.empty,
}) async {
Completer<ui.Image> completer = Completer<ui.Image>(); //完成的回調(diào)
ImageStreamListener listener;
ImageStream stream = provider.resolve(config); //獲取圖片流
listener = ImageStreamListener((ImageInfo frame, bool sync) {
//監(jiān)聽
final ui.Image image = frame.image;
completer.complete(image); //完成
stream.removeListener(listener); //移除監(jiān)聽
});
stream.addListener(listener); //添加監(jiān)聽
return completer.future; //返回
}
}
對于ClickNotification類,看一下代碼:
import 'package:flutter/material.dart';/// [ClickNotification]列表項點擊事件通知類
class ClickNotification extends Notification {
ClickNotification({this.deletedIndex,this.clickIndex});
/// 觸發(fā)了關(guān)閉事件的列表項索引
int deletedIndex = -1;
/// 觸發(fā)了點擊事件的列表項索引
int clickIndex = -1;
}
它繼承自Notification,自定義了一個通知用于處理列表項點擊或關(guān)閉時整個列表發(fā)生的變化。單個列表項在執(zhí)行完關(guān)閉動畫后分發(fā)通知,通知父級進(jìn)行一個列表項上移填補(bǔ)被刪除列表項位置的的動畫。
列表動畫
單個列表項的關(guān)閉動畫,我們已經(jīng)在FlotingItem中實現(xiàn)了。而列表動畫是,列表項關(guān)閉后,索引在其后的其他列表項向上平移填充的動畫,示意圖如下:
已知單個列表項的關(guān)閉動畫是由自身管理實現(xiàn)的,那么單個列表項關(guān)閉后引起的列表動畫由誰進(jìn)行管理呢?自然是由列表進(jìn)行管理。每個列表項除了原始的第一個列表項都可能會發(fā)生向上平移的動畫,因此我們需要對單個的列表項再進(jìn)行一層AnimatedWidget的加裝,方便動畫的傳入與管理,具體代碼如下:
FloatingItemAnimatedWidget:
/// [FloatingItemAnimatedWidget] 列表項進(jìn)行動畫類封裝,方便傳入平移向上動畫class FloatingItemAnimatedWidget extends AnimatedWidget{
FloatingItemAnimatedWidget({
Key key,
Animation<double> animation,
this.index,
}):super(key:key,listenable: animation);
/// [index] 列表項索引
final int index;
@override
Widget build(BuildContext context) {
// TODO: implement build
/// 獲取列表數(shù)據(jù)
var data = FloatingWindowSharedDataWidget.of(context).data;
final Animation<double> animation = listenable;
return FloatingItem(top: animation.value, isLeft: data.isLeft, title: data.dataList[index]['title'],
imageProvider: AssetImage(data.dataList[index]['imageUrl']), index: index);
}
}
代碼中引用到了一個新類FloatingWindowSharedDataWidget,它是一個InheritedWidget,共享了FloatingWindowModel類型的數(shù)據(jù),FloatingWindowModel中包括了懸浮窗用到的一些數(shù)據(jù),例如判斷列表在左側(cè)或右側(cè)的isLeft,列表的數(shù)據(jù)dataList等,避免了父組件向子組件傳數(shù)據(jù)時大量參數(shù)的編寫,一定程度上增強(qiáng)了可維護(hù)性,例如FloatingItemAnimatedWidget中只需要傳入索引值就可以在共享數(shù)據(jù)中提取到相應(yīng)列表項的數(shù)據(jù)。FloatingWindowSharedDataWidget和FloatingWindowModel的代碼及注釋如下:
FloatingWindowSharedDataWidget
/// [FloatingWindowSharedDataWidget]懸浮窗數(shù)據(jù)共享Widgetclass FloatingWindowSharedDataWidget extends InheritedWidget{
FloatingWindowSharedDataWidget({
@required this.data,
Widget child
}) : super(child:child);
final FloatingWindowModel data;
/// 靜態(tài)方法[of]方便直接調(diào)用獲取共享數(shù)據(jù)
static FloatingWindowSharedDataWidget of(BuildContext context){
return context.dependOnInheritedWidgetOfExactType<FloatingWindowSharedDataWidget>();
}
@override
bool updateShouldNotify(FloatingWindowSharedDataWidget oldWidget) {
// TODO: implement updateShouldNotify
/// 數(shù)據(jù)發(fā)生變化則發(fā)布通知
return oldWidget.data != data && data.deleteIndex != -1;
}
}
FloatingWindowModel
/// [FloatingWindowModel] 表示懸浮窗共享的數(shù)據(jù)class FloatingWindowModel {
FloatingWindowModel({
this.isLeft = true,
this.top = 100.0,
List<Map<String,String>> datatList,
}) : dataList = datatList;
/// [isLeft]:懸浮窗位于屏幕左側(cè)/右側(cè)
bool isLeft;
/// [top] 懸浮窗縱坐標(biāo)
double top;
/// [dataList] 列表數(shù)據(jù)
List<Map<String,String>>dataList;
/// 刪除的列表項索引
int deleteIndex = -1;
}
列表的實現(xiàn)
上述已經(jīng)實現(xiàn)了單個列表項并進(jìn)行了動畫的封裝,現(xiàn)在只需要實現(xiàn)列表,監(jiān)聽列表項的點擊和關(guān)閉事件并執(zhí)行相應(yīng)的操作。為了方便,我們實現(xiàn)了一個作為列表的FloatingItems類然后實現(xiàn)了一個懸浮窗類TestWindow來對列表的操作進(jìn)行監(jiān)聽和管理,在以后的文章中還會繼續(xù)完善TestWindow類和FloatingWindowModel類,把前兩節(jié)的實現(xiàn)的FloatingButton加進(jìn)去并實現(xiàn)聯(lián)動。目前的具體實現(xiàn)代碼和注釋如下:
FloatingItems
/// [FloatingItems] 列表class FloatingItems extends StatefulWidget {
@override
_FloatingItemsState createState() => _FloatingItemsState();
}
class _FloatingItemsState extends State<FloatingItems> with TickerProviderStateMixin{
/// [_controller] 列表項動畫的控制器
AnimationController _controller;
/// 動態(tài)生成列表
/// 其中一項觸發(fā)關(guān)閉事件后,索引在該項后的列表項執(zhí)行向上平移的動畫。
List<Widget> getItems(BuildContext context){
/// 釋放和申請新的動畫資源
if(_controller != null){
_controller.dispose();
_controller = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));
}
/// widget列表
List<Widget>widgetList = [];
/// 獲取共享數(shù)據(jù)
var data = FloatingWindowSharedDataWidget.of(context).data;
/// 列表數(shù)據(jù)
var dataList = data.dataList;
/// 遍歷數(shù)據(jù)生成列表項
for(int i = 0; i < dataList.length; ++i){
/// 在觸發(fā)關(guān)閉事件列表項的索引之后的列表項傳入向上平移動畫
if(data.deleteIndex != - 1 && i >= data.deleteIndex){
Animation animation;
animation = new Tween<double>(begin: data.top + (70.0 * (i + 1)),end: data.top + 70.0 * i).animate(_controller);
widgetList.add(FloatingItemAnimatedWidget(animation: animation,index: i));
}
/// 在觸發(fā)關(guān)閉事件列表項的索引之前的列表項則位置固定
else{
Animation animation;
animation = new Tween<double>(begin: data.top + (70.0 * i),end: data.top + 70.0 * i).animate(_controller);
widgetList.add(FloatingItemAnimatedWidget(animation: animation,index: i,));
}
}
/// 執(zhí)行動畫
if(_controller != null)
_controller.forward();
/// 返回列表
return widgetList;
}
@override
void initState() {
// TODO: implement initState
super.initState();
_controller = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));
}
@override
Widget build(BuildContext context) {
return Stack(children: getItems(context),);
}
}
TestWindow
/// [TestWindow] 懸浮窗class TestWindow extends StatefulWidget {
@override
_TestWindowState createState() => _TestWindowState();
}
class _TestWindowState extends State<TestWindow> {
List<Map<String,String>> ls = [
{'title': "測試以下","imageUrl":"assets/Images/vnote.png"},
{'title': "Flutter自繪組件:微信懸浮窗(三)","imageUrl":"assets/Images/vnote.png"},
{'title': "微信懸浮窗","imageUrl":"assets/Images/vnote.png"}
];
/// 懸浮窗數(shù)據(jù)類
FloatingWindowModel windowModel;
@override
void initState() {
// TODO: implement initState
super.initState();
windowModel = new FloatingWindowModel(datatList: ls,isLeft: true);
}
@override
Widget build(BuildContext context) {
return FloatingWindowSharedDataWidget(
data: windowModel,
child:Stack(
fit: StackFit.expand, /// 未定義長寬的子類填充屏幕
children:[
/// 遮蓋層
Container(
decoration:
BoxDecoration(color: Color.fromRGBO(0xEF, 0xEF, 0xEF, 0.9))
),
/// 監(jiān)聽點擊與關(guān)閉事件
NotificationListener<ClickNotification>(
onNotification: (notification) {
/// 關(guān)閉事件
if(notification.deletedIndex != - 1) {
windowModel.deleteIndex = notification.deletedIndex;
setState(() {
windowModel.dataList.removeAt(windowModel.deleteIndex);
});
}
if(notification.clickIndex != -1){
/// 執(zhí)行點擊事件
print(notification.clickIndex);
}
/// 禁止冒泡
return false;
},
child: FloatingItems(),),
])
);
}
}
main代碼
void main(){runApp(MultiProvider(providers: [
ChangeNotifierProvider<ClosingItemProvider>(
create: (_) => ClosingItemProvider(),
)
],
child: new MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue
),
home: new Scaffold(
appBar: new AppBar(title: Text('Flutter Demo')),
body: Stack(
children: [
/// 用于測試遮蓋層是否生效
Positioned(
left: 250,
top: 250,
child: Container(width: 50,height: 100,color: Colors.red,),
),
TestWindow()
],
)
)
);
}
}
總結(jié)
對于列表項的編寫,難度就在于狀態(tài)的管理上和動畫的管理上,繪制上來來去去還是那幾個函數(shù)。組件存在多個復(fù)雜動畫,每個動畫由誰進(jìn)行管理,如何觸發(fā),狀態(tài)量如何傳遞,都是需要認(rèn)真思考才能解決的提出的解決方案,本篇文章采用了一個比較“原始”的方式進(jìn)行實現(xiàn),但能使對狀態(tài)的管理和動畫的管理有更深入的理解,在下篇文章中采用更為簡單的方式進(jìn)行實現(xiàn),通過AnimatedList即動畫列表來實現(xiàn)。
總結(jié)
以上是生活随笔為你收集整理的flutter 图解_Flutter自绘组件:微信悬浮窗(三)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: springboot拦截器拦截提示_Sp
- 下一篇: 矩阵低秩张量分解_【线性代数】张量-张量