Flutter : 好看的底部栏微件

8,761 阅读3分钟

灵感来自DesignCode

效果
example.gif
思路

很简单,一眼就是Stack打底,先是中间一个毛玻璃层,后面是一个Shape,后置于中间层,zIndex最顶层是一个设定好宽高Container,用AnimationController根据bottombar选中的索引来做动画,根据系统深浅色主题,改变外观

实现

最外层先包裹一层Container用来显示边线和切割顶部左右圆角:

Container(
  height: barHeight,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.only(
        topLeft: radius,
        topRight: radius
    ),
    border: Border.all(
        color: Colors.grey.withOpacity(0.35),
        width: borderWidth,
        strokeAlign: BorderSide.strokeAlignOutside
      )
  ),
  child: ClipRRect(
    borderRadius: BorderRadius.only(
        topLeft: radius,
        topRight: radius
    ),
    child: ...
    )
)

核心代码:

Stack(children: [
  // 背景Shape
  Backdrop(
      controller: _animation,
      color: sColor,
      width: itemWidth,
      height: barHeight,
      previousIndex: _previousIndex,
      selectedIndex: selectedIndex),
  Container(color: Colors.grey.withOpacity(0.2)),/*填色*/
  BackdropFilter(
    blendMode: BlendMode.src,
    filter: ImageFilter.blur(sigmaX: 26, sigmaY: 26),
    child: Container(
      width: double.infinity,
      color: (() { /*设置背景色,根据样式*/
        final lightColor = Colors.white.withOpacity(.2);
        final darkColor = Colors.black.withOpacity(.2);
        switch(style) {
          case BlurEffectStyle.auto:
            return MediaQuery.of(context).platformBrightness == Brightness.light
                ? lightColor
                : darkColor;
          case BlurEffectStyle.light:
            return lightColor;
          case BlurEffectStyle.dark:
            return darkColor;
        }
      })(),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: items
            .asMap()
            .map((idx, item) => MapEntry(idx, _itemBuilder(context, idx, item, itemWidth)))
            .values
            .toList()
      ),
    ),
  ),
  // 游标
  Cursor(
    color: sColor,
    width: itemWidth,
    controller: _animation,
    previousIndex: _previousIndex,
    selectedIndex: selectedIndex,
  )
])
坑1 - Border

原设计是顶部左右两个圆角,所以开始直接

BoxDecoration(
borderRadius: BorderRadius.only(
    topLeft: radius,
    topRight: radius
),
border: Border.all(
    color: Colors.grey.withOpacity(0.35),
    width: borderWidth
  )
),

但仔细看 会 发现 左右和底部亦会有一个边线存在

1.png

所以就想着把边框只设置为顶部一条,随又改成了:

border: const Border(top: BorderSide(width: 1, color: Colors.white))

悲剧的是,直接就报错了: A borderRadius can only be given for a uniform Border.

顾名思义,只有设置统一的Border才能设置Radius, 于是查了好多资料发现,普通的设置好像是无解,只能通过自定义CustomPaint来绘制Border,正当我准备paint的时候突然想到,之所以会显示出Border,是因为border的strokeAlign默认属性是-1! 也就是strokeAlignInside,那把它改成strokeAlignOutside不就直接可以了,机智如我。当然自定义绘画也已经实现,但不在本文章描述范围,就不赘述了。

none.png(看不到了吧)

坑2 - BackdropFilter

大家都知道,毛玻璃的效果是在使用BackdropFilter下实现的,但是无意中发现有一个问题,那就是如果容器背景色为透明的话,毛玻璃也会变成透明了:

Scaffold(
   backgroundColor: Colors.transparent,
)

transparent.png (这...)

经过研究后发现,问题出在:BlendMode

观察BackdropFilter的属性后,会发现有一个属性:blendMode,学过安卓的可能会很熟悉,但对我来说有点陌生,进去一看好家伙,这么多,足足20+个枚举:

// https://skia.org/docs/user/api/skpaint_overview/#SkXfermode
SkBlendMode modes[] = {
        SkBlendMode::kClear,
        SkBlendMode::kSrc,
        SkBlendMode::kDst,
        SkBlendMode::kSrcOver,
        SkBlendMode::kDstOver,
        SkBlendMode::kSrcIn,
        SkBlendMode::kDstIn,
        SkBlendMode::kSrcOut,
        SkBlendMode::kDstOut,
        SkBlendMode::kSrcATop,
        SkBlendMode::kDstATop,
        SkBlendMode::kXor,
        SkBlendMode::kPlus,
        SkBlendMode::kModulate,
        SkBlendMode::kScreen,
        SkBlendMode::kOverlay,
        SkBlendMode::kDarken,
        SkBlendMode::kLighten,
        SkBlendMode::kColorDodge,
        SkBlendMode::kColorBurn,
        SkBlendMode::kHardLight,
        SkBlendMode::kSoftLight,
        SkBlendMode::kDifference,
        SkBlendMode::kExclusion,
        SkBlendMode::kMultiply,
        SkBlendMode::kHue,
        SkBlendMode::kSaturation,
        SkBlendMode::kColor,
        SkBlendMode::kLuminosity,
    };

更直观的展示:

WMiNe.png

更多具体可见: [BlendMode enum - dart:ui library - Dart API (flutter.dev)](https://api.flutter.dev/flutter/dart-ui/BlendMode.html)

而Flutter默认的属性值是BlendMode.srcOver

  • srcOver → const BlendMode
  • Composite the source image over the destination image. This is the default value. It represents the most intuitive case, where shapes are painted on top of what is below, with transparent areas showing the destination layer.

所以当我们换成blendMode: BlendMode.src后

  • src → const BlendMode
  • Drop the destination image, only paint the source image. Conceptually, the destination is first cleared, then the source image is painted.

问题就解决了

结束

一个小组件,希望能给你带来一些帮助,如果喜欢的话,就请支持一下吧:

github: bottom_blur_bar

pub.dev: bottom_blur_bar