Flutter 边缘渐隐遮罩实现指南v2

高桥凉介
    分享互动规则

    image.png

    复现抖音导航栏「内容边缘消隐」效果 — 核心:ShaderMask + LinearGradient


    原理

    ShaderMask 将一个 Shader(着色器)作为透明度蒙版叠加在子 Widget 上:

    • 蒙版颜色为 白色 → 子内容完全可见
    • 蒙版颜色为 透明(黑色) → 子内容隐藏

    配合 LinearGradient,在列表两端制造「内容渐隐消失」效果。

    图层结构(从上到下):

    LinearGradient Shader    透明 → 白 → 白 → 透明(blendMode: dstIn)
    SingleChildScrollView    水平可滚动的导航标签
    Container(背景)         决定渐隐消失到哪个颜色
    

    Step 1:最简实现(固定两端渐隐)

    ShaderMask(
      shaderCallback: (bounds) {
        return LinearGradient(
          // 从左→右:透明 白 白 透明
          colors: [
            Colors.transparent,  // 左侧边缘消失
            Colors.white,        // 左侧渐隐结束
            Colors.white,        // 右侧渐隐开始
            Colors.transparent,  // 右侧边缘消失
          ],
          stops: [0.0, 0.05, 0.95, 1.0],
        ).createShader(bounds);
      },
      blendMode: BlendMode.dstIn, // 关键:用 Shader 控制透明度
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(children: tabs), // 你的标签行
      ),
    )
    

    ⚠️ BlendMode.dstIn 是关键参数 — 它让 Shader 的 Alpha 通道直接控制子内容的透明度,而非改变颜色。


    Step 2:进阶 — 根据滚动位置动态显/隐遮罩

    当列表在最左端时不显示左遮罩;到最右端时不显示右遮罩。监听 ScrollController 即可:

    class FadeNavBar extends StatefulWidget {
      const FadeNavBar({super.key, required this.tabs});
      final List<String> tabs;
      @override
      State<FadeNavBar> createState() => _FadeNavBarState();
    }
    
    class _FadeNavBarState extends State<FadeNavBar> {
      final _ctrl = ScrollController();
      bool _showLeft  = false;
      bool _showRight = true;
    
      @override
      void initState() {
        super.initState();
        _ctrl.addListener(() {
          final pos = _ctrl.position;
          setState(() {
            _showLeft  = pos.pixels > 0;
            _showRight = pos.pixels < pos.maxScrollExtent;
          });
        });
      }
    
      @override
      void dispose() {
        _ctrl.dispose();
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        final left  = _showLeft  ? 0.06 : 0.0;
        final right = _showRight ? 0.94 : 1.0;
    
        return ShaderMask(
          shaderCallback: (bounds) => LinearGradient(
            colors: [
              Colors.transparent,
              Colors.white,
              Colors.white,
              Colors.transparent,
            ],
            stops: [0.0, left, right, 1.0], // 动态改变
          ).createShader(bounds),
          blendMode: BlendMode.dstIn,
          child: SingleChildScrollView(
            controller: _ctrl,
            scrollDirection: Axis.horizontal,
            child: Row(
              children: widget.tabs
                  .map((t) => Padding(
                        padding: const EdgeInsets.symmetric(horizontal: 12),
                        child: Text(t, style: const TextStyle(color: Colors.white)),
                      ))
                  .toList(),
            ),
          ),
        );
      }
    }
    

    Step 3:封装成通用 Widget

    class EdgeFadeWrapper extends StatelessWidget {
      const EdgeFadeWrapper({
        super.key,
        required this.child,
        this.fadeWidth = 0.08,  // 渐隐区域占总宽比例(0~0.3)
        this.showLeft  = true,
        this.showRight = true,
      });
    
      final Widget child;
      final double  fadeWidth;
      final bool    showLeft, showRight;
    
      @override
      Widget build(BuildContext context) {
        final l = showLeft  ? fadeWidth       : 0.0;
        final r = showRight ? 1.0 - fadeWidth : 1.0;
    
        return ShaderMask(
          shaderCallback: (b) => LinearGradient(
            colors: [
              Colors.transparent,
              Colors.white,
              Colors.white,
              Colors.transparent,
            ],
            stops: [0.0, l, r, 1.0],
          ).createShader(b),
          blendMode: BlendMode.dstIn,
          child: child,
        );
      }
    }
    

    使用示例:

    // 配合 Step 2 的动态状态使用
    EdgeFadeWrapper(
      showLeft:  _showLeft,
      showRight: _showRight,
      fadeWidth: 0.08,
      child: SingleChildScrollView(
        controller: _ctrl,
        scrollDirection: Axis.horizontal,
        child: Row(children: tabs),
      ),
    )
    

    三种方案对比

    方案实现方式性能适用场景
    ShaderMaskLinearGradient + dstIn最佳,GPU 合成通用,背景任意
    Stack + 渐变容器Container 覆盖在列表上中等背景必须纯色
    CustomPainterdrawRect + saveLayer较差,频繁重绘需完全自定义时

    注意事项

    1. 背景为渐变/图片时,必须用 ShaderMask,Stack 方案会穿帮。
    2. child 含 Image/Canvas 时,在 ShaderMask 外层加 RepaintBoundary 避免不必要重绘。
    3. stops 数组长度必须和 colors 数组一致,否则运行时报错。
    4. fadeWidth 建议保持在 0.05~0.15 之间,太大影响内容可读性。
    评论 0

    支持 @用户名 提醒对方(需为站内已注册用户名);回复仅支持一层楼中楼。

    登录后发表评论、回复与 @ 提及。

    举报

    举报会匿名发送给管理员审核。

    • 暂无评论,来发表第一条。

    码谱 · The Digital Atelier · 技术内容社区

    下载 Android 版

    下载完成后,点击通知栏中的安装包完成安装