为什么要自定义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); }
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);
测量模式 表示意思 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.用户自己制定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主。
我们要实现的效果是一个小正方形。
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"/>
这样的效果就是一个正方形了。
上面我们通过重写 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文件中的标签
//即属性集合的标签,在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:
* 邮箱:[email protected]
*/
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文件中的标签
//即属性集合的标签,在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);
}
}
自定义View的过程简单,其实也就那几步,可自定义ViewGroup 可就比较麻烦了,因为不仅要管好自己,还要兼顾子View。因为 ViewGroup是一个容器,他装纳 子视图 并且负责把 子视图 放入指定的位置。
一般自定义viewgroup我们从这几方面去思考:
- 首先,我们需要知道各个子 view 的大小,只有知道子 view 的大小,我们才知道当前的View Group该设置为多大去容纳它们。
- 根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出ViewGroup的大小。
- ViewGroup 和 子View 的大小算出来之后,接下来就需要去摆放,具体的排放规则,一般按照我们的需求去定制。
- 知道了怎么摆放之后,还需要决定每个子view对号入座,把他们放进它们该放的地方。
我们仿照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;
}
}
更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。
参照博客:非常感谢https://www.jianshu.com/p/c84693096e41