自定义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)
主要用来测量布局,其参数widthMeasureSpec和heightMeasureSpec包含宽和高的信息和测量模式。
为什么一个 数里面能放两个信息呢?我们知道,我们在设置宽高时有三个选择,
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);
上面的测量模式和我们布局时的 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我们从这几方面去思考:
- 首先,我们需要知道各个子 view 的大小,只有知道子 view 的大小,我们才知道当前的View Group该设置为多大去容纳它们。
- 根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出ViewGroup的大小。
- ViewGroup 和 子View 的大小算出来之后,接下来就需要去摆放,具体的排放规则,一般按照我们的需求去定制。
- 知道了怎么摆放之后,还需要决定每个子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>