如何用flutter 封装一个仿iOS提示弹窗

2,814 阅读5分钟

开篇废话:不知道大家对flutter 是否熟悉,其实作者也是刚接触一周左右,先说一下个人感受。flutter 它可以理解为最突出的一面就是单独的页面绘制引擎,用dart语言实现多端的统一绘制,同时,它还可以与原生通信,这样即可处理超出flutter 能力范围的原生系统级功能调用。其实大家在androidStudio 下的终端命令行下输入 open ios/Runner.xcworkspace 命令,那么就会打开flutter 工程对应的iOS项目,熟悉iOS开发的朋友知道keywindow.rootViewController是app项目根视图,但是flutter 打开的iOS项目的根视图是FlutterViewController,这样的话就好理解了,flutter在根视图进行的全部的UI绘制。既然拿到了根视图,其实作为iOS开发者来说,就可以想怎么玩就怎么玩了。

对于搭建环境、创建flutter项目不熟悉的朋友可以去flutter中文网阅读一下。

问题一、怎么添加全局覆盖view,作为灰色蒙板

static OverlayEntry? _backOverlayEntry;//开辟一个渲染图层
OverlayState? overlayState = Overlay.of(context);//创建一个涂层管理器
overlayState!.insert(_overlayEntry!);//将蒙版涂层添加到管理器内
//蒙板的绘制
_backOverlayEntry = OverlayEntry(
  builder: _backBuilder,
);

这里的builder需要传进入一个渲染函数,_backBuilder就是自定义的渲染函数,tip:_是私有化的标识。

static Widget _backBuilder(BuildContext context){
  return new Opacity(
    opacity: 0.3,
    child: MaterialApp(
      debugShowCheckedModeBanner: false,
      home: new Scaffold(
        body: new Container(
          color: Colors.black,
        ),
      ),
      ),
    );
}

这里的UI、约束、属性基本上都是组件化管理,所谓组件化管理的意思就是设置一些特性虚需要最外层包裹对应的组件或者对应的属性值为创建一个组件进行赋值。

举例说明:灰色的蒙板需要一个透明度来透视下面的页面,那么一般移动端就是设置背景颜色的透明度为0.4或者整个view的透明度为0.4。这里采用的是设置组件的透明度,所以在最外层包裹了一层Opacity组件对内部UI组件进行透明度的设置。

到这里灰色的蒙板就创建好了。

同理,在上面的代码基础上创建一个提示框组件,同样放在屏幕的最上面。

static OverlayEntry? _overlayEntry;
static void alert({required BuildContext context,required dynamic block}) {
  OverlayState? overlayState = Overlay.of(context);
  tapBlock = block;
  if(_backOverlayEntry == null){
    //没有进行创建
    _backOverlayEntry = OverlayEntry(
      builder: _backBuilder,
    );
    overlayState!.insert(_backOverlayEntry!);
  } else {
    //存在进行刷新
    _backOverlayEntry!.markNeedsBuild();
  }
  if (_overlayEntry == null) {
    //没有进行创建
    _overlayEntry = OverlayEntry(
      builder: _builder,
    );
    overlayState!.insert(_overlayEntry!);
  } else {
    //存在进行刷新
    _overlayEntry!.markNeedsBuild();
  }
}

Simulator Screen Shot - iPhone 11 - 2021-07-26 at 08.51.39.png

问题二、里面的弹出框布局怎么写?

这里直接代码放上,感兴趣的可以阅读一下,代码拙略,高手勿喷。

import 'package:flutter/material.dart';

class AlertContentView extends StatefulWidget {
  dynamic? tapBlock;
  AlertContentViewState? _alertContentViewState;
  @override
  State<StatefulWidget> createState() {
    _alertContentViewState = new AlertContentViewState();
    _alertContentViewState!.tapBlock = (isDone){
      if(tapBlock != null){
        tapBlock(isDone);
      }
    };
    return _alertContentViewState!;
  }
}

class AlertContentViewState extends State<AlertContentView> with TickerProviderStateMixin {
  dynamic? tapBlock;
  AnimationController? controller;
  Animation<double>? animation;
  CurvedAnimation? curve;
  double _addW = 0;
  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 100), vsync: this);
    curve = CurvedAnimation(parent: controller!, curve: Curves.fastOutSlowIn);
    animation = Tween(begin: 1.0, end: 1.1).animate(controller!);
    animation!.addListener(() {
      setState(() {
      });
    });
    controller!.forward();
    Future.delayed(Duration(milliseconds: 100), () {
      controller!.reverse();
    });
  }

  @override
  void dispose() {
    controller!.dispose();
    super.dispose();
  }
  @override
  Widget? _alertContain;
  Widget build(BuildContext context) {
    return new ScaleTransition(
      scale: animation!,
      child: new Container(
        child: new Container(
          decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8)),color: Colors.white),
          width: 280,
          height: 160,
          child: new Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: <Widget>[
              new Container(
                margin: EdgeInsets.only(top: 8),
                //height: 20,
                color: Colors.white,
                child: new Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: <Widget>[
                    new Text("提示",style: new TextStyle(color: Colors.black,fontSize: 18,fontWeight: FontWeight.bold,decoration: TextDecoration.none),)
                  ],
                ),
              ),
              new Expanded(
                  child: new Container(
                    color: Colors.white,
                    child: new Center(
                      child: new Text('是否去登录?',style: new TextStyle(color: Color.fromARGB(255, 133, 133, 133),fontWeight: FontWeight.w500,fontSize: 15,decoration: TextDecoration.none),),
                    ),
                  )
              ),
              new Container(
                color: Color.fromARGB(255 ,238, 238, 238,),
                height: 0.8,
                width: 180,
              ),
              new Container(
                margin: EdgeInsets.only(bottom: 0),
                decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8)),color: Colors.white),
                height: 50,
                child: new Row(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    //点击组件
                    new GestureDetector(
                      child: new Container(
                        decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8)),color: Colors.white),
                        width:139,
                        height: 45,
                        child: new Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: <Widget>[
                            new Text(
                              '取消',
                              style:
                              new TextStyle(
                                  color: Color.fromARGB(255, 33, 33, 33),
                                  fontWeight: FontWeight.w500,
                                  fontSize: 15,
                                  decoration: TextDecoration.none)
                              ,
                            )
                          ],
                        ),
                      ),
                      onTap: (){
                        if(tapBlock != null){
                          tapBlock(false);
                        }
                      },
                    ),
                    new Container(
                      color: Color.fromARGB(255 ,238, 238, 238,),
                      height: 50,
                      width: 0.8,
                    ),
                    //点击组件
                    new GestureDetector(
                      child: new Container(
                        decoration: BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(8)),color: Colors.white),
                        width:139,
                        height: 45,
                        child: new Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: <Widget>[
                            new Text(
                              '确定',
                              style:
                              new TextStyle(
                                  color: Color.fromARGB(255, 33, 33, 33),
                                  fontWeight: FontWeight.w500,
                                  fontSize: 15,
                                  decoration: TextDecoration.none),
                            ),
                          ],
                        ),
                      ),
                      onTap: (){
                        if(tapBlock != null){
                          tapBlock(true);
                        }
                      },
                    )
                  ],
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

tip:这里其实是加了一个弹窗动画,修改了弹窗内容组件的缩放比例。这里着重解释一下里面的参数。

创建动画控制对象的时候需要传vsync参数,所以别忘了对类进行如下设置:

image.png

//组件继承自 StatefulWidget 类型的 State类的初始化方法
void initState() {
  super.initState();
  controller = AnimationController(
      duration: const Duration(milliseconds: 100), vsync: this);
  curve = CurvedAnimation(parent: controller!, curve: Curves.fastOutSlowIn);
  animation = Tween(begin: 1.0, end: 1.1).animate(controller!);
  //监听并进行UI重绘,这里不进行监听也会有弹出动画,因为在页面布局里面已经添加了缩放动画组件
  animation!.addListener(() {
    //执行此方法可触发UI重绘
    setState(() {
    });
  });
  //开始动画放大动画
  controller!.forward();
  //延迟操作
  Future.delayed(Duration(milliseconds: 100), () {
    //开始反向缩小动画
    controller!.reverse();
  });
}
//别忘了在销毁的时候同时销毁动画,这里注意,要先处理自己的事,在调用super.dispose()
@override
void dispose() {
  controller!.dispose();
  super.dispose();
}

点击事件回调:

dynamic? tapBlock;

这里在类里面添加了一个dynamic 类型的点击事件,可将点击事件往外进行回调。

tip: dynamic 与 var 的区别 ,二者同为类型推断标识符,dynamic 可多次修改值类型,var 一旦初次赋值确认后,后面类型不可变。

效果图:

Simulator Screen Shot - iPhone 11 - 2021-07-26 at 09.11.44.png

问题三:如何取消当前的蒙板?

//蒙板移除屏幕
_overlayEntry!.remove();
//蒙板置空
_overlayEntry = null;
//提示框移除屏幕
_backOverlayEntry!.remove();
//提示框置空
_backOverlayEntry = null;

问题四:如何调用?

//弹窗初始化
AlertView.alert(context: context,block: (bool isDone){
  //确认按钮点击事件处理
  if(isDone == true){
    导航器进行跳转登录操作
    Navigator.push(context, MaterialPageRoute(builder: (_) {
      //初始化登录界面
      return new Login(ValueKey("login"));
    }));
  }
});

作者菜鸡一枚,上面的全部代码纯手撸,高手勿喷,大家互相学习,个人权当总结。欢迎大家指正。[抱拳][抱拳][抱拳]