Flutter全局懸浮按鈕

方法一

image-20210822124957439
  Offset _offset = Offset.zero;
  Scaffold(
        body: Stack(
          children: [
            _pageList[_currentIndex],
            Positioned(
              left: _offset.dx,
              top: _offset.dy,
              child: GestureDetector(
                onPanUpdate: (d) =>
                    setState(() => _offset += Offset(d.delta.dx, d.delta.dy)),
                child: FloatingActionButton(
                  onPressed: () {},
                  backgroundColor: Colors.orange,
                  child: Icon(Icons.add),
                ),
              ),
            ),
          ],
        ),

方法二

 WidgetsBinding.instance
        .addPostFrameCallback((_) => _insertOverlay(context));
void _insertOverlay(BuildContext context) {
    return Overlay.of(context).insert(
      OverlayEntry(builder: (context) {
        final size = MediaQuery.of(context).size;
        print(size.width);
        return Positioned(
          width: 56,
          height: 56,
          top: size.height - 72,
          left: size.width - 72,
          child: Material(
            color: Colors.transparent,
            child: GestureDetector(
              onTap: () => print('ON TAP OVERLAY!'),
              child: Container(
                decoration: BoxDecoration(
                    shape: BoxShape.circle, color: Colors.redAccent),
              ),
            ),
          ),
        );
      }),
    );
  }

方法三

1.場(chǎng)景
現(xiàn)在需要做一個(gè)Test按鈕宿接,懸浮在所有頁(yè)面之上,并且可以拖拽厨埋。

2.思路
1)懸浮按鈕可以使用flutter提供的Overlay + OverlayEntry 組合實(shí)現(xiàn)

2)拖拽功能可以使用GestureDetector手勢(shì)按鈕或者Draggable實(shí)現(xiàn)(PS:我做了一版Draggable實(shí)現(xiàn)的饿悬,但是發(fā)現(xiàn)它會(huì)有原本的widget浮在原地,顯然不是我要的效果)

3)點(diǎn)擊的時(shí)候我是讓它彈出一個(gè)底部彈框叽唱,這里你們可以自由發(fā)揮屈呕,本篇文章不做多余贅述

PubScaffold(
      child: MaterialApp(
        theme: CustomTheme.lightTheme,
        darkTheme: CustomTheme.darkTheme,
        themeMode: currentTheme.currentTheme,
        home: Scaffold(
          body: Stack(
            children: [
              _pageList[_currentIndex],
              // Positioned(
              //   left: _offset.dx,
              //   top: _offset.dy,
              //   child: GestureDetector(
              //     onPanUpdate: (d) =>
              //         setState(() => _offset += Offset(d.delta.dx, d.delta.dy)),
              //     child: FloatingActionButton(
              //       onPressed: () {},
              //       backgroundColor: Colors.orange,
              //       child: Icon(Icons.add),
              //     ),
              //   ),
              // ),
            ],
          ),
          bottomNavigationBar: CurvedNavigationBar(
            // key: _bottomNavigationKey,
            index: 0,
            height: 60.0,
            items: <Widget>[
              Icon(Icons.home, size: 30),
              Icon(Icons.list, size: 30),
              Icon(Icons.compare_arrows, size: 30),
              // Icon(Icons.call_split, size: 30),
            ],
            color: Colors.white,
            buttonBackgroundColor: Colors.white,
            backgroundColor: Colors.blueAccent,
            animationCurve: Curves.easeInOut,
            animationDuration: Duration(milliseconds: 600),
            onTap: (index) {
              setState(() {
                _currentIndex = index;
              });
            },
            // letIndexChange: (index) => true,
          ),
        ),
      ),
    );

這里的PubScaffold就是我封裝的一個(gè)懸浮按鈕組件,把它包裹在MaterialApp外面棺亭,就可以實(shí)現(xiàn)懸浮在所有的組件之上的一個(gè)按鈕啦(當(dāng)然也可以不是按鈕虎眨,具體樣式可以自己定義)。下面我們來看一下PubScaffold中的代碼吧~

import 'dart:math';

import 'package:flutter/material.dart';

class PubScaffold extends StatefulWidget {
  final Widget child;
  PubScaffold({this.child});

  @override
  _PubScaffoldState createState() => _PubScaffoldState();
}

class _PubScaffoldState extends State<PubScaffold> {
  bool draggable = false;

  //靜止?fàn)顟B(tài)下的offset
  Offset idleOffset = Offset(0, 0);
  //本次移動(dòng)的offset
  Offset moveOffset = Offset(0, 0);
  //最后一次down事件的offset
  Offset lastStartOffset = Offset(0, 0);

  int count = 0;

  final List<String> testWidgetList = [
    '測(cè)試1',
    '測(cè)試2',
  ];

  testAppFun(e) {
    // TODO: 你的代碼邏輯
  }

  // 顯示一個(gè)底部彈窗,這里是一個(gè)測(cè)試列表
  showTestList() {
    showModalBottomSheet(
      context: context,
      enableDrag: false,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(20.0),
          topRight: Radius.circular(20.0),
        ),
      ),
      builder: (BuildContext context) {
        return ListView(
          children: testWidgetList
              .map(
                (e) => Container(
                  decoration: BoxDecoration(
                    border: Border(
                      bottom: BorderSide(color: Color(0xFFe3e3e3)),
                    ),
                  ),
                  child: ListTile(
                    onTap: () => testAppFun(e),
                    title: Text(e),
                  ),
                ),
              )
              .toList(),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // 顯示懸浮按鈕
        WidgetsBinding.instance
            .addPostFrameCallback((_) => _insertOverlay(context));
        return widget.child;
      },
    );
  }

  // 懸浮按鈕嗽桩,可以拖拽(可自定義樣式)
  void _insertOverlay(BuildContext context) {
    return Overlay.of(context).insert(
      OverlayEntry(builder: (context) {
        final size = MediaQuery.of(context).size;
        print(size.width);
        return Positioned(
          top: draggable ? moveOffset.dy : size.height - 102,
          left: draggable ? moveOffset.dx : size.width - 72,
          child: GestureDetector(
            // 移動(dòng)開始
            onPanStart: (DragStartDetails details) {
              setState(() {
                lastStartOffset = details.globalPosition;
                draggable = true;
              });
              if (count <= 1) {
                count++;
              }
            },
            // 移動(dòng)中
            onPanUpdate: (DragUpdateDetails details) {
              setState(() {
                moveOffset =
                    details.globalPosition - lastStartOffset + idleOffset;
                if (count > 1) {
                  moveOffset = Offset(max(0, moveOffset.dx), moveOffset.dy);
                } else {
                  moveOffset = Offset(max(0, moveOffset.dx + (size.width - 72)),
                      moveOffset.dy + (size.height - 102));
                }
              });
            },
            // 移動(dòng)結(jié)束
            onPanEnd: (DragEndDetails detail) {
              setState(() {
                idleOffset = moveOffset * 1;
              });
            },
            child: TestContainer(
              onPress: () => showTestList(),
            ),
          ),
        );
      }),
    );
  }
}

// 懸浮按鈕的樣式
class TestContainer extends StatelessWidget {
  final Function onPress;
  TestContainer({this.onPress});
  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: GestureDetector(
        onTap: onPress,
        child: Container(
          width: 56,
          height: 56,
          alignment: Alignment.center,
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Colors.green[600],
          ),
          child: Text(
            "Test",
            style: TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
      ),
    );
  }
}

1.全局懸浮按鈕

這里我們用的是flutter自帶的material庫(kù)中的Overlay組件岳守,具體使用方法如下:

void _insertOverlay(BuildContext context) {
    return Overlay.of(context).insert(
      OverlayEntry(builder: (context) {
        final size = MediaQuery.of(context).size;
        print(size.width);
        return Positioned(
          top: draggable ? moveOffset.dy : size.height - 102,
          left: draggable ? moveOffset.dx : size.width - 72,
          child: GestureDetector(
            // 移動(dòng)開始
            onPanStart: (DragStartDetails details) {
              setState(() {
                lastStartOffset = details.globalPosition;
                draggable = true;
              });
              if (count <= 1) {
                count++;
              }
            },
            // 移動(dòng)中
            onPanUpdate: (DragUpdateDetails details) {
              setState(() {
                moveOffset =
                    details.globalPosition - lastStartOffset + idleOffset;
                if (count > 1) {
                  moveOffset = Offset(max(0, moveOffset.dx), moveOffset.dy);
                } else {
                  moveOffset = Offset(max(0, moveOffset.dx + (size.width - 72)),
                      moveOffset.dy + (size.height - 102));
                }
              });
            },
            // 移動(dòng)結(jié)束
            onPanEnd: (DragEndDetails detail) {
              setState(() {
                idleOffset = moveOffset * 1;
              });
            },
            child: TestContainer(
              onPress: () => showTestList(),
            ),
          ),
        );
      }),
    );
  }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碌冶,隨后出現(xiàn)的幾起案子湿痢,更是在濱河造成了極大的恐慌,老刑警劉巖扑庞,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件譬重,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡罐氨,警方通過查閱死者的電腦和手機(jī)害幅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來岂昭,“玉大人,你說我怎么就攤上這事狠怨≡及。” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵佣赖,是天一觀的道長(zhǎng)恰矩。 經(jīng)常有香客問我,道長(zhǎng)憎蛤,這世上最難降的妖魔是什么外傅? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮俩檬,結(jié)果婚禮上萎胰,老公的妹妹穿的比我還像新娘。我一直安慰自己棚辽,他們只是感情好技竟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著屈藐,像睡著了一般榔组。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上联逻,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天搓扯,我揣著相機(jī)與錄音,去河邊找鬼包归。 笑死锨推,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播爱态,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谭贪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了锦担?” 一聲冷哼從身側(cè)響起俭识,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎洞渔,沒想到半個(gè)月后套媚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡磁椒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年堤瘤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浆熔。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡本辐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出医增,到底是詐尸還是另有隱情慎皱,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布叶骨,位于F島的核電站茫多,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏忽刽。R本人自食惡果不足惜天揖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望跪帝。 院中可真熱鬧今膊,春花似錦、人聲如沸伞剑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)纸泄。三九已至赖钞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間聘裁,已是汗流浹背雪营。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衡便,地道東北人献起。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓洋访,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親谴餐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子姻政,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容