自定义控件
整理自:参考:https://www.jianshu.com/p/c84693096e41
《第一行代码》
View是安卓中最基本的UI组件,可以在屏幕上绘制一个矩形区域,并相应这个区域各种事件。ViewGroup是特殊的View,它包含很多子View和子ViewGroup,是一个用来放置布局和控件的容器。
当系统提供的控件不足以满足我们的业务需求的时候,就要自定义控件了。
一.例子:自制一个自定义标题栏控件
(1)首先定义一个标题栏布局
我先来一个丑丑的,名字叫title
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:background="@color/colorTitle"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/back"
android:text="back"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/title"
android:text="title"
android:layout_weight="1"
android:layout_width="0dp"
android:gravity="center"
android:textSize="24dp"
android:layout_height="wrap_content" />
<Button
android:id="@+id/edit"
android:text="edit"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
如果想要复用布局,不需要逻辑可以直接用这个语句引入布局,需要逻辑处理才需要自定义控件,这个时候直接引入,和普通控件用法差不多,但是要完整包名
<?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"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.uicustomviewtitle.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.uicustomviewtitle.TitleLayout>
</LinearLayout>
(2)然后让这个布局继承自已有的一些布局,这里我继承LinearLayout,重写有LinearLayout两个参数的的构造方法,在引用这个自定义标题栏的时候就会调用构造函数。
接着用LayoutInflater动态加载布局,用其from()方法可以构造出LayoutInflater对象,然后调用inflate()方法实现动态加载,其接受两个参数,第一个是要加载的布局,第二个是给加载好的布局传入一个父布局,这里我们指定父布局TitleLayout,所以传入this
最后为按钮增加逻辑。
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){
super(context,attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
//为按钮添加逻辑
Button backButton=(Button)findViewById(R.id.back);
backButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity)getContext()).finish();
}
});
Button editButton=(Button)findViewById(R.id.edit);
editButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(),"hhah",Toast.LENGTH_SHORT).show();
}
});
}
}
以上就是自定义控件了
二.继续分析自定义View
通常来讲,自定义View需要重写两个方法,一个是onMeasure(),另一个是onDraw(),
1.onMeasure()方法
(1)用于测量屏幕大小,当自带的(wrap_content以及match_parent)满足不了需求时,就需要重写这个了。用于测量屏幕的大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
两个参数widthMeasureSpec, heightMeasureSpec,包含控件的宽高信息,此外还有测量模式信息,测量模式有以下三种
测量模式 | 表示意思 |
---|---|
UNSPECIFIED | 父容器没有对当前View有任何限制,当前View可以任意取尺寸 |
EXACTLY | 当前的尺寸就是当前View应该取的尺寸 |
AT_MOST | 当前尺寸是当前View能取的最大尺寸 |
获得测量模式以及宽高用以下函数
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
因为int有32位,三种测量模式用2位可以表示,余下的30位就是用来表示宽高的。
对应于wrap_content以及match_parent以及设置固定尺寸对应如下
match_parent
—>EXACTLY。怎么理解呢?match_parent
就是要利用父View给我们提供的所有剩余空间,而父View剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content
—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的view内容,那么尺寸大小就是父View给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)
—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。
(2)重写onMeasure方法
这里我们实现一个正方形控件,默认边长100dp
首先自定义一个MyView,继承自View,然后实现至少下面两个构造方法,再重写onMeasure(),最后在布局文件中引入
package com.example.uicustomviewtitle;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
MyView(Context context){
super(context);
}
MyView(Context context, AttributeSet attrs){
super(context,attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//这里实现绘制一个正方形控件的功能
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width=getSize(100,widthMeasureSpec);
int height=getSize(100,heightMeasureSpec);
//不确定hight,width那个小
int a=height>width?width:height;
setMeasuredDimension(a,a);
}
private int getSize(int defaultSize,int measureSize){
int mySize=defaultSize;
int size=MeasureSpec.getSize(measureSize);
int mode=MeasureSpec.getMode(measureSize);
switch(mode){
case MeasureSpec.UNSPECIFIED://没有指定大小,设置为默认大小
mySize=defaultSize;
break;
case MeasureSpec.AT_MOST://指定的是最大值,可以设置为指定的值,也可以设置成为别的
mySize=size;
break;
case MeasureSpec.EXACTLY://指定了固定值,就设置为测量的值
mySize=size;
break;
default: break;
}
return mySize;
}
}
布局
<com.example.uicustomviewtitle.MyView
android:background="@color/colorAccent"
android:layout_width="100dp"
android:layout_height="match_parent" />
2.onDraw()方法
onMeasure()方法让我们可以确定控件的尺寸,具体样子可以通过onDraw()方法绘制出来,直接在画板Canvas对象上绘制即可
这里我画一个圆
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);//继承父类的onDraw()方法,里面实现了绘制背景颜色等方法
int r=getMeasuredWidth()/2;//这里也可以是getMeasuredWidth/2,因为前面宽高已经相等了。圆半径
//定义圆心坐标
int scaleX=getLeft()+r;//横坐标当前View控件距离左部距离加半径
int scaleY=getTop()+r;//纵坐标当前View距离顶部距离加半径
Paint paint=new Paint();
paint.setColor(Color.GREEN);
//开始绘制
canvas.drawCircle(scaleX,scaleY,r,paint);
}
最后就是这样了
三.自定义ViewGroup
ViewGroup不仅有本身,还有里面包含的子view,所以处理起来比自定义view复杂一些
(1)首先测量所有子view的大小,接下来根据子view和ViewGroup的逻辑给出ViewGroup的大小
重写onMeasure()方法实现这一点
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);//可以触发所有子View的onMeasure,区分measureChild,只有一个
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int childCount=getChildCount();
if(childCount==0){//没有子控件,不占据位置
setMeasuredDimension(0,0);
}
else{//如果宽高都装填内容
if(widthMode==MeasureSpec.AT_MOST&&heightMode==MeasureSpec.AT_MOST){
//我们将高度设置为所有子View的高度相加,宽度设为子View中最大的宽度
int width=getMaxWidth();
int height=getTotalHeight();
setMeasuredDimension(width,height);
}else if(widthMode==MeasureSpec.AT_MOST){//只有宽度包裹内容
setMeasuredDimension(getMaxWidth(),heightSize);
}else if(heightMode==MeasureSpec.AT_MOST){//只有高度包裹内容
setMeasuredDimension(widthSize,getTotalHeight());
}
}
}
public int getMaxWidth(){
int maxWidth=0;
for(int i=0;i<getChildCount();i++){
View childView=getChildAt(i);
if(childView.getMeasuredWidth()>maxWidth){
maxWidth=childView.getMeasuredWidth();
}
}
return maxWidth;
}
public int getTotalHeight(){
int totalHeight=0;
for(int i=0;i<getChildCount();i++){
totalHeight+=getChildAt(i).getMeasuredHeight();
}
return totalHeight;
}
}
(2)然后就是每个view怎么摆放的问题,水平垂直,给他们分配出空间进行摆放,重写onLayout()方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount=getChildCount();
int curHeight=t;
for(int i=0;i<childCount;i++){
View child=getChildAt(i);
int height=child.getMeasuredHeight();
int width=child.getMeasuredWidth();
//控件的左上右下布局
child.layout(l,curHeight,l+width,curHeight+height);
curHeight+=height;
}
}
(3)定义一下布局
<?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"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customviewgroup.MyViewGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff9900">
<Button
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="btn" />
<Button
android:layout_width="50dp"
android:layout_height="wrap_content"
android:text="btn" />
</com.example.customviewgroup.MyViewGroup>
</LinearLayout>
最后效果