UI-绘制

绘制(一)

最重要的是测量

本节目标

掌握基本绘制的三个重点:

  1. 图形的位置、尺寸、角度的计算
  2. Xfermode 的使用
  3. 文字的位置和尺寸计算(挪到下一节)

从而达到对于任意的图形、文字以及二者的结合都能轻松完成绘制。

绘制的 API 不是这一节内容的重点。

绘制的基本要素:

  • 重写 onDraw(Canvas)

  • 使用 Canvas 来绘制

  • 使用 Paint 来配置(调粗细、颜色等)

  • 坐标系

  • 尺寸单位是像素,而不是 dp。转换方式:

    1
    2
    3
    public static float dp2px(float dp) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
    }

Canvas常用API

  • drawColor()
  • drawLine()
  • drawRect()
  • drawRoundRect()
  • drawCircle()
  • drawOval()
  • drawArc()
  • drawPoint()
  • drawPath()
  • drawBitmap()
  • drawText()

dp转像素px

1
2
3
public static float dp2px(float dp){
return TypeValue.applyDimension(TypeValue.COMPLEX_UNIT_DIP, 150, Resources.getSystem().getDisplayMetrics());
}

layout结束后大小有变化才会调用onSizeChanged()

cw:clockwise顺时针

ccw:counter clockwise逆时针

Path 的方向以及封闭图形的内外判断:

  • Winding(画的方向): 如果方向相反的穿插次数相等则为内部,不等则为外部:

    • 单圆:

    • 双圆同向:

      中间的是内部(同方向的Winding是1+1=2)

    • 双圆不同向:

      中间是外部(不同方向的Winding是1+(-1)=0)

  • Even Odd:不考虑方向。穿插奇数次则为内部,偶数次则为外部:(镂空效果可考虑它)

    • 单圆:
    • 双圆:

PathMeasure

把 Path 对象填入,用于对 Path 做针对性的计算(例如图形周长pathMeasure.getLength())。

图形一:仪表盘

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
public Dashboard extends View{
private static final int ANGEL = 120;
private static final RADIUS = Utils.dp2px(150);
private static final LENGTH = Utils.dp2px(100);//指针长度
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿
Path dash = new Path();//这个是尺度线
PathDashPathEffect effect;

public Dashbord(Context context, @Nullable AttributeSet attrs){
super(context, attrs);
}

//单独大括号,会在构造方法调用super方法后调用
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(Utils.dp2px(2));
dash.addRect(0, 0, Utils.dp2px(2), Utils.dp2px(10), Path.Direction.CW);//起始点(0,0),宽度2,长度10,顺时针

//设置刻度间距
Path arc = new Path();
arc.addArc(getWidth()/2-RADIUS, getHeight()/2-RADIUS, getWidth()/2+RADIUS, getHeight()/2+RADIUS, 90+ANGLE/2, 360-ANGLE);
PathMeasure pathMeasure = new PathMeasure();
pathMeasure.getLength() / 20;

effect = new PathDashPathEffect(dash, (pathMeasure.getLength()-Utils.dp2px(2))/20, 0, PathDashPathEffect.Style.ROTATE);
}

@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
//画线
canvas.drawArc(getWidth()/2-RADIUS, getHeight()/2-RADIUS, getWidth()/2+RADIUS, getHeight()/2+RADIUS, 90+ANGLE/2, 360-ANGLE, false, paint);
//画刻度
paint.setPathEffect(effect);
canvas.drawArc(getWidth()/2-RADIUS, getHeight()/2-RADIUS, getWidth()/2+RADIUS, getHeight()/2+RADIUS, 90+ANGLE/2, 360-ANGLE, false, paint);
paint.setPathEffect(null);

//画指针(涉及到三角函数)
int currenAngle = 90 + ;
canvas.drawLine(getWidth()/2, getHeight()/2, (float)Math.cos(Math.toRadians(getAngleFromMark(5))) * LENGTH, (float)Math.sin(Math.toRadians(getAngleFromMark(5))) * LENGTH, paint);
}

//从刻度获取夹角
int getAngleFromMark(int mark){
return (int)(90 + ANGLE/2 //起始角度
+ (360-ANGLE)/20 //剩余角度
* mark);
}
}
  • 用 drawArc() 绘制弧形

  • 三角函数的计算 横向的位移是 cos,纵向的位移是 sin

  • PathDashPathEffect

    • 加上 PathEffect 之后,就只绘制 effect,而不绘制原图形。所以需要弧线和刻度分别绘制,一共两次。

    • dash 的方向

      • 绘制 dash 的轨迹是这样的:

      • 而由于 x 轴的正向是轨迹的正向(这是规定),所以实际上的物理模型是这样的:

      • 那么,如果你要加上刻度,就应该这样设置:

      • 然后,你的刻度就会沿着轨迹绘制:

    • advance 计算

图形二:饼图

  • 用 drawArc() 绘制扇形
  • 用 Canvas.translate() 来移动扇形,并用 Canvas.save() 和 Canvas.restore() 来保存和恢复位置
  • 用三角函数 cos 和 sin 来计算偏移

图形三:圆形图像

  • Xfermode:
    • 为什么要 Xfermode?为了把多次绘制进行「合成」,例如蒙版效果:用 A 的形状和 B 的图案
    • 怎么做?
      • Canvs.saveLayer() 把绘制区域拉到单独的离屏缓冲里
      • 绘制 A 图形
      • 用 Paint.setXfermode() 设置 Xfermode
      • 绘制 B 图形
      • 用 Paint.setXfermode(null) 恢复 Xfermode
      • 用 Canvas.re

绘制(二)

文字的测量

  • 绘制⽂字:drawText()

文字测量难点之一:居中的纵向测量

  • ⽅式⼀:Paint.getTextBounds() 之后,使⽤ (bounds.top + bounds.bottom) / 2
  • ⽅式⼆:Paint.getFontMetrics() 之后,使⽤ (fontMetrics.ascend + fontMetrics.descend) / 2

安卓/绘制

文字测量难点之二:左对⻬

  • getTextBounds() 之后的 left 来计算

文字测量难点 之三:换行

  • breakText() 来计算

安卓/绘制2

Canvas 的范围裁切

  • clipRect()
  • clipPath()clipPath()切出来的圆为什么没有抗锯齿效果?因为“强行切边”
  • clipOutRect()/clipOutPath()

Canvas 的几何变换

  • translate(x, y)

  • route(degree)

  • scale(x, y)

  • skew(x, y)

    重点:Canvas 的集合变换参照的是 View 的坐标系,而绘制方法(drawXxx())参照的是 Canvas 自己的坐标系。

关于多重变换

Canvas 的变换方法多次调用的时候,由于 Canvas 的坐标系会整体被变换,一次当平移、旋转、放缩、错切等变换多重存在的时候,Canvas的变换参数会非常难以计算,因此可以改用倒序的理解方式:

将 Canvas 的变换理解为 Canvas 的坐标系不变,每次变换是只对内部的绘制内容进行变换,同时把 Canvas 的变换顺序看作是倒序的(即写在下面的变换先执行),可以更加方便进行多重变换的参数计算。

Matrix的几何变换

  • preTranslate(x, y) / postTranslate(x, y)
  • preRotate(degree) / postRotate(degree)
  • preScale(x, y) / postScale(x, y)
  • preSkew(x, y) / postSkew(x, y)

其中preXxx()效果和 Canvas 的准同名⽅法相同,postXxx()效果和 Canvas 的准同名⽅法顺序相反。

注意

如果多次重复使⽤ Matrix,在使⽤之前需要⽤Matrix.reset()来把 Matrix 重置。

使⽤ Camera 做三维旋转

  • rotate() / rotateX() / rotateY() / rotateZ()

  • translate()

  • setLocation()

    其中,⼀般只⽤rotateX()rorateY()来做沿 x 轴或 y 轴的旋转,以及使⽤setLocation()来调整放缩的视觉幅度。
    对 Camera 变换之后,要⽤ Camera.applyToCanvas(Canvas) 来应⽤到 Canvas

setLocation()

这个⽅法⼀般前两个参数都填 0,第三个参数为负值。由于这个值的单位是硬编码写死的,因此像素密度越⾼的⼿机,相当于 Camera 距离 View 越近,所以最好把这个值写成与机器的 density 成正⽐的⼀个负值,例如 -6 * density

安卓/绘制3