简单自定义View 的实现
自定义View的实现
开始自定义View 的实现
我们自定义view 需要重写其中的两个方法:onMeasure()
是测量当前View 的尺寸onDraw()
负责把这个View 绘制出来
还得重写至少两个函数:
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
onMeasure
自定义View 的时候首先需要测量一下这个View 的尺寸。为什么要测量呢?首先我们来看google在xml 中给我们了两个
属性layout_widht
和layout_height
并且还有还有两个值:match_parent
和wrap_content
一个是包裹内容,一个自适应。但是这两个属性值并没有给我们具体的数值,但是在屏幕上显示东西必须得有数值,所以只能我们来计算一下。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
乍一看参数中的这两个东西是什么鬼?看着无非是宽高呀?其实这个里面不只是有宽高,还有测量模式。那么一定会有人问什么测量模式是什么东西?这个我们稍后讲解。我们知道,我们在设置宽高时有3个选择:wrap_content
、match_parent
以及指定固定尺寸
,而测量模式也有3种:UNSPECIFIED
,EXACTLY
,AT_MOST
。可以看出来我们设置的无非就是这三种。
如果使用二进制数据保存,2个bit 就可以搞定了,因为两个bit 的取值范围是[0,3] 里面四个数据完全够使用的了。我们知道int型数据占用32个bit,而google实现的是,将int数据的前面2个bit用于区分不同的布局模式,后面30个bit存放的是尺寸的数据。
那么我们怎么从里面取出来测量模式与尺寸呢?
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
这里的尺寸并不是最终的大小,否则测量模式又有什么用呢。请看下面
MeasureSpec 的mode 解释
MeasureSpec
的测量模式mode
一共有三种分别是:MeasureSpec.UNSPECIFIED
这个表示 父容器没有对当前View有任何限制,当前View可以任意取尺寸也就是我们自定义的 大小MeasureSpec.AT_MOST
可以用wrap_content
表示,是当前View能取的最大尺寸,使用系统测量出来的大小MeasureSpec.EXACTLY
可以用 match_parent
表示,当前View应该取的尺寸,使用系统测量出来的大小
由此可知测量模式的具体用法。
动手写写 onMeasure
函数
假设我们要实现一个正方形的View 那么该怎么写呢? 首先宽高需要一样,并且默认的宽高是 100dp 。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(defaultSize, widthMeasureSpec);
int height = getMySize(defaultSize, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
/**
* 获取View尺寸大小
* @param defaultSize 默认大小
* @param measureSpec
* @return
*/
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;
case MeasureSpec.AT_MOST: // wrap_content 自适应大小,取测量出来的数据
mySize = size;
break;
case MeasureSpec.EXACTLY: //match_parent或者设置了默认的大小
mySize = size;
break;
}
return mySize;
}
看我们的布局
<com.stevefat.myapplication.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/colorAccent"/>
最终效果图
不重写onMeasure,效果则是如下:
重写onDraw()
我们已经学会了如何测量View 的尺寸,那么我们来结合实际画一个圆来实操一下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设置圆的的半径,
int r = getMeasuredWidth()/2;
//圆心的横坐标为左起加上半径
int centenX = r+getLeft();
//圆心的纵坐标为头部加上半径
int centexY= r+getTop();
//定义画笔
Paint paint = new Paint();
//画笔设置颜色
paint.setColor(Color.GREEN);
canvas.drawCircle(centenX,centexY,r,paint);
}
效果如下
自定义属性
有时候我们需要灵活运用,不在代码中写死,这时候我们就需要自定义属性了
在res/values/styles.xml
中 声明我们的自定义属性
<resources>
<!--name为声明的"属性集合"名,最好是设置为跟我们的View一样的名称-->
<declare-styleable name="MyView">
<!--声明我们的属性,名称为default_size,取值类型为尺寸类型(dp,px等)-->
<attr name="default_size" format="dimension"/>
</declare-styleable>
</resources>
接下来在布局中运用起来
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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.stevefat.myapplication.MyView
android:layout_width="match_parent"
android:layout_height="100dp"
app:default_size="100dp"
android:background="@color/colorAccent"/>
</LinearLayout>
注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如hc,命名空间后面取得值是固定的:"http://schemas.android.com/apk/res-auto"
最后在我们的类里面取出来自定义的属性
private int defaultSize;
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
//即属性集合的标签,在R文件中名称为R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
defaultSize = a.getDimensionPixelSize(R.styleable.MyView_default_size,100);
//最后记得将TypedArray对象回收
a.recycle();
}
最后附上完整的自定义View
package com.stevefat.myapplication;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
private int defaultSize;
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//第二个参数就是我们在styles.xml文件中的<declare-styleable>标签
//即属性集合的标签,在R文件中名称为R.styleable+name
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.MyView);
//第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称
//第二个参数为,如果没有设置这个属性,则设置的默认的值
defaultSize = a.getDimensionPixelSize(R.styleable.MyView_default_size,100);
//最后记得将TypedArray对象回收
a.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(defaultSize, widthMeasureSpec);
int height = getMySize(defaultSize, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
/**
* 获取View尺寸大小
* @param defaultSize 默认大小
* @param measureSpec
* @return
*/
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;
case MeasureSpec.AT_MOST: // wrap_content 自适应大小,取测量出来的数据
mySize = size;
break;
case MeasureSpec.EXACTLY: //match_parent或者设置了默认的大小
mySize = size;
break;
}
return mySize;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//设置圆的的半径,
int r = getMeasuredWidth()/2;
//圆心的横坐标为左起加上半径
int centenX = r+getLeft();
//圆心的纵坐标为头部加上半径
int centexY= r+getTop();
//定义画笔
Paint paint = new Paint();
//画笔设置颜色
paint.setColor(Color.GREEN);
canvas.drawCircle(centenX,centexY,r,paint);
}
}
自定义ViewGroup 布局
自定义View 很简单,但是自定义ViewGroup 可就不一样了,不仅要兼顾自身,还要考虑子View的设置。ViewGroup 是个容器,里面存放了很多子View。怎么设计一个ViewGroup 的容器呢?
1.首先需要知道每个View 的大小,只有知道子View 的大小才知道ViewGroup 需要多大才能包容他们
2.根据子View 的大小,以及ViewGroup 要实现的功能,来巨鼎ViewGroup 的大小
3.ViewGroup的大小计算出来了,那么就需要按照自己想要的格式去摆放了。
4.知道怎么摆放还不行,摆放的位置决定了要把已有的空间划分成一个一个的的空间,然后根据摆放规则对号入座。
简单写一个案例,参照Linearlayout 从上到下摆放
首先重新onMeasureSpec,实现测量子View 以及设置ViewGroup的大小
@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);
//获取子View 的总数
int childCount = getChildCount();
//如果没有子View,当前ViewGroup 没有存在的意义,不占用空间
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else {
//如果宽高都是包裹内容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我们将高度设置为所有子View的高度总,宽度设置为子View 最大的宽度
int heightTotal = getHeightTotal();
int widthMax = getwidthMax();
setMeasuredDimension(widthMax, heightTotal);
} else if (heightMode == MeasureSpec.AT_MOST) { //如果只有高度是包裹内容
//宽度设置为测量的宽度,高度为所有子view 的综合
setMeasuredDimension(widthSize, getHeightTotal());
} else if (widthMode == MeasureSpec.AT_MOST) { //如果只有宽度是包裹内容
//宽度设置为最宽的那个,高度设置为测量的高度
setMeasuredDimension(getwidthMax(), heightSize);
}
}
}
/**
* 获取子View的高度总和
*
* @return
*/
private int getHeightTotal() {
int childCount = getChildCount();
int heightTotal = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
heightTotal += childView.getMeasuredHeight();
}
return heightTotal;
}
/**
* 获取最宽的那个view的宽度
*
* @return
*/
private int getwidthMax() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getMeasuredWidth() > maxWidth) {
maxWidth = childView.getMeasuredWidth();
}
}
return maxWidth;
}
以上代码功能可以看注释,不在解释了,子view 都已经测量好了,下面就是开始摆放子View 的位置了
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//获取子view 的总数
int childCount = getChildCount();
//记录当前的高度位置
int currentHeight= top;
//将子View 逐一摆放
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int height= childView.getMeasuredHeight();
int width = childView.getMeasuredWidth();
//设置子View 的位置,左 上 右 下
childView.layout(left,currentHeight,left+width,currentHeight+height);
currentHeight +=height;
}
}
我们来测试一下,向容器里面摆放三个Button 试试看效果。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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.stevefat.myapplication.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent">
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="btn1" />
<Button
android:layout_width="180dp"
android:layout_height="wrap_content"
android:text="btn2" />
<Button
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="btn3" />
</com.stevefat.myapplication.MyViewGroup>
</LinearLayout>
最后效果图
至此我们已经实现了LinnerLayout 的效果。
附上完整代码:
package com.stevefat.myapplication;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* 实现一个LinerLayout 的布局
*/
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//获取子view 的总数
int childCount = getChildCount();
//记录当前的高度位置
int currentHeight= top;
//将子View 逐一摆放
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int height= childView.getMeasuredHeight();
int width = childView.getMeasuredWidth();
//设置子View 的位置,左 上 右 下
childView.layout(left,currentHeight,left+width,currentHeight+height);
currentHeight +=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);
//获取子View 的总数
int childCount = getChildCount();
//如果没有子View,当前ViewGroup 没有存在的意义,不占用空间
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else {
//如果宽高都是包裹内容
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//我们将高度设置为所有子View的高度总,宽度设置为子View 最大的宽度
int heightTotal = getHeightTotal();
int widthMax = getwidthMax();
setMeasuredDimension(widthMax, heightTotal);
} else if (heightMode == MeasureSpec.AT_MOST) { //如果只有高度是包裹内容
//宽度设置为测量的宽度,高度为所有子view 的综合
setMeasuredDimension(widthSize, getHeightTotal());
} else if (widthMode == MeasureSpec.AT_MOST) { //如果只有宽度是包裹内容
//宽度设置为最宽的那个,高度设置为测量的高度
setMeasuredDimension(getwidthMax(), heightSize);
}
}
}
/**
* 获取子View的高度总和
*
* @return
*/
private int getHeightTotal() {
int childCount = getChildCount();
int heightTotal = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
heightTotal += childView.getMeasuredHeight();
}
return heightTotal;
}
/**
* 获取最宽的那个view的宽度
*
* @return
*/
private int getwidthMax() {
int childCount = getChildCount();
int maxWidth = 0;
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
if (childView.getMeasuredWidth() > maxWidth) {
maxWidth = childView.getMeasuredWidth();
}
}
return maxWidth;
}
}