日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

flutter 图解_Flutter自绘组件:微信悬浮窗(三)

發布時間:2025/5/22 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 flutter 图解_Flutter自绘组件:微信悬浮窗(三) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前期指路:

????Flutter自繪組件:微信懸浮窗(一)

????Flutter自繪組件:微信懸浮窗(二)

上兩講中講解了微信懸浮窗按鈕形態的實現,在本章中講解如何實現懸浮窗列表形態。廢話不多說,先上效果對比圖。

效果對比

實現難點

這部分的難點主要有以下:

  • 列表的每一項均是不規則的圖形。

  • 該項存在多個動畫,如關閉時從屏幕中間返回至屏幕邊緣的動畫,關閉某項后該項往下的所有項向上平移的動畫,以及出現時由屏幕邊緣伸展至屏幕中間的動畫。

  • 列表中存在動畫的銜接,如某列表項關閉是會有從中間返回至屏幕邊緣的消失動畫,且在消失之后,該列表項下面的列表項會產生一個往上移動的動畫效果,如何做到這兩個動畫的無縫鏈接?

  • 實現思路

    列表項非規則圖形,依舊按照按鈕形態的方法,使用CustomPainter和CustomPaint進行自定義圖形的繪制。多個動畫,根據觸發的條件和環境不同,選擇直接使用AnimationController進行管理或編寫一個AnimatedWidget的子類,在父組件中進行管理。至于動畫銜接部分,核心是狀態管理。不同的列表項同屬一個Widget,當其中一個列表項關閉完成后通知父組件列表,然后父組件再控制該列表項下的所有列表項進行一個自下而上的平移動畫,直至到達關閉的列表項原位置。

    這個組件的關鍵詞列表和動畫,可能很多人已經想到了十分簡單的實現方法,就是使用AnimatedList組件,它其內包含了增、刪、插入時動畫的接口,實現起來十分方便,但在本次中為了更深入了解狀態管理和培養邏輯思維,并沒有使用到這個組件,而是通過InheritedWidget和Notification的方法,完成了狀態的傳遞,從而實現動畫的銜接。在下一篇文章中會使用AnimatedList重寫,讀者可以把兩種實現進行一個對比,加深理解。

    使用到的新類

    AnimationWidget:鏈接 :《Flutter實戰》--動畫結構

    Notification和NotificationListener:鏈接:《Flutter實戰》--Notification

    InheritedWidget?:?鏈接:《Flutter實戰 》--數據共享

    列表項圖解及繪制代碼

    圖解對比如下:

    image

    在設計的時候我把列表項的寬度設為屏幕的寬度的一般再加上50.0,左右列表項在中間的內容部分的布局是完全一樣的,只是在外層部分有所不同,在繪制的時候,我分別把列表項的背景部分(背景陰影,外邊緣,以及內層)、Logo部分、文字部分、交叉部分分別封裝成了一個函數,避免了重復代碼的編寫,需要注意的是繪制Logo的Image對象的獲取,在上一章中有講到,此處不再詳述。其他詳情看代碼及注釋:

    /// [FloatingItemPainter]:畫筆類,繪制列表項
    class FloatingItemPainter extends CustomPainter{

    FloatingItemPainter({
    @required this.title,
    @required this.isLeft,
    @required this.isPress,
    @required this.image
    });

    /// [isLeft] 列表項在左側/右側
    bool isLeft = true;
    /// [isPress] 列表項是否被選中,選中則繪制陰影
    bool isPress;
    /// [title] 繪制列表項內容
    String title;
    /// [image] 列表項圖標
    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]對象繪制左側列表項外邊緣,陰影以及內層
    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;

    /// 通過填充去除列表項內部多余的陰影
    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] 內層路徑
    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);

    /// 繪制列表項內層
    canvas.drawPath(innerPath, paint);



    /// 繪制選中陰影
    if(isPress)
    canvas.drawShadow(edgePath, Color.fromRGBO(0xDA, 0xDA, 0xDA, 0.3), 0, true);

    }

    /// 通過傳入[Canvas]對象和[Size]對象繪制左側列表項外邊緣,陰影以及內層
    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;

    /// 通過填充白色去除列表項內部多余陰影
    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);

    /// 列表項內層路徑
    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);

    /// 繪制列表項內層
    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){
    //繪制中間圖標
    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); //設置填充顏色為白色
    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();//圖片繪制完畢恢復圖層
    }

    /// 通過傳入[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, //行數限制
    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);
    }
    }

    列表項的實現代碼

    實現完列表項的繪制代碼FloatingItemPainter類,你還需要一個畫布CustomPaint和事件邏輯。一個完整列表項類除了繪制代碼外還需要補充繪制區域的定位,列表項手勢方法的捕捉(關閉和點擊事件,關閉動畫的邏輯處理。對于定位,縱坐標是根據傳進來的top值決定的,對于列表項的Letf值則是根據列表項位于左側 / 右側的,左側很好理解就為0。而右側的坐標,由于列表項的長度為width + 50.0,因此列表項位于右側時,橫坐標為width - 50.0,如下圖:

    對于關閉動畫,則是對橫坐標Left取動畫值來實現由中間收縮回邊緣的動畫效果。

    對于事件的捕捉,需要確定當前列表項的點擊區域和關閉區域。在事件處理的時候需要考慮較為極端的情況,就是把UI使用者不當正常人來看。正常的點擊包括按下和抬起兩個事件,但如果存在按下后拖拽出區域的情況呢?這時即使抬起后列表項還是處于選中的狀態,還需要監聽一個onTapCancel的事件,當拖拽離開列表項監聽區域時將列表項設為未選中狀態。

    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坐標值
    double top;
    /// [left]列表項的x坐標值
    double left;

    ///[isLeft] 列表項是否在左側,否則是右側
    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] 列表關閉動畫的控制器
    AnimationController animationController;
    /// [animation] 列表項的關閉動畫
    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(
    /// 監聽按下事件,在點擊區域內則將[isPress]設為true,若在關閉區域內則不做任何操作
    onPanDown: (details) {
    if (widget.isLeft) {
    /// 點擊區域內
    if (details.globalPosition.dx < width) {
    setState(() {
    isPress = true;
    });
    }
    }
    else{
    /// 點擊區域內
    if(details.globalPosition.dx < width * 2 - 50){
    setState(() {
    isPress = true;
    });
    }
    }
    },
    /// 監聽抬起事件
    onTapUp: (details) async {
    /// 通過左右列表項來決定關閉的區域,以及選中區域,觸發相應的關閉或選中事件
    if(widget.isLeft){
    /// 位于關閉區域
    if(details.globalPosition.dx >= width && !isPress){
    /// 設置從中間返回至邊緣的關閉動畫
    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;
    });
    });
    /// 等待關閉動畫結束后通知父級已關閉
    await animationController.forward();
    /// 銷毀動畫資源
    animationController.dispose();
    /// 通知父級觸發關閉事件
    ClickNotification(deletedIndex: widget.index).dispatch(context);
    }
    else{
    /// 通知父級觸發相應的點擊事件
    ClickNotification(clickIndex: widget.index).dispatch(context);
    }
    }
    else{
    /// 位于關閉區域
    if(details.globalPosition.dx >= width * 2 - 50.0 && !isPress){
    /// 設置從中間返回至邊緣的關閉動畫
    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;
    });
    });
    /// 等待執行完畢
    await animationController.forward();
    /// 銷毀動畫資源
    animationController.dispose();
    /// 通知父級觸發關閉事件
    ClickNotification(deletedIndex: widget.index).dispatch(context);
    }
    else{
    /// 通知父級觸發選中事件
    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>(); //完成的回調
    ImageStreamListener listener;
    ImageStream stream = provider.resolve(config); //獲取圖片流
    listener = ImageStreamListener((ImageInfo frame, bool sync) {
    //監聽
    final ui.Image image = frame.image;
    completer.complete(image); //完成
    stream.removeListener(listener); //移除監聽
    });
    stream.addListener(listener); //添加監聽
    return completer.future; //返回
    }
    }

    對于ClickNotification類,看一下代碼:

    import 'package:flutter/material.dart';

    /// [ClickNotification]列表項點擊事件通知類
    class ClickNotification extends Notification {
    ClickNotification({this.deletedIndex,this.clickIndex});
    /// 觸發了關閉事件的列表項索引
    int deletedIndex = -1;
    /// 觸發了點擊事件的列表項索引
    int clickIndex = -1;
    }

    它繼承自Notification,自定義了一個通知用于處理列表項點擊或關閉時整個列表發生的變化。單個列表項在執行完關閉動畫后分發通知,通知父級進行一個列表項上移填補被刪除列表項位置的的動畫。

    列表動畫

    單個列表項的關閉動畫,我們已經在FlotingItem中實現了。而列表動畫是,列表項關閉后,索引在其后的其他列表項向上平移填充的動畫,示意圖如下:

    已知單個列表項的關閉動畫是由自身管理實現的,那么單個列表項關閉后引起的列表動畫由誰進行管理呢?自然是由列表進行管理。每個列表項除了原始的第一個列表項都可能會發生向上平移的動畫,因此我們需要對單個的列表項再進行一層AnimatedWidget的加裝,方便動畫的傳入與管理,具體代碼如下:

    FloatingItemAnimatedWidget:

    /// [FloatingItemAnimatedWidget] 列表項進行動畫類封裝,方便傳入平移向上動畫
    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
    /// 獲取列表數據
    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類型的數據,FloatingWindowModel中包括了懸浮窗用到的一些數據,例如判斷列表在左側或右側的isLeft,列表的數據dataList等,避免了父組件向子組件傳數據時大量參數的編寫,一定程度上增強了可維護性,例如FloatingItemAnimatedWidget中只需要傳入索引值就可以在共享數據中提取到相應列表項的數據。FloatingWindowSharedDataWidget和FloatingWindowModel的代碼及注釋如下:

    FloatingWindowSharedDataWidget

    /// [FloatingWindowSharedDataWidget]懸浮窗數據共享Widget
    class FloatingWindowSharedDataWidget extends InheritedWidget{

    FloatingWindowSharedDataWidget({
    @required this.data,
    Widget child
    }) : super(child:child);

    final FloatingWindowModel data;

    /// 靜態方法[of]方便直接調用獲取共享數據
    static FloatingWindowSharedDataWidget of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<FloatingWindowSharedDataWidget>();
    }

    @override
    bool updateShouldNotify(FloatingWindowSharedDataWidget oldWidget) {
    // TODO: implement updateShouldNotify
    /// 數據發生變化則發布通知
    return oldWidget.data != data && data.deleteIndex != -1;
    }
    }

    FloatingWindowModel

    /// [FloatingWindowModel] 表示懸浮窗共享的數據
    class FloatingWindowModel {

    FloatingWindowModel({
    this.isLeft = true,
    this.top = 100.0,
    List<Map<String,String>> datatList,
    }) : dataList = datatList;


    /// [isLeft]:懸浮窗位于屏幕左側/右側
    bool isLeft;

    /// [top] 懸浮窗縱坐標
    double top;

    /// [dataList] 列表數據
    List<Map<String,String>>dataList;

    /// 刪除的列表項索引
    int deleteIndex = -1;
    }

    列表的實現

    上述已經實現了單個列表項并進行了動畫的封裝,現在只需要實現列表,監聽列表項的點擊和關閉事件并執行相應的操作。為了方便,我們實現了一個作為列表的FloatingItems類然后實現了一個懸浮窗類TestWindow來對列表的操作進行監聽和管理,在以后的文章中還會繼續完善TestWindow類和FloatingWindowModel類,把前兩節的實現的FloatingButton加進去并實現聯動。目前的具體實現代碼和注釋如下:

    FloatingItems

    /// [FloatingItems] 列表
    class FloatingItems extends StatefulWidget {
    @override
    _FloatingItemsState createState() => _FloatingItemsState();
    }

    class _FloatingItemsState extends State<FloatingItems> with TickerProviderStateMixin{


    /// [_controller] 列表項動畫的控制器
    AnimationController _controller;


    /// 動態生成列表
    /// 其中一項觸發關閉事件后,索引在該項后的列表項執行向上平移的動畫。
    List<Widget> getItems(BuildContext context){
    /// 釋放和申請新的動畫資源
    if(_controller != null){
    _controller.dispose();
    _controller = new AnimationController(vsync: this,duration: new Duration(milliseconds: 100));
    }
    /// widget列表
    List<Widget>widgetList = [];
    /// 獲取共享數據
    var data = FloatingWindowSharedDataWidget.of(context).data;
    /// 列表數據
    var dataList = data.dataList;
    /// 遍歷數據生成列表項
    for(int i = 0; i < dataList.length; ++i){
    /// 在觸發關閉事件列表項的索引之后的列表項傳入向上平移動畫
    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));
    }
    /// 在觸發關閉事件列表項的索引之前的列表項則位置固定
    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,));
    }
    }
    /// 執行動畫
    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"}
    ];
    /// 懸浮窗數據類
    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))
    ),
    /// 監聽點擊與關閉事件
    NotificationListener<ClickNotification>(
    onNotification: (notification) {
    /// 關閉事件
    if(notification.deletedIndex != - 1) {
    windowModel.deleteIndex = notification.deletedIndex;
    setState(() {
    windowModel.dataList.removeAt(windowModel.deleteIndex);
    });
    }
    if(notification.clickIndex != -1){
    /// 執行點擊事件
    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()
    ],
    )
    )
    );
    }
    }

    總結

    對于列表項的編寫,難度就在于狀態的管理上和動畫的管理上,繪制上來來去去還是那幾個函數。組件存在多個復雜動畫,每個動畫由誰進行管理,如何觸發,狀態量如何傳遞,都是需要認真思考才能解決的提出的解決方案,本篇文章采用了一個比較“原始”的方式進行實現,但能使對狀態的管理和動畫的管理有更深入的理解,在下篇文章中采用更為簡單的方式進行實現,通過AnimatedList即動畫列表來實現。

    總結

    以上是生活随笔為你收集整理的flutter 图解_Flutter自绘组件:微信悬浮窗(三)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 亚洲精品免费在线观看视频 | 人人草网站 | exo妈妈mv在线播放高清免费 | 成人xxxx| 91精品啪在线观看国产 | 亚洲精选在线观看 | 国产女人高潮的av毛片 | 91丝袜呻吟高潮美腿白嫩 | 99国产精品久久久久久久成人热 | av播播 | 一级视频片 | 激情四射网站 | 亚洲二区在线播放视频 | 九九涩| 日韩久久免费 | 精品亚洲乱码一区二区 | 娇小萝被两个黑人用半米长 | 国产露脸无套对白在线播放 | 久久久久久久国产精品 | 狠狠澡 | 精品欧美一区二区三区久久久 | av福利在线观看 | 久久福利视频网 | 涩涩成人网 | 天天射天天爽 | 天天鲁| 日本在线视频www色 国产在线视频网址 | 国产精品88 | www.av777| 天堂在线中文在线 | 97视频免费在线 | 国产一级淫片免费 | 亚洲在线播放 | 青娱乐导航| 99re视频在线播放 | 国产男同gay网站 | 亚洲一区人妻 | 原来神马电影免费高清完整版动漫 | julia一区二区 | 精品黑人一区二区三区久久 | 手机在线免费看av | 晨勃顶到尿h1v1 | 半推半就一ⅹ99av | 中文字幕精品一区二区三区视频 | 久久免费国产视频 | 亚洲免费视频大全 | 精品人妻一区二区三区在线视频 | 午夜在线播放视频 | 毛片传媒| 亚洲一区二区三区在线免费观看 | 香蕉久操 | 无码内射中文字幕岛国片 | 欧美三级网| 在线色站 | 手机成人av在线 | 日本aa大片| 超碰免费在 | 国产a级免费视频 | av黄色片| 欧美黑吊大战白妞 | 国产精品久久久一区二区三区 | 粗大挺进潘金莲身体在线播放 | 神马午夜精品 | 香蕉久久国产av一区二区 | 456亚洲影视 | 黄色a网站| 成人国产毛片 | 欧美 日韩 国产 高清 | 九九热精品视频 | 国产精品美女毛片真酒店 | 青青草草 | 午夜激情一区 | www操操操| 中文字幕一区二区在线视频 | 撒尿free性hd | 国产精品电影一区 | av在线免费网站 | 久久成人a | 男人的天堂影院 | 青青草原综合久久大伊人精品 | 99热久久这里只有精品 | 欧美日本日韩 | 精品无人国产偷自产在线 | 欧美交受高潮1 | 欧美精品免费一区二区三区 | 成人免费片 | 成人h动漫在线 | 国产第一福利 | 欧美一极片 | 午夜精品小视频 | 公侵犯人妻一区二区三区 | 无码国模国产在线观看 | 国产性猛交╳xxx乱大交一区 | 国产三级按摩推拿按摩 | 国产精品一区二区三区四区视频 | 精品+无码+在线观看 | 亚洲免费av网| 午夜dv内射一区二区 | 天天躁狠狠躁狠狠躁夜夜躁68 |