Android的自定义控件(一)

192 阅读8分钟

Android的自定义控件是非常有意思的一样东西,同时也是很需要花时间钻研的,往往可以看到别人制作出的控件高大上,而自己着手却经常感慨理想与现实的差距...

1. 基础概念

如果不使用代码,仅仅是在现实世界中,如果你想要进行绘制,会使用什么工具?
其实无非纸和笔,或者类似这二者的东西,像是手指和哈了气的窗玻璃,木棍和沙滩等等

image.png

画笔用来勾勒线条、涂色、展现创作者的意图,而纸则是用作载体,呈现内容
了解到这里,对于Android绘画所需要的工具也就有了基本的认识,这里完全可以与现实进行对应

Canvas类正是Android里面扮演“纸”这个角色的类,作用也是呈现内容
Paint类便是Android里的“画笔”,通过Paint可以让Canvas上呈现各种形状和图案

2. 准备工作

自定义控件总是离不开View,因此如果要进行显示,还需要做一些简单工作
创建一个继承自View的子类,满足这个条件,这个类便是自定义View
继承了View的4个构造方法并覆写onDraw()

这4个构造方法对应不同的创建View的场景,onDraw()方法会提供Canvas对象供我们用来作画,之后绘制的操作会放到onDraw()方法当中

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

import androidx.annotation.Nullable;

public class FirstView extends View {

    public FirstView(Context context) {
        super(context);
    }

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

    public FirstView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public FirstView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);    // 提供了Canvas,借助Canvas进行绘制
    }
}

接下来,像添加其他控件一样将这个新创建的View放到xml布局中,比如直接放在启动默认的Activity的布局当中,比较方便显示,宽高先配置为match_parent,撑满父布局

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!--将自定义控件添加到布局中-->
    <com.minos.customviewdemo.customview.FirstView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

此时由于还没有进行绘制的操作,因而即使运行,页面也没有任何东西进行呈现,画布是一片“空白”

image.png

3. 画笔——Paint

就像现实生活中进行绘画一样,需要有什么作为画笔,能够进行线条的勾勒,颜色的填充

在Android当中,充当“画笔”这个角色的是Paint

Paint  |  Android Developers (google.cn)

The Paint class holds the style and color information about how to draw geometries, text and bitmaps.

关于Paint类,官方文档对此简单地一句话带过,其中最主要的就是Paint类包含了样式和颜色信息,对比日常绘画,其实也会发现二者很相似
画笔的笔头有粗细之分,还有些是圆的,有些是方的,这些都可以对应于样式

image.png


要想创作出丰富多彩的画面,需要有各种颜色的画笔,这便是颜色

image.png

接下来具体看下代码层面的方法(常用的):

3.1. 颜色

setColor()方法就是用来设置画笔的颜色的,这个代表颜色的int类型参数是包含透明度的,颜色是采用ARGB的形式进行表示的,A代表透明度,R代表红色分量,G代表绿色分量,B代表蓝色分量,由于int是32位,因此每个分量占用8位,8位放到二进制表示的便是0~255的范围

image.png

透明度0~255,数字越大,不透明度越高
而RGB部分,前端基本功,可以使用AS的这个小工具帮帮忙

image.png

image.png

public class FirstView extends View {

    private Paint mPaint = new Paint();  // 创建对象

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFFFF0000);   // 红色画笔
        canvas.drawCircle(0, 0, 300, mPaint);   // 画个圆,圆心在屏幕左上角
    }
}

image.png

由于坐标原点在屏幕左上角,因此只显示了1/4的圆,这是调用了Canvasdraw系列方法中的drawCircle()方法画了一个圆(先不深究),主要目的还是看一下PaintsetColor()实际使用的效果,就像是用红色的彩笔去填充这样一个形状

3.2. 样式

虽然setStyle()直接翻译就是“设置样式”,但是感觉并不具体,样式还是太宽泛了
其实该方法主要影响的还是对于图形的填充

image.png

image.png

该方法接收的类型为Paint.Style,这是一个枚举类,主要有这3个变体,对应的注释中其实已经描述得比较详细了

FILL

FILL变体会填充对应的几何图形或文字,与线条相关的设置将会被忽略

public class FirstView extends View {

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFFFF0000);
        mPaint.setStyle(Paint.Style.FILL);   // 填充模式
        canvas.drawCircle(100, 100, 100, mPaint);
    }
}

image.png

image.png

其实默认情况下也会是FILL(除画位图)

STROKE

STROKE则是对应描边,因此绘制出来的仅仅是轮廓、线框

public class FirstView extends View {

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFFFF0000);
        mPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(100, 100, 100, mPaint);
    }
}

image.png

FILL_AND_STROKE

最后一种看名称就知道是前面二者的结合,同时拥有线框和填充,就是在第二种的基础上顺带涂上中心的颜色,看上去和第一种差不多,仅仅多出了描边的宽度

3.3.线宽

既然在样式当中提及了STROKE,那么很自然地,会有用于描边粗细的设置,这就像是不同粗细的笔一样,在绘制画面的时候,有着明确的分工

image.png

需要注意的是,setStroke()FILL下不生效,因为该模式没有线,因此没有意义,单位是像素

public class FirstView extends View {

    ...
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFFFF0000);
        mPaint.setStyle(Paint.Style.STROKE);  // 画线模式,确保设置线宽能够生效
        mPaint.setStrokeWidth(10);   // 设置线宽,10px
        canvas.drawCircle(100, 100, 100, mPaint);
    }
}

image.png

4. 画布——Canvas

Canvas就如其名称一般,扮演着“画布”的角色,作为绘画内容的载体

Canvas  |  Android Developers (google.cn)

The Canvas class holds the "draw" calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

其实之前也有稍稍提到,Canvas持有一系列draw开头的方法,就像一个工具集,负责各种执行各种绘制工作,而我们负责去调用,让它亲自执笔

4.1. 面——drawColor

image.png

主要使用drawColor()方法对当前的区域涂上统一的背景色

public class FirstView extends View {

    ...
   
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(0xFF000000);   // 黑色画布
    }
}

image.png

4.2. 线——drawLine

image.png

drawLine()根据给定的起止点坐标绘制一条线段,并且需要画笔Paint对象控制其样式

public class FirstView extends View {

    ...
    
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFF880066);   // 颜色
        mPaint.setStrokeWidth(10);    // 线宽
        canvas.drawLine(0, 0, 300, 400, mPaint);   // 左上角(0,0)到(300,400)的线段
    }
}

image.png

由于线本身就是线框的样式,因此修改PaintStyle毫无作用(FILLSTROKE都一样),只能修改StrokeWidth更改线的宽度

4.3. 点——drawPoint

image.png

在指定的坐标位置画一个点

public class FirstView extends View {

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFF880066);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);   // 圆形笔头
        canvas.drawPoint(100, 100, mPaint);   // (100, 100)处画个点
    }
}

image.png

在使用drawPoint()时,也是和drawLine()类似,Style也会被忽略,使用StrokeWidth可以修改宽度,另外StrokeCap表示笔头,圆形笔头点一下是圆,方形笔头就是方

5. 基本技法

首先,在Android当中,进行绘制使用的参考系都是以左上角位置作为坐标原点,向右为x轴正方向,向下为y轴正方向

image.png

5.1. 平移——translate

image.png

public class FirstView extends View {

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFF880066);
        mPaint.setStrokeWidth(50);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        canvas.drawPoint(100, 100, mPaint);
        canvas.translate(200, 200);    // 向右下平移,坐标轴跟随移动
        mPaint.setColor(0xFF000066);
        canvas.drawPoint(100, 100, mPaint);
    }
}

image.png

坐标系与画布是绑定的,一旦画布移动坐标轴会跟随移动,这就像是平时绘画时,一边画一边移动纸一样,而屏幕是桌子,桌子是不动的,但我们可以移动纸张,方便我们作画

5.2. 保存与恢复——save & restore

image.png

image.png

save()restore()是成对出现的,利用栈保存内容,然后进行恢复,防止对于画布的变换操作影响原来的画面元素

一种常用的场景就是绘画的时候,发现某个元素计算距离比较麻烦,那么可以将坐标系,进行平移,放到中心,画完后再恢复原来的位置

比方说现在有一个在(600, 200),半径为400的圆,现在我想画它的同心圆
一种方法是直接找到圆心(600,200),再画一个
另一种是save(),把圆心放到原点,画圆,再restore()

public class FirstView extends View {

   ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFF00FFFF);
        canvas.drawCircle(600, 200, 400, mPaint);
    }
}

image.png

public class FirstView extends View {

    ...

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPaint.setColor(0xFF00FFFF);
        canvas.drawCircle(600, 200, 400, mPaint);

        canvas.save();
        canvas.translate(600, 200);   // 坐标原点右移,到了圆心
        mPaint.setColor(Color.MAGENTA);
        canvas.drawCircle(0, 0, 200, mPaint);
        canvas.restore();
    }
}

image.png

似乎从代码量上更多了呢,但是其实思路会很明确,也符合平时画画的习惯,就像PS里的抓手工具,我们会把要画的东西放到中央

当视图复杂后,会涉及各种计算,而使用平移、保存、恢复这三板斧,可以免去很多距离的计算,每次都从原点作画,能够减少出错的概率