<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Flutter實現閑魚底部導航欄中間突出效果

      Posted on 2025-10-02 08:41  lifeisastory  閱讀(54)  評論(0)    收藏  舉報

      實現思路

      Scaffold 組件中使用 bottomNavigationBarfloatingActionButton 屬性建立底部導航欄和浮動按鈕,同時使用 floatingActionButtonLocation 屬性指定浮動按鈕的位置。

      默認情況下,當 floatingActionButton 融入 bottomNavigationBar 時,僅可實現如下圖效果:(指定 bottomNavigationBarBottomAppBar 組件,其 shape 屬性為 CircularNotchedRectangle,指定 floatingActionButtonLocationFloatingActionButtonLocation.centerDocked

      所以需要自定義實現一個類似 CircularNotchedRectangle 類和 FloatingActionButtonLocation.centerDocked 類。

      CircularNotchedRectangle 的實現原理

      我們的目的是實現如下圖的效果:

      可以將缺口部分分成三部分:

      其中,A 和 C 段是關于圓心對稱的兩段二次貝塞爾曲線,用來平滑過渡,B 是一段圓弧。

      B 通過圓的半徑可以輕松得到,重點來討論二次貝塞爾曲線如何實現。對于一段二次貝塞爾曲線,有三個點,即 P0(起始點)、P1(控制點)、P2(結束點):

      我們希望 P0 和 P2 處的連接是平滑的,即連接點處兩段曲線的切線相同。由于 P0 所在的曲線為直線,因此我們僅考慮 P2 的平滑連接即可。

      注意,下面的計算的笛卡爾坐標系原點為圓心\(O\)

      先指定 \(P_1(a,b)\)\(P_0(c,b)\)(a、c是經驗值)。現在問題轉化為求 \(P_2\) 的坐標 \((x_2,y_2)\),另外我們還有下面的條件:

      • 圓的方程:\(x^2 + y^2 = R^2\ (R = r + Notch)\)
      • 直線方程:\(xx_2 + yy_2 = R^2\)
      • \(P_1\) 在直線上:\(ax_2 + by_2 = R^2\ ①\)
      • \(P_2\) 在圓上:\(x_2^2 + y_2^2 = R^2\ ②\)

      通過聯立①式和②式,可得:

      \((a^2 + b^2)x_2^2\ -\ 2aR^2x_2\ +\ R^4\ -\ b^2R^2 = 0\)

      \((a^2 + b^2)y_2^2\ -\ 2bR^2y_2\ +\ R^4\ -\ a^2R^2 = 0\)

      通過求根公式可得:

      \(x_2 = \frac{aR^2\ \pm\ \sqrt{a^2R^4\ -\ (a^2\ +\ b^2)(R^4\ -\ b^2R^2)}}{a^2\ + \ b^2} = \frac{aR^2\ \pm\ \sqrt{a^2b^2R^2\ +\ b^4R^2\ -\ b^2R^4}}{a^2\ + \ b^2}\)

      \(y_2 = \frac{bR^2\ \pm\ \sqrt{b^2R^4\ -\ (a^2\ +\ b^2)(R^4\ -\ a^2R^2)}}{a^2\ + \ b^2} = \frac{bR^2\ \pm\ \sqrt{a^2b^2R^2\ +\ a^4R^2\ -\ a^2R^4}}{a^2\ + \ b^2}\)

      \(P_1\) 坐標 \((a,b)\) 代入即可得到 \(P_2\) 的坐標。(注意,還需要使用 \(P_2\) 在圓上這一條件判斷兩個 \(x_2\) 和兩個 \(y_2\) 的對應情況)

      選取在圓心下面的一組解,再使用二次貝塞爾曲線連接 A 的兩端點即可得到 CircularNotchedRectangle 的效果。

      所以,仿照 CircularNotchedRectangle 的實現,我們根據推導公式和入參選擇圓心上方或者下方的一組解即可實現我們需要的效果。

      FloatingActionButtonLocation.centerDocked 的實現原理

      通過查看源代碼可以發現,FloatingActionButtonLocation.centerDocked 調用了 _CenterDockedFabLocation 類,它繼承自 StandardFabLocation 類,并混入了 FabCenterOffsetXFabDockedOffsetY 兩個類。

      StandardFabLocation 類繼承自 FloatingActionButtonLocation,需要重寫 getOffset() 來得到 FAB 的偏移量。

      StandardFabLocation 類中已經重寫了 getOffset() ,它還定義了 getOffsetX()getOffsetY() 來獲取 X 和 Y 軸的偏移量。getOffsetX()getOffsetY()FabCenterOffsetXFabDockedOffsetY 兩個混入類中實現,得到正確的 X 和 Y 軸的偏移。

      所以,我們的自定義類只需繼承 FloatingActionButtonLocation 并根據自定義位置重寫 getOffset() 即可。

      代碼實現

      自定義類 CircularCustomRectangle

      class CircularCustomRectangle extends NotchedShape {
        /// FAB 融合進 bottomNavigationBar 的自定義樣式
        /// 如果 [guest] 向上或向下移動過半,則不對 [host] 處理
        const CircularCustomRectangle({
          this.inverted = false,
          this.protruded = true,
        });
      
        /// 控制在導航欄頂部還是底部作用效果,默認頂部,設置[true]表示作用在底部
        final bool inverted;
      
        /// 控制向上突出還是向下凹入,默認向上突出,設置[false]表示向下凹入
        final bool protruded;
      
        @override
        Path getOuterPath(Rect host, Rect? guest) {
          // 判斷 guest是否為 null 或沒有覆蓋 host,如果是則不對 host處理
          if (guest == null || !host.overlaps(guest)) {
            return Path()..addRect(host);
          }
      
          // 判斷 guest 是否向上或向下移動過半,如果過半則不對 host 處理
          if (protruded && guest.center.dy < 0) {
            return Path()..addRect(host);
          } else if (!protruded && guest.center.dy > 0) {
            return Path()..addRect(host);
          }
      
          // 設置對 host 處理的圓弧半徑
          final double r = guest.width / 2.0;
      
          // 生成一個圓弧半徑,用于在 B 段連接 P2、P3
          final Radius radius = Radius.circular(r);
      
          // 根據傳入參數進行指定位置、方向的處理
          final double invertMultiplier = inverted ? -1.0 : 1.0;
          final double protrudedMultiplier = protruded ? 1.0 : -1.0;
      
          /// 下面的計算邏輯,當前坐標原點全部為 guest 的圓心
      
          // 根據 guest 的位置動態計算圓周上的點到 y 軸的距離,用來參與決定圓滑過渡開始的位置
          double d = math.sqrt(r * r - guest.center.dy * guest.center.dy);
          const double s1 = 15; // 經驗值,調整過渡的長度
          const double s2 = 2; // 經驗值,調整過渡的高度
      
          // a 是以圓心為坐標原點時 P1 的橫坐標
          final double a = -d - s2;
          // b 是以圓心為坐標原點時 P1 的縱坐標
          final double b = guest.center.dy;
      
          // 計算 x、y 的解的 delta
          final double sqrtDeltax = b.abs() * r * math.sqrt(a * a + b * b - r * r);
          final double sqrtDeltay = a.abs() * r * math.sqrt(b * b + a * a - r * r);
          // 計算 x 的兩個解
          final double p2xA = ((a * r * r) - sqrtDeltax) / (a * a + b * b);
          final double p2xB = ((a * r * r) + sqrtDeltax) / (a * a + b * b);
      
          // 計算 y 的兩個解
          // 先判斷兩個解的對應關系
          double p2yAtemp = ((b * r * r) - sqrtDeltay) / (a * a + b * b);
          double p2yBtemp = ((b * r * r) + sqrtDeltay) / (a * a + b * b);
          if (!(((p2xA * p2xA + p2yAtemp * p2yAtemp) - r * r).abs() < 5.0)) {
            double temp = p2yAtemp;
            p2yAtemp = p2yBtemp;
            p2yBtemp = temp;
          }
          // 再根據判斷結果確定兩個 x 的解與 y 的解的對應關系
          final double p2yA = p2yAtemp * invertMultiplier;
          final double p2yB = p2yBtemp * invertMultiplier;
      
          final List<Offset> p = List<Offset>.filled(6, Offset.zero);
      
          // 下面計算 P0、P1、P2,再根據 P0、P1、P2 鏡像得到 P3、P4、P5
          p[0] = Offset(-d - s1, b);
          p[1] = Offset(a, b);
          // 根據 protrudedMultiplier 的值,選擇要縱坐標大于0的向上凸出還是縱坐標小于0的向下凹入的坐標
          p[2] = protrudedMultiplier * p2yA > protrudedMultiplier * p2yB
              ? Offset(p2xA, p2yA)
              : Offset(p2xB, p2yB);
          p[3] = Offset(-1.0 * p[2].dx, p[2].dy);
          p[4] = Offset(-1.0 * p[1].dx, p[1].dy);
          p[5] = Offset(-1.0 * p[0].dx, p[0].dy);
      
          /// 下面將坐標原點從圓心轉換成以 host 的左上角為坐標原點
      
          for (int i = 0; i < p.length; i += 1) {
            double x = p[i].dx + guest.center.dx; // x軸方向沒有變化,直接加 guest 的中心點坐標即可
            double y =
                -p[i].dy + guest.center.dy; // y軸方向反向,需要先將原 y 軸反向再加 guest 的中心點坐標
            p[i] = Offset(x, y);
          }
      
          // 根據位置點生成路徑
          final Path path = Path()..moveTo(host.left, host.top);
          if (!inverted) {
            path
              ..lineTo(p[0].dx, p[0].dy)
              ..quadraticBezierTo(p[1].dx, p[1].dy, p[2].dx, p[2].dy)
              ..arcToPoint(
                p[3],
                radius: radius,
                clockwise: protruded,
              ) // 這里的 clockwise 控制了圓弧的方向
              ..quadraticBezierTo(p[4].dx, p[4].dy, p[5].dx, p[5].dy)
              ..lineTo(host.right, host.top)
              ..lineTo(host.right, host.bottom)
              ..lineTo(host.left, host.bottom);
          } else {
            path
              ..lineTo(host.right, host.top)
              ..lineTo(host.right, host.bottom)
              ..lineTo(p[5].dx, p[5].dy)
              ..quadraticBezierTo(p[4].dx, p[4].dy, p[3].dx, p[3].dy)
              ..arcToPoint(p[2], radius: radius, clockwise: protruded)
              ..quadraticBezierTo(p[1].dx, p[1].dy, p[0].dx, p[0].dy)
              ..lineTo(host.left, host.bottom);
          }
      
          return path..close();
        }
      }
      

      自定義類 FloatingButtonCustomLocation

      class FloatingButtonCustomLocation extends FloatingActionButtonLocation {
        /// 控制 FAB 位置的自定義類
        FloatingButtonCustomLocation(
          this.location, {
          this.offsetX = 0,
          this.offsetY = 0,
        });
      
        /// [location] 表示參照物,比如 [FloatingActionButtonLocation.startTop] 表示以 [bottomNavigationBar] 左上角為原點
        FloatingActionButtonLocation location;
      
        /// [offsetX] 表示 X 方向的偏移量
        final double offsetX;
      
        /// [offsetY] 表示 Y 方向的偏移量
        final double offsetY;
      
        @override
        Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
          Offset offset = location.getOffset(scaffoldGeometry);
          return Offset(offset.dx + offsetX, offset.dy + offsetY);
        }
      }
      

      優化

      如果 floatingActionButtonLocation 屬性使用了自定義的類,在點擊導航欄按鈕時會讓 FAB 執行縮放動畫,我們可以在 floatingActionButtonAnimator 屬性中使用自定義的動畫類來取消這個縮放動畫:

      class ScalingCustomAnimation extends FloatingActionButtonAnimator {
        /// 控制 FAB 動畫的自定義類
        ScalingCustomAnimation();
      
        @override
        Offset getOffset({
          required Offset begin,
          required Offset end,
          required double progress,
        }) {
          return Offset.lerp(begin, end, progress)!;
        }
      
        @override
        Animation<double> getRotationAnimation({required Animation<double> parent}) {
          return Tween<double>(begin: 1.0, end: 1.0).animate(parent);
        }
      
        @override
        Animation<double> getScaleAnimation({required Animation<double> parent}) {
          return Tween<double>(begin: 1.0, end: 1.0).animate(parent);
        }
      }
      

      參考資料

      https://zhuanlan.zhihu.com/p/394087615

      https://juejin.cn/post/7153097948195192863

      https://goo.gl/Ufzrqn

      主站蜘蛛池模板: 国产不卡一区不卡二区| 亚洲一区二区三区水蜜桃| 亚洲永久视频| 久久精品女人的天堂av| 亚洲精品中文字幕二区| 久久精品丝袜高跟鞋| 久久婷婷成人综合色| 91色老久久精品偷偷性色| 护士张开腿被奷日出白浆| 亚洲精品宾馆在线精品酒店| 最新精品露脸国产在线| 一本色道久久88亚洲综合| 在线a亚洲老鸭窝天堂| 蜜臀av人妻国产精品建身房| 老司机亚洲精品一区二区| 久久国产成人av蜜臀| 日韩一区在线中文字幕| 久久精品国产99久久6| 日韩激情成人| 最近中文字幕完整版2019| 国产AV一区二区三区| 亚洲AV成人片不卡无码| 色吊丝二区三区中文字幕| 国产欧美综合在线观看第十页 | 五月婷婷中文字幕| 亚洲国产欧美在线看片一国产| 嘉黎县| 日韩精品一卡二卡在线观看| 无码人妻aⅴ一区二区三区蜜桃| 乱码视频午夜在线观看| 亚洲成人av在线资源| 欧美成人精品手机在线| 国产婷婷精品av在线| 激情综合网激情五月伊人| 欧美日本国产va高清cabal| av亚洲在线一区二区| 极品少妇xxxx| 蜜臀91精品高清国产福利| 国产suv精品一区二区四| 国产美女自卫慰黄网站| 高清自拍亚洲精品二区|