阅读 633

2小时内封装一个 Flutter 仿iOS全屏移动悬浮窗?干就完了! |8月更文挑战

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

废话开篇:大家都知道在苹果手机设置里面有“辅助触控”这样的一个开关,它其实就相当于一个屏幕软键盘,可以拖动到屏幕内的任意位置。闲置的时候为半透明状,用户操作的时候为不透明,在iOS原生上面可以将自定义view加载到keyWindow上,这样就view就处在所有的view最上面,那么,下面来具体在flutter上实现这样的功能。

效果展示:

屏幕录制2021-08-10 上午11.05.52.gif

步骤一、封装自定义全屏浮动组件,将app内全部组件以child形式展示

其实,这里写的有点浮夸了,大致的意思就是封装一个组件,内部采用帧布局的形式进行排列。将app全部的内容作为主要组件进行展示,这时再简单的封装一下悬浮框组件作为帧布局上层组件即可,再利用 GestureDetector 进行拖拽手势监听,随即移动悬浮组件,那么,大体上的布局就完成了。

下面展示一下主要布局代码:

GeneralFloatOnScreenView 代码:

class GeneralFloatOnScreenView extends StatefulWidget {
  Widget child;
  GeneralFloatOnScreenView({required this.child});
  @override
  State<StatefulWidget> createState() {
    return new GeneralFloatOnScreenViewState();
  }

}
复制代码

GeneralFloatOnScreenViewState 代码

属性声明

//帧布局顶部距离
double _top = 0;
//帧布局左侧距离
double _left = 0;
//悬浮组件宽度,这里设置为宽高一直,因此height并没有声明
double _width = 50;
//记录屏幕或者父类组件宽度,用来判断拖拽听指挥后回归左右边缘判断
double _parentWidth = 0;
bool _isInitData = false;
//悬浮组件透明度
double _opacity = 0.3;
//动画控制器
late AnimationController _controller;
//动画
late Animation<double> _animation;
//这里的浮动组件声明成了属性,目的就是防止多次刷新当此组件内部有一些单独的逻辑的情况下。
late Widget _contentWidget;
复制代码

初始化,在widget初始化里面去创建组件,这样只要当前组件不在屏幕上消失,那么即使不进行 wantKeepAlive 设置也不会重新初始。在悬浮组件上嵌套了一层 GestureDetector 手势来进行拖拽的移动。

@override
void initState() {
  // TODO: implement initState
  super.initState();
  _contentWidget = new GestureDetector(
    onPanUpdate: (DragUpdateDetails details){
      _left += details.delta.dx;
      _top += details.delta.dy;
      _changePosition();
    },
    onPanEnd: (DragEndDetails details){
      _changePosition();
      //判断悬浮组件左右回归操作
      _animateMoveBackAction();
    },
    onPanCancel: (){
      //当取消手势时进行边缘判断
      _changePosition();
    },
    onPanStart: (DragStartDetails details){
      //开始拖拽时将悬浮框透明度设置为1.0
      setState(() {
        _opacity = 1.0;
      });
    },
    child: new Container(
      decoration: BoxDecoration(
        borderRadius: BorderRadius.all(Radius.circular(_width / 2.0)),
        color: Colors.red,
      ),
      width: _width,
      height: _width,
    ),
  );
  
  //这里初始化动画类
  _controller =
      AnimationController(duration: Duration(milliseconds: 0), vsync: this);
  _animation =
      Tween(begin: _left, end: _left).animate(_controller);
}
复制代码

build 方法,这里没有什么特别要注意的,里面只是添加了帧布局进行包裹。

@override
Widget build(BuildContext context) {
  //对于必要属性只进行一次计算
  if(_isInitData == false) {
    _top = MediaQuery.of(context).size.height - 200;
    _left = 15;
    _parentWidth = MediaQuery.of(context).size.width;
    _isInitData = true;
  }
  return new Stack(
    fit: StackFit.passthrough,
    children: <Widget>[
      this.widget.child,
      Positioned(
          top: _top,
          left: _left,
          child: new Opacity(
              child: _contentWidget,
              opacity: _opacity
          ),
      )
    ],
  );
}
复制代码

步骤二,如何进行手势的拖拽移动及边缘判断?

当 GestureDetector 手势监听 onPanUpdate 方法里进行悬浮组件的位置移动,直接修改属性 _left、_top值即可,这里注意进行一下屏幕边缘判断。

//位置边界判断
void _changePosition(){
  if(_left < 0) {
    _left = 0;
  }

  if(_left >= MediaQuery.of(context).size.width - _width){
    _left = MediaQuery.of(context).size.width - _width;
  }

  if(_top < 0) {
    _top = 0;
  }

  if(_top >= MediaQuery.of(context).size.height - _width) {
    _top = MediaQuery.of(context).size.height - _width;
  }
  //刷新界面
  setState(() {

  });
}
复制代码

步骤三、如何进行拖拽手势结束悬浮组件回归边缘动画?

当GestureDetector 手势监听 onPanEnd 方法执行的时候判断当前悬浮组件中轴线停留的位置与屏幕(或者父组件中轴线)的位置关系,偏左向左边缘靠,偏右向有边缘靠。这里的动画并没有添加到某个动画组件上,而是直接执行,监听animation的值的变化进行悬浮组件的回归移动。当动画直接状态为结束后再将悬浮组件的透明度设置为半透明状态。

//中轴线回弹动画
void _animateMoveBackAction(){
  double centerX = _left + _width / 2.0;
  double toPositionX = 0;
  double needMoveLength = 0;
  if(centerX <= _parentWidth / 2.0) {
    needMoveLength = _left;
  } else {
    needMoveLength = (_parentWidth - _left - _width);
  }
  double precent = (needMoveLength / (_parentWidth / 2.0));
  int time = (600 * precent).ceil();
  if(centerX <= _parentWidth / 2.0){
    //回到左边缘
    toPositionX = 0;
  } else {
    //回到右边缘
    toPositionX = _parentWidth - _width;
  }
  //这里由于根据需要偏移的距离需要重新设置动画执行时长,那么之前的动画控制器就先销毁再创建。
  _controller.dispose();
  _controller =
      AnimationController(duration: Duration(milliseconds: time), vsync: this);
  //这里对监听 animation 执行过程进行监听,重新绘制悬浮组件位置
  _animation =
      Tween(begin: _left, end: toPositionX * 1.0).animate(_controller);
  _animation.addListener(() {
    _left = _animation.value.toDouble();
    setState(() {

    });
  });
  _animation.addStatusListener((status) {
    if(status == AnimationStatus.completed){
      Future.delayed(Duration(microseconds: 200),(){
        setState(() {
          _opacity = 0.3;
        });
      });
    }
  });
  _controller.forward();

}
复制代码

注意组件销毁时进行动画控制器的销毁操作,注意先执行自身的相关操作后执行super.dispos()

@override
void dispose() {
  // TODO: implement dispose
  _controller.dispose();
  super.dispose();
}
复制代码

步骤四、如何用这个封装好的组件?

将之前 app 的全部组件用 GeneralFloatOnScreenView 包裹起来,将它作为child传给自定义组件即可

home: new Scaffold(
  //这里的 new BottomTabbarWidget() 是 app 的全部组件,将它作为child传给自定义组件即可
  body: new GeneralFloatOnScreenView(child: new BottomTabbarWidget()),
)
复制代码

好了,简单的仿iOS全屏悬浮窗就实现好了,代码拙劣,大神勿喷,如果对大家有帮助,更是深感欣慰。

文章分类
前端
文章标签