先放上 安卓大佬flutter大佬镇楼

引言:我为什么要学绘制

​ 其实对于flutter来说,自绘的需求并不是很大,尤其是组件的自绘,一方面,flutter的组件就是依据MaterialDesign风格绘制的,本来就很好看,另一方面,一些复杂的组件也完全可以通过组件组装的方式进行构建,比自绘要省事。但这并不意味着flutter没有自绘组件的需求,一些高度自定义的组件(比如QQ编辑图片的时候能在图片上贴表情或者加文字时显示的文字框以及文字输入框的拖动缩放旋转等等等等)还是需要我们去自绘组件的。

​ 不过对于安卓原生嘛,嘿嘿,就开始离谱了。原生组件UI也不算。。特别丑,但是相较于flutter来说,高下立判。一方面,原生组件最好还是不要直接拿来用,还是建议进行封装。另一方面,原生的动画效果做的并不比flutter好,而且组件也比较单一,都留给了自绘很大的发展空间(给开发者增加了工作量)。

以下内容选自张风捷特烈的小册Flutter 绘制指南 - 妙笔生花,我觉得说的挺好:

为什么要学绘制

打开你的手机电脑平板,你可视的所有的一切在本质上都是依靠绘制实现的。每个平台都会有自身的绘制体系,平台自身的控件很多时候可能并不能满足设计的需求,也有很多控件是和项目特点高度契合的,所以平台会暴露出绘制的接口给开发者,让开发者对界面元素拥有 高度的可定制性

但凡可定制性的东西,都意味着一定的门槛,这可能会让很多人望而却步,所以绘制这个技能总是被开发者所冷漠,毕竟抱着又不是不能用心态的人不在少数,而且伟大的先驱者们也为我们留下来丰富的资源,这些轮子,装上就能跑,岂不美哉,干嘛费心费力地自己画?

但不要忘记: 用别人的东西,是被约束的一方,用起来束手束脚。而且别人的代码不一定能百分百符合你的需求,很多时候还是需要自己改改,如果你不会绘制的知识,那将非常痛苦。如果找不到能用的轮子,自己的绘制技能又这么蹩脚,那就只能去问,或花钱找别人实现。为什么要让自己混这么惨呢,何不食肉糜?

有轮子和自己会绘制并不冲突,就像有钱花拥有挣钱的能力一样,两者可以很好地相辅相成,一旦你懂了,就能更好的去用,甚至去修改轮子来满足自身的设计需求,或发现轮子中的缺点加以改正,使用者和创造者的身份并不冲突。

总而言之,没有剑,和有剑不用,是两码事

绘图基础介绍与对比

原生:Paint&Canvas,即画笔与画布

代码结构与调用:如何让IDE知道你是在进行绘制呢?绘制完如何使用呢?

其实方法很简单,让你的类继承view类并重写OnDraw()函数,OnDraw()函数中进行画笔画板配置并调用画板对象即可完成绘制。结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyView extends View {
Context m_context;
public MyView(Context context) {
super(context);
m_context=context;
}
//重写OnDraw()函数,在每次重绘时自主实现绘图
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//设置画笔
Paint paint=new Paint();

paint.set....
//画
canvas.draw....
}
}

调用:大佬在他的博客里使用的是通过类文件进行添加,个人不是很喜欢,但还是放上:

默认的XML改成FrameLayout布局,布局代码如下:

1
2
3
4
5
6
7
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"//注意这里在类文件中会用到
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.try_paint_blog.MainActivity" >
</FrameLayout>

类文件如下:

1
2
3
4
5
6
7
8
9
10
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
FrameLayout root=(FrameLayout)findViewById(R.id.root);//对应xml中的framelayout
root.addView(new MyView(MainActivity.this));//添加
}
}

​ 我为什么不喜欢这么用呢?个人认为,除非必要(指需要动态添加或者删除的组件),静态组件就应该在xml文件中进行声明,避免因视图层与逻辑层耦合过高而导致后期维护困难。举个例子,当你重写一个界面,却发现总有个xml中没定义过的组件出现在UI里,那八成是在逻辑层里动态添加的。但如果万一你动态添加的这个组件是透明的,那你就会发现自己的UI排布非常奇怪又找不出原因。

bb了半天,写一下我推荐的xml中的写法:

1
2
3
4
5
6
7
8
9
10
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.try_paint_blog.MainActivity" >

<com.zwn.view.MyView //用路径去进行定义
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

Paint的基本设置函数:

1
2
3
4
5
paint.setAntiAlias(true);//抗锯齿功能
paint.setColor(Color.RED); //设置画笔颜色
paint.setStyle(Style.FILL);//设置填充样式
paint.setStrokeWidth(30);//设置画笔宽度
paint.setShadowLayer(10, 15, 15, Color.GREEN);//设置阴影

另外,对于填充样式:

1
2
3
4
5
void setStyle (Paint.Style style)     //设置填充样式

Paint.Style.FILL //:填充内部
Paint.Style.FILL_AND_STROKE //:填充内部和描边
Paint.Style.STROKE //:仅描边

FILL与FILL_AND_STROKE在画笔比较细时没什么区别,描边一般搭配画笔宽度使用

对于阴影:

1
2
3
4
5
6
void setShadowLayer (float radius, float dx, float dy, int color)    //添加阴影

//参数:
//radius:阴影的倾斜度
//dx:水平位移
//dy:垂直位移

然后是Canvas的设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
canvas.drawColor(Color.BLUE);
canvas.drawRGB(255, 255, 0); //这两个功能一样,都是用来设置背景颜色的。
canvas.drawRect();
canvas.drawRoundRect();
canvas.drawCircle();
canvas.drawPath();
canvas.drawLine();
canvas.drawArc();
canvas.drawOval();
canvas.drawPoint();
canvas.drawPoints();
canvas.drawText();
canvas.drawTextOnPath();
canvas.drawBitmap();

canvas的各个绘制函数
canvas的变换

基本几何图形:

啊不想写了,这部分太基础了,一般用到的情况也不太多,直接看大佬写的吧。

概述及基本几何图形绘制
路径及文字

感觉最基本的还是个人抽象思维与逻辑思维。

flutter :Paint&Canvas&Path,即画笔、画布与路径,

同样地,代码结构:

与原生不同的是,flutter在开发过程中并不存在动态添加的问题。想要调用自定义的组件,只需要调用一个CustomPaint(),在其painter类中对绘制的逻辑进行设置即可。样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Paper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: CustomPaint( // 使用CustomPaint
painter: PaperPainter(),
),
);
}
}

class PaperPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 创建画笔
final Paint paint = Paint();
// 绘制圆
canvas.drawCircle(Offset(100, 100), 10, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

其中painter类继承了CustomPainter,并重写paint方法用于绘制,其实与原生一样的逻辑。最终painter参数传给CustomPaint进行绘制。

Canvas 方法一览 :

Canvas 的方法非常多,但大多数顾名思义。其中画布状态变换将是最难的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
---->[画布状态]----
void save()
void saveLayer(Rect bounds, Paint paint)
void restore()
int getSaveCount()

---->[画布变换]----
void skew(double sx, double sy)
void rotate(double radians)
void scale(double sx, [double sy])
void translate(double dx, double dy)
void transform(Float64List matrix4)

---->[画布裁剪]----
void clipRect(Rect rect, { ClipOp clipOp = ClipOp.intersect, bool doAntiAlias = true })
void clipRRect(RRect rrect, {bool doAntiAlias = true})
void clipPath(Path path, {bool doAntiAlias = true})

---->[画布绘制--点相关]----
void drawPoints(PointMode pointMode, List<Offset> points, Paint paint)
void drawRawPoints(PointMode pointMode, Float32List points, Paint paint)
void drawLine(Offset p1, Offset p2, Paint paint)
void drawVertices(Vertices vertices, BlendMode blendMode, Paint paint)

---->[画布绘制--矩形相关]----
void drawRect(Rect rect, Paint paint)
void drawRRect(RRect rrect, Paint paint)
void drawDRRect(RRect outer, RRect inner, Paint paint)

---->[画布绘制--类圆相关]----
void drawOval(Rect rect, Paint paint)
void drawCircle(Offset c, double radius, Paint paint)
void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint)

---->[画布绘制--图片相关]----
void drawImage(Image image, Offset p, Paint paint)
void drawImageRect(Image image, Rect src, Rect dst, Paint paint)
void drawImageNine(Image image, Rect center, Rect dst, Paint paint)
void drawAtlas(Image atlas,List<RSTransform> transforms,List<Rect> rects,List<Color> colors,BlendMode blendMode,Rect cullRect,Paint paint)
void drawRawAtlas(Image atlas,Float32List rstTransforms,Float32List rects,Int32List colors,BlendMode blendMode,Rect cullRect,Paint paint)

---->[画布绘制--文字]----
void drawParagraph(Paragraph paragraph, Offset offset)

---->[画布绘制--其他]----
void drawColor(Color color, BlendMode blendMode)
void drawPath(Path path, Paint paint)
void drawPaint(Paint paint)
void drawShadow(Path path, Color color, double elevation, bool transparentOccluder)
//第一个参数时绘制一个图形 Path,第二个是设置阴影颜色,第三个为阴影范围,最后一个阴影范围是否填充满
void drawPicture(Image image)

Paint 属性一览 :

粗略数了一下大概有 14 个属性: 这些都是之后需要详细介绍的
下面代码是简单使用 Paint 和 Canvas 绘制的斜线:

1
2
3
4
isAntiAlias(抗锯齿) color(颜色)          blendMode(混合模式)     style(画笔样式)
strokeWidth(线宽) strokeCap(线帽类型) strokeJoin(线接类型) strokeMiterLimit(斜接限制)
maskFilter(遮罩滤镜) shader(着色器) colorFilter(颜色滤镜) imageFilter(图片滤镜)
invertColors(是否反色) filterQuality(滤镜质量)
1
2
3
4
5
6
7
8
9
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint(); // 创建画笔
paint
..color = Colors.blue //颜色
..strokeWidth = 4 //线宽
..style = PaintingStyle.stroke; //模式--线型
canvas.drawLine(Offset(0, 0), Offset(100, 100), paint); //绘制线
}

Path 方法一览 :

可以说 Canvas 的一个 drawPath 方法,为绘制打开了一扇通往无限可能的大门。
通过 Path 可以完成非常多的效果,Path 的这些方法将在 [Path篇] 进行详细阐述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
---->[路径绝对移动]----
void moveTo(double x, double y)
void lineTo(double x, double y)
void quadraticBezierTo(double x1, double y1, double x2, double y2)
void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
void conicTo(double x1, double y1, double x2, double y2, double w)
void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
void arcToPoint(Offset arcEnd, {Radius radius = Radius.zero, double rotation = 0.0, bool largeArc = false, bool clockwise = true,})

---->[路径相对移动]----
void relativeMoveTo(double dx, double dy)
void relativeLineTo(double dx, double dy)
void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2)
void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3)
void relativeConicTo(double x1, double y1, double x2, double y2, double w)
void relativeArcToPoint(Offset arcEndDelta, { Radius radius = Radius.zero, double rotation = 0.0, bool largeArc = false, bool clockwise = true, })

---->[路径添加]----
void addRect(Rect rect)
void addRRect(RRect rrect)
void addOval(Rect oval)
void addArc(Rect oval, double startAngle, double sweepAngle)
void addPolygon(List<Offset> points, bool close)
void addPath(Path path, Offset offset, {Float64List matrix4})
void extendWithPath(Path path, Offset offset, {Float64List matrix4})

---->[路径操作]----
void close()
void reset()
bool contains(Offset point)
Path shift(Offset offset)
Path transform(Float64List matrix4)
Rect getBounds()
set fillType(PathFillType value)
static Path combine(PathOperation operation, Path path1, Path path2)
PathMetrics computeMetrics({bool forceClosed = false})

以上dart代码均引自https://juejin.cn/book/6844733827265331214/section/6844733827214999565

​ 粗略对比,其实flutter的绘制逻辑跟原生基本是一模一样,flutter在paint的属性上要多一点,不过一般来说其实用到的也就是跟原生一样的那几个。不过flutter的滤镜不出意外是一层封装,因为安卓原生也能通过矩阵变换实现滤镜效果,就是有点费头发(雾)。

​ 不过呢,flutter的canvas的drawShadow方法还是很有趣的,这大概也从侧面说明了flutter组件中广泛存在的阴影的最基本绘制方法。而原生canvas.drawBitmap()与flutter的canvas.drawPicture(Picture picture)是相对应的,但原生的bitmap速度理论上是比较快的,而flutter使用的是image作为参数而不是file或者Uint8List,其性能尚未可知。

​ 另外,flutter显然对Path类更加上心,相对于原生,flutter考虑到了绘制的灵活性,为绘制添加了通过相对位置添加路径的方法,使得在组合类组件中,使用flutter开发更加简单,不过还是因需求而异。

而且有级联语法糖

​ 又,canvas的变换是要经常用到的,主要是为了避免绘制出界以及画布方向的问题。但是呢,matrix4或者说矩阵变换相关的问题,显然是非人类简单能理解的,并不建议使用。