美文网首页Flutter学习
Flutter 带指示器的悬浮窗口

Flutter 带指示器的悬浮窗口

作者: 旺仔_100 | 来源:发表于2022-02-23 15:08 被阅读0次
一、需求
需求.png
二、思路

先把下面的悬浮窗背景和三角形自定义绘制一个widget。然后再把标签和悬浮框做一个联动(CompositedTransformFollower + CompositedTransformTarget),这样即使整个标签弹窗滑动,悬浮窗也能跟着标签跑。

三、悬浮窗带三角指示器背景自定义widget
  • 先确定起点,使用path绘制下一个三角形。
  • 在使用path绘制一个矩形。
  • 使用path的api把两个path联合起来。

代码展示如下:


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

///
/// @ProjectName:    plugin_base
/// @Package:        lib.common.global_store
/// @ClassName:      base_appbar
/// @Description:     写一个指定三角大小,指定三角偏移量的,带背景颜色的widget
/// @Author:         yang
/// @CreateDate:     2021/8/5 10:18
/// @UpdateDate:     2021/8/5 10:18
/// @UpdateRemark:   更新说明
/// @Version:        1.0

class ThreeSan extends StatelessWidget{
  ///三角形是否居中
  bool isCenter;
  ///偏移量
 final double offset;
 ///三角形的高
 final double sanHeight;
 ///三角形的宽度
  double sanWidth;
 ///三角形的顶部角度
 final int sanAngle;
 ///间距
 double? padding;
  Key? key;
  ///子Widget
  Widget child;
  ///矩形的圆角
  double radius;
  ///背景颜色
  Color background;



  ThreeSan(this.isCenter,this.offset,this.sanWidth,this.sanHeight,this.child,this.radius,this.padding,
      this.background,this.sanAngle,
      {this.key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    var _padding = padding;
    return  Padding(
      padding: EdgeInsets.all(_padding!),
      child: CustomPaint(
        child: child,
        painter: ThreeSanPainter(isCenter,offset,radius,sanWidth,sanHeight,sanAngle,background),
      ),
    );
  }

}

class ThreeSanPainter extends CustomPainter{
  bool isCenter;
  double offset;
  double radius;
  double sanWidth;
  double sanHeight;
  int sanAngle;
  Color background;

  ThreeSanPainter(this.isCenter,this.offset,this.radius,this.sanWidth,this.sanHeight,
      this.sanAngle,this.background);

  @override
  void paint(Canvas canvas, Size size) {
    /// 绘制矩形
   var height = size.height ;
   var width = size.width;

    Rect rect = Rect.fromCenter(center: Offset(width/2,height/2), width: width, height: height);
    Path path = Path()..addRRect(RRect.fromRectXY(rect, radius, radius));


    ///绘制三角
   double sanJaoHeight = sanHeight;
    var sanJiaoMovex = sanWidth/2;
    var sanJiaoMovey = sanJaoHeight;

    Path sanjiaoPath = Path()..moveTo(isCenter ? size.width/2 - sanWidth/2 : offset, 0)
      ..relativeLineTo(sanJiaoMovex, -sanJiaoMovey)
      ..relativeLineTo(sanJiaoMovex, sanJiaoMovey);

    ///联合两个path
   path = Path.combine(PathOperation.union, sanjiaoPath, path);
   canvas.drawPath(path, Paint()..color = background);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
   return false;
  }

}

结果展示如下:


少量文字展示.png
大量文字展示.png
三、联动标签和悬浮窗

参考之前的文章 Flutter绑定两个View,CompositedTransformFollower 与 CompositedTransformTarget

这里是因为整个标签弹窗是可以上下滑动的,所以,点击标签弹出悬浮窗需要跟着标签和标签弹窗一起动。
其中也遇到了几个问题。
1,点击标签的悬浮窗大小不受控制。后面也是通过试出来,需要使用根节点为ConstrainedBox来保证悬浮窗不至于充满屏幕。


975ef209874355bb6b5be2673ce7548.png

2.通过发消息来解决:点击下一个标签的时候,把上一个悬浮窗关闭。
3.监听标签底部弹窗关闭来销毁悬浮窗,销毁弹窗时候注意_overlayEntry?.remove(); 不能重复去销毁,会直接报错。

联动控件代码
///悬浮框控件  两个空间联动
class TipBox extends StatefulWidget{
  Color? textColor;
  String? text;
  String? labelDefinition;


  TipBox(this.textColor, this.text, this.labelDefinition);

  @override
  State<StatefulWidget> createState() {
    return TipBoxState();
  }

}

class TipBoxState extends State<TipBox>{
  final LayerLink layerLink = LayerLink();
  bool show = false;
  OverlayEntry? _overlayEntry;

  @override
  void initState() {
    eventBus.on<OverlayEntry?>().listen((event) {
      debugPrint("接收到取消弹窗的消息");
      if(_overlayEntry != event && show){
        hideOverlay() ;
        show = !show;
      }
    });
    super.initState();
  }

  @override
  void dispose() {
    _overlayEntry?.remove();
    _overlayEntry = null;
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: ()=>_toggleOverlay(context),
      child: CompositedTransformTarget(
        link: layerLink,
        child: backgroundText(widget.textColor,widget.text),
      ),
    );
  }

  void _toggleOverlay(BuildContext context){

      show ? hideOverlay() : showOverlay(context);
      show = !show;
      ///发消息,把除了当前的弹窗都关闭掉
      eventBus.fire(_overlayEntry);
  }


  showOverlay(BuildContext context){
    _overlayEntry = _createOverlayEntry();
    Overlay.of(context)!.insert(_overlayEntry!);
  }

  OverlayEntry _createOverlayEntry() => OverlayEntry(
      builder: (BuildContext context) => CompositedTransformFollower(
        link: layerLink,
        followerAnchor: Alignment.topCenter,
        targetAnchor: Alignment.bottomCenter,
        offset: Offset(0,5),
        child: ConstrainedBox(
          constraints: BoxConstraints(maxWidth: 1.sw,maxHeight: 1.sh),
          child: Align(
            alignment: Alignment.topCenter,
            child: Container(
              child: ThreeSan(true,0,16.0,6.0,
              Padding(
              padding: const EdgeInsets.all(10),
                child: Text(interceptString(widget.labelDefinition),
                  style: const TextStyle(color: Colors.white),),
              ),
                5,10,const Color(0xff999999),30),
            ),
          ),
        ),
      ));

  ///大于30,截取
  String interceptString(String? s){
    if(s == null || s.length == 0) return "";
   if(s.length > 30) return s.substring(0,30);
   return s;
  }



  hideOverlay(){
    _overlayEntry?.remove();
    _overlayEntry = null;
  }

}


底部弹窗核心代码
 void showLableDialog(
      List<BorderBean>? customList, List<BorderBean>? systemList) {
    ///设置一个最小高度和一个最大高度
    debugPrint("scaleH 是多少:$scaleH");
    List<Widget> customChildren = generateList(customList);
    List<Widget> systemChildren = generateList(systemList);

    if(customChildren.length == 0 && systemChildren.length == 0) return ;

    showModalBottomSheet(
        context: context,
        isScrollControlled: true,
        backgroundColor: Colors.transparent,
        constraints: BoxConstraints(minHeight: 200.h, maxHeight: 1.sh * 3 / 4),
        builder: (BuildContext context) {
          return Container(
            width: 1.sw,
            decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(40.w),
                    topRight: Radius.circular(40.w))),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Container(
                  margin: EdgeInsets.all(13.5.h * scaleH),
                  child: Row(children: [
                    Expanded(
                      child: Container(
                        alignment: Alignment.center,
                        child: Text(
                          "标签",
                          style: blackStyle_16,
                        ),
                      ),
                    ),
                    InkWell(
                      onTap: () => Navigator.of(context).pop(),
                      child: Image(
                          image: AssetImage("assets/images/close_btn.png")),
                    )
                  ]),
                ),
                dividerLineD8(),
                Expanded(
                  child: Container(
                    width: 1.sw,
                    padding: EdgeInsets.all(16.w),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          "自定义标签",
                          style: blackMeduimStyle_16,
                        ),
                        SizedBox(
                          height: 12.h * scaleH,
                        ),
                        Container(
                          constraints: BoxConstraints(
                              minHeight: 100.h, maxHeight: 400.h),
                          child: SingleChildScrollView(
                            child: Wrap(
                              runSpacing: 8.h * scaleH,
                              children: customChildren,
                            ),
                          ),
                        ),
                        Text(
                          "系统标签",
                          style: blackMeduimStyle_16,
                        ),
                        SizedBox(
                          height: 12.h * scaleH,
                        ),
                        Expanded(
                          child: SingleChildScrollView(
                            child: Wrap(
                              runSpacing: 8.h * scaleH,
                              children: systemChildren,
                            ),
                          ),
                        ),
                      ],
                    ),
                  ),
                )
              ],
            ),
          );
        });
  }

最终实现图


image.png

相关文章

  • Flutter悬浮控件

    一、背景: 其实我之前做项目写过这么个控件 Flutter 带指示器的悬浮窗口[https://www.jians...

  • Flutter 带指示器的悬浮窗口

    一、需求 二、思路 先把下面的悬浮窗背景和三角形自定义绘制一个widget。然后再把标签和悬浮框做一个联动(Com...

  • Flutter自绘组件:微信悬浮窗(四)

    系列指路:Flutter自绘组件:微信悬浮窗(一)Flutter自绘组件:微信悬浮窗(二)Flutter自绘组件:...

  • Flutter自绘组件:微信悬浮窗(五)

    系列指路:Flutter自绘组件:微信悬浮窗(一)Flutter自绘组件:微信悬浮窗(二)Flutter自绘组件:...

  • 悬浮窗口

    概述 在熊猫直播,等视频APP中,都有这样的一个功能,当退出播放界面的时候,会有一个小窗口播放视频,那么怎么实现的...

  • Flutter 中悬浮窗口Widget之Overlay

    在开发中常常需要一个悬浮窗口来做各种筛选,实现悬浮控件需要知道悬浮控件应该出现在什么位置以及窗口的大小,而获取悬浮...

  • Flutter 进度指示器

    Flutter 进度指示器分为两种:LinearProgressIndicator 和CircularProgre...

  • Android悬浮窗口

    1、最近在做视频通话功能,用到了悬浮窗。项目代码不能随便上,写一个简单的Demo记录下!更复杂的功能,自由发挥吧!...

  • Flutter自绘组件:微信悬浮窗(三)

    前期指路: Flutter自绘组件:微信悬浮窗(一)Flutter自绘组件:微信悬浮窗(二) 上两讲中讲解了微信悬...

  • 修改flutter_swiper指示器样式

    flutter_swiper指示器样式默认只有SwiperPagination.dots(圆点)和SwiperPa...

网友评论

    本文标题:Flutter 带指示器的悬浮窗口

    本文链接:https://www.haomeiwen.com/subject/chmylrtx.html