Android自定义View学习笔记03
参考gitHub上面的开源项目CircleImageView
预备知识
BitMap
类
BitMap
位图类,其中有一个嵌套类叫Bitmap.Config
,内部有四个枚举值。这个类的作用是定义位图存储质量,即存储一个像素的位数,以及是否能显示透明、半透明颜色(Possible bitmap configurations. A bitmap configuration describes how pixels are stored. This affects the quality (color depth) as well as the ability to display transparent/translucent colors)。
A 透明度 R 红色 G 绿色 B 蓝色
Bitmap.Config ALPHA_8 只存储透明度,其他颜色不存储
Bitmap.Config ARGB_4444 16 每个像素 占四位
Bitmap.Config ARGB_8888 32 每个像素 占八位
Bitmap.Config RGB_565 16 R占5位 G占6位 B占5位 没有透明度(A)
参考博客
矩形类
Rect
类,通过定义四条(左、上、右、下)边来组成一个矩形。别扭的是,参数代表的是每条边距离x轴或者y轴的距离。RectF
类,同上,不同的是Rect
的参数类型的整形,带F的是单精度浮点数。另外Rect
对象可以作为参数来构造RectF
对象。
一个很重要的类BitmapShader
位图渲染器
BitmapShader
位图渲染器,用位图当做纹理来画图,通过设置模式来设置不同的渲染效果(Shader used to draw a bitmap as a texture. The bitmap can be repeated or mirrored by setting the tiling mode)。
- 构造方法
BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
,参数分别是,当做纹理的位图、横轴方向的渲染方式、纵轴方向的渲染方式。 - 有个嵌套类
Shader.TileMode
瓦片模式,里面定义了三个枚举类型的变量。
CLAMP
如果渲染器超出了原始边界范围,会复制原始边界的颜色在范围外渲染。
REPEAT
如果显示范围大于图片的大小,则会横向和纵向的重复渲染图片,进行平铺。
MIRROR
和上一个类似,只不过是用镜像方式进行平铺。
一般用的时候会用第一个参数。
- 设置变形矩阵的方法
public void setLocalMatrix(Matrix localM)
。 - 参考博客Android学习笔记进阶16之BitmapShader
Paint
类
该类内有一个设置Shader
渲染器对象的方法public Shader setShader(Shader shader)
来设置渲染器。
Matrix
矩阵类
Matrix
内部存有一个3*3的矩阵,里面存有变形信息,没有构造函数。只能通过具体的方法来设置相关参数。这个矩阵被分为四部分,分别是比例旋转、平移、等比例变换、透视变换。运算原理就是矩阵的变换,参考Android Matrix理论与应用详解
- 设置缩放的方法public void setScale(float sx,float sy)
,参数是水平缩放比例和竖直缩放比例。
代码
//CustomRoundImageView.java
package mmrx.com.myuserdefinedview.textview;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.widget.ImageView;
import mmrx.com.myuserdefinedview.R;
/**
* Created by mmrx on 2015/4/16.
*/
public class CustomRoundImageView extends ImageView {
private Context mContext;
private Bitmap mBitmap;
//图片画笔和边框画笔
private Paint mBitMapPaint;
private Paint mBorderPaint;
//边框颜色
private int mBorderColor = Color.WHITE;
//边框宽度
private float mBorderWidth = 0f;
//渲染器
private BitmapShader mBitMapShader;
//图片拉伸方式 默认为按比例缩放
private int mImageScale = 1;
//控件的长宽
private int mWidth;
private int mHeight;
//边框和圆形图片的半径
private float mBorderRadius;
private float mDrawableRadius;
//矩形
private RectF mDrawableRect;
private RectF mBorderRect;
//变换矩形
private Matrix mBitMapMatrix;
public CustomRoundImageView(Context mContext){
super(mContext);
this.mContext = mContext;
}
public CustomRoundImageView(Context mContext,AttributeSet attr){
this(mContext, attr, R.attr.CustomImageView03Style);
this.mContext = mContext;
}
public CustomRoundImageView(Context mContext,AttributeSet attr,int defSytle){
super(mContext,attr,defSytle);
TypedArray ta = mContext.obtainStyledAttributes(attr,R.styleable.CustomRoundImageView,
defSytle,R.style.CustomizeStyle03);
int count = ta.getIndexCount();
for(int i=0;i<count;i++){
int index = ta.getIndex(i);
switch (index){
case R.styleable.CustomRoundImageView_borderColor:
mBorderColor = ta.getColor(index,Color.WHITE);
break;
case R.styleable.CustomRoundImageView_borderWidth:
mBorderWidth = ta.getDimension(index,0f);
break;
case R.styleable.CustomRoundImageView_image:
mBitmap = BitmapFactory.decodeResource(getResources(),ta.getResourceId(index,0));
break;
case R.styleable.CustomRoundImageView_imageScaleType:
mImageScale = ta.getInt(index,0);
break;
default:
break;
}
}
ta.recycle();
mBorderPaint = new Paint();
mBitMapPaint = new Paint();
mDrawableRect = new RectF();
mBorderRect = new RectF();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightMod = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMod = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//march_parent & exactly dimen
if(heightMod == MeasureSpec.EXACTLY){
mHeight = heightSize;
}
//wrap_content & others
else{
mHeight = mBitmap.getHeight();
}
if(widthMod == MeasureSpec.EXACTLY){
mWidth = widthSize;
}
//wrap_content & others
else{
mWidth = mBitmap.getWidth();
}
Log.v("--wrap_content--dimen1", "width: " + mWidth + " height: " + mHeight);
//获得屏幕的宽高,代码里面获取和设置的都是像素作为单位
DisplayMetrics dm = new DisplayMetrics();
((Activity)mContext).getWindowManager().getDefaultDisplay().getMetrics(dm);
int screenWidth = dm.widthPixels;
int screenHeight = dm.heightPixels;
//比较图片的尺寸和屏幕的尺寸,尺寸最大不得超出屏幕
mWidth = mWidth<=screenWidth? mWidth : screenWidth;
mHeight = mHeight<=screenHeight? mHeight : screenHeight;
// Log.v("--wrap_content--dimen2","screenHeight: "+ screenHeight + " screenWidth: " + screenWidth);
// Log.v("--wrap_content--dimen1","width: "+ mWidth + " height: " + mHeight);
mWidth = Math.min(mWidth,mHeight);
mHeight = Math.min(mWidth,mHeight);
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
if(mBitmap == null)
return;
//设置渲染器
mBitMapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
//抗锯齿
mBitMapPaint.setAntiAlias(true);
//设置渲染器
mBitMapPaint.setShader(mBitMapShader);
//设置外框的相关参数
mBorderPaint.setStyle(Paint.Style.STROKE);
mBorderPaint.setAntiAlias(true);
mBorderPaint.setColor(mBorderColor);
mBorderPaint.setStrokeWidth(mBorderWidth);
//边框矩形
mBorderRect.set(0,0,getWidth(),getHeight());
//边框半径,考虑线条的宽度,如果没有考虑线条宽度,显示会把线条的一部分遮蔽
mBorderRadius = Math.min((mBorderRect.width()-mBorderWidth)/2,(mBorderRect.height()-mBorderWidth)/2);
//图片的矩形,因为图片是在外边框内部,所以位置矩形的坐标要考虑到边框的宽度
mDrawableRect.set(mBorderWidth,mBorderWidth,
mBorderRect.width()-mBorderWidth,mBorderRect.height()-mBorderWidth);
//圆形图片的半径
mDrawableRadius = mBorderRadius-mBorderWidth;
//设置图片的缩放
setBitMapScale();
canvas.drawCircle(getWidth()/2,getHeight()/2,mDrawableRadius,mBitMapPaint);
if(mBorderWidth != 0)
canvas.drawCircle(getWidth()/2,getHeight()/2,mBorderRadius,mBorderPaint);
}
//根据控件的尺寸和设置的图片缩放模式,来对图片进行缩放
private void setBitMapScale(){
float scaleX = 0,scaleY = 0;
//获得圆形的直径和图片的尺寸
float diameter = mDrawableRadius*2;
float mBitMapWidth = mBitmap.getWidth();
float mBitMapHeight = mBitmap.getHeight();
mBitMapMatrix = new Matrix();
mBitMapMatrix.set(null);
//fillXY 宽高单独缩放
if(mImageScale == 0){
scaleX = diameter/mBitMapWidth;
scaleY = diameter/mBitMapHeight;
}
//center 等比例缩放
else{
float scale = 0;
scaleX = diameter/mBitMapWidth;
scaleY = diameter/mBitMapHeight;
//如果宽度和高度至少有一个需要放大
if(scaleX > 1 || scaleY > 1){
scale = Math.max(scaleX,scaleY);
}
else{
scale = Math.min(scaleX, scaleY);
}
scaleX = scale;
scaleY = scale;
}
mBitMapMatrix.setScale(scaleX,scaleY);
mBitMapShader.setLocalMatrix(mBitMapMatrix);
}
//设置图片
public void setImage(Bitmap bm){
this.mBitmap = bm;
invalidate();
}
//设置图片
public void setImage(int rid){
this.mBitmap = BitmapFactory.decodeResource(getResources(),rid);
invalidate();
}
//像素转换为dp
// private int px2dip(Context context, float pxValue){
// final float scale = context.getResources().getDisplayMetrics().density;
// return (int)(pxValue * scale + 0.5f);
// }
}
相关xml文件
<!-- activity_main.xml-->
<mmrx.com.myuserdefinedview.textview.CustomRoundImageView
android:layout_width="200dp"
android:layout_height="200dp"
customview:borderWidth="5dp"
customview:borderColor="#ff868686"
customview:image="@drawable/hello"
/>
<!-- attrs.xml-->
<attr name="image" format="reference"/>
<attr name="imageScaleType">
<enum name="fillXY" value="0"/>
<enum name="center" value="1"/>
</attr>
<attr name="borderWidth" format="dimension"/>
<attr name="borderColor" format="color"/>
<declare-styleable name="CustomRoundImageView">
<attr name="image"/>
<attr name="borderWidth"/>
<attr name="borderColor"/>
<attr name="imageScaleType"/>
</declare-styleable>
<!-- styles.xml-->
<style name="AppTheme"
<item name="CustomImageView03Style">@style/CustomizeStyle03</item>
</style>
<style name="CustomizeStyle03">
<item name="imageScaleType">center</item>
<item name="image">@drawable/ic_launcher</item>
<item name="borderWidth">0dp</item>
<item name="borderColor">#ffffffff</item>
</style>
有图有内啥
一些补充
- 我参考了文章开头提到的那个开源项目,同样继承了
ImageView
,也同样没有重写onMeasure
方法,于是在xml文件中使用这个自定义ImageView
的时候,报错,提示我android:layout_height
没有wrap_content
这个属性。当时百思不得其解,参考的其他的圆形ImageView
都没有重写onMeasure
方法,怎么他们的就好使?一番对比之后,发现在xml中设置图片的时候,都是使用的android:src
设置图片,而我则是使用的自定义的属性来设置、获取图片。因此需要重写onMeasure
方法来计算图片的大小。 - 在
onMeasure
方法中,当高度宽度属性设置为wrap_content
时,有可能因为图片本身过大,显示的时候在屏幕上显示不完整。因此我在onMeasure
方法中增加了一个与屏幕尺寸的对比,图片的尺寸不得超出屏幕的尺寸。在这个过程中,发现在onMeasure
中设置的尺寸都是以像素为单位的(当我将像素和dip之间进行转换后在填充到setMeasuredDimension
方法中后,实际效果并不是我想象中的样子)。 - 不管是参考代码中还是我自己写的代码中,有好几个
Rect
或者RectF
类,但是在最终的绘图过程中却都没有被用到。之前我也不理解这些矩形有什么用处,但是在思考和写代码的过程中,逐渐理解了其中的原理:这些矩形是用来表示范围数据的,就好像在做月饼时候的月饼模子一样。里面包含了一些尺寸、位置数据。在上面的代码中,矩形被用于计算圆形半径、计算图片缩放比例。 - 实现圆形
ImageView
有好几种不同的方式,上面使用Shader
渲染器只是其中的一种,还有一种是用图片遮挡的方式来进行表示。 - 本来打算完全自己写的…提笔了还是感觉很多东西是模模糊糊的,结论,模糊的东西还是没有被完全转化为自己的知识储备,仍然需要多多练习,用代码量和时间来巩固。
发现问题
如果该控件在Fragment
的布局文件中中被使用,下面这句话会在编译时报错((Activity)mContext).getWindowManager().getDefaultDisplay().getMetrics(dm);
原因是在Fragment
中需要调用getActivity()
来获取所在的Activity
,暂时未找到解决方法…所以只能把获得屏幕尺寸的那部分注释掉,在布局文件中只能定义具体数值,才能在Fragment
的布局文件中中被使用。
更新时间2015年5月3日10:27:02
问题:
自定义控件无法再Fragment的布局文件中使用。
解决:
将private Context mContext;
这行代码去掉,同时去掉其他代码中对mContext
变量的引用;在CustomRoundImageView(Context mContext,AttributeSet attr,int defSytle)
方法中将TypedArray ta = mContext.obtainStyledAttributes(attr,R.styleable.CustomRoundImageView,defSytle,R.style.CustomizeStyle03);
这句话修改为TypedArray ta = context.getTheme().obtainStyledAttributes(attr,R.styleable.CustomRoundImageView,defSytle,R.style.CustomizeStyle03);
之前的相关博文
Android自定义view学习笔记01
Android自定义view学习笔记02
Android自定义view学习笔记04