Flutter实战项目-第六篇 自定义dropdown,toast

 

概要

  • 自定义下拉dropdown
  • 适用于tips、dropdown
  • 自定义toast

一、实现思路(1)

        获取父元素的大小及位置,借助Stack定位,实现自定义浮层。为了方便获取大小及编程方便,我们创建一个Dropdown组件,用于处理获取父节点信息逻辑。

import 'package:flutter/material.dart';

import 'show.dropdrown.dart';

class Dropdown extends StatefulWidget {
  final Widget child; 
  final Widget? dropdown; 
  final double? dropdownWidth;
  final double? dropdownHeight;
  final Function? open;
  final Function? close;
  /// * 根据元素位置定位
  /// * dropdownWidth 下拉内容宽度
  /// * dropdownHeight 下拉内容高度
  /// * open 展开触发
  /// * close 收起触发
  const Dropdown({
    Key? key,
    required this.child,
    this.dropdown,
    this.dropdownWidth,
    this.dropdownHeight,
    this.open,
    this.close
  }) : super(key: key);

  @override
  State<Dropdown> createState() => _DropdownState();
}

class _DropdownState extends State<Dropdown> {
  void _onAfterRendering() {
    RenderObject? renderObject = context.findRenderObject();
    Size size = renderObject!.paintBounds.size;
    var vector3 = renderObject.getTransformTo(null).getTranslation();
    FocusScope.of(context).requestFocus(FocusNode());
    showChooseDialog(
      context:context,
      size:size,
      vector3:vector3,
      child: widget.dropdown??const SizedBox(),
      width:widget.dropdownWidth,
      height: widget.dropdownHeight,
      close:widget.close
    );
  }
  
  @override
  Widget build(BuildContext context) {
    return InkWell(
      child:widget.child,
      onTap: () {
        _onAfterRendering();
        if(widget.open!=null){
          widget.open!();
        }
      },
    );
  }
}

创建showChooseDialog函数,用于生成和处理浮层定位

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

showChooseDialog({
    required BuildContext context,
    required Size size,
    required var vector3,
    required Widget child,
    double? width,
    double? height,
    Function? close
  }) {
    final double wHeight =height??200;
    final double wWidth = width??size.width;
    final double dx = vector3[0];
    final double dy = vector3[1];
    final double viewWidth = MediaQuery.of(context).size.width;
    final double viewHeight = MediaQuery.of(context).size.height;
    //X轴定位
    late double positionLeft;
    late double positionLeftIcon;
    //Y轴定位
    late double positionTop;
    late double positionTopIcon;
    //方向标识
    late CustomClipper<Path> icon;

    late double barHeight =MediaQuery.of(context).padding.top;
    //计算处理X轴定位
    if(dx+wWidth<=viewWidth){
      //向左未超出视窗
      positionLeft =dx;
      positionLeftIcon =dx+10;
    }else{
      //向左超出视窗
      positionLeft=dx-(wWidth-size.width);
      positionLeftIcon=dx+size.width-40;
    }

    //计算处理Y轴定位
    if(dy+wHeight+10-barHeight<=viewHeight){
      //向下未超出视窗
      positionTop =dy +size.height+ 10-barHeight;
      positionTopIcon =dy + size.height-barHeight;
      icon=Triangle(dir: 1);
    }else{
      //向下超出视窗
      positionTop=dy-wHeight-10-barHeight;
      positionTopIcon=dy-10-barHeight;
      icon=Triangle(dir: 0);
    }

    showDialog(
      context: context,
      barrierColor:Colors.transparent,
      builder: (BuildContext context) {
        return Material(
          color: Colors.transparent,
          elevation: 0,
          child: Container(
            width: wWidth,
            height: double.infinity,
            child: Stack(
              children: <Widget>[
                GestureDetector(
                  onTap: () {
                    Navigator.of(context).pop();
                  },
                ),
                Positioned(
                  left:positionLeft,
                  top: positionTop,
                  width: wWidth,
                  height: wHeight,
                  child: GestureDetector(
                    child:Container(
                      decoration: const BoxDecoration(
                        color: Colors.white,
                        borderRadius: BorderRadius.all(
                          Radius.circular(4.0),
                        ),
                        boxShadow: [
                          BoxShadow(
                            color: Color.fromRGBO(0, 0, 0, 0.1),
                            offset: Offset(0.0, 1.0), //阴影y轴偏移量
                            blurRadius: 16, //阴影模糊程度
                            spreadRadius: 1 //阴影扩散程度
                          )
                        ]
                      ),
                      child: child
                    ),
                    onTap:(){
                      // Navigator.of(context).pop();
                    }
                  )
                ),
                Positioned(
                  left:positionLeftIcon,
                  top: positionTopIcon,
                  child: ClipPath(
                    clipper: icon,
                    child: Container(
                      width: 20.0,
                      height: 10.0,
                      child: null,
                      decoration: const BoxDecoration(
                        color: Colors.white,
                        boxShadow: [
                          BoxShadow(
                            color: Color.fromRGBO(0, 0, 0, 0.1),
                            offset: Offset(0.0, 1.0), //阴影y轴偏移量
                            blurRadius: 16, //阴影模糊程度
                            spreadRadius: 1 //阴影扩散程度
                          )
                        ]
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    ).then((value) => {
      if(close!=null){
        close()
      }
    });
  }

Triangle函数这是用于画三角符号

import 'package:flutter/material.dart';

class Triangle extends CustomClipper<Path> {
  double dir;
  Triangle({required this.dir});
   @override
  Path getClip(Size size) {
   var path = Path();
    if (dir ==1) {
      path.moveTo(size.width / 2, 0);
      path.lineTo(0, size.height);
      path.lineTo(size.width, size.height);
      path.close();
    } else {
      path.moveTo(0, 0);
      path.lineTo(size.width , 0);
      path.lineTo(size.width/2, size.height);
      path.close();
    }
    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    return false;
  }
}

实际调用

class TestToast extends StatefulWidget {
  const TestToast({Key? key}) : super(key: key);
  static const routeName = '/testtoast'; 
  @override
  State<TestToast> createState() => _TestToastState();
}

class _TestToastState extends State<TestToast> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
     body: SingleChildScrollView(
       child: Container(
          height: 100,
          width: 100,
          color:Colors.red,
          alignment: Alignment.center,
          child: const Dropdown(
            child: Text('父节点')
          ),
      ),
     ),
    );
  }
}

效果

二、实现思路(2)

        借助CompositedTransformFollower及CompositedTransformTarget组件,制作联合组件。结合OverlayEntry动态插入节点。相比方案一,这个方案更加的优越,其下拉不会影响页面的其他插座。

新建类PickerDownDialog.dart用于下拉框生成。

import 'dart:ui';

import 'package:flutter/material.dart';

class PickerDownDialog{
  BuildContext context;
  final Widget? child;
  final Widget Function(BuildContext context)? builder;
  double? width;
  double? height;
  final LayerLink layerLink;
  PickerDownDialog({
    required this.context,
    this.builder,
    this.child,
    this.height,
    this.width,
    required this.layerLink
  });
  late OverlayEntry? _overlayEntry=null;
  late bool hasOpenedOverlay = false;
  void openOverlay() {
    if (!(_overlayEntry != null)) {
      RenderBox renderBox = context.findRenderObject() as RenderBox;
      var size = renderBox.size;
      var offset = renderBox.localToGlobal(Offset.zero);

    final double wHeight =height??240;
    final double wWidth = width??size.width;
    final double dx = offset.dx;
    final double dy = offset.dy;
    final double viewWidth = MediaQuery.of(context).size.width;
    final double viewHeight = MediaQuery.of(context).size.height;
    //X轴定位
    late double positionLeft;

    //Y轴定位
    late double positionTop;

    //计算处理X轴定位
    if(dx+wWidth<=viewWidth){
      //向左未超出视窗
      positionLeft =dx;
    }else{
      //向左超出视窗
      positionLeft=dx-(wWidth-size.width);
    }

    //计算处理Y轴定位
    if(dy+wHeight+5<=viewHeight){
      //向下未超出视窗
      positionTop =dy +size.height+ 5;
    }else{
      //向下超出视窗
      positionTop=dy-wHeight;
    }

      _overlayEntry ??= OverlayEntry(
        builder: (context) => Positioned(
          left:positionLeft,
          top: positionTop,
          width: wWidth,
          height: wHeight,
          child:CompositedTransformFollower(
            link: layerLink,
            showWhenUnlinked: false,
            offset: Offset(0.0, size.height + 5.0),
            child:Material(
            child: Container(
              decoration: const BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.all(
                  Radius.circular(4.0),
                ),
                boxShadow: [
                  BoxShadow(
                    color: Color.fromRGBO(0, 0, 0, 0.1),
                    offset: Offset(0.0, 1.0), //阴影y轴偏移量
                    blurRadius: 16, //阴影模糊程度
                    spreadRadius: 1 //阴影扩散程度
                  )
                ]
              ),
              child: child??builder!(context),
            ),
          )
          )
        )
      );
    }
    if (!hasOpenedOverlay) {
      Overlay.of(context)!.insert(_overlayEntry!);
     hasOpenedOverlay = true;
    }
  }

  void closeOverlay() {
    if (hasOpenedOverlay) {
      _overlayEntry!.remove();
      hasOpenedOverlay = false;
    }
  }
}

在组件关联,CompositedTransformTarget,通过pickerDownDialog.openOverlay();pickerDownDialog.openOverlay();实现下拉框显示隐藏控制。这样实现的效果,在切换页面tab时会不影响。

三、自定义Toast

        通过使用OverlayEntry插入、Positioned层叠定位来实现。这个提示层不会影响用户对其他页面的操作,外层不会有笼罩层。纯展示提示。

// ignore_for_file: unnecessary_new

import 'package:flutter/material.dart';


class Toast {
  static void show({required BuildContext context, required String message}) {
    //创建一个OverlayEntry对象
    OverlayEntry overlayEntry = new OverlayEntry(builder: (context) {
    //外层使用Positioned进行定位,控制在Overlay中的位置
      return new Positioned(
          top: MediaQuery.of(context).size.height * 0.9,
          child: new Material(
            color: Colors.transparent,
            child: new Container(
              color: Colors.transparent,
              width: MediaQuery.of(context).size.width,
              alignment: Alignment.center,
              child: new Center(
                child: new Card(
                  child: new Padding(
                    padding: const EdgeInsets.all(8),
                    child: new Text(message),
                  ),
                  color: Colors.transparent,
                  shadowColor: Colors.transparent,
                  elevation: 0,
                ),
              ),
            ),
          ));
    });
    //往Overlay中插入插入OverlayEntry
    Overlay.of(context)!.insert(overlayEntry);
    //两秒后,移除Toast
    new Future.delayed(const Duration(seconds: 2)).then((value) {
      overlayEntry.remove();
    });
  }
}

实现效果

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值