Flutter实现一个评分组件的几种思路

最近在玩AI绘图,先来只大猫镇场子。

RatingBar 作为一个常见不常用的控件,你能想到几种计划去完成。

Flutter实现一个评分组件的几种思路

各个平台都有自己的rating组件,咱们能够先看看flutter自带的什么样。

但是并没有。

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
        );
      }
    }),
  );
}
~ 省掉代码 ~

Flutter实现一个评分组件的几种思路

如果想给一个它加上交互,能够运用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 ,
      );
    }
  }),
)

Flutter实现一个评分组件的几种思路


裁剪方法进步精度

上边的计划依据图片的原因,如果要提供分数精度到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,它们一般都会有一个childchildren属性用于接纳子 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分), 就会呈现出这样的作用

Flutter实现一个评分组件的几种思路

如果是0.1分的星,便是这样了。

Flutter实现一个评分组件的几种思路

这样,咱们就能够完成准确到恣意分数的星星了。

Flutter实现一个评分组件的几种思路

完好代码我放到文章结尾。


自定义烘托

先回忆下根底:

Widget 按功用区分有三大类:

1 Component Widget ,组合类 Widget,这类 Widget 都直接或间接承继于StatelessWidgetStatefulWidget,经过组合功用相对单一的 Widget 能够得到功用更为杂乱的 Widget。往常的业务开发主要是在开发这一类型的 Widget

2 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.
  );
}

看看作用

Flutter实现一个评分组件的几种思路

单个孩子的容器布局其实便是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('下边的'),
      ],
    ), 
  );
}

Flutter实现一个评分组件的几种思路

评分组件如果期望彻底自绘和布局也是这个思路,不过完成一个这种东西的话代码过于杂乱,还是直接参阅一些 现成的 吧 ,作用上和上边两种方法是一样的。

三种方法的代码和一样进程样例都放到了git上

推荐:雪峰的flutter博客