Flutter进阶|最佳实践:组件内阴影实现

一、需求来源

开发需求过程中遇到一个痛点问题:内阴影,需求是组件向内组件淡化的阴影效果。flutter默认不支持内阴影,之前都是通过图片实现此类需求。但是图片不是最佳实践。所以一直在研究如何用代码实现内阴影,今天灵光一闪,最终实现了此效果,分享给大家。

二、使用示例

1、给 EnBoxDecoration 扩展 内阴影属性 innerBoxShadow实现。

dart 复制代码
Widget buildEnBoxDecoration() {
  return Container(
    height: 100.0,
    alignment: Alignment.center,
    decoration: EnBoxDecoration(
      color: Colors.white,
      borderRadius: BorderRadius.circular(8),
      boxShadow: [
        BoxShadow(
          color: Colors.red,
          blurRadius: 10,
          spreadRadius: 5,
        ),
      ],
      innerBoxShadow: [
        BoxShadow(
          color: Colors.blue,
          blurRadius: 10,
          spreadRadius: 5,
        ),
      ],
    ),
    child: Text([
      "EnBoxDecoration",
      "boxShadow color: Colors.red",
      "innerBoxShadow color: Colors.blue",
    ].join("\n")),
  );
}

2、以独立组件 NInnerShadow 实现。

dart 复制代码
Row(
  children: [
    Expanded(
      child: buildInnerShadow(
        boxShadow: [
          BoxShadow(
            color: Colors.red,
            blurRadius: 30,
            spreadRadius: 5,
          ),
          BoxShadow(
            color: Colors.blue,
            blurRadius: 20,
            spreadRadius: 5,
          ),
        ],
        borderRadius: BorderRadius.circular(12),
        child: Text("NInnerShadowBox - 30"),
      ),
    ),
    SizedBox(width: 20),
    Expanded(
      child: buildInnerShadow(
        boxShadow: [
          BoxShadow(
            color: shadowColor,
            blurRadius: 15,
            spreadRadius: 5,
          ),
          BoxShadow(
            color: Colors.blue,
            blurRadius: 5,
            spreadRadius: 5,
          ),
        ],
        borderRadius: BorderRadius.circular(12),
        child: Text("NInnerShadowBox - 8"),
      ),
    ),
  ],
),

...


Widget buildInnerShadow({
  List<BoxShadow>? boxShadow,
  BorderRadius borderRadius = BorderRadius.zero,
  double blurExtent = 4,
  Widget? child,
}) {
  return NInnerShadow(
    boxShadow: boxShadow,
    blurExtent: blurExtent,
    borderRadius: borderRadius,
    child: Container(
      height: 100.0,
      alignment: Alignment.center,
      child: child,
    ),
  );
}

三、源码 NInnerShadow

java 复制代码
//
//  NInnerShadowBox.dart
//  flutter_templet_project
//
//  Created by shang on 2026/7/24 09:19.
//  Copyright © 2026/7/24 shang. All rights reserved.
//

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class NInnerShadow extends StatelessWidget {
  const NInnerShadow({
    super.key,
    this.boxShadow,
    this.borderRadius = BorderRadius.zero,
    this.blurExtent = 4.0,
    required this.child,
  });

  final List<BoxShadow>? boxShadow;
  final BorderRadius borderRadius;
  final Widget child;

  /// Multiplier used to expand the outer rect to avoid clipping
  /// the blurred shadow.
  ///
  /// The actual padding is:
  ///
  /// `padding = blur * blurExtent`
  ///
  /// Usually 3.0~4.0 is sufficient.
  final double blurExtent;

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      foregroundPainter: _InnerShadowPainter(
        boxShadow: boxShadow,
        borderRadius: borderRadius,
        blurExtent: blurExtent,
      ),
      child: child,
    );
  }
}

class _InnerShadowPainter extends CustomPainter {
  const _InnerShadowPainter({
    this.boxShadow,
    this.blurExtent = 3.0,
    this.borderRadius = BorderRadius.zero,
  });

  final List<BoxShadow>? boxShadow;

  /// Multiplier used to expand the outer rect to avoid clipping
  /// the blurred shadow.
  ///
  /// The actual padding is:
  ///
  /// `padding = blur * blurExtent`
  ///
  /// Usually 3.0~4.0 is sufficient.
  final double blurExtent;

  final BorderRadius borderRadius;

  @override
  void paint(Canvas canvas, Size size) {
    final shadows = boxShadow;
    if (shadows == null || shadows.isEmpty) {
      return;
    }
    // 当前组件的绘制区域,从 (0, 0) 到 (width, height)
    final rect = Offset.zero & size;
    // 创建组件内部区域(圆角矩形)
    // 后续会使用它作为裁剪区域,确保阴影只显示在组件内部
    final clipRRect = borderRadius.toRRect(rect);
    final inner = Path()..addRRect(clipRRect);
    // 只允许绘制到组件内部
    canvas.save();
    canvas.clipPath(inner);
    for (final shadow in shadows) {
      paintShadow(canvas: canvas, rect: rect, clipRRect: clipRRect, shadow: shadow);
    }
    // 恢复 Canvas 状态
    // 撤销 clipPath() 的影响,避免影响后续绘制内容。
    canvas.restore();
  }

  void paintShadow({
    required Canvas canvas,
    required Rect rect,
    required RRect clipRRect,
    required BoxShadow shadow,
  }) {
    /// Gaussian blur sigma.
    final blurSigma = shadow.blurSigma;
    final offset = shadow.offset;
    final spreadRadius = shadow.spreadRadius;
    final padding = blurSigma * blurExtent;
    // spreadRadius > 0:缩小空洞,阴影向内扩散(覆盖更多内部区域)
    // spreadRadius < 0:扩大空洞,阴影向内收缩
    final holeRRect = clipRRect.deflate(spreadRadius);
    // 创建外部路径
    //
    // PathFillType.evenOdd 表示偶奇填充规则:
    // 最终得到的是「外部矩形 - 内部圆角矩形」形成的一个环形区域。
    //
    // inflate(blur * blurExtent) 用于扩大外部矩形,
    // 防止高斯模糊向外扩散时被裁剪。
    final outer = Path()
      ..fillType = PathFillType.evenOdd
      ..addRect(rect.inflate(padding))
      ..addRRect(holeRRect);
    canvas.save();
    // 平移整个阴影
    //
    // offset 决定光源方向:
    // Offset(2,2) -> 左上亮,右下暗
    // Offset(-2,-2)-> 右下亮,左上暗
    canvas.translate(offset.dx, offset.dy);
    // 绘制环形路径
    canvas.drawPath(outer, shadow.toPaint());
    canvas.restore();
  }

  @override
  bool shouldRepaint(covariant _InnerShadowPainter oldDelegate) =>
      !listEquals(boxShadow, oldDelegate.boxShadow) ||
      borderRadius != oldDelegate.borderRadius ||
      blurExtent != oldDelegate.blurExtent;
}

最后、总结

1、之前以为是内阴影 无法实现,所以官方不不支持。实现之后发现真的挺简单。

2、EnBoxDecoration 文件太长,点击链接查看即可。

github

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家15 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端15 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue16 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风16 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈
windliang16 小时前
Claude Code 源码分析(三):一次模型回答如何流进 Agent
前端·算法·ai编程