当前位置: 移动技术网 > 移动技术>移动开发>Android > Android进阶之光笔记一:自定义View《一》

Android进阶之光笔记一:自定义View《一》

2020年07月18日  | 移动技术网移动技术  | 我要评论

重写一个view一般情况下只需要重写onDraw()方法。那么什么时候需要重写onMeasure()、onLayout()、onDraw()
方法呢,这个问题只要把这几个方法的功能弄清楚就应该知道怎么做了。
①、如果需要绘制View的图像,那么需要重写onDraw()方法。(这也是最常用的重写方式。)
②、如果需要改变view的大小,那么需要重写onMeasure()方法。
③、如果需要改变View的(在父控件的)位置,那么需要重写onLayout()方法。
④、根据上面三种不同的需要你可以组合出多种重写方案。

一、常用方法及控件

1、Paint类——画笔

Paint类代表画笔,用来描述图形的颜色和风格,如线宽,颜色,透明度,和填充效果等信息,使用Paint时,需要先创建该类的对象,这可以通过该类提供的构造方法来实现。通常情况下,只需要使用无参数的构造方法来创建一个使用默认设置的Paint对象:

Paint paint = new Paint();
常用方法:
  • setARGB(int a, int r, int g, int b)
    用于设置颜色,各参数值均为0~255之间的整数
  • setColor(int color)
    用于设置颜色
  • setAlpha(int a)
    用于设置透明度,值为0~255之间的整数
  • setAntiAlias(boolean aa)
    用于指定是否使用抗锯齿功能,如果使用会使绘图速度变慢
  • setDither(boolean dither)
    用于指定是否使用图像抖动处理,如果使用会使图像颜色更加平滑和饱满,更加清晰
  • setPathEffect(PathEffect effect)
    用于设置绘制路径时的路径效果,例如点画线
  • setShader(Shader shader)
    用于设置渐变,可以使用LinearGradient(线性渐变)、RadialGradient(径向渐变)或者SweepGradient(角度渐变)
    TileMode.REPEAT 重复的
    TileMode.CLAMP 固定的–渐变过去
    TileMode.MIRROR 镜像的,对称的
  • setStrokeCap(Cap cap)
    用于当画笔的填充样式为STROKE或FILL_AND_STROKE时,设置笔刷的图形样式
    参数值可以是Cap.BUTT、Cap.ROUND或Cap.SQUARE。
    主要体现在线的端点上
  • setStrokeJoin(Join join)
    用于设置画笔转弯处的连接风格
    参数值为Join.BEVEL、Join.MITER或Join.ROUND
  • setStrokeWidth(float width)
    用于设置笔触的宽度
  • setStyle(Style style)
    用于设置填充风格
    参数值为Style.FILL、Style.FILL_AND_STROKE或Style.STROKE
  • setTextAlign(Align align)
    设置字文本的排列方式
  • setTextSize(float textSize)
    用于设置字体大小
  • setFakeBoldText(boolean fakeBoldText)
    用于设置是否为粗体文字

2、Canvas类——画布

Android提供了强大的二维图形库,比较常用的是绘制几何图形,绘制文本,路径和图片等
比较常见的图形包括点,线,弧,圆形,矩形,在Android中,Canvas类提供了丰富的绘制几何图形的方法,通过这些方法可以绘制出各种几何图形

常用方法
  • drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
    绘制圆弧
    left:矩形左侧的X坐标。必须确保left<=right
    top:矩形顶部的Y坐标。必须确保top<=bottom
    right:矩形右侧的X坐标
    bottom:矩形底部的Y坐标
    startAngle:弧的起始角度。负数或大于或等于360则对360模除(水平方向为0度)
    sweepAngle:顺时针扫描的角度。大于或等于360则画成圆
    useCenter:是否显示半径连线,true表示显示圆弧与圆心的半径连线,false表示不显示
    paint:绘制时所使用的画笔
//距离左上为0的矩形中半径为150/2的弧
canvas.drawArc(0,0,150,150,0,90,true,mPaint)

在这里插入图片描述

  • drawCircle(float cx, float cy, float radius, Paint paint)
    绘制圆
    cx,cy:所画圆的中心坐标
    radius:圆的半径
//圆心为(100,100),半径为100
canvas.drawCircle(100,100,100,mPaint);

在这里插入图片描述

  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
    绘制一条线
    前四个参数为直线的起点和终点的 XY 轴坐标
int height = getHeight();
int width = getWidth();
canvas.drawLine(0,height/2,width,height/2,mPaint);

在这里插入图片描述

  • drawLines(float[] pts, Paint paint)
    绘制多条线
canvas.drawText("A1(20,100)", 0, 90, mPaint);
canvas.drawText("A2(100,250)", 40, 270, mPaint);
canvas.drawText("B1(100,100)", 80, 90, mPaint);
canvas.drawText("B2(180,250)", 150, 270, mPaint);
float[] points=new float[]{20,100,100,250,100,250,100,100,100,100,180,250};//至少4个值,即能够绘制一条直线
canvas.drawLines(points,mPaint);

在这里插入图片描述

  • drawLines(float[] pts, int offset, int count, Paint paint)
    绘制多条线
    pts:待画的坐标点数组,格式为(x1,y1,x2,y2,…),至少4个值
    offset:要跳过坐标点数组中几个值才开始画(必须是4的倍数)
    count:至少为2,offset 之后数组的大小。
canvas.drawText("A1(100,250)", 40, 270, mPaint);
canvas.drawText("B2(100,100)", 80, 90, mPaint);
canvas.drawText("B3(180,250)", 150, 270, mPaint);
float[] points=new float[]{20,100,100,250,100,250,100,100,100,100,180,250};//至少4个点
canvas.drawLines(points,4,8,mPaint);

在这里插入图片描述

  • drawOval(float left, float top, float right, float bottom, Paint paint)
    绘制椭圆
//x轴占200,y轴占100的椭圆
canvas.drawOval(0,0,200,100,mPaint);

在这里插入图片描述

  • drawPoint(float x, float y, Paint paint)
    绘制一个点
canvas.drawPoint(100,100,mPaint);

在这里插入图片描述

  • drawPoints(float[] pts, Paint paint)
    绘制多个点
float[] points=new float[]{20,100,100,250,100,100,180,250};//至少2个点
canvas.drawPoints(points,mPaint);

在这里插入图片描述

  • drawRect(float left, float top, float right, float bottom, Paint paint)
    绘制矩形
//长为200,宽为200-50的矩形
canvas.drawRect(0,50,200,200,mPaint);

在这里插入图片描述

  • drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
    绘制圆角矩形
    rx,ry 表示 left 到 left+rx 与 left 到 left+ry 所围区域做弧
canvas.drawRoundRect(0,50,200,200,20,20,mPaint);

在这里插入图片描述

  • scale(float sx, float sy)
    sx和sy用于指定x轴和轴y轴的缩放比例,默认做左上角开始缩放
  • scale(float sx, float sy, float px, float py)
    px 和py是以它们为轴心进行缩放
  • rotate(float degrees)
    参数用于指定旋转的角度,默认做左上角开始旋转
  • rotate(float degrees, float px, float py)
    px py为旋转的轴心
  • translate(float dx, float dy)
    dx和dy用于指定移动到的位置的x和y的坐标距离
  • skew(float sx, float sy)
    sx和sy用于指定x轴和轴y轴的倾斜量,默认做左上角开始倾斜
  • save()
    保存画布,这句代码之前的图像不会被特效影响
  • restore()
    恢复画布到canvas.save()之前的状态
注意:画布一旦经过特效处理,比如旋转、缩放等,那么之后所绘制的图像都是旋转过后的。

3、onDraw(Canvas canvas)

  • 所有 View类都包含了一个 onDraw(Canvas c)方法,这个方法的作⽤就是绘制View自身
  • invalidate()
    onDraw方法在View初始化的时候会调用一次,之后,只要在代码中调用invalidate()方法,onDraw方法就会被调用一次,重新绘制自己。简称“重绘自己”
  • postInvalidateDelayed(long delayMilliseconds)
    每隔delayMilliseconds 毫秒重绘View

4、onMeasure(int widthMeasureSpec, int heightMeasureSpec)

  • 测量控件的宽和高,该方法在onDraw之前调用
MeasureSpec

MeasureSpec是View的内部类,它封装了一个View的尺寸,在onMeasure()当中会根据这个MeasureSpec的值来确定View的宽高。
MeasureSpec的值保存在一个int值当中。一个int值有32位,前两位表示模式mode后30位表示大小size。即MeasureSpec = mode + size。
在MeasureSpec当中一共存在三种mode:UNSPECIFIED、EXACTLY 和AT_MOST

  • UNSPECIFIED
    无限制,View对尺寸没有任何限制,View设置为多大就应当为多大。此种模式比较少见
  • EXACTLY
    精准模式,View需要一个精确值,这个值即为MeasureSpec当中的Size,对应match_parent
  • AT_MOST
    最大模式,View的尺寸有一个最大值,View不可以超过MeasureSpec当中的Size值,对应wrap_content

5、onLayout(boolean changed, int left, int top, int right, int bottom)

  • 用来改变View的(在父控件的)位置

6、构造函数

public class TestView extends View {
    /**
     * 在java代码里new的时候会用到
     * @param context
     */
    public TestView(Context context) {
        super(context);
    }

    /**
     * 在xml布局文件中使用时自动调用
     * @param context
     */
    public TestView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     */
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    /**
     * 只有在API版本>21时才会用到
     * 不会自动调用,如果有默认style时,在第二个构造函数中调用
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public TestView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

7、自定义属性

Android系统的控件以android开头的(比如android:layout_width)都是系统自带的属性。为了方便配置自定义控件的属性,我们也可以自定义属性。

1)自定义属性步骤
  • 步骤一:在values目录下创建 attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LayoutView">
        <attr name="background_view" format="color" />
    </declare-styleable>
</resources>
  • 步骤二:布局文件中使用自定义的属性
<com.example.customview.view.LayoutView
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_margin="50dp"
        android:text="这是一个TextView的自定义View"
        app:background_view="@color/colorAccent"/>
  • 步骤三:在View的构造方法中通过TypedArray获取
	public LayoutView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LayoutView);
        int color = typedArray.getColor(R.styleable.LayoutView_background_view, Color.BLACK);
        setBackgroundColor(color);
        //获取资源后要及时回收
        typedArray.recycle();
    }
  • 步骤四:设置该自定义属性
//如:设置为背景
setBackgroundColor(color);
2)属性值的类型format
(1) reference:参考某一资源ID
  • 属性定义
<attr name = "background" format = "reference" />
  • 属性使用
<ImageView android:background = "@drawable/图片ID" />
(2) color:颜色值
  • 属性定义
<attr name = "textColor" format = "color" />
  • 属性使用
<TextView android:textColor = "#00FF00" />
(3) boolean:布尔值
  • 属性定义
<attr name = "focusable" format = "boolean" />
  • 属性使用
<Button android:focusable = "true" />
(4) dimension:尺寸值
  • 属性定义
<attr name = "layout_width" format = "dimension" />
  • 属性使用
<Button android:layout_width = "42dip" />
(5) float:浮点值
  • 属性定义
<attr name = "fromAlpha" format = "float" />
  • 属性使用
<alpha android:fromAlpha = "1.0" />
(6) integer:整型值
  • 属性定义
<attr name = "framesCount" format="integer" />
  • 属性使用
<animated-rotate android:framesCount = "12" />
(7) string:字符串
  • 属性定义
<attr name = "text" format = "string" />
  • 属性使用
<TextView android:text = "我是文本" />
(8) fraction:百分数
  • 属性定义
<attr name = "pivotX" format = "fraction" />
  • 属性使用
<rotate android:pivotX = "200%" />
(9) enum:枚举值
  • 属性定义
<declare-styleable name="名称">
    <attr name="orientation">
        <enum name="horizontal" value="0" />
        <enum name="vertical" value="1" />
    </attr>
</declare-styleable>
  • 属性使用
<LinearLayout  
    android:orientation = "vertical">
</LinearLayout>
注意:枚举类型的属性在使用的过程中只能同时使用其中一个,不能 android:orientation = “horizontal|vertical"
(10) flag:位或运算
  • 属性定义
<declare-styleable name="名称">
    <attr name="gravity">
            <flag name="top" value="0x01" />
            <flag name="bottom" value="0x02" />
            <flag name="left" value="0x04" />
            <flag name="right" value="0x08" />
            <flag name="center_vertical" value="0x16" />
            ...
    </attr>
</declare-styleable>
  • 属性使用
<TextView android:gravity="bottom|left"/>
注意:位运算类型的属性在使用的过程中可以使用多个值
(11) 混合类型:属性定义时可以指定多种类型值
  • 属性定义
<declare-styleable name = "名称">
     <attr name = "background" format = "reference|color" />
</declare-styleable>
  • 属性使用
<ImageView
android:background = "@drawable/图片ID" />
或者:
<ImageView
android:background = "#00FF00" />

二、继承系统控件的自定义View

1、概念

继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。

2、案例

继承EditText实现一个记事本

package com.example.customview.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.EditText;

public class NotePad extends EditText {

    private int lineWidth = 1;
    private int lineColor = Color.BLACK;
    private int spacing_line = 20;
    private int padding = 10;
    private Paint paint;

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

    public NotePad(Context context, AttributeSet attrs) {
        super(context, attrs);
        setFocusableInTouchMode(true);
        //设置光标处于左上角
        setGravity(Gravity.TOP);
        //设置行间距
        setLineSpacing(spacing_line, 1);

        setPadding(padding, 10, padding, 10);

        paint = new Paint();
        paint.setColor(lineColor);
        paint.setStrokeWidth(lineWidth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获得当前控件的高度
        int height = getHeight();
        //获得每一行的高度
        int lineHeight = getLineHeight();

        //计算出每页的行数
        int pageCount = height / lineHeight;
        for (int i = 0; i < pageCount; i++) {
            canvas.drawLine(padding, (i + 1) * lineHeight, getWidth() - padding, (i + 1) * lineHeight, paint);
        }
    }
}

在这里插入图片描述

三、继承View的自定义View

1、概念

继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能。

2、案例

实现自定义时钟

package com.example.customview.view;

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

import androidx.annotation.Nullable;

import java.util.Calendar;

public class AlarmClockView extends View {
    private Paint circlePaint = new Paint();
    private Paint circlePaint2 = new Paint();
    private Paint dotPaint = new Paint();
    private Paint hourPaint = new Paint();
    private Paint minutePaint = new Paint();
    private Paint secondPaint = new Paint();

    private Calendar calendar;

    public AlarmClockView(Context context) {
        super(context);
        initDraw();
    }

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

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

    private void initDraw() {
        circlePaint.setColor(Color.BLUE);
        circlePaint.setStrokeWidth(8);
        //设置填充效果为空心
        circlePaint.setStyle(Paint.Style.STROKE);

        circlePaint2.setColor(Color.BLUE);
        circlePaint2.setStrokeWidth(4);
        circlePaint2.setStyle(Paint.Style.STROKE);

        dotPaint.setColor(Color.BLUE);
        dotPaint.setStrokeWidth(2);
        dotPaint.setStyle(Paint.Style.STROKE);

        hourPaint.setStrokeWidth(4);

        minutePaint.setStrokeWidth(3);

        secondPaint.setStrokeWidth(2);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();

        calendar = Calendar.getInstance();
        int radius = width / 2 - 10;

        //画大小3个圆
        canvas.drawCircle(width / 2, height / 2, radius, circlePaint);
        canvas.drawCircle(width / 2, height / 2, radius - 15, circlePaint2);
        canvas.drawCircle(width / 2, height / 2, 15, dotPaint);

        //绘制12个刻钟的位置
        for (int i = 1; i < 13; i++) {
            //在旋转之前保存画布状态
            canvas.save();
            canvas.rotate(i * 30, width / 2, height / 2);
            //1.2表示起点坐标,3.4表示终点坐标,5.画笔
            canvas.drawLine(width / 2, height / 2 - radius, width / 2, height / 2 - radius + 35, circlePaint2);
            //恢复画布状态
            canvas.restore();
        }

        //获得当前小时
        int hour = calendar.get(Calendar.HOUR);
        //在旋转之前保存画布状态
        canvas.save();
        //旋转
        canvas.rotate(hour * 30, width / 2, height / 2);
        //画时针
        canvas.drawLine(width / 2, height / 2 + 20, width / 2, height / 2 - 140, hourPaint);
        //恢复画布状态
        canvas.restore();

        //获得当前分钟
        int minute = calendar.get(Calendar.MINUTE);
        //在旋转之前保存画布状态
        canvas.save();
        //旋转
        canvas.rotate(minute * 6, width / 2, height / 2);
        //画分针
        canvas.drawLine(width / 2, height / 2 + 30, width / 2, height / 2 - 170, minutePaint);
        //恢复画布状态
        canvas.restore();

        //获得当前秒数
        int second = calendar.get(Calendar.SECOND);
        //在旋转之前保存画布状态
        canvas.save();
        //旋转
        canvas.rotate(second * 6, width / 2, height / 2);
        //画秒针
        canvas.drawLine(width / 2, height / 2 + 40, width / 2, height / 2 - 200, secondPaint);
        //恢复画布状态
        canvas.restore();

        //每隔1秒重绘View,重绘会调用onDraw()方法
        postInvalidateDelayed(1000);


    }
}

在这里插入图片描述

本文地址:https://blog.csdn.net/m0_46211029/article/details/107365532

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网