本文介绍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讨论
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。