Android自定义View-入门(明白自定义View和自定义ViewGroup)

自定义View

为什么要自定义View? 主要是Andorid系统内置的View 无法实现我们的 需求,我们需要针对我们的业务需求定制我们想要的 View.自定义View 我们大部分时候只需重写两个函数: onMeasure(),onDraw(). onMeasure()负责对当前View 的尺寸进行测量,onDraw负责把当前这个View绘制出来,当然了,还需要写构造函数。

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

public Views(Context context,  AttributeSet attrs) {
    super(context, attrs);
}

onMeasure 概念

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

主要用来测量布局,其参数 widthMeasureSpecheightMeasureSpec 包含宽和高的信息和测量模式。

为什么一个 数里面能放两个信息呢?我们知道,我们在设置宽高时有三个选择,wrap_content,match_partent以及 指定固定尺寸,而测量模式也有三种:UNSPECIFIED,EXACTLY,AT_MOST,但它们并不是一一对应的关系。

那么google 是如何做到把一个 int同时放测量模式 和尺寸信息呢?我们知道 int型数据占用 32个bit,而google实现的是,将 int数据的前面2个 bit用于区分不同的布局模式,后面 30个bit 存放的是尺寸的数据。

如何提取测量模式与尺寸呢?

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
测量模式表示意思
UNSPECIFIED父容器没有对当前View有任何限制,当前View可以任意取尺寸
EXACTLY当前的尺寸就是当前View应该取的尺寸
AT_MOST当前尺寸是当前View能取的最大尺寸

上面的测量模式和我们布局时的 warp_content,match_parent以及写成固定的尺寸有什么对应关系呢?

match_parent--->EXACTLY。怎么理解呢?match_parent就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
warp_parent---> AT_MOST
我们想要将大小设置为包裹我们的View内容,那么尺寸大小就是父View给我作为参考的尺寸,至于不超过这个尺寸就可以啦。具体尺寸就根据我们的需求去设定。
固定尺寸(100dp)--->EXACTLY.用户自己制定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主。

重写 onMeasure 函数Demo

我们要实现的效果是一个小正方形。

private int getMySize(int defaultSize,int measureSpec){
    int mySize=defaultSize;
    //取测量模式
    int mode=MeasureSpec.getMode(measureSpec);
    //取测量长度
    int size=MeasureSpec.getSize(measureSpec);

    switch (mode){
        //如果没有指定大小,就设置为默认大小
        case MeasureSpec.UNSPECIFIED:
            mySize=defaultSize;
            break;
            //如果测量模式是最大取值size
            //我们将大小取最大值,你也可以取其他值
        case MeasureSpec.AT_MOST:
            mySize=size;
            break;

            //如果是固定的大小,那就不要去改变它
        case MeasureSpec.EXACTLY:
            mySize=size;
            break;
    }
    return mySize;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width=getMySize(100,widthMeasureSpec);
    int heigth=getMySize(100,heightMeasureSpec);
    if (width<heigth){
        heigth=width;
    }else{
        width=heigth;
    }
    setMeasuredDimension(width,heigth);
}

xml

<com.petterp.studybook.View.Views
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:background="#000"/>

这样的效果就是一个正方形了。

重写onDraw

上面我们通过重写 onMeasure 实现了布局的测量与设定,接下来就是绘制了。绘制的话 我们直接在画板 Canvas 对象上绘制就好。

我们以一个简单Demo来实现效果。

@Override
protected void onDraw(Canvas canvas) {
    //调用父View的onDraw函数,因为View这个类帮我们实现了一些
    //基本的绘制功能,拨入绘制背景颜色,背景图片等。
    super.onDraw(canvas);
    //也可以是 getMeasuredHieght()/2,因为这个例子中我们已将宽高设置相等了
    int r=getMeasuredWidth()/2;
    //圆心的横坐标为当前View的View左边起始位置+半径
    int centerx=getLeft()+r;
    //圆心的纵坐标表为当前的View的顶部起始位置+半径
    int centery =getTop()+r;

    //设置画笔
    @SuppressLint("DrawAllocation") Paint paint=new Paint();
    //设置画笔颜色
    paint.setColor(Color.GREEN);
    //开始绘制
    canvas.drawCircle(centerx,centery,r,paint);
}

效果出来就是一个小圆

在这里插入图片描述

自定义布局属性

如果有些属性我们希望由用户指定,只有当用户不指定的时候采用我们硬编码的值,比如上面的默认尺寸,我们想要由用户自己在布局文件里面指定该怎么做呢?所以这个时候就需要我们自定属性,让用户用我们定义的属性。

过程

首先我们需要在 res/values/styles.xml 文件(如果没有就需要新建),里面声明一个我们自定义的属性:

<!--name为声明的“属性集合”名,可以随便取,但是最好是设置为根我们的view一样的名称-->
<declare-styleable name="Views">
    <!--声明我们的属性,名称为 default_size,取值类型为尺寸(dp,dx等)-->
    <attr name="default_size" format="dimension"/>
</declare-styleable>

然后在我们自定义View里面吧我们自定义的属性值取出来,在构造函数中,有个AttributeSet的属性,我们需要用它来帮我们把布局里面的属性取出来。

private int defaultSize;
public Views(Context context,  AttributeSet attrs) {
    super(context, attrs);
    //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
    //即属性集合的标签,在R 文件中名称为 R,styleable+name
    TypedArray a=context.obtainStyledAttributes(attrs, R.styleable.Views);

    //第一个参数为属性几个里面的属性,R文件名称:R。styleable+属性集合名称+下划线+属性名称
    //第二个参数为,如果没有设置这个属性,则设置的默认的值
    defaultSize=a.getDimensionPixelSize(R.styleable.Views_default_size,100);

    //最后记得将TypedArray对象回收
    a.recycle();
}

最后附上完整的代码

package com.petterp.studybook.View;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.view.View;

import com.petterp.studybook.R;

/**
 * @author Petterp on 2019/6/27
 * Summary:
 * 邮箱:1509492795@qq.com
 */
public class Views extends View {
    public Views(Context context) {
        super(context);
    }
    private int defaultSize;
    public Views(Context context,  AttributeSet attrs) {
        super(context, attrs);
        //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
        //即属性集合的标签,在R 文件中名称为 R,styleable+name
        TypedArray a=context.obtainStyledAttributes(attrs, R.styleable.Views);

        //第一个参数为属性几个里面的属性,R文件名称:R。styleable+属性集合名称+下划线+属性名称
        //第二个参数为,如果没有设置这个属性,则设置的默认的值
        defaultSize=a.getDimensionPixelSize(R.styleable.Views_default_size,100);

        //最后记得将TypedArray对象回收
        a.recycle();
    }

    private int getMySize(int defaultSize,int measureSpec){
        int mySize=defaultSize;
        //取测量模式
        int mode=MeasureSpec.getMode(measureSpec);
        //取测量长度
        int size=MeasureSpec.getSize(measureSpec);

        switch (mode){
            //如果没有指定大小,就设置为默认大小
            case MeasureSpec.UNSPECIFIED:
                mySize=defaultSize;
                break;
                //如果测量模式是最大取值size
                //我们将大小取最大值,你也可以取其他值
            case MeasureSpec.AT_MOST:
                mySize=size;
                break;

                //如果是固定的大小,那就不要去改变它
            case MeasureSpec.EXACTLY:
                mySize=size;
                break;
        }
        return mySize;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width=getMySize(defaultSize,widthMeasureSpec);
        int heigth=getMySize(defaultSize,heightMeasureSpec);
        if (width<heigth){
            heigth=width;
        }else{
            width=heigth;
        }
        setMeasuredDimension(width,heigth);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //调用父View的onDraw函数,因为View这个类帮我们实现了一些
        //基本的绘制功能,拨入绘制背景颜色,背景图片等。
        super.onDraw(canvas);
        //也可以是 getMeasuredHieght()/2,因为这个例子中我们已将宽高设置相等了
        int r=getMeasuredWidth()/2;
        //圆心的横坐标为当前View的View左边起始位置+半径
        int centerx=getLeft()+r;
        //圆心的纵坐标表为当前的View的顶部起始位置+半径
        int centery =getTop()+r;

        //设置画笔
        @SuppressLint("DrawAllocation") Paint paint=new Paint();
        //设置画笔颜色
        paint.setColor(Color.GREEN);
        //开始绘制
        canvas.drawCircle(centerx,centery,r,paint);
    }
}

自定义ViewGroup

自定义View的过程简单,其实也就那几步,可自定义ViewGroup 可就比较麻烦了,因为不仅要管好自己,还要兼顾子View。因为 ViewGroup是一个容器,他装纳 子视图 并且负责把 子视图 放入指定的位置。

一般自定义viewgroup我们从这几方面去思考:

  1. 首先,我们需要知道各个子 view 的大小,只有知道子 view 的大小,我们才知道当前的View Group该设置为多大去容纳它们。
  2. 根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出ViewGroup的大小。
  3. ViewGroup 和 子View 的大小算出来之后,接下来就需要去摆放,具体的排放规则,一般按照我们的需求去定制。
  4. 知道了怎么摆放之后,还需要决定每个子view对号入座,把他们放进它们该放的地方。

实例Demo

我们仿照LinearLayout的垂直布局,将子view按从上到下垂直顺序一个接一个摆放。

public class MyViewGroup extends ViewGroup {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 切记,这里都是相对于父view
     * @param changed
     * @param l //相对于父view left距离
     * @param t //相对于父view top距离
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        //top重置为0
        int curHeight = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            int height = child.getMeasuredHeight();
            int width = child.getMeasuredWidth();
            //真正绘制的方法,其内部又有onlayout的调用,因为子view也是一个viewgroup
            child.layout(l, curHeight, l + width, curHeight + height);
            curHeight += height;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //将所有的子view进行测量,这会触发每个子view的 onMeasure函数
        //注意要与 measureChild 区分,measureChild 是对单个view进行测量
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        //测量模式
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        //测量大小
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);

        //获取子元素数量
        int childCount=getChildCount();

        //如果没有子view,当前viewGroup没有存在的意义,不用占用空间
        if (childCount==0){
            setMeasuredDimension(0,0);
        }else{
            //如果宽高都是包裹内容
            if (widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
                //我们将高度设置为所有子view的高度相加,宽度设置为子view中最大的宽度
                int height=getTotleHeight();
                int width=getMaxChildWidth();
                setMeasuredDimension(width,height);
            }else if (heightMode==MeasureSpec.AT_MOST){
                //如果只有高度是warp
                //宽度设置为ViewGroup自己的测量宽度,高度设置为所有view的高度总和
                setMeasuredDimension(widthSize,getTotleHeight());
            }else if (widthMode==MeasureSpec.AT_MOST){
                //宽度设置为子View中宽度最大的值,高度设置为 ViewGroup自己测量的值
                setMeasuredDimension(getMaxChildWidth(),heightSize);
            }
        }
    }

    /**
     * 获取子view中宽度最大的值
     * @return
     */
    private int getMaxChildWidth(){
        int childCount=getChildCount();
        int maxWidth=0;
        for (int i=0;i<childCount;i++){
            //获取相应的子view
            View  childView =getChildAt(i);
            //对比出最宽的子view -> 因为是垂直布局
            if (childView.getMeasuredWidth()>maxWidth){
                maxWidth=childView.getMeasuredWidth();
            }
        }
        return maxWidth;
    }

    /**
     * 将子view的高度相加
     * @return
     */
    private int getTotleHeight(){
        int childCount=getChildCount();
        int height=0;
        for (int i=0;i<childCount;i++){
            View childView=getChildAt(i);
            height+=childView.getMeasuredHeight();
        }
        return height;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_height="match_parent"
    tools:context=".View.ViewActivity">
    <com.petterp.studybook.View.MyViewGroup
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:text="1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:text="2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
        <Button
            android:text="3"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />
    </com.petterp.studybook.View.MyViewGroup>
</LinearLayout>

在这里插入图片描述
更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。

参照博客:非常感谢https://www.jianshu.com/p/c84693096e41

©️2020 CSDN 皮肤主题: 成长之路 设计师:Amelia_0503 返回首页