最近在玩AI绘图,先来只大猫镇场子。
RatingBar 作为一个常见不常用的控件,你能想到几种计划去完成。
各个平台都有自己的rating组件,咱们能够先看看flutter自带的什么样。
但是并没有。
这个图是用stable-diffusion-webui的text2img生成的,想画一只为难的朋克猫,大概了解有问题,它貌似也不为难。
先用最简的方法,自己组装一个
组合控件
用List生成一个固定的row列, 前四个是实心,终究一个是空心,构成了一个评分组件,当然这种目前只能纯显现。
~ 省掉代码 ~
@override
Widget build(BuildContext context) {
return Row(
children: List.generate(5, (index) {
if(index < 4){
return Icon(
Icons.star,
color: _color,
size :34.0 ,
);
}else{
return Icon(
Icons.star_border,
color: _color,
size :34.0
);
}
}),
);
}
~ 省掉代码 ~
如果想给一个它加上交互,能够运用Listener 或 GestureDetector
GestureDetector 有丰厚的接触和点击手势,能满意大部分功用; Listener 是监听原始指针 ,其实GestureDetector的底层也是包装的Listener ,他们的区别便是Listener 在反馈原始指针的时分不会触发竞赛机制。 而 GestureDetector 处理了竞赛, 例如父控件和子控件都监听了 GestureDetector 的 onTap办法 , 点击子控件不会触发父控件的onTop, 反之点击父控件也不会触发子控件的onTap, 内部经过冒泡竞赛来完成.
具体能够参阅
咱们就用GestureDetector来试试, 监听水平滑动接触更新这个办法:
GestureDetector(
onHorizontalDragUpdate: (DragUpdateDetails details) {
double newPosition = details.localPosition.dx;
// 依据变化值修正数据
print('$newPosition');
}),
如何运用这个相对坐标的偏移量newPosition
假定咱们的星星宽度固定,互相没有间距,newPosition / 宽度 能够了解为有多少星星能够被填充溢, 因为咱们的图标只要了体系内置的整个星和半个星图标, 能够四舍五入到0.5精度
void _updateRating(double newPosition) {
setState(() {
_rating = newPosition / 34.0;
if (_rating > 5.0) {
_rating = 5.0;
} else if (_rating < 0.0) {
_rating = 0.0;
}
// 将评分四舍五入到最近的 0.5 分
_rating = (_rating * 2).roundToDouble() / 2;
});
}
能够得到_rating 的评分值, 然后在控件中显现需求填充多少颗星
Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(5, (index) {
var currentIndex = index + 1;
if (currentIndex - 0.5 <= _rating && _rating < currentIndex) {
// 填充一半
return Icon(
Icons.star_half,
color: _color,
size :34.0 ,
);
} else if (currentIndex <= _rating) {
// 彻底填充
return Icon(
Icons.star,
color: _color,
size :34.0 ,
);
} else {
// 彻底不填充
return Icon(
Icons.star_border,
color: _color,
size :34.0 ,
);
}
}),
)
裁剪方法进步精度
上边的计划依据图片的原因,如果要提供分数精度到0.1,那岂不是得找一堆图片,0.1分的图片需求十分之一的星星被填充,太麻烦了,换个思路。
取五个都是空心星星的图片,固定放到底下, 上层放五个都是实心星星的图片,依据得到的分数,来实时裁剪掉上层图片的多余部分。 如果是0.3分,你需求裁剪掉第一个实心星星的70%部分, 剩下的四张图都裁掉。
这儿用到了一个Widget : ClipRect,它直接承继于SingleChildRenderObjectWidget
const ClipRect({
super.key,
this.clipper,
this.clipBehavior = Clip.hardEdge,
super.child,
}) : assert(clipBehavior != null);
这是一个布局类组件 ,布局类组件便是指直接或间接承继(包括)SingleChildRenderObjectWidget 和 MultiChildRenderObjectWidget的Widget,它们一般都会有一个
child
或children
属性用于接纳子 Widget。 咱们看一下承继关系 Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild)RenderObjectWidget 。 RenderObjectWidget 类中定义了创建、更新 RenderObject 的办法,子类有必要完成他们,关于RenderObject 咱们现在只需求知道它是终究布局、烘托 UI 界面的对象即可,也便是说,对于布局类组件来说,其布局算法都是经过对应的 RenderObject 对象来完成的。
咱们给定一个彻底填充的星星,用ClipRect组件包裹下 ,设置下填充矩阵 ,结构办法里有个clipper参数,便是一个CustomClipper, 能够设置裁剪矩形
class MyClipper extends CustomClipper<Rect> {
final double value;
MyClipper({required this.value});
@override
Rect getClip(Size size) => Rect.fromLTWH(0, 0, value, size.height);
@override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
ClipRect(
clipper: MyClipper(value: 32),
child: Icon(
Icons.star,
color: _color,
size: 64.0,
),
)
很简单看出,咱们只给宽度裁剪了一半(0.5分), 就会呈现出这样的作用
如果是0.1分的星,便是这样了。
这样,咱们就能够完成准确到恣意分数的星星了。
完好代码我放到文章结尾。
自定义烘托
先回忆下根底:
Widget 按功用区分有三大类:
1 Component Widget ,组合类 Widget,这类 Widget 都直接或间接承继于
StatelessWidget
或StatefulWidget
,经过组合功用相对单一的 Widget 能够得到功用更为杂乱的 Widget。往常的业务开发主要是在开发这一类型的 Widget2 Proxy Widget, 署理类 Widget ,自身并不触及 Widget 内部逻辑,仅仅为「Child Widget」提供一些附加的中心功用。典型的如:
InheritedWidget
用于在「Descendant Widgets」间传递共享信息、ParentDataWidget
用于装备「Descendant Renderer Widget」的布局信息3 Renderer Widget,烘托类 Widget,会直接参与后面的「Layout」、「Paint」流程,无论是「Component Widget」还是「Proxy Widget」终究都会映射到「Renderer Widget」上,否则将无法被绘制到屏幕上。这 3 类 Widget 中,只要「Renderer Widget」有与之一一对应的「Render Object」。
Render-Widget 大致有三类:
- 作为『 Widget Tree 』的叶节点,也是最小的 UI 表达单元,一般承继自
LeafRenderObjectWidget
;- 有一个子节点 ( Single Child ),一般承继自
SingleChildRenderObjectWidget
;- 有多个子节点 ( Multi Child ),一般承继自
MultiChildRenderObjectWidget
。
自定义一个布局便是承继 RenderObjectWidget的进程, 绝大部分常用布局容器都直接或间接承继自RenderObject , 只要一个孩子布局(如Center,Padding)的承继 SingleChildRenderObjectWidget 杂乱些的布局(Row ,Column等)承继MultiChildRenderObjectWidget
SingleChildRenderObjectWidget
咱们先自己模仿一个Center,看看作用
class CustomCenter extends SingleChildRenderObjectWidget {
CustomCenter({Key? key, Widget? child})
: super(key: key, child: child);
@override
RenderObject createRenderObject(BuildContext context) {
return RenderCustomCenter();
}
}
class RenderCustomCenter extends RenderShiftedBox {
RenderCustomCenter({RenderBox? child}) : super(child);
@override
void performLayout() {
//1 先对子组件进行layout,随后获取它的size
child?.layout(constraints.loosen(), //将束缚传递给子节点
parentUsesSize: true //因为接下来要运用 child 的 size,所以不能为 false
);
//2 依据子组件的巨细确定自身的巨细
size = constraints.constrain(Size(
constraints.maxWidth == double.infinity
? (child?.size.width ?? 0)
: double.infinity,
constraints.maxHeight == double.infinity
? (child?.size.height ?? 0)
: double.infinity));
//3 依据父节点巨细,算出子节点在父节点中居中后的偏移
//4 然后将这个偏移保存在子节点的 parentData 中,在后续的绘制节点会用到
BoxParentData parentData = child?.parentData as BoxParentData;
parentData.offset = ((size - (child?.size ?? const Size(0, 0))) as Offset) / 2;
}
}
调用一下
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('RatingBar'),
),
body: CustomCenter(
child: Container(color: Colors.red, width: 120,height: 120,),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
看看作用
单个孩子的容器布局其实便是performLayout的进程,也便是经过孩子巨细位置来确定自己身巨细的进程
MultiChildRenderObjectWidget
如果你的布局里期望有多个子Widget,能够承继下 MultiChildRenderObjectWidget 并完成createRenderObject()办法和updateRenderObject()办法来创建和更新RenderObject
具体完成一个布局,让两个子孩子上下摆放,其实便是一个简易的Column
class TopBottomLayout extends MultiChildRenderObjectWidget {
TopBottomLayout({Key? key, required List<Widget> list})
: assert(list.length == 2, "只能传两个 child"),
super(key: key, children: list);
@override
RenderObject createRenderObject(BuildContext context) {
return TopBottomRender();
}
}
class TopBottomParentData extends ContainerBoxParentData<RenderBox> {}
class TopBottomRender extends RenderBox
with
ContainerRenderObjectMixin<RenderBox, TopBottomParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, TopBottomParentData> {
/// 初始化每一个 child 的 parentData
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TopBottomParentData)
child.parentData = TopBottomParentData();
}
@override
void performLayout() {
//获取当前束缚(从父组件传入的),
final BoxConstraints constraints = this.constraints;
//获取第一个组件,和他父组件传的束缚
RenderBox? topChild = firstChild;
TopBottomParentData childParentData =
topChild?.parentData as TopBottomParentData;
//获取下一个组件
//至于这儿为什么能够获取到下一个组件,是因为在 多子组件的 mount 中,遍历创建一切的 child 然后将其插入到到 child 的 childParentData 中了
RenderBox? bottomChild = childParentData.nextSibling;
//约束下孩子高度不超过总高度的一半
bottomChild?.layout(
constraints.copyWith(maxHeight: constraints.maxHeight / 2),
parentUsesSize: true);
//设置下孩子的 offset
childParentData = bottomChild?.parentData as TopBottomParentData;
//位于最下边
childParentData.offset = Offset(0, constraints.maxHeight - (bottomChild?.size.height ?? 0));
//上孩子的 offset 默认为 (0,0),为了保证上孩子能始终显现,咱们不修正他的 offset
topChild?.layout(
constraints.copyWith(
//上侧剩余的最大高度
maxHeight: constraints.maxHeight - (bottomChild?.size.height ?? 0)),
parentUsesSize: true);
//设置上下组件的 size
size = Size(
max((topChild?.size.width ?? 0), (bottomChild?.size.width ?? 0)),
constraints.maxHeight);
}
double max(double height, double height2) {
if (height > height2)
return height;
else
return height2;
}
@override
void paint(PaintingContext context, Offset offset) {
defaultPaint(context, offset);
}
@override
bool hitTestChildren(BoxHitTestResult result, {Offset? position}) {
return defaultHitTestChildren(result, position: position ?? Offset.zero);
}
}
调用及作用
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('RatingBar'),
),
body: TopBottomLayout(
list: [
Text('上边的'),
Text('下边的'),
],
),
);
}
评分组件如果期望彻底自绘和布局也是这个思路,不过完成一个这种东西的话代码过于杂乱,还是直接参阅一些 现成的 吧 ,作用上和上边两种方法是一样的。
三种方法的代码和一样进程样例都放到了git上
推荐:雪峰的flutter博客