对于Android开发者来说,自定义View的重要性不言而喻,于是自己写了个小demo,自定义一个继承于ImageView的View。demo很简单,没有设计源码也没有过多的深入去研究,只是又学了些新的东西,就拿出来做做笔记,分享下。
写这篇真是费我好多时间花了将近4个小时,就这么点内容,不知道这么回事写着写着自己把电源线给踢掉了T.T 后来重写就很浮躁了。。。不知道有么有哪里写错了,欢迎加我微信沟通YCL774908055
自定义的流程
讲真一开始我对自定义真的很模糊,什么测量,绘制啊,我都不知道测量要测量什么怎么测量的,反正就是网上其实很多解释但是都没能理解(。。。),然后想想要不自己写个简单的试试可能就懂了。事实如此,自己写了些发现原来有些不理解的马上就通了然后感觉今天天气怎么那么好。
首先我们创建一个FoundImageView,
第一步肯定先走构造方法
上代码:
public FoundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context,attrs);
}
private void initView(Context context,AttributeSet attrs){
matrix = new Matrix();//矩阵
paint = new Paint();//画笔
paint.setAntiAlias(true);//抗锯齿
//从attr.xml下的declare-styleable获取控件的属性集合
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FoundImageView);
//默认为10dp,得到的尺寸是dp乘以屏幕密度得到的是px
borderRadius = typedArray.getDimensionPixelSize(R.styleable.FoundImageView_borderRadius,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,getResources().getDisplayMetrics()));
type = typedArray.getInt(R.styleable.FoundImageView_type,TYPE_CRICLE);//默认为circle
typedArray.recycle();//回收
}
构造方法里初始化了矩阵、画笔。
第二步定义属性
在values下创建一个attr.xml的文件,先看代码,这里的属性可以在代码里获取
上代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="borderRadius" format="dimension" />
<attr name="type">
<enum name="cricle" value="1" />
<enum name="found" value="0" />
</attr>
<attr name="apiKey" format="integer"/>
<!-- 控件的属性定义 写个是为了高度定义控件 这些属性可以在布局文件下直接使用 通过obtainStyledAttributes来获取这些属性 -->
<declare-styleable name="FoundImageView">
<attr name="borderRadius" />
<attr name="type"/>
<attr name="apiKey"/>
</declare-styleable>
</resources>
第三步在构造函数里调用obtainStyledAttributes获取控件的属性集
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.FoundImageView);
//默认为10dp,得到的尺寸是dp乘以屏幕密度得到的是px
borderRadius = typedArray.getDimensionPixelSize(R.styleable.FoundImageView_borderRadius,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,getResources().getDisplayMetrics()));
type = typedArray.getInt(R.styleable.FoundImageView_type,TYPE_CRICLE);//默认为circle
typedArray.recycle();//回收
TypedArray是一个集合通过obtainStyledAttributes获取到attr.xml里的所以属性集合,R.styleable.FoundImageView就是attr下的<declare-styleable name="FoundImageView">
集合就是下面的节点type等。typedArray.recycle();这个方法记得调用,原因百度很多自行手动哈。
第四步在布局文件里引用这个FounImageView,和正常使用view一样,只是多了包名路径
<com.yclgamesdk.newbee.ui.FoundImageView
app:borderRadius="50dp"
app:type="cricle"
android:src="@drawable/qwe"
android:layout_width="200dp"
android:layout_height="250dp" />
第五步现在可以测量了,
这个就是我之前迷惑的地方,要测量肯定得有指定的宽高才可以测量,所以所谓的测量就是在代码里得到布局文件下设置的宽高。
上代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int viewWidth = MeasureSpec.getSize(widthMeasureSpec);
int viewHeight = MeasureSpec.getSize(heightMeasureSpec);
LogUtil.i(Constants.TAG,"onMeasure");
if(type == TYPE_CRICLE){//如果是圆形,就强制改变view的宽高,使他的宽高一致,如果圆角矩形宽高多少就是多少
width = Math.min(getMeasuredWidth(),getMeasuredHeight());
raduis = width / 2;//圆角为直径的一半
setMeasuredDimension(width,width);//因为是圆形的,所以测量尺寸得到宽高后取小的作为直径重新设置尺寸
}
}
setMeasuredDimension(width,width);这个方法可以重新设置view的宽高,这里是要画圆形头像所以重新设置直径.代码解释应该算清晰的了。
第六步这里说下onLayout,在view继承了ViewGroup时候才需要重写onLyout,这个见名知意,就是在设置布局的时候使用到,这里继承的是ImageView,所以不重写onLayout,所以我们可以直接绘制
上菜:
/**
* 绘制,把drawable资源转换成Bitmap格式图片,然后用BitmapShader把图片作为纹理渲染
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
LogUtil.i(Constants.TAG,"onDraw");
if(getDrawable() == null){
LogUtil.i(Constants.TAG,"getDrawable null");
return;
}
setUpShader();
if(type ==TYPE_CRICLE){
canvas.drawCircle(raduis,raduis,raduis,paint);//圆心的位置,x、y轴的位置
}else {
canvas.drawRoundRect(rectF,borderRadius,borderRadius,paint);
}
}
setUpShader()这是着色器
/**
* 设置BitmapShader着色器
*/
private void setUpShader(){
Drawable drawable = getDrawable();
if(drawable == null){
LogUtil.i(Constants.TAG,"drawable = null");
return;
}
Bitmap bitmap = drawable2Bitmap(drawable);
bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);//CLAMP拉伸 拉伸的是图片最后的哪一个像素,不断重复
float scale = 1.0f;
if(type == TYPE_CRICLE){
// 拿到bitmap宽或高的小值
int size = Math.min(bitmap.getWidth(),bitmap.getHeight());
scale = width*1.0f/size;
}else if (type == TYPE_ROUND){
// 如果图片的宽或者高与view的宽高不匹配,计算出需要缩放的比例;缩放后的图片的宽高,一定要大于我们view的宽高;所以我们这里取大值;
scale = Math.max(getWidth()*1.0f/bitmap.getWidth(),getHeight()*1.0f/bitmap.getHeight());
}
matrix.setScale(scale,scale);
bitmapShader.setLocalMatrix(matrix);
paint.setShader(bitmapShader);
}
着色器,其实就是通过getDrawable去获取drawable资源然后转换成Bitmap,然后实例化BitmapShader着色器,
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
bitmap是我们要进行着色的位图 tileX是我们在X轴的平铺模式tileY是我们在Y轴的平铺模式,平铺模式有三种CLAM拉伸(就是图像最边缘的最后一个像素会被拉伸)、
MIRROR(说白了,就是图像不停翻转来平铺,直到平铺完毕)、REPEAT(其实就是重复复制图像)
这里盗图了(图片来自http://www.jianshu.com/p/1efcc9c9f286,兄弟莫怪)
X和Y都设置MIRROR,就翻转翻转翻转
X和Y都设置REPEAT,就是复制复制复制
X和Y都设置CLAM,这个拉伸有点夸张
那么应该能知道这个BitmapShader的作用了吧,官方文档说的很清楚了:Shader used to draw a bitmap as a texture.BitmapShader的作用是使用特定的图片来作为纹理来使用。那么官方文档有时总是感觉像在看天书,具体大伙自个去理解吧我也不知道自己的理解对不对说出来怕误导大家,下面找到了个对三个模式的解释。
* CLMP 如果需要填充的内容大小超过了bitmap size就选bitmap边界的颜色进行扩展
* REPEAT重复,不断的重复bitmap去填满,如果绘制的区域大于纹理图片的话,纹理图片会在这片区域不断重复
* MIRROR镜像的去填满。如果绘制的区域大于纹理图片的话,纹理图片会以镜像的形式重复出现
来自:http://www.jianshu.com/p/94093d8b168a
第六步开始绘制
这步很简单直接上代码
/**
* 绘制,把drawable资源转换成Bitmap格式图片,然后用BitmapShader把图片作为纹理渲染
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
LogUtil.i(Constants.TAG,"onDraw");
if(getDrawable() == null){
LogUtil.i(Constants.TAG,"getDrawable null");
return;
}
setUpShader();
if(type ==TYPE_CRICLE){
canvas.drawCircle(raduis,raduis,raduis,paint);//圆心的位置,x、y轴的位置
}else {
canvas.drawRoundRect(rectF,borderRadius,borderRadius,paint);
}
}
前面有个方法,这里贴出来
/**
* drawable转换成Bitmap
* @param drawable
* @return
*/
public Bitmap drawable2Bitmap(Drawable drawable){
if(drawable instanceof BitmapDrawable){//判断drawable是否属于BitmapDrawable,即继承关系
BitmapDrawable bd = (BitmapDrawable) drawable;//drawable强转BitmapDrawable
return bd.getBitmap();//强转后获取Bitmap
}
int w = drawable.getIntrinsicWidth();
int h = drawable.getIntrinsicHeight();
Bitmap bitmap = Bitmap.createBitmap(w,h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);//实例画布
drawable.setBounds(0,0,w,h);
drawable.draw(canvas);
return bitmap;
}
基本就是这样了,现在可以直接在布局文件使用这个控件了。自己也可以尝试去写个修改图片类型,圆角大小,半径大小的接口,这个也挺简单大家自己去试吧。
这里捋下流程:构造方法(初始化画笔什么的)---->布局文件引用控件(设置属性)---->TypedArray获取属性集(圆角大小等等)---->测量(得到布局文件设置的宽高,自个再去修改宽高等等)---->把drawable转换成bitmap---->BitmapShader着色器(设置好着色器给画笔绑定上)---->绘制。