参考资料: 《Flutter实战·第二版》 10.2 组合现有组件
在Flutter中页面UI通常都是由一些低级别组件组合而成,当我们需要封装一些通用组件时,应该首先考虑是否可以通过组合其他组件来实现,如果可以,则应优先使用组合,因为直接通过现有组件拼装会非常简单、灵活、高效。
10.2.1 实例:自定义渐变按钮
Flutter中自带的按钮组件不支持渐变背景,这里自定义一个GradientButton
组件,具有下面的功能:
- 背景支持渐变色
- 手指按下时有涟漪效果
- 可以支持圆角
自带的按钮组件是下面的效果:
其实除了不是渐变色背景其它都满足了。在Flutter中,ElevatedButton
组件默认不具有圆角。然而,可以通过自定义其形状来为其添加圆角。可以在ElevatedButton
的style
属性中设置shape
属性来实现:
ElevatedButton(
onPressed: () {
print('Button pressed!');
},
child: Text('Press Me'),
style: ElevatedButton.styleFrom(
primary: Colors.blue, // 设置按钮的主色
onPrimary: Colors.white, // 设置按钮上文字的颜色
shape: RoundedRectangleBorder( // 设置按钮的形状为圆角矩形
borderRadius: BorderRadius.circular(20.0), // 设置圆角的大小
),
),
)
想有渐变背景的话,不能通过style
属性设置背景色,因为数据类型是有限制的。可以利用DecoratedBox
和InkWell
组合来实现,前者能够设置圆角和渐变背景色,后者可以提供涟漪效果。下面是实现代码,思路比较简单,主要是UI的绘制逻辑:
class GradientButton extends StatelessWidget {
const GradientButton({Key? key,
this.colors,
this.width,
this.height,
this.onPressed,
this.borderRadius,
required this.child,
}) : super(key: key);
// 渐变色数组
final List<Color>? colors;
// 按钮宽高
final double? width;
final double? height;
final BorderRadius? borderRadius;
//点击回调
final GestureTapCallback? onPressed;
final Widget child;
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
//确保colors数组不空
List<Color> _colors =
colors ?? [theme.primaryColor, theme.primaryColorDark];
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors: _colors),
borderRadius: borderRadius,
//border: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
splashColor: _colors.last,
highlightColor: Colors.transparent,
borderRadius: borderRadius,
onTap: onPressed,
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(height: height, width: width),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: DefaultTextStyle(
style: const TextStyle(fontWeight: FontWeight.bold),
child: child,
),
),
),
),
),
),
);
}
}
需要注意组件的输入属性,按钮点击的回调为GestureTapCallback
类型,其属性除了child
之外,均为可选属性。其具有默认样式,如果没有传入color
属性,则默认为主题色主色调和暗色调。通过ConstrainedBox
可以定义按钮的大小,还可以设置默认的字体样式(这里可以按需求去掉)。
定义好之后就可以使用了,这里定义了三个不同的按钮,代码和效果如下:
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const MyHomePage(title: 'TEAL WORLD'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(
widget.title,
style: TextStyle(
color: Colors.teal.shade800, fontWeight: FontWeight.w900),
),
actions: [
ElevatedButton(
child: const Icon(Icons.refresh),
onPressed: () {
setState(() {});
},
)
],
),
body: const ComponentTestRoute(),
floatingActionButton: FloatingActionButton(
onPressed: () {},
tooltip: 'Increment',
child: Icon(
Icons.add_box,
size: 30,
color: Colors.teal[400],
),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
class ComponentTestRoute extends StatefulWidget {
const ComponentTestRoute({Key? key}) : super(key: key);
ComponentTestRouteState createState() => ComponentTestRouteState();
}
class ComponentTestRouteState extends State<ComponentTestRoute> {
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 80),
child: Column(
children: [
GradientButton(
colors: const [Colors.purple, Colors.red],
height: 50.0,
child: const Text("Submit"),
onPressed: () {},
),
GradientButton(
height: 50.0,
colors: [Colors.yellow, Colors.green.shade700],
child: const Text("Submit"),
onPressed: () {},
),
GradientButton(
height: 50.0,
//borderRadius: const BorderRadius.all(Radius.circular(5)),
colors: const [Colors.cyanAccent, Colors.blueAccent],
child: const Text("Submit"),
onPressed: () {},
),
],
),
))
],
);
}
}
class GradientButton extends StatelessWidget {
const GradientButton({
Key? key,
this.colors,
this.width,
this.height,
this.onPressed,
this.borderRadius,
required this.child,
}) : super(key: key);
// 渐变色数组
final List<Color>? colors;
// 按钮宽高
final double? width;
final double? height;
final BorderRadius? borderRadius;
//点击回调
final GestureTapCallback? onPressed;
final Widget child;
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
//确保colors数组不空
List<Color> _colors =
colors ?? [theme.primaryColor, theme.primaryColorDark];
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(colors: _colors),
borderRadius: borderRadius,
//border: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
),
child: Material(
type: MaterialType.transparency,
child: InkWell(
splashColor: _colors.last,
highlightColor: Colors.transparent,
borderRadius: borderRadius,
onTap: onPressed,
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(height: height, width: width),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: DefaultTextStyle(
style: const TextStyle(fontWeight: FontWeight.bold),
child: child,
),
),
),
),
),
),
);
}
}
按下之后涟漪的颜色为colors
中最后一个颜色:
虽然实现起来较为容易,但是在抽离出单独的组件时要考虑代码规范性,如必要参数要用required
关键词标注,对于可选参数在特定场景需要判空或设置默认值等。为了保证代码健壮性,我们需要在用户错误地使用组件时能够兼容或报错提示(使用assert
断言函数)。
在Dart语言中,
assert
是一种用于在开发过程中捕获错误的工具。assert
语句用于在调试模式下测试某个条件是否为真。如果条件为假,那么assert
语句将抛出一个AssertionError
异常,并停止程序的执行。这主要用于在开发过程中捕捉不应该发生的错误情况,从而帮助开发者定位问题。
assert
语句在发布模式(即生产环境)下会被忽略,因此不会影响最终用户的体验。这使得assert
成为开发过程中进行条件测试的理想工具,而无需担心在最终应用中引入额外的性能开销或错误处理逻辑。
下面是assert
在Dart中的基本用法:
void main() {
int number = 5;
// 使用 assert 语句检查 number 是否大于 0
assert(number > 0, 'number should be greater than 0');
// 如果条件为真,程序继续执行
print('Number is positive.');
// 如果条件为假,抛出 AssertionError,并显示提供的错误消息
// assert(number < 0, 'number should be less than 0'); // 这会抛出 AssertionError
}