9

本文介绍flutter的剪裁(Clip,也可以叫遮罩)的常见使用场景,以及使用剪裁(Clip)功能制作一款评分控件(Rating Bar)。

前言

今天发现flutter居然连个评分控件都没有,项目中要使用,于是研究了一番,顺便把flutter中的Clip涉及到的几个控件也看了下。

Flutter中的剪裁

圆形剪裁(ClipOval)

这个可以用来剪裁圆形头像

 new ClipOval(
    child: new SizedBox(
      width: 100.0,
      height:100.0,
      child:  new Image.network("https://sfault-avatar.b0.upaiyun.com/206/120/2061206110-5afe2c9d40fa3_huge256",fit: BoxFit.fill,),
    ),
  ),

圆角矩形剪裁(ClipRRect)

这个控件的borderRadius参数用于控制圆角的位置大小。

 new ClipRRect(
    borderRadius: new BorderRadius.all(
        new Radius.circular(10.0)),

    child:  new SizedBox(
      width: 100.0,
      height:100.0,
      child:  new Image.network("https://sfault-avatar.b0.upaiyun.com/206/120/2061206110-5afe2c9d40fa3_huge256",fit: BoxFit.fill,),
    ),

  )

矩形剪裁(ClipRect)

这个控件需要定义clipper参数才能使用,不然没有效果。

class _MyClipper extends CustomClipper<Rect>{
  @override
  Rect getClip(Size size) {
    return new Rect.fromLTRB(10.0, 10.0, size.width - 10.0,  size.height- 10.0);
  }

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) {
      return true;
  }

}

这里定义剪裁掉周边10像素的大小

 new ClipRect(

            clipper: new _MyClipper(),

            child:new SizedBox(
              width: 100.0,
              height:100.0,
              child:  new Image.network("https://sfault-avatar.b0.upaiyun.com/206/120/2061206110-5afe2c9d40fa3_huge256",fit: BoxFit.fill,),
            ) ,
          ),

路径剪裁(ClipPath)

这个就比较有意思了,可以剪裁任意形状,比如五角星、三角形



class _StarCliper extends CustomClipper<Path>{

  final double radius;

  _StarCliper({this.radius});

  /// 角度转弧度公式
  double degree2Radian(int degree) {
    return (Math.pi * degree / 180);
  }

  @override
  Path getClip(Size size) {
    double radius = this.radius;
    Path path = new Path();
    double radian = degree2Radian(36);// 36为五角星的角度
    double radius_in = (radius * Math.sin(radian / 2) / Math
        .cos(radian)); // 中间五边形的半径

    path.moveTo((radius * Math.cos(radian / 2)), 0.0);// 此点为多边形的起点
    path.lineTo((radius * Math.cos(radian / 2) + radius_in
        * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) * 2),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) + radius_in
        * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo(
        (radius * Math.cos(radian / 2) + radius
            * Math.sin(radian)), (radius + radius
        * Math.cos(radian)));
    path.lineTo((radius * Math.cos(radian / 2)),
        (radius + radius_in));
    path.lineTo(
        (radius * Math.cos(radian / 2) - radius
            * Math.sin(radian)), (radius + radius
        * Math.cos(radian)));
    path.lineTo((radius * Math.cos(radian / 2) - radius_in
        * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo(0.0, (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) - radius_in
        * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));

    path.close();// 使这些点构成封闭的多边形

    return path;
  }

  @override
  bool shouldReclip(_StarCliper oldClipper) {
      return this.radius != oldClipper.radius;
  }

}

先定义好五角星的路径ClipRect,然后:

 new ClipPath(
    clipper: new _StarCliper(radius: 50.0),

    child:new SizedBox(
      width: 100.0,
      height:100.0,
      child:  new Image.network("https://sfault-avatar.b0.upaiyun.com/206/120/2061206110-5afe2c9d40fa3_huge256",fit: BoxFit.fill,),
    ) ,

  )

评分控件(Rating Bar)的制作

我们已经了解了Flutter中的剪裁,那么制作一个评分控件已经很简单了。
先准备两个版本的五角星,一个用于高亮展示分数,一般是实心的,另一个用于底图,一般是空心的。使用矩形剪裁(ClipRect)对上一层的五角星进行宽度剪裁。

五角星可以使用作图工具做出来,也可以采用自绘图形。

大概思路:

静态展示控件StaticRatingBar


import 'package:flutter/widgets.dart';
import 'dart:math' as Math;

const double kMaxRate = 5.0;
const int kNumberOfStarts = 5;
const double kSpacing = 3.0;
const double kSize = 50.0;

class StaticRatingBar extends StatelessWidget {
  /// number of stars
  final int count;

  /// init rate
  final double rate;

  /// size of the starts
  final double size;

  final Color colorLight;

  final Color colorDark;

  StaticRatingBar({
    double rate,
    Color colorLight,
    Color colorDark,
    int count,
    this.size: kSize,
  })  : rate = rate ?? kMaxRate,
        count = count ?? kNumberOfStarts,
        colorDark = colorDark ?? new Color(0xffeeeeee),
        colorLight = colorLight ?? new Color(0xffFF962E);

  Widget buildStar() {
    return new SizedBox(
        width: size * count,
        height: size,
        child: new CustomPaint(
          painter: new _PainterStars(
              size: this.size / 2,
              color: colorLight,
              style: PaintingStyle.fill,
              strokeWidth: 0.0),
        ));
  }

  Widget buildHollowStar() {
    return new SizedBox(
        width: size * count,
        height: size,
        child: new CustomPaint(
          painter: new _PainterStars(
              size: this.size / 2,
              color: colorDark,
              style: PaintingStyle.fill,
              strokeWidth: 0.0),
        ));
  }

  @override
  Widget build(BuildContext context) {
    return new Stack(
      children: <Widget>[
        buildHollowStar(),
        new ClipRect(
          clipper: new _RatingBarClipper(width: rate * size),
          child: buildStar(),
        )
      ],
    );
  }
}

class _RatingBarClipper extends CustomClipper<Rect> {
  final double width;

  _RatingBarClipper({this.width}) : assert(width != null);

  @override
  Rect getClip(Size size) {
    return new Rect.fromLTRB(0.0, 0.0, width, size.height);
  }

  @override
  bool shouldReclip(_RatingBarClipper oldClipper) {
    return width != oldClipper.width;
  }
}



class _PainterStars extends CustomPainter {
  final double size;
  final Color color;
  final PaintingStyle style;
  final double strokeWidth;

  _PainterStars({this.size, this.color, this.strokeWidth, this.style});

  /// 角度转弧度公式
  double degree2Radian(int degree) {
    return (Math.pi * degree / 180);
  }

  Path createStarPath(double radius, Path path) {
    double radian = degree2Radian(36); // 36为五角星的角度
    double radius_in = (radius * Math.sin(radian / 2) / Math.cos(radian)) *
        1.1; // 中间五边形的半径,太正不是很好看,扩大一点点

    path.moveTo((radius * Math.cos(radian / 2)), 0.0); // 此点为多边形的起点
    path.lineTo((radius * Math.cos(radian / 2) + radius_in * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) * 2),
        (radius - radius * Math.sin(radian / 2)));
    path.lineTo(
        (radius * Math.cos(radian / 2) + radius_in * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) + radius * Math.sin(radian)),
        (radius + radius * Math.cos(radian)));
    path.lineTo((radius * Math.cos(radian / 2)), (radius + radius_in));
    path.lineTo((radius * Math.cos(radian / 2) - radius * Math.sin(radian)),
        (radius + radius * Math.cos(radian)));
    path.lineTo(
        (radius * Math.cos(radian / 2) - radius_in * Math.cos(radian / 2)),
        (radius + radius_in * Math.sin(radian / 2)));
    path.lineTo(0.0, (radius - radius * Math.sin(radian / 2)));
    path.lineTo((radius * Math.cos(radian / 2) - radius_in * Math.sin(radian)),
        (radius - radius * Math.sin(radian / 2)));

    path.lineTo((radius * Math.cos(radian / 2)), 0.0);
    return path;
  }

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint();
    //   paint.color = Colors.redAccent;
    paint.strokeWidth = strokeWidth;
    paint.color = color;
    paint.style = style;

    Path path = new Path();

    double offset = strokeWidth > 0 ? strokeWidth + 2 : 0.0;

    path = createStarPath(this.size - offset, path);
    path = path.shift(new Offset(this.size * 2, 0.0));
    path = createStarPath(this.size - offset, path);
    path = path.shift(new Offset(this.size * 2, 0.0));
    path = createStarPath(this.size - offset, path);
    path = path.shift(new Offset(this.size * 2, 0.0));
    path = createStarPath(this.size - offset, path);
    path = path.shift(new Offset(this.size * 2, 0.0));
    path = createStarPath(this.size - offset, path);

    if (offset > 0) {
      path = path.shift(new Offset(offset, offset));
    }
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(_PainterStars oldDelegate) {
    return oldDelegate.size != this.size;
  }
}

使用静态显示评分:

new StaticRatingBar(
    size: 20.0,
    rate: 4.5,
  )

效果:

动态评分控件:


class RatingBar extends StatefulWidget {
  /// 回调
  final ValueChanged<int> onChange;

  /// 大小, 默认 50
  final double size;

  /// 值 1-5
  final int value;

  /// 数量 5 个默认
  final int count;

  /// 高亮
  final Color colorLight;

  /// 底色
  final Color colorDark;

  /// 如果有值,那么就是空心的
  final double strokeWidth;

  /// 越大,五角星越圆
  final double radiusRatio;

  RatingBar(
      {this.onChange,
      this.value,
      this.size: kSize,
      this.count: kNumberOfStarts,
      this.strokeWidth,
      this.radiusRatio: 1.1,
      Color colorDark,
      Color colorLight})
      : colorDark = colorDark ?? new Color(0xffDADBDF),
        colorLight = colorLight ?? new Color(0xffFF962E);

  @override
  State<StatefulWidget> createState() {
    return new _RatingBarState();
  }
}

class _PainterStar extends CustomPainter {
  final double size;
  final Color color;
  final PaintingStyle style;
  final double strokeWidth;
  final double radiusRatio;

  _PainterStar(
      {this.size, this.color, this.strokeWidth, this.style, this.radiusRatio});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint();
    paint.strokeWidth = strokeWidth;
    paint.color = color;
    paint.style = style;
    Path path = new Path();
    double offset = strokeWidth > 0 ? strokeWidth + 2 : 0.0;

    path = createStarPath(this.size - offset, radiusRatio, path);

    if (offset > 0) {
      path = path.shift(new Offset(offset, offset));
    }
    path.close();

    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(_PainterStar oldDelegate) {
    return oldDelegate.size != this.size ||
        oldDelegate.color != this.color ||
        oldDelegate.strokeWidth != this.strokeWidth;
  }
}

class _RatingBarState extends State<RatingBar> {
  int _value;

  @override
  void initState() {
    _value = widget.value;
    super.initState();
  }

  Widget buildItem(int index, double size, count) {
    bool selected = _value != null && _value > index;

    bool stroke = widget.strokeWidth != null && widget.strokeWidth > 0;

    return new GestureDetector(
      onTap: () {
        if (widget.onChange != null) {
          widget.onChange(index + 1);
        }

        setState(() {
          _value = index + 1;
        });
      },
      behavior: HitTestBehavior.opaque,
      child: new SizedBox(
          width: size,
          height: size,
          child: new CustomPaint(
            painter: new _PainterStar(
                radiusRatio: widget.radiusRatio,
                size: size / 2,
                color: selected ? widget.colorLight : widget.colorDark,
                style: !selected && stroke
                    ? PaintingStyle.stroke
                    : PaintingStyle.fill,
                strokeWidth: !selected && stroke ? widget.strokeWidth : 0.0),
          )),
    );
  }

  @override
  Widget build(BuildContext context) {
    double size = widget.size;
    int count = widget.count;

    List<Widget> list = [];
    for (int i = 0; i < count; ++i) {
      list.add(buildItem(i, size, count));
    }

    return new Row(
      children: list,
    );
  }
}

效果

完整代码这里:

https://github.com/jzoom/flut...

如有疑问,请加qq群854192563讨论


jzoom
1.2k 声望334 粉丝

A simple way to solving problems is using tools like docker/Spring boot/React Native/React/Vue… Technology should not become a bottleneck in thinking.