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

Flutter自定义按钮:如何实现订阅计划按钮的指定样式?

应用内购买计划按钮实现方案

问题说明

需要实现3个应用内购买计划的选择按钮(单选逻辑),目标样式要求右上角带图标角标,当前代码实现效果不符合预期,尤其是角标样式无法达标。

当前尝试代码

SizedBox(
  width: 250.0,
  height: 60.0,
  child: Stack(
    children: [
      const Positioned(
        top: 0,
        right: 0,
        child: Text(
          "%",
          style: TextStyle(
            color: Colors.white,
            backgroundColor: Colors.amberAccent,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
      OutlinedButton(
          style: OutlinedButton.styleFrom(
            padding: const EdgeInsets.symmetric(
                horizontal: 5, vertical: 5),
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(10.0)),
            side: const BorderSide(
              color: mainColor,
            ),
          ),
          onPressed: () {},
          child: Container(
            padding: const EdgeInsets.symmetric(
                horizontal: 5, vertical: 5),
            child: Column(
              children: [
                const Row(
                  mainAxisAlignment:
                      MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      "1 year",
                      style: TextStyle(
                        color: Colors.white,
                      ),
                    ),
                    Text(
                      "EGP 150.00",
                      style: TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
                Row(
                  mainAxisAlignment:
                      MainAxisAlignment.spaceBetween,
                  children: [
                    const Text(
                      "Save 60%",
                      style: TextStyle(
                        color: Colors.amberAccent,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      "EGP 12.33 / month",
                      style: TextStyle(
                        color: Colors.grey[350],
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ))
    ],
  ),
),

优化方案

1. 实现右上角带图标的角标样式

通过Stack+Positioned配合自定义裁剪器,实现斜角标签效果并加入图标:

Positioned(
  top: 0,
  right: 0,
  child: ClipPath(
    clipper: TriangleClipper(),
    child: Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
      color: Colors.amberAccent,
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: const [
          Icon(Icons.local_offer, color: Colors.white, size: 14),
          SizedBox(width: 4),
          Text(
            "60% OFF",
            style: TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.bold,
              fontSize: 12,
            ),
          ),
        ],
      ),
    ),
  ),
)

自定义裁剪器代码:

class TriangleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(size.width, 0);
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

2. 实现单选按钮逻辑

用StatefulWidget管理选中状态,动态切换按钮的边框和背景样式:

// 定义选中状态变量
String? selectedPlan = "1year";

// 按钮样式根据选中状态切换
OutlinedButton.styleFrom(
  padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
  shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  side: BorderSide(
    color: selectedPlan == "1year" ? Colors.amberAccent : Colors.grey,
    width: 2,
  ),
  backgroundColor: selectedPlan == "1year" ? Colors.amberAccent.withOpacity(0.1) : Colors.transparent,
)

3. 优化内部布局

调整按钮内文字的间距、字号和对齐方式,避免内容拥挤:

child: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        const Text(
          "1 year",
          style: TextStyle(color: Colors.white, fontSize: 14),
        ),
        Text(
          "EGP 150.00",
          style: TextStyle(
            color: Colors.white,
            fontWeight: FontWeight.bold,
            fontSize: 16,
          ),
        ),
      ],
    ),
    const SizedBox(height: 4),
    Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        const Text(
          "Save 60%",
          style: TextStyle(
            color: Colors.amberAccent,
            fontWeight: FontWeight.bold,
            fontSize: 12,
          ),
        ),
        Text(
          "EGP 12.33 / month",
          style: TextStyle(
            color: Colors.grey[400],
            fontSize: 12,
          ),
        ),
      ],
    ),
  ],
)

完整示例代码

import 'package:flutter/material.dart';

class PurchasePlans extends StatefulWidget {
  const PurchasePlans({super.key});

  @override
  State<PurchasePlans> createState() => _PurchasePlansState();
}

class _PurchasePlansState extends State<PurchasePlans> {
  String? selectedPlan = "1year";

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildPlanButton(
          planId: "1month",
          title: "1 month",
          price: "EGP 37.99",
          monthlyPrice: "EGP 37.99 / month",
          hasTag: false,
        ),
        const SizedBox(height: 12),
        _buildPlanButton(
          planId: "6months",
          title: "6 months",
          price: "EGP 149.99",
          monthlyPrice: "EGP 24.99 / month",
          saveText: "Save 34%",
          hasTag: true,
        ),
        const SizedBox(height: 12),
        _buildPlanButton(
          planId: "1year",
          title: "1 year",
          price: "EGP 150.00",
          monthlyPrice: "EGP 12.33 / month",
          saveText: "Save 60%",
          hasTag: true,
        ),
      ],
    );
  }

  Widget _buildPlanButton({
    required String planId,
    required String title,
    required String price,
    required String monthlyPrice,
    String? saveText,
    bool hasTag = false,
  }) {
    return SizedBox(
      width: 280,
      height: 70,
      child: Stack(
        children: [
          if (hasTag && saveText != null)
            Positioned(
              top: 0,
              right: 0,
              child: ClipPath(
                clipper: TriangleClipper(),
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
                  color: Colors.amberAccent,
                  child: Row(
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      const Icon(Icons.local_offer, color: Colors.white, size: 14),
                      const SizedBox(width: 4),
                      Text(
                        saveText.toUpperCase(),
                        style: const TextStyle(
                          color: Colors.white,
                          fontWeight: FontWeight.bold,
                          fontSize: 12,
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          OutlinedButton(
            style: OutlinedButton.styleFrom(
              padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
              side: BorderSide(
                color: selectedPlan == planId ? Colors.amberAccent : Colors.grey.shade700,
                width: 2,
              ),
              backgroundColor: selectedPlan == planId ? Colors.amberAccent.withOpacity(0.1) : Colors.transparent,
            ),
            onPressed: () {
              setState(() {
                selectedPlan = planId;
              });
            },
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    Text(
                      title,
                      style: const TextStyle(color: Colors.white, fontSize: 14),
                    ),
                    Text(
                      price,
                      style: const TextStyle(
                        color: Colors.white,
                        fontWeight: FontWeight.bold,
                        fontSize: 16,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 4),
                Row(
                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
                  children: [
                    if (saveText != null)
                      Text(
                        saveText,
                        style: const TextStyle(
                          color: Colors.amberAccent,
                          fontWeight: FontWeight.bold,
                          fontSize: 12,
                        ),
                      ),
                    Text(
                      monthlyPrice,
                      style: TextStyle(
                        color: Colors.grey[400],
                        fontSize: 12,
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class TriangleClipper extends CustomClipper<Path> {
  @override
  Path getClip(Size size) {
    final path = Path();
    path.moveTo(size.width, 0);
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

内容的提问来源于stack exchange,提问作者Emad Younan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 06:44:54