You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Flutter中实现类似Instagram、Snapchat带顶部凹槽的底部滑出面板

Bottom Sheet 顶部凹槽效果实现方案

期望实现的效果

通用实现思路

  • 不需要在弹窗顶部单独加一个带缺口的遮罩层,这种方案在深色模式、背景高斯模糊场景下会出现适配异常
  • 优先用路径裁剪(Path Clip)的方式处理弹窗的顶部边缘,凹槽和圆角可以同时在路径里定义,适配所有背景样式
  • 凹槽尺寸建议固定:常见宽度32pt、高度4pt、圆角2pt,和大部分系统设计规范对齐

iOS 实现(Swift)

用UIBezierPath给弹窗的容器视图做裁剪,示例代码:

// 自定义Bottom Sheet容器类
class CustomBottomSheetView: UIView {
    override func layoutSubviews() {
        super.layoutSubviews()
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: 16)
        // 添加顶部凹槽路径
        let notchWidth: CGFloat = 32
        let notchHeight: CGFloat = 4
        let notchX = (bounds.width - notchWidth) / 2
        // 移动到凹槽起点
        path.move(to: CGPoint(x: notchX - 2, y: 0))
        // 凹槽左圆角
        path.addQuadCurve(to: CGPoint(x: notchX, y: notchHeight), controlPoint: CGPoint(x: notchX, y: 0))
        // 凹槽底部横线
        path.addLine(to: CGPoint(x: notchX + notchWidth, y: notchHeight))
        // 凹槽右圆角
        path.addQuadCurve(to: CGPoint(x: notchX + notchWidth + 2, y: 0), controlPoint: CGPoint(x: notchX + notchWidth, y: 0))
        path.close()
        // 应用裁剪
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        layer.mask = shapeLayer
    }
}

如果用系统的UISheetPresentationController,给presentedView添加上述裁剪逻辑即可。

Android 实现(Kotlin Jetpack Compose)

用Shape接口自定义弹窗形状,示例代码:

// 自定义带顶部凹槽的形状
val BottomSheetWithNotchShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp).let { baseShape ->
    GenericShape { size, _ ->
        // 先画基础圆角矩形
        baseShape.addOutline(this, size, layoutDirection)
        // 画顶部凹槽
        val notchWidth = 32.dp.toPx()
        val notchHeight = 4.dp.toPx()
        val notchX = (size.width - notchWidth) / 2
        moveTo(notchX - 2f, 0f)
        quadraticBezierTo(notchX, 0f, notchX, notchHeight)
        lineTo(notchX + notchWidth, notchHeight)
        quadraticBezierTo(notchX + notchWidth, 0f, notchX + notchWidth + 2f, 0f)
        close()
    }
}

// 使用时给ModalBottomSheet指定shape
ModalBottomSheet(
    onDismissRequest = { /* 关闭逻辑 */ },
    shape = BottomSheetWithNotchShape
) {
    // 弹窗内容
}

如果用XML实现,可以给根布局设置自定义OutlineProvider做裁剪。

前端实现(CSS)

用clip-path属性裁剪弹窗容器,方案更简洁且适配性高:

.custom-bottom-sheet {
    width: 100%;
    border-top-left-radius: 16px;
    border-top-right-radius: 16px;
    background: #fff;
    /* 核心裁剪属性,path里的数值可以按需求调整 */
    clip-path: path('M0 16 Q0 0 16 0 H calc(50% - 18px) Q calc(50% - 16px) 0 calc(50% - 16px) 4 H calc(50% + 16px) Q calc(50% + 16px) 0 calc(50% + 18px) 0 H calc(100% - 16px) Q 100% 0 100% 16 V 100% H 0 Z');
}

如果需要兼容旧浏览器,可以用一个绝对定位的伪元素做凹槽,设置和弹窗背后的背景同色即可。

Flutter 实现

自定义ShapeBorder给showModalBottomSheet指定形状:

class BottomSheetWithNotchShape extends RoundedRectangleBorder {
  const BottomSheetWithNotchShape({
    BorderRadius borderRadius = const BorderRadius.vertical(top: Radius.circular(16)),
  }) : super(borderRadius: borderRadius);

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    final path = super.getOuterPath(rect, textDirection: textDirection);
    const notchWidth = 32.0;
    const notchHeight = 4.0;
    final notchX = (rect.width - notchWidth) / 2;
    path.moveTo(notchX - 2, 0);
    path.quadraticBezierTo(notchX, 0, notchX, notchHeight);
    path.lineTo(notchX + notchWidth, notchHeight);
    path.quadraticBezierTo(notchX + notchWidth, 0, notchX + notchWidth + 2, 0);
    return path;
  }
}

// 使用
showModalBottomSheet(
  context: context,
  shape: const BottomSheetWithNotchShape(),
  builder: (context) => /* 弹窗内容 */,
);

内容的提问来源于stack exchange,提问作者Saffron-codes

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 21:27:00