Flutter组件重构:自定义 Button 组件及 Theme 配置

371 阅读4分钟

一、Button 基础组件

Flutter开发中用 ElevatedButton、OutlinedButton、TextButton配置麻烦且不支持渐进色背景,于是通过子类的方式实现了常见几种样式按钮封装。好处是代码精简,支持主题文件统一配置全局效果。效果如下:

simulator_screenshot_A7FBF95C-6989-479D-9E30-2876C58215DC.png

二、示例代码

1、ThemeData 配置
extensions: [
  AppTheme(
    primary: const Color(0xFF00B451),
    primary2: const Color(0xFF00B451).withOpacity(0.8),
    bgColor: const Color(0xFFF3F3F3),
    fontColor: const Color(0xFF1A1A1A),
    titleStyle: const TextStyle(
      color: Color(0xFF1A1A1A),
      fontSize: 18,
      fontWeight: FontWeight.w500,
      decoration: TextDecoration.none,
    ),
    textStyle: const TextStyle(
      color: Color(0xFF1A1A1A),
      fontSize: 16,
      fontWeight: FontWeight.w400,
      decoration: TextDecoration.none,
    ),
    cancelColor: const Color(0xFFE65F55),
    lineColor: const Color(0xffE4E4E4),
    borderColor: const Color(0xFFE5E5E5),
    disabledColor: const Color(0xffB3B3B3),
  ),
  NButtonTheme(
    primary: const Color(0xFF00B451),//按钮主题色(绿)
  ),
],
2、按钮效果展示
NSectionHeader(
  title: "NButton",
  child: Wrap(
      crossAxisAlignment: WrapCrossAlignment.center,
      children: [
        ...[
          NButton(
            title: "NButton",
            onPressed: () {
              DLog.d("NButton");
            },
          ),
          NButton(
            title: "NButton: 禁用",
            enable: false,
            onPressed: () {},
          ),
          NButton(
            title: "NButton: red",
            primary: Colors.red,
            onPressed: () {},
          ),
          NButton.tonal(
            title: "NButton.tonal",
            // primary: Colors.red,
            // border: Border.all(color: Colors.transparent),
            onPressed: () {},
          ),
          NButton.tonal(
            title: "NButton.tonal",
            primary: Colors.red,
            // border: Border.all(color: Colors.transparent),
            onPressed: () {},
          ),
          NButton.tonal(
            primary: Colors.white,
            title: "NButton.tonal",
            style: TextStyle(color: Colors.black87),
            border: Border.all(color: Color(0xffe4e4e4)),
            onPressed: () {},
          ),
          NButton.text(
            // primary: Colors.red,
            title: "NButton.text",
            onPressed: () {},
          ),
        ]
            .map((e) => Container(
                  width: 180,
                  padding: const EdgeInsets.all(8.0),
                  child: e,
                ))
            .toList(),
        NButton.text(
          // primary: Colors.red,
          width: 26,
          child: Icon(Icons.arrow_back_ios_new, color: Colors.blue),
          onPressed: () {},
        ),
      ]),
),

三、Button 源码

1、NButtonTheme 源码
/// 确定按钮 NButton
class NButtonTheme extends ThemeExtension<NButtonTheme> {
  /// 确定按钮
  const NButtonTheme({
    this.primary,
    this.height,
    this.margin,
    this.padding,
    this.gradient,
    this.gradientDisable,
    this.boxShadow,
    this.boxShadowDisable,
    this.border,
    this.borderDisable,
    this.borderRadius,
    this.borderRadiusDisable,
    this.textStyle,
    this.textStyleDisable,
  });

  final Color? primary;

  /// 点击区域高
  final double? height;

  /// 点击区域外边距
  final EdgeInsets? margin;

  /// 点击区域内边距
  final EdgeInsets? padding;

  /// 点击区域背景颜色(渐进)
  final Gradient? gradient;
  final Gradient? gradientDisable;

  /// 阴影
  final List<BoxShadow>? boxShadow;

  /// 阴影
  final List<BoxShadow>? boxShadowDisable;
  final Border? border;
  final Border? borderDisable;

  final BorderRadius? borderRadius;
  final BorderRadius? borderRadiusDisable;

  /// 文字颜色
  final TextStyle? textStyle;
  final TextStyle? textStyleDisable;

  @override
  ThemeExtension<NButtonTheme> copyWith({
    Color? primary,
    double? height,
    EdgeInsets? margin,
    EdgeInsets? padding,
    Gradient? gradient,
    Gradient? gradientDisable,
    List<BoxShadow>? boxShadow,
    List<BoxShadow>? boxShadowDisable,
    BorderRadius? borderRadius,
    BorderRadius? borderRadiusDisable,
    Border? border,
    Border? borderDisable,
  }) =>
      NButtonTheme(
        primary: primary ?? this.primary,
        height: height ?? this.height,
        margin: margin ?? this.margin,
        padding: padding ?? this.padding,
        gradient: gradient ?? this.gradient,
        gradientDisable: gradientDisable ?? this.gradientDisable,
        boxShadow: boxShadow ?? this.boxShadow,
        boxShadowDisable: boxShadowDisable ?? this.boxShadowDisable,
        borderRadius: borderRadius ?? this.borderRadius,
        borderRadiusDisable: borderRadiusDisable ?? this.borderRadiusDisable,
        border: border ?? this.border,
        borderDisable: borderDisable ?? this.borderDisable,
      );

  @override
  ThemeExtension<NButtonTheme> lerp(
    covariant NButtonTheme? other,
    double t,
  ) =>
      NButtonTheme(
        primary: Color.lerp(primary, other?.primary, t),
        margin: EdgeInsets.lerp(margin, other?.margin, t),
        padding: EdgeInsets.lerp(padding, other?.padding, t),
        gradient: Gradient.lerp(gradient, other?.gradient, t),
        gradientDisable:
            Gradient.lerp(gradientDisable, other?.gradientDisable, t),
        borderRadius: BorderRadius.lerp(borderRadius, other?.borderRadius, t),
        borderRadiusDisable: BorderRadius.lerp(
            borderRadiusDisable, other?.borderRadiusDisable, t),
        border: Border.lerp(border, other?.border, t),
        borderDisable: Border.lerp(borderDisable, other?.borderDisable, t),
      );
}
2、NButton 源码
//
//  NButton.dart
//  flutter_templet_project
//
//  Created by shang on 2023/12/28 11:56.
//  Copyright © 2023/12/28 shang. All rights reserved.
//

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_text.dart';
import 'package:flutter_templet_project/basicWidget/theme/n_button_theme.dart';
import 'package:flutter_templet_project/extension/build_context_ext.dart';
import 'package:flutter_templet_project/extension/ddlog.dart';

/// 主题色背景确定按钮
class NButton extends StatelessWidget {
  const NButton({
    super.key,
    this.primary,
    this.enable = true,
    this.title = '确定',
    this.style,
    this.width,
    this.height,
    this.margin,
    this.padding,
    this.borderRadius,
    this.gradient,
    this.boxShadow,
    this.border,
    required this.onPressed,
    this.onLongPressed,
    this.onTitle,
    this.child,
  });

  factory NButton.tonal({
    Key? key,
    Color? primary,
    String title,
    TextStyle? style,
    bool enable,
    double? width,
    double? height,
    EdgeInsets? margin,
    EdgeInsets? padding,
    BorderRadius? borderRadius,
    Border? border,
    VoidCallback? onLongPressed,
    required VoidCallback? onPressed,
    ValueChanged<String>? onTitle,
    Widget? child,
  }) = _NButtonTonal;

  factory NButton.text({
    Key? key,
    Color? primary,
    String title,
    TextStyle? style,
    bool enable,
    double? width,
    double? height,
    EdgeInsets? margin,
    EdgeInsets? padding,
    BorderRadius? borderRadius,
    Border? border,
    VoidCallback? onLongPressed,
    required VoidCallback? onPressed,
    ValueChanged<String>? onTitle,
    Widget? child,
  }) = _NButtonText;

  /// 主题色
  final Color? primary;

  /// 标题
  final String title;

  /// 标题样式
  final TextStyle? style;

  /// 按钮是否可点击
  final bool enable;

  /// 高度
  final double? height;

  /// 宽度
  final double? width;

  /// 外边距
  final EdgeInsets? margin;

  /// 内边距
  final EdgeInsets? padding;

  /// 渐进色背景
  final Gradient? gradient;

  /// 阴影
  final List<BoxShadow>? boxShadow;

  /// 圆角
  final BorderRadius? borderRadius;

  /// 边框线
  final Border? border;

  /// 点击事件
  final VoidCallback? onPressed;

  /// 长按事件
  final VoidCallback? onLongPressed;

  /// 标题回调
  final ValueChanged<String>? onTitle;

  final Widget? child;

  @override
  Widget build(BuildContext context) {
    final theTheme = Theme.of(context).extension<NButtonTheme>();

    final primaryNew = primary ?? theTheme?.primary ?? context.primaryColor;
    final heightNew = height ?? theTheme?.height ?? 44;
    final marginNew = margin ?? theTheme?.margin;
    final paddingNew = padding ?? theTheme?.padding;
    final gradientNew = gradient ??
        theTheme?.gradient ??
        LinearGradient(
          colors: [primaryNew, primaryNew],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        );
    final gradientDisableNew = theTheme?.gradientDisable ??
        LinearGradient(
          colors: [
            const Color(0xffF3F3F3),
            const Color(0xffF3F3F3),
          ],
          begin: Alignment.topLeft,
          end: Alignment.bottomRight,
        );

    final boxShadowNew = boxShadow ?? theTheme?.boxShadow;
    final boxShadowDisableNew = theTheme?.boxShadowDisable;
    final borderRadiusNew =
        theTheme?.borderRadius ?? const BorderRadius.all(Radius.circular(8));
    final borderRadiusDisableNew = theTheme?.borderRadiusDisable ??
        const BorderRadius.all(Radius.circular(8));

    final borderNew = theTheme?.border;
    final borderDisableNew = theTheme?.borderDisable;

    final decorationEnable = BoxDecoration(
      // color: Colors.transparent,
      border: borderNew,
      borderRadius: borderRadiusNew,
      gradient: gradientNew,
      boxShadow: boxShadowNew,
    );

    final decorationDisable = BoxDecoration(
      // color: Colors.transparent,
      border: borderDisableNew,
      borderRadius: borderRadiusDisableNew,
      gradient: gradientDisableNew,
      boxShadow: boxShadowDisableNew,
    );

    final decoration = enable ? decorationEnable : decorationDisable;
    final textColor = enable ? Colors.white : Color(0xffb3b3b3);

    return GestureDetector(
      onTap: () {
        if (!enable) {
          DLog.d("按钮禁用");
          return;
        }
        if (onTitle != null) {
          onTitle?.call(title);
          return;
        }
        onPressed?.call();
      },
      onLongPress: onLongPressed,
      child: Container(
        width: width,
        height: heightNew,
        margin: marginNew,
        padding: paddingNew,
        alignment: Alignment.center,
        decoration: decoration,
        child: child ??
            NText(
              title,
              color: textColor,
              fontSize: 16,
              maxLines: 1,
              style: style ?? theTheme?.textStyle,
            ),
      ),
    );
  }
}

/// 主题浅色背景
class _NButtonTonal extends NButton {
  const _NButtonTonal({
    super.key,
    super.primary,
    super.enable,
    super.title = '确定',
    super.style,
    super.width,
    super.height,
    super.margin,
    super.padding,
    super.gradient,
    super.borderRadius,
    super.border,
    required super.onPressed,
    super.onLongPressed,
    super.onTitle,
    super.child,
  });

  @override
  Widget build(BuildContext context) {
    if (!enable) {
      return NButton(
        key: key,
        enable: enable,
        title: title,
        style: style,
        width: width,
        height: height,
        margin: margin,
        padding: padding,
        gradient: gradient,
        borderRadius: borderRadius,
        border: border,
        onPressed: onPressed,
        onLongPressed: onLongPressed,
        onTitle: onTitle,
        child: child,
      );
    }

    final theTheme = Theme.of(context).extension<NButtonTheme>();

    final primaryNew = primary ?? theTheme?.primary ?? context.primaryColor;
    final heightNew = height ?? theTheme?.height ?? 44;
    final marginNew = margin ?? theTheme?.margin;
    final paddingNew = padding ?? theTheme?.padding;

    final decoration = BoxDecoration(
      color: primaryNew.withOpacity(0.1),
      border: border ?? theTheme?.border ?? Border.all(color: primaryNew),
      borderRadius:
          theTheme?.borderRadius ?? const BorderRadius.all(Radius.circular(8)),
    );

    final textColor = primaryNew;

    return GestureDetector(
      onTap: () {
        if (onTitle != null) {
          onTitle?.call(title);
          return;
        }
        onPressed?.call();
      },
      onLongPress: onLongPressed,
      child: Container(
        width: width,
        height: heightNew,
        margin: marginNew,
        padding: paddingNew,
        alignment: Alignment.center,
        decoration: decoration,
        child: child ??
            NText(
              title,
              color: textColor,
              fontSize: 16,
              maxLines: 1,
              style: style ?? theTheme?.textStyle,
            ),
      ),
    );
  }
}

/// 纯文字
class _NButtonText extends NButton {
  const _NButtonText({
    super.key,
    super.primary,
    super.enable,
    super.title = '确定',
    super.style,
    super.width,
    super.height,
    super.margin,
    super.padding,
    super.borderRadius,
    super.border,
    required super.onPressed,
    super.onLongPressed,
    super.onTitle,
    super.child,
  });

  @override
  Widget build(BuildContext context) {
    if (!enable) {
      return NButton(
        key: key,
        enable: enable,
        title: title,
        style: style,
        width: width,
        height: height,
        margin: margin,
        padding: padding,
        gradient: gradient,
        borderRadius: borderRadius,
        border: border,
        onPressed: onPressed,
        onLongPressed: onLongPressed,
        onTitle: onTitle,
        child: child,
      );
    }

    final theTheme = Theme.of(context).extension<NButtonTheme>();

    final primaryNew = primary ?? theTheme?.primary ?? context.primaryColor;
    final heightNew = height ?? theTheme?.height;
    final marginNew = margin ?? theTheme?.margin;
    final paddingNew = padding ?? theTheme?.padding;

    final decoration = BoxDecoration(
      // color: primaryNew.withOpacity(0.1),
      border:
          border ?? theTheme?.border ?? Border.all(color: Colors.transparent),
      borderRadius:
          theTheme?.borderRadius ?? const BorderRadius.all(Radius.circular(8)),
    );

    final textColor = primaryNew;

    return GestureDetector(
      onTap: () {
        if (onTitle != null) {
          onTitle?.call(title);
          return;
        }
        onPressed?.call();
      },
      onLongPress: onLongPressed,
      child: Container(
        width: width,
        height: heightNew,
        margin: marginNew,
        padding: paddingNew,
        alignment: Alignment.center,
        decoration: decoration,
        child: child ??
            NText(
              title,
              color: textColor,
              fontSize: 16,
              maxLines: 1,
              style: style ?? theTheme?.textStyle,
            ),
      ),
    );
  }
}

最后、总结

1、通过 NButton 及子类 _NButtonTonal、_NButtonText 实现,可以满足常见效果配置。再通过 NButtonTheme 主题配置,三种样式颜色,阴影,渐变色可统一设置。极大的提高了开发效率!

2、程序的世界是一通百通,其他组件的多样式封装也就不是问题了。

github