flutter使用剪裁製做評分控件

本文介紹flutter的剪裁(Clip,也能夠叫遮罩)的常見使用場景,以及使用剪裁(Clip)功能製做一款評分控件(Rating Bar)。git

前言

今天發現flutter竟然連個評分控件都沒有,項目中要使用,因而研究了一番,順便把flutter中的Clip涉及到的幾個控件也看了下。
github

Flutter中的剪裁

圓形剪裁(ClipOval)

這個能夠用來剪裁圓形頭像canvas

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參數用於控制圓角的位置大小。less

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參數才能使用,否則沒有效果。ide

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)

這個就比較有意思了,能夠剪裁任意形狀,好比五角星、三角形ui

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,而後:this

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)對上一層的五角星進行寬度剪裁。spa

五角星能夠使用做圖工具作出來,也能夠採用自繪圖形。 .net

大概思路:

靜態展現控件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討論

相關文章
相關標籤/搜索