最近做了一个小需求,卡片样式是这样的:
按照我常规的做法,一般都是切个图,然后就实现了(可能会变形),是不是感觉很low,我就从来没想过用代码实现这个。
前两天看了这篇文章: juejin.im/post/684490… 把我给惊讶到了,咦,原来优惠券界面可以手写呀,这次碰到这个需求,正好试着搞一把。原文中用的是圆形,我这是锯齿形,这个应该怎么做呢?下面我细细讲解下,相信能学习到一些知识,重点不在代码,而在实现过程:
- 实现两边的扣圆:
按照上面文章的说法,这个好实现,首先原始图形代码是这样的:
蓝色块:
Material(
shape: CouponShapeBorder(),
color: Colors.blue,
child: Container(
width: 100,
height: 100,
),
)
CouponShapeBorder:
import 'dart:math';
import 'package:flutter/material.dart';
class CouponShapeBorder extends ShapeBorder {
final double circleSize;// 圆形直径
final double topMargin;// 圆形距上距离
CouponShapeBorder({
this.circleSize = 12,
this.topMargin = 65,
});
@override
EdgeInsetsGeometry get dimensions => null;
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return null;
}
// 获取外轮廓Path
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
var path = Path();
path.addRect(rect);
_formHoldLeft(path, rect);
_formHoldRight(path, rect);
path.fillType = PathFillType.evenOdd;
return path;
}
_formHoldLeft(Path path, Rect rect) {
path.addArc(
Rect.fromCenter(
center: Offset(0, topMargin),
width: circleSize,
height: circleSize,
),
pi * 3 / 2,
pi);
}
_formHoldRight(Path path, Rect rect) {
path.addArc(
Rect.fromCenter(
center: Offset(rect.width, topMargin),
width: circleSize,
height: circleSize,
),
pi / 2,
pi);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
}
@override
ShapeBorder scale(double t) {
throw UnimplementedError();
}
}
整个代码中,核心的是:getOuterPath()这个方法,从名字上我们可以看出是用来处理外轮廓的,并返回path,再就是用到了path.fillType = PathFillType.evenOdd;,这个作用是,当绘画的时候,绘制奇数遍的时候,是可见的,偶数的时候是不可见的,及异或画笔,这个在很多语言中都有。
说到这里,做过绘制的同学,就知道了,我们只要返回一个圆去擦除就好,但如果只是写一个圆,会出现这种情况:
哇哦,长出了两个耳朵!
所以我们应该不能画正圆,而应该画半圆: 使用Path的这个方法:
addArc(Rect oval, double startAngle, double sweepAngle)
第一个参数:圆的大小位置 第二个参数:起始角度 第三个参数:圆弧扫过的度数
这里重点说一下第二、第三两个参数是怎么结合的?看下图示:
根据图来画弧形就好,找好起始点和要扫过的度数。
例如demo里面,左面半弧形,就应该是 起始:pi*3/2, 扫半圆也就是:pi,因此我们的代码是:
path.addArc(
Rect.fromCenter(
center: Offset(0, topMargin),
width: circleSize,
height: circleSize,
),
pi * 3 / 2,
pi);
同理,右半弧就是起始:pi/2,扫:pi,假如有上半弧,就是起始0,扫:pi,有兴趣的可以上下左右都试一下~~
- 画分割线:
划分割线,是在paint方法中进行绘制:
这个比较简单,我们使用
canvas.drawLine,根据宽度、短线长度、短线间隙进行循环画即可:
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
var paint = Paint()
..color = dashColor
..strokeWidth = 1
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round;
_drawDashLine(canvas, Offset(circleSize / 2 + dashLinePadding, topMargin),
Offset(rect.width - circleSize / 2 - dashLinePadding, topMargin), paint);
}
_drawDashLine(Canvas canvas, Offset start, Offset end, Paint paint) {
double currentLength = start.dx;
// 循环去画即可
while (currentLength <= end.dx) {
Offset lineStart = Offset(currentLength, start.dy);
Offset lineEnd = Offset(currentLength + dashWidth, start.dy);
canvas.drawLine(lineStart, lineEnd, paint);
currentLength += dashGap;
}
}
- 接下来我们重点讲一下下面的锯齿怎么画呢?
我们先画个简单的(holeCount=4)个锯齿的优惠券,如图:
按照上面扣圆的方式,我们这里扣三角形,我画了个示意图:
每个红框就是我们要扣掉的三角形。要扣三角形,我们需要一些参数即三角形的三个顶点坐标。
从这个方法中:Path getOuterPath(Rect rect, {TextDirection textDirection}) {
我们可以拿到这个优惠券矩形的长宽、位置。
再来几个前提条件: 假如设计给我们的设计稿,上面有(holeCount=4)个锯齿,每个锯齿的深度是5,即三角形的高为5。 从图片我们可以看出,(holeCount=4)个锯齿,我们需要用(holeCount+1)个三角形去擦除(多出一个为两边半个三角形之和)。
var w = rect.width;//优惠券整个宽度
//计算每个三角形的宽度,此宽度跟要擦除的三角形一样大
var polygonWidth = w / (holeCount);
根据图示,我们开始画三角形,先看下代码:
void _formHoleBottom(Path path, Rect rect, double polygonWidth) {
final w = rect.width; // 优惠券的宽度
final h = rect.height; // 优惠券的高度
for (int i = 0; i <= holeCount; i++) {
Offset top = Offset(i * polygonWidth, h - 5);
Offset bottomLeft = Offset(max(i * polygonWidth - polygonWidth / 2, 0), h);
Offset bottomRight = Offset(min(i * polygonWidth + polygonWidth / 2, w), h);
path.addPolygon([top, bottomLeft, bottomRight], true);
}
我们讲解一下: 我们总共需要画holeCount+1个三角形,那我们就直接从0到holeCount循环即可。
a. 三角形的顶点:
x坐标为每个三角形的中心,分别是:0,polygonWidth, polygonWidth * 2 ... polygonWidth * n,
y坐标:为优惠券高度-预设的5 即:(h-5)
b. 三角形的左脚坐标:
x坐标:0,polygonWidth - polygonWidth/2, 2 * polygonWidth - polygonWidth/2 ... n * polygonWidth - polygonWidth/2,
总结后是:
max(i * polygonWidth - polygonWidth / 2, 0)。为什么用max,原因是第一个三角形,如果补全,其实是-polygonWidth/2,但我们不能越界,因此只需要跟0取最大值即可。
y坐标都是优惠券高度h,就不说什么啦
c. 三角形右脚坐标:
x坐标:将左脚坐标每个值加上polygonWidth, 规律即是:
i * polygonWidth + polygonWidth / 2。同样的考虑到越界问题,我们对优惠券总宽度取最小值,即
min(i * polygonWidth + polygonWidth / 2, w)y坐标也是优惠券高度h
有了三角形的三个点坐标,我们就可以调用画多边形的方法啦:
path.addPolygon([top, bottomLeft, bottomRight], true);//闭合曲线
至此我们的优惠券就完成啦,思路是重要的,以后遇到其他形状也可以类推解决了。最后附一下整体代码,大家可以改改参数试试:
import 'dart:math';
import 'package:flutter/material.dart';
class CouponShapeBorder extends ShapeBorder {
final double circleSize;
final int holeCount;
final double topMargin;
final Color dashColor;
final double dashWidth = 3;
final double dashGap = 7;
final double dashLinePadding = 6;
CouponShapeBorder({
this.holeCount = 4,
this.circleSize = 12,
this.topMargin = 65,
this.dashColor = const Color(0xffaaaaaa),
});
@override
EdgeInsetsGeometry get dimensions => null;
@override
Path getInnerPath(Rect rect, {TextDirection textDirection}) {
return null;
}
@override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
var w = rect.width;
var polygonWidth = w / (holeCount);
var path = Path();
path.addRect(rect);
_formHoldLeft(path, rect);
_formHoldRight(path, rect);
_formHoleBottom(path, rect, polygonWidth);
path.fillType = PathFillType.evenOdd;
return path;
}
void _formHoleBottom(Path path, Rect rect, double polygonWidth) {
final w = rect.width;
final h = rect.height;
for (int i = 0; i <= holeCount; i++) {
Offset top = Offset(i * polygonWidth, h - 5);
Offset bottomLeft = Offset(max(i * polygonWidth - polygonWidth / 2, 0), h);
Offset bottomRight = Offset(min(i * polygonWidth + polygonWidth / 2, w), h);
path.addPolygon([top, bottomLeft, bottomRight], true);
}
}
_formHoldLeft(Path path, Rect rect) {
path.addArc(
Rect.fromCenter(
center: Offset(0, topMargin),
width: circleSize,
height: circleSize,
),
pi * 3 / 2,
pi);
}
_formHoldRight(Path path, Rect rect) {
path.addArc(
Rect.fromCenter(
center: Offset(rect.width, topMargin),
width: circleSize,
height: circleSize,
),
pi / 2,
pi);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {
var paint = Paint()
..color = dashColor
..strokeWidth = 1
..style = PaintingStyle.stroke
..strokeJoin = StrokeJoin.round;
_drawDashLine(canvas, Offset(circleSize / 2 + dashLinePadding, topMargin),
Offset(rect.width - circleSize / 2 - dashLinePadding, topMargin), paint);
}
_drawDashLine(Canvas canvas, Offset start, Offset end, Paint paint) {
double currentLength = start.dx;
while (currentLength <= end.dx) {
Offset lineStart = Offset(currentLength, start.dy);
Offset lineEnd = Offset(currentLength + dashWidth, start.dy);
canvas.drawLine(lineStart, lineEnd, paint);
currentLength += dashGap;
}
}
@override
ShapeBorder scale(double t) {
throw UnimplementedError();
}
}