2小時內封裝一個 Flutter 仿iOS全屏移動懸浮窗?幹就完了! |8月更文挑戰

頭疼腦脹的程式碼搬運工發表於2021-08-10

這是我參與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全屏懸浮窗就實現好了,程式碼拙劣,大神勿噴,如果對大家有幫助,更是深感欣慰。

相關文章