learn-tech/专栏/Flutter入门教程/21白板画笔的参数设置.md
2024-10-16 00:01:16 +08:00

308 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
21 白板画笔的参数设置
1. 线粗选择器
如下所示,左下角可以选择线的粗细,激活状态通过蓝色线框表示。在选择之后,激活状态发生变化,绘制时线的宽度也会变化。这个选择器的构建逻辑相对独立,以后也有复用的可能,可以单独抽离为一个组件维护,这里创建 StorkWidthSelector 组件,并将其通过 Stack 组件叠放在画板之上:
线粗 = 1
线粗 = 8
通过选择器在界面上的展示效果,不难看出组件需要依赖的数据有:
支持的线宽列表 List<double>
激活索引 int
条目的颜色 Color
点击条目时的回调 ValueChanged<int>
类定义如下:
class StorkWidthSelector extends StatelessWidget {
final List<double> supportStorkWidths;
final int activeIndex;
final Color color;
final ValueChanged<int> onSelect;
const StorkWidthSelector({
Key? key,
required this.supportStorkWidths,
required this.activeIndex,
required this.onSelect,
required this.color,
}) : super(key: key);
在组件构建逻辑中,主要是便历 supportStorkWidths 列表,通过 _buildByIndex 方法根据索引值构建条目。其中可以通过 index == activeIndex 来确定当前条目是否被激活;再通过激活状态确定是否添加边线:
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(
supportStorkWidths.length,
_buildByIndex,
)),
);
}
Widget _buildByIndex(int index) {
bool select = index == activeIndex;
return GestureDetector(
onTap: () => onSelect(index),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: 70,
height: 18,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: select ? Border.all(color: Colors.blue) : null),
child: Container(
width: 50,
color: color,
height: supportStorkWidths[index],
),
),
);
}
最后通过 Stack + Positioned 组件,将 StorkWidthSelector 组件叠放在界面的右下角;并通过 _onSelectStorkWidth 作为选择的回调处理界面状态数据的变化逻辑:
处理逻辑很简单,只要更新 _activeStorkWidthIndex 激活索引即可;另外在 _onPanStart 创建 Line 对象时,设置激活线宽即可:
void _onSelectStorkWidth(int index) {
if (index != _activeStorkWidthIndex) {
setState(() {
_activeStorkWidthIndex = index;
});
}
}
void _onPanStart(DragStartDetails details) {
_lines.add(Line(
points: [details.localPosition],
// 使用激活线宽
strokeWidth: supportStorkWidths[_activeStorkWidthIndex],
));
}
到这里,就完成了线条宽度的选择功能,当前代码位置 paper 。
2. 颜色选择器
颜色选择器的原理也是一样,选择激活,在创建 Line 对象时设置颜色。只不过是条目的界面表现不同罢了,条目的构建逻辑也是通过 _buildByIndex 实现的。这里通过圆圈也表示颜色,点击时激活条目,激活状态由外圈的圆形边线进行表示:
标题
class ColorSelector extends StatelessWidget {
final List<Color> supportColors;
final ValueChanged<int> onSelect;
final int activeIndex;
const ColorSelector({
Key? key,
required this.supportColors,
required this.activeIndex,
required this.onSelect,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8),
child: Wrap(
// crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(
supportColors.length,
_buildByIndex,
)),
);
}
Widget _buildByIndex(int index) {
bool select = index == activeIndex;
return GestureDetector(
onTap: () => onSelect(index),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.all(2),
width: 24,
height: 24,
alignment: Alignment.center,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: select ? Border.all(color: Colors.blue) : null
),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: supportColors[index],
),
),
),
);
}
}
同理,在 _PaperState 中,颜色选择器也通过 Stack + Positioned 叠放在画板的左下角;点击回调时处理激活颜色索引数据的更新;以及创建 Line 对象时设置激活颜色:
void _onSelectColor(int index) {
if (index != _activeColorIndex) {
setState(() {
_activeColorIndex = index;
});
}
}
void _onPanStart(DragStartDetails details) {
_lines.add(Line(
points: [details.localPosition],
strokeWidth: supportStorkWidths[_activeStorkWidthIndex],
color: supportColors[_activeColorIndex],
));
}
到这里,颜色选择和线宽选择功能就已经实现了,当前代码位置 paper 。但这里在布局上还有些问题,下面来分析处理一下。
3. 布局分析
这里通过 Positioned 将两块分别叠放在 Stack 的两侧,上面颜色少时没有什么问题。但如果颜色过多,可以发现这种方式的叠放会让后者把前者覆盖住
多颜色时
布局边界
在布局树中可以发现,默认情况下 Positioned 之下的约束为无限约束,也就是子组件想要多大都可以。所以子组件在竖直方向上没有约束,颜色太多时就会溢出的原因。
解决方案方案有很多,其中最简单的是指定 Positioned 组件的 width 参数,在水平方向施加紧约束。如下所示,可以限制 ColorSelector 的宽度等于 240。 ColorSelector 内部通过 Wrap 进行构建,在区域之内会自动换行:
Positioned(
bottom: 40,
width: 240,
child: ColorSelector(
supportColors: supportColors,
activeIndex: _activeColorIndex,
onSelect: _onSelectColor,
),
),
通过布局查看器可以看出,此时 ColorSelector 受到的约束宽度就固定在 240。
另外,我们还可以将 Positioned 提供的约束尺寸设为屏幕宽度,通过 Row 来水平排列,其中 ColorSelector 的宽度通过 Expanded 延展成剩余宽度。当前代码位置 paper
Positioned(
bottom: 0,
width: MediaQuery.of(context).size.width,
child: Row(
children: [
Expanded(
child: ColorSelector(
supportColors: supportColors,
activeIndex: _activeColorIndex,
onSelect: _onSelectColor,
),
),
StorkWidthSelector(
supportStorkWidths: supportStorkWidths,
color: supportColors[_activeColorIndex],
activeIndex: _activeStorkWidthIndex,
onSelect: _onSelectStorkWidth,
),
],
),
),
如果这里不提供 width ,而使用 Row + Expanded 组件的话,就会报错;根本原因是 Positioned 施加了无限约束,而 Row 使用了 Expanded 组件,延展无限的宽度区域是不被允许的:
通过这个小问题,带大家简单认识一下布局中约束的分析。很多布局上的问题,都可以从约束的角度解决。这里点到为止,如果对约束感兴趣,或有很多布局的困扰,可以研读一下我的布局小册: Flutter 布局探索 - 薪火相传
4. 本章小结
本章主要任务是完成白板画笔的参数设置,为用户提供修改颜色和线宽的操作,以便于绘制更复杂多彩的图案。从中可以体会出:新增加一个需求,往往会引入相关的数据来实现功能。比如对于修改颜色的需求,需要引入支持的颜色列表和激活的颜色索引两个数据。所以对于任何功能需求而言,不要只看其表面的界面呈现,更重要的是分析其背后的用户交互过程中的数据变化情况。
下一章,将继续对当前的画板项目进行一些小优化,比如支持回退和撤销回退的功能;以及优化一下点集的收集策略,来尽可能地避免收录过多无用点,减小绘制的压力。