本文代码来自于张鸿洋老师的博客之Android 自定义View (二) 进阶 学习笔记,对代码进行些许修改,并补充一些在coding过程中遇到的问题、学习的新东西。
//CustomImageView.java
package mmrx.com.myuserdefinedview.textview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import mmrx.com.myuserdefinedview.R;
/**
* Created by mmrx on 2015/4/6.
*/
public class CustomImageView extends View {
//文本内容
private String mTitleText;
//文本的颜色
private int mTitleColor;
//文本大小
private int mTitleTextSize;
//图片资源
private Bitmap mImage;
//图片缩放
private int mImageScale;
//文本时候的文字显示范围
private Rect mBound;
private Paint mPaint;
//图片和字外面的矩形
private Rect rect;
//高度和宽度
int mWidth,mHeight;
//在代码中调用
public CustomImageView(Context context){
this(context,null);
}
//在布局文件中调用
public CustomImageView(Context context,AttributeSet set){
this(context,set,R.attr.CustomImageView02Style);
}
//显示被调用
public CustomImageView(Context context,AttributeSet set,int defStyle){
super(context,set,defStyle);
TypedArray ta = context.obtainStyledAttributes(set, R.styleable.CustomImageView,
defStyle,R.style.CustomizeStyle02);
int count = ta.getIndexCount();
for(int i=0;iint index = ta.getIndex(i);
switch (index){
case R.styleable.CustomImageView_image:
/*
* decodeResource(Resources res, int id)
* res 是一个Resources类对象,用来读取res文件夹下的资源
* 这个res是的来源是该view创建时传入的Context对象, context.getResources()
* */
mImage = BitmapFactory.decodeResource(getResources(),ta.getResourceId(index,0));
// assert mImage!=null;
break;
case R.styleable.CustomImageView_imageScaleType:
mImageScale = ta.getInt(index,0);
break;
case R.styleable.CustomImageView_titleColor:
mTitleColor = ta.getColor(index, Color.BLACK);
break;
case R.styleable.CustomImageView_titleSize:
/*
* TypedValue.applyDimension是转变为标准尺寸的函数
* 第一个参数是 单位,第二个参数是要数值,第三个是DisplayMetircs 对象,用于获取屏幕分辨率
* */
mTitleTextSize = ta.getDimensionPixelSize(index,
(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
16,getResources().getDisplayMetrics()));
break;
case R.styleable.CustomImageView_titleText:
mTitleText = ta.getString(index);
break;
default:
break;
}//end switch
}//end for
ta.recycle();
rect = new Rect();
mPaint = new Paint();
mBound = new Rect();
mPaint.setTextSize(mTitleTextSize);
//计算字体所需范围
mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
}//end method
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int hightMod = MeasureSpec.getMode(heightMeasureSpec);
int hightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMod = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//高度有具体数值
if(hightMod == MeasureSpec.EXACTLY){
mHeight = hightSize;
}
//wrap_content & others
else{
//获得图片的高度
int imageHeight = mImage.getHeight();
//获得文字高度
int textHeight = mBound.height();
//总体高度
int totalHeight = imageHeight + textHeight + getPaddingTop() + getPaddingBottom();
if(hightMod == MeasureSpec.AT_MOST){
//warp_content
//获得相对较小的高度
mHeight = Math.min(hightSize,totalHeight);
}
else{
//多个自定义CustomImageView放入ScrollView中,只会显示出最后一个处理办法
mHeight = hightSize;
}
}
//宽度有具体数值
if(widthMod == MeasureSpec.EXACTLY){
mWidth = widthSize;
}
//wrap_content & others
else{
// 由图片决定的宽
int desireByImg = getPaddingLeft() + getPaddingRight() + mImage.getWidth();
// 由字体决定的宽
int desireByTitle = getPaddingLeft() + getPaddingRight() + mBound.width();
Log.w("CustomImageView","desireByTitle:"+desireByTitle+" desireByImg:"+desireByImg+" widthSize: "+widthSize);
if(widthMod == MeasureSpec.AT_MOST){
//warp_content
Log.w("CustomImageView","MeasureSpec.AT_MOST");
//获得相对较大的宽度
int desire = Math.max(desireByImg,desireByTitle);
mWidth = Math.min(widthSize,desire);
}
else{
Log.w("CustomImageView","NOT MeasureSpec.AT_MOST");
//多个自定义CustomImageView放入ScrollView中,只会显示出最后一个处理办法
mWidth = widthSize;
}
}
//将计算好的高度宽度放入,一定要记得这句话不要丢了...
setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
//边框
//设置线宽
mPaint.setStrokeWidth(4);
//设置样式为空心
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.RED);
/*开画,前四个参数是 左 上 右 下 边的位置(注意是边,不是点) 画笔
* left 矩形左边的x坐标 right 矩形右边的x坐标
* top 矩形上边的y坐标 bottom 矩形下边的y坐标
* */
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
//获得绘制图片的矩形框
rect.left = getPaddingLeft();
rect.right = mWidth - getPaddingRight();
rect.top = getPaddingTop();
rect.bottom = mHeight - getPaddingBottom();
//设置画笔风格为 实心
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mTitleColor);
//当当前字体的宽度大于边框的宽度,截取字符串的,改为xxxx...
if(mBound.width()>mWidth){
//TextPaint 是 Paint 的一个子类
TextPaint paint = new TextPaint(mPaint);
/*TextUtils是处理字符串的工具类,ellipsize是用来截断给定长度的字符串的方法
* 参数 需要截断的字符串 显示的字符串的宽度 省略号的位置
* */
String msg = TextUtils.ellipsize(mTitleText,paint,
(float)mWidth-getPaddingLeft()-getPaddingRight(),TextUtils.TruncateAt.END).toString();
/*
* 坐标参数 x,y x默认是字符串左边在屏幕的位置,如果设置了paint.setTextAlign(Paint.Align.CENTER);
* 那就是字符的中心,y是指定这个字符baseline在屏幕上的位置
* */
canvas.drawText(msg, getPaddingLeft(), mHeight - getPaddingBottom(), mPaint);
}
//正常情况,字体居中
else{
canvas.drawText(mTitleText,mWidth/2-mBound.width()*1.0f/2,
mHeight-getPaddingBottom(),mPaint);
}
//这句话是什么意思?如果图片缩放选择的是FITXY,则需要将rect的bottom边向上提一下,防止把
//字覆盖掉,如果选择的是CENTER,rect的位置会重新定义就不用这句话了。我感觉这句话放到
//if(mImageScale == 0)语句块里比较好懂一些。
rect.bottom -= mBound.height();
//如果缩放为 FITXY
if(mImageScale == 0){
canvas.drawBitmap(mImage,null,rect,mPaint);
}
//如果为居中 CENTER
else{
//计算居中的矩形范围
rect.left = mWidth/2 - mImage.getWidth()/2;
rect.right = mWidth/2 + mImage.getWidth()/2;
rect.bottom = (mHeight-mBound.height())/2 + mImage.getHeight()/2;
rect.top = (mHeight-mBound.height())/2 - mImage.getHeight()/2;
canvas.drawBitmap(mImage,null,rect,mPaint);
}
}
}
xml文件的相关内容
<attr name="titleText" format="string"/>
<attr name="titleColor" format="color"/>
<attr name="titleSize" format="dimension"/>
<attr name="CustomImageView02Style" format="reference"/>
<attr name="image" format="reference"/>
<attr name="imageScaleType">
<enum name="fillXY" value="0"/>
<enum name="center" value="1"/>
attr>
<declare-styleable name="CustomImageView">
<attr name="titleText" />
<attr name="titleColor" />
<attr name="titleSize" />
<attr name="imageScaleType"/>
<attr name="image"/>
declare-styleable>
-- style.xml-->
-- Base application theme. -->
<mmrx.com.myuserdefinedview.textview.CustomImageView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
customview:imageScaleType="center"
customview:titleText="androidandroidandroidandroid"
customview:image="@drawable/ic_launcher"
android:padding="20dp"/>
<mmrx.com.myuserdefinedview.textview.CustomImageView
android:layout_marginTop="20dp"
android:layout_width="150dp"
android:layout_height="200dp"
customview:imageScaleType="fillXY"
customview:titleText="板娘"
customview:image="@drawable/hello"
android:padding="5dp"/>
02-06 14:25:08.383 30804-30804/mmrx.com.myuserdefinedview E/AndroidRuntime﹕ FATAL EXCEPTION: main
java.lang.IllegalStateException: onMeasure() did not set the measured dimension by calling setMeasuredDimension()
at android.view.View.measure(View.java:15561)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5109)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1396)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15556)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5109)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at android.view.View.measure(View.java:15556)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5109)
at android.widget.LinearLayout.measureChildBeforeLayout(LinearLayout.java:1396)
at android.widget.LinearLayout.measureVertical(LinearLayout.java:681)
at android.widget.LinearLayout.onMeasure(LinearLayout.java:574)
at android.view.View.measure(View.java:15556)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:5109)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:310)
at com.android.internal.policy.impl.PhoneWindow$DecorView.onMeasure(PhoneWindow.java:2397)
at android.view.View.measure(View.java:15556)
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java:1987)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java:1228)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1401)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1121)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:4598)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:725)
at android.view.Choreographer.doCallbacks(Choreographer.java:555)
at android.view.Choreographer.doFrame(Choreographer.java:525)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:711)
at android.os.Handler.handleCallback(Handler.java:615)
at android.os.Handler.dispatchMessage(Handler.java:92)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4921)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1038)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:805)
at dalvik.system.NativeStart.main(Native Method)
一长串的错误信息。。。看了下代码,是onMeasure
方法中没有调用setMeasuredDimension(int, int)
方法。
Bitmap decodeResource(Resources res, int id)
这个方法属于类BitmapFactory
。在示例代码中的调用语句为mImage = BitmapFactory.decodeResource(getResources(),ta.getResourceId(index,0));
这第一个参数,当时有点不理解…查询到的资料和通过看源码的收获如下:
res 是一个
Resources
类对象,用来读取res文件夹下的资源。
每一个View类型的对象都拥有一个Resources
实例mResources
,而这个实例又是通过实例化View
类型对象时传入的Context
对象获取的。
说明一个Activity
的View共有一个Resources
对象的引用。
TypedValue.applyDimension
是转变为标准尺寸的函数,在示例代码中有相关的注释。
关于canves.drawRect()
方法的坐标参数问题,我找到了一篇写的很不错的博客android Draw Rect 坐标图示,里面有一张很生动的图来表示坐标的位置,清晰易懂。需要注意的是坐标原点在左上方,x轴(横轴)向右递增,y轴(纵轴)向下递增。于是也解决了代码中这些运算的具体含义了。
rect.left = mWidth/2 - mImage.getWidth()/2;
rect.right = mWidth/2 + mImage.getWidth()/2;
rect.bottom = (mHeight-mBound.height())/2 + mImage.getHeight()/2;
rect.top = (mHeight-mBound.height())/2 - mImage.getHeight()/2;
canvas.drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
这个方法里面的坐标参数也是很奇怪…坐标参数 x,y x默认是字符串左边在屏幕的位置,如果设置了paint.setTextAlign(Paint.Align.CENTER);
那就是字符的中心,y是指定这个字符baseline在屏幕上的位置。这里有一篇博文讲的很不错android中canvas.drawText参数的介绍以及绘制一个文本居中的案例
偶然找到几篇很棒的介绍canves
绘图的博文,这是第四篇android Graphics(四):canvas变换与操作,有空得好好学习一下。
Android自定义view学习笔记01
Android自定义view学习笔记03
Android自定义view学习笔记04
源码同步到gitHub