Android自定義view學習筆記02
本文代碼來自於張鴻洋老師的博客之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;i<count;i++){
int 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文件的相關內容
<!--attrs.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-->
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="CustomView01Style">@style/CustomizeStyle01</item>
<item name="CustomImageView02Style">@style/CustomizeStyle02</item>
</style>
<style name="CustomizeStyle01">
<item name="titleText">@string/hello_world</item>
<item name="titleColor">#ff0000</item>
<item name="titleSize">14dp</item>
</style>
<style name="CustomizeStyle02">
<item name="titleText">@string/hello_world</item>
<item name="titleColor">#ff0000</item>
<item name="titleSize">14dp</item>
<item name="imageScaleType">center</item>
<item name="image">@drawable/ic_launcher</item>
</style>
</resources>
<!--activity_main.xml-->
<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"/>
遇到的問題
- 啓動過程中logcat打印以下錯誤後程序崩潰
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)
方法。
- 類似的還有一些比如說變量名用錯…case後的標籤用錯…導致上次coding的時候心煩意亂的,事隔半周後重新拿起來再看,錯誤都是一些及其低級的錯。
補充的知識點
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