自定义View①绘制基础

2,378 阅读4分钟

canvas绘制,画一条线 drawLine, 画一个圆 drawCircle

val RADIUS = 50f.px
class TestView(context:Context?,attrs:AttributeSet):View(context,attrs) {
  private val parint = Paint(Paint.ANTI_ALIAS_FLAG)
  override fun onDraw(canvas: Canvas) {
    canvas.drawLine(100f,100f,200f,200f,parint)
    canvas.drawCircle(width/2f,200f,
        RADIUS,parint);
  }
}

效果如下
1

dp2px

以前开发很多人用的都是context.getResources().getxxX的方式获取资源,其实有一种更简单的方式Resources.getSystem().getXxx,这样就不需要Context了

class Utils {
  public static float dp2px(float value) {
    return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, Resources.getSystem().getDisplayMetrics());
  }
}

利用kotlin的扩展属性,可以继续简化
Extensions.kt

val Float.px
  get() = TypedValue.applyDimension(
      TypedValue.COMPLEX_UNIT_DIP,
      this,
      Resources.getSystem().displayMetrics
  )

然后使用的时候直接写dp值

val RADIUS = 100f.px

path绘制,画圆,画方形

同向填充

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    path.reset()
    //画圆
    path.addCircle(width/2f,200f, RADIUS,Path.Direction.CCW)
    //画方
    path.addRect(width/2f- RADIUS,200f,width/2f+ RADIUS,200f+2*RADIUS,Path.Direction.CCW)
  }

  override fun onDraw(canvas: Canvas) {
    canvas.drawPath(path,parint)
  }
2

反向,相交镂空

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    path.reset()
    //画圆
    path.addCircle(width/2f,200f, RADIUS,Path.Direction.CCW)
    //画方
    path.addRect(width/2f- RADIUS,200f,width/2f+ RADIUS,200f+2*RADIUS,Path.Direction.CW)
  }
3

fillType

EVEN_ODD,相交,奇数填充 偶数镂空

  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    path.reset()
    //画圆
    path.addCircle(width/2f,200f, RADIUS,Path.Direction.CCW)
    //画方
    path.addRect(width/2f- RADIUS,200f,width/2f+ RADIUS,200f+2*RADIUS,Path.Direction.CW)
    path.addCircle(width/2f,200f, RADIUS*1.5f,Path.Direction.CCW)
    //相交 奇数填充 偶数留空
    path.fillType = Path.FillType.EVEN_ODD;
  }
4

INVERSE_EVEN_ODD 反向,偶数填充,奇数镂空

path.fillType = Path.FillType.EVEN_ODD;
5

PathMeasure

把 Path 对象填入,用于对 Path 做针对性的计算(例如图形周⻓)
如下,根据我们的计算,方形是400f周长,打印结果是1100,说明dp转px的系数是2.75倍

    var pathMeasure:PathMeasure
    path.addRect(width/2f- RADIUS,200f,width/2f+ RADIUS,200f+2*RADIUS,Path.Direction.CW)
    pathMeasure = PathMeasure(path,false)
    Log.d(TAG, "onSizeChanged: "+pathMeasure.length)//100f*4 * 2.75 = 1100

画一个仪表盘

  1. 首先画一个开口为120度的圆形

    • 可以使用canvas.drawArc方式,如下,画一个150dp半径的扇形
    canvas.drawArc(width/2f-150f.px,height/2-150f.px,width/2f+150f.px,height/2f+150f.px,90 + OPEN_ANGLE/2,360- OPEN_ANGLE,false,paint)
    
    • 为了方便后续添加刻度效果,我们使用path绘制,和上面的效果是等效的
    path.addArc(width/2f-CIRCLE_RADIUS,height/2-CIRCLE_RADIUS,width/2f+CIRCLE_RADIUS,height/2f+CIRCLE_RADIUS,
        90 + OPEN_ANGLE/2,360- OPEN_ANGLE)
        
    canvas.drawPath(path,paint);
    
  2. 然后画笔风格设置为空心有边框

  3. 为表盘画上刻度

    • 首先定义刻度的宽高大小
    • 然后添加虚线刻度效果
    • 我们为表盘绘制20个刻度,需要算出刻度间隔,使用PathMeasure测量一下
    val pathMessure = PathMeasure(path,false)
        pathEffect = PathDashPathEffect(dash,(pathMessure.length - DASH_WIDTH)/20,0f, ROTATE)
    
    • 最后第二遍绘制,绘制出刻度效果
        //3.4 画刻度(第二遍绘制)
    paint.setPathEffect(pathEffect)
    canvas.drawPath(path,paint);
    paint.pathEffect = null;
    
  4. 画指针

    • 需要结合角度和指针长度,通过三角函数计算指针的坐标
  5. 完整代码如下

const val OPEN_ANGLE = 120F
//扇形半径
val CIRCLE_RADIUS = 150f.px
val LENGTH = 120f.px
//3.1 虚线宽高
val DASH_WIDTH = 2f.px
val DASH_LENGTH = 10f.px


class DashboardView (context: Context?,attrs: AttributeSet): View(context,attrs){
  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private val dash = Path();
  private val path = Path();
  private lateinit var pathEffect: PathDashPathEffect;

  init {
    //2.画笔风格设置为空心有边框
    paint.strokeWidth = 3f.px
    paint.style = Paint.Style.STROKE
    //3.2 添加刻度效果 矩形刻度条
    dash.addRect(0f,0f, DASH_WIDTH, DASH_LENGTH,Path.Direction.CCW)

  }

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    //3.3 设置刻度间隔 20个间隔
    path.reset()
    path.addArc(width/2f-CIRCLE_RADIUS,height/2-CIRCLE_RADIUS,width/2f+CIRCLE_RADIUS,height/2f+CIRCLE_RADIUS,
        90 + OPEN_ANGLE/2,360- OPEN_ANGLE)
    val pathMessure = PathMeasure(path,false)
    pathEffect = PathDashPathEffect(dash,(pathMessure.length - DASH_WIDTH)/20,0f, ROTATE)
  }

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onDraw(canvas: Canvas) {
    //1.首先画一个开口为120度的圆形(第一遍绘制)
//    canvas.drawArc(width/2f-150f.px,height/2-150f.px,width/2f+150f.px,height/2f+150f.px,
//        90 + OPEN_ANGLE/2,360- OPEN_ANGLE,false,paint)
    canvas.drawPath(path,paint);

    //3.4 画刻度(第二遍绘制)
    paint.setPathEffect(pathEffect)
    canvas.drawPath(path,paint);
    paint.pathEffect = null;

    //4 画指针  指向第5格(第六个刻度)
    canvas.drawLine(width / 2f, height / 2f,
        width / 2f + LENGTH * cos(markToRadians(5)).toFloat(),
        height / 2f + LENGTH * sin(markToRadians(5)).toFloat(), paint)
  }

  private fun markToRadians(mark: Int) =
    Math.toRadians((90 + OPEN_ANGLE / 2f + (360 - OPEN_ANGLE) / 20f * mark).toDouble())
}

效果这样
6

画饼图

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

绘制流程

  1. 首先定义四个扇形及其颜色
  2. 然后for循环依次绘制他们
  3. 最后对某个扇形进行偏移
  4. 完整代码如下
//1. 半径,角度,颜色设置
private val RADIUS = 150f.px
private val ANGLES = floatArrayOf(60f, 90f, 150f, 60f)
private val COLORS = listOf(Color.parseColor("#C2185B"), Color.parseColor("#00ACC1"), Color.parseColor("#558B2F"), Color.parseColor("#5D4037"))
private val OFFSET_LENGTH = 20f.px

class PieView  (context: Context?,attrs: AttributeSet): View(context,attrs){
  private val paint = Paint(Paint.ANTI_ALIAS_FLAG)

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {

  }

  @RequiresApi(VERSION_CODES.LOLLIPOP)
  override fun onDraw(canvas: Canvas) {
    //画弧
    var startAngle = 0f;
    //2 for循环绘制
    for ((index,angle)in ANGLES.withIndex()){
      paint.color = COLORS[index];
      //3.1 偏移某个扇形
      if (index==1){
        canvas.save()
        canvas.translate(
            (OFFSET_LENGTH*Math.cos(Math.toRadians((startAngle+angle/2).toDouble()))).toFloat(),
            (OFFSET_LENGTH*Math.sin(Math.toRadians((startAngle+angle/2).toDouble()))).toFloat()
        )
      }
      canvas.drawArc(width/2f-RADIUS,height/2-RADIUS,width/2f+RADIUS,height/2f+RADIUS,
          startAngle,angle,true,paint)
      startAngle += angle;
      //3.2 重置canvas
      if (index==1){
        canvas.restore()
      }
    }

  }
}

效果如下
7