自定义控件---自定义view
对于Android中的控件,已经完全不模式了,Android提供了很多已有的控件,比如TextView,Button,ImageView之类的,我们对其的使用也很熟练了,但是如果要问这些控件是怎么实现的,之前我还真说不上来,今天学习了一些,大致了解了自定义view的方法。
回想一下android中的视图,是一个viewroot->viewgrop->view的一个模式,可以理解为android中有一个视图树;那么问题来了,一个view,它展现在界面上,需要哪些必要条件呢?我的理解是:1.父容器(顶层容器为PhoneWindwo->DecorView,记录一中介绍过);2view所显示的位置;3.view所展示的内容;4.view的尺寸大小。
现在回想一下我们定义一个Button的过程,我们通常会给这一个Button设置一些属性,比如height,width,background,padding,text之类的,这就是定义view的第一步,设置这个view的属性,我们在values/attr.xml中定义一个view的属性,比如这个view的name,这个view所接受的参数名称,参数类型,如下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SimpleImageView">
<attr name="src" format="integer">
</attr>
</declare-styleable>
</resources>
上述代码定义了一个名为SimpleImageView的控件,接受一个名为src,类型为整形的参数,也就是一张图片(R.drawable.xxx),这就是一个简单的图片view的定义;
有了这个定义,我们要去做的是一个简单的图片显示控件,在此之前介绍一下画布跟画笔(canvas、Paint),这俩东西的理解就从字面上理解就好,我们要去实现一个控件,我们需要一张画布去展示这个控件,同时还需要一支画笔去把这个控件画出来,我们通常的做法是先定义画笔的属性,比如粗细和颜色,然后再调用画布的方法,比如调用canvas.drawBitmap(),去实现绘制。
介绍完了画布和画笔,现在我们需要写一个继承自View类的控件类,其中构造方法传入的参数有二:1.Context---上下文;2.AttributeSet---属性<key,value>;如下代码为一个简单的图片显示控件类的代码,需要理解的地方已用注释解释,在文章中就不多做介绍:
package com.example.administrator.dentifyview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
import android.icu.util.Measure;
import android.util.AttributeSet;
import android.view.View;
import java.util.jar.Attributes;
/**
* Created by Administrator on 2018/2/10.
*/
public class SimpleImageView extends View {
private Paint mBitmapPaint;
private Drawable mDrawable;
private int mWidth;
private int mHeight;
public SimpleImageView(Context context)
{
super(context,null);
}
//attrs理解为控件的属性集合
public SimpleImageView(Context context, AttributeSet attrs)
{
super(context,attrs);
initAttrs(attrs);
mBitmapPaint = new Paint();
//抗锯齿,设为true
mBitmapPaint.setAntiAlias(true);
}
private void initAttrs(AttributeSet attrs)
{
if(attrs != null)
{
TypedArray array = null;
try{
array = getContext().obtainStyledAttributes(
attrs,R.styleable.SimpleImageView);
mDrawable = array.getDrawable(R.styleable.SimpleImageView_src);
measureDrawable();
}finally {
if(array != null){
//必须recycle,此处array是在池中获取一个以后的对象,使用后将对象重置放回池中;
//使用了池+单例模式,具体见TypedArray相关源码
array.recycle();
}
}
}
}
private void measureDrawable()
{
if(mDrawable == null)
{
throw new RuntimeException("drawable 不能为空");
}
//获取drawable固有的宽度和高度
mWidth = mDrawable.getIntrinsicWidth();
mHeight = mDrawable.getIntrinsicHeight();
}
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec)
{
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//此方法决定了当前view的大小
setMeasuredDimension(measureWidth(widthMode,width),measureHeight(heightMode,height));
}
private int measureWidth(int mode,int width)
{
switch(mode)
{
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.EXACTLY:
mWidth = width;
break;
}
return mWidth;
}
private int measureHeight(int mode,int height)
{
switch(mode)
{
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.EXACTLY:
mHeight = height;
break;
}
return mHeight;
}
@Override
protected void onDraw(Canvas canvas)
{
if(mDrawable == null)
{
return;
}
// 用于将图像改变尺寸,先根据View的尺寸重新创建一个图片,在绘制到View上。
// Bitmap.createScaledBitmap(drawableToBitmap(mDrawable),getMeasuredWidth(),getMeasuredHeight(),true)
canvas.drawBitmap(Bitmap.createScaledBitmap(drawableToBitmap(mDrawable),getMeasuredWidth(),getMeasuredHeight(),true),getLeft(),getTop(),mBitmapPaint);
}
//写一个函数完成drawable向bitmap的转换
private Bitmap drawableToBitmap(Drawable drawable)
{
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
// 取 drawable 的颜色格式
Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
: Bitmap.Config.RGB_565;
// 建立对应 bitmap
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
// 建立对应 bitmap 的画布
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, w, h);
// 把 drawable 内容画到画布中
drawable.draw(canvas);
return bitmap;
}
}
其中对于onMeasure()和onDraw()函数的功能和作用我第一次看的时候也不是很清楚,这是两个需要被override的函数,其实measure,draw,layout三个函数分别对应着尺寸,绘图,和视图布局,因为我们这个控件只用于展示图片,所以不需要处理layout。measure和draw的意思就是测量尺寸与绘制。测量尺寸是为了得到这个控件的大小,绘制的作用就不用说了,大家应该能理解。
我们知道对于一个控件的尺寸往往在布局界面中我们会设置这样一些取指:wrap_content,match_parent,xxxdp之类的,所以我们需要onMeasure函数的存在,在得到控件应该展示的尺寸之后,我们使用setMeasuredDimension(width,height)函数进行设置控件的尺寸。
此处需要注意,onMeasure函数传入的参数为:int widthMeasureSpec,int heightMeasureSpec;
这两个参数分别用于确定视图的宽度高度的规格和大小,MeasureSpec的值有specSize和specMode共同构成,其中specSize记录的是父容器大小,specMode记录的是规格:
规格的对于一般为:EXACTLY---match_parent/具体数值;AT_MOST---wrap_content;UNSPECIFIED---没有任何限制;
以上述简单的图片view为例子,我们要去展示一张图片,我们先获取了图片的长宽,但这个长宽受限于view的尺寸,如果在使用这个view的时候,我们定义的是wrap_content,OK,我们就以图片的长宽定义view的尺寸即可,但是如果不是这样,比如match_parent,我们的图片的显示就得依赖于控件的尺寸了,所以如代码中所示,添加了一些switch-case语句进行判断;并且注意,在绘制的时候,我们可以用bitmap的压缩方法进行图片尺寸的修改,比如match_parent的时候,我们就需要将图片改变尺寸获得一张新的图片,与控件的长宽一致,最后再绘制到画布当中。
使用此控件的布局如下代码:<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:img="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="com.example.administrator.dentifyview.MainActivity"> <com.example.administrator.dentifyview.SimpleImageView android:layout_width="match_parent" android:layout_height="match_parent" img:src = "@drawable/yiwu"/> </RelativeLayout>