學習資料
- Android開發(fā)藝術探索
- 愛哥的自定義控件系列
在整個系列的第一篇Android自定義View學習(一)——準備中急凰,簡單學習來了測量涉及到的onMeasure()
方法我碟,本篇進行補充學習竹观。由于目前水平還很低,測量過程涉及到的源碼沒有能力去分析妻献,只是記錄一下學習怎么調用方法喇辽,很多地方都是"知其然而不知其所以然"
,不能對調用的方法本身原理進行說明姚炕。愛哥是從源碼進行分析摊欠,寫的非常好,可以去深入學習愛哥的博客
1. onMeasure() 測量方法 <p>
在之前的學習中柱宦,很多地方都用到了這個方法些椒,而且?guī)缀趺看问褂茫a都是不變的掸刊,套路是固定的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 300);
}
}
偶爾做出的改動便是300
這個數值
這個方法內免糕,主要是對于MeasureSpec.AT_MOST
這個模式,也就是針對在布局xml
文件控件的寬高寫wrap_content
時的處理石窑。無論是MeasureSpec.EXACTLY(match_parent)
還是MeasureSpec.AT_MOST
,這兩種模式都是根據當前控件以及所在的父控件大小共同來確定的蚓炬。
如果不進行重寫這個方法松逊,直接引用這個當前的自定義控件,無論是wrap_content
還是match_parent
都會沾滿全屏肯夏,原因下面嘗試分析
1.1 嘗試分析原因<p>
在一個Activity
中经宏,在onCreate()
方法中,都會調用setContentView(@LayoutRes int layoutResID)
這樣一個方法熄捍,layoutResId
就是Activity
的xml
布局文件烛恤。
例如母怜,在MainActivity
中余耽,setContentView(R.layout.main_activity)
。在R.layout.main_activity
中苹熏,最外層的布局往往會是一個LinearLayout
(或者RelativeLayout
)碟贾。之前認為這個最外層的布局就是一個MainActivity
的根布局栓始,而對應的LinearLayout
(或者RelativeLayout
)便是父容器判没。但根據上面的圖可以知道,MainActivity
的最外層布局是添加到了ContentView
中据途,這個ContentView
可以通過LinearLayout.getParent()
獲得干发,返回的結果是一個id
為android:id="@android:id/content"
的FramLayout
朱巨,這個android:id="@android:id/content
才是Activity
的根布局,R.layout.main_activity
包含在其中
除了ContentView
外枉长,手機屏幕上展示的還有ContentView
上面的TitleView
冀续,TitleView
便是ActionBar
或者5.0
后ToolBar
。也就是說必峰,屏幕真正可見的根View
是DecorView
洪唐。
每個Activity
都包含有一個Window
對象,通常是PhoneWindow
吼蚁。當Activity
在onCreate()
方法中調用了setContentView()
方法后凭需,ActivityManagerService
會回調onResume()
方法,系統(tǒng)會把整個DecorView
添加進PhoneWindow
中
上面大概敘述了一下UI界面架構圖
的關系。DecorView
的大小粒蜈,無論什么時候顺献,默認都是全屏的。而DecorVew
包含兩部分枯怖,TitleView
和ContentView
滚澜,除去TitleView
,屏幕可見的就是ContentView
嫁怀。ContentView
包含了Activity
的xml
布局文件中最外層Layout
设捐,自定義View
則是在這個Layout
中。而整個Activity
的繪制過程是從最外層的DecorView
開始的塘淑,由外向內萝招。最終確定自定義View
的大小,需要根據由MeasureSpec.getMode(widthMeasureSpec)
得到的SpecMode
當做判斷依據來進行確定
- AT_MOST 最大值模式存捺,
對應LayoutParams.WARP_CONTENT
槐沼。由父容器指定了一個可用大小SpecSize
,自定義View
的大小不能超過這個SpecSize
值捌治,而這個SpecSize
也不會超過窗口的值 - EXACTLY 精準模式
對應LayoutParams.MATCH_PARENT
或者固定大小值岗钩。父容器已經檢測出View
所需大小的值SpecSize
,最終大小也就是窗口的大小或者固定值肖油。
一開始兼吓,很不理解
warp_content
,怎么就對應的是最大值模式森枪,而平常經常用到這個模式的TextView
或者Button
中视搏,理解為包裹內容
。那是因為在TextView
中县袱,都已經做了針對處理浑娜,之后使得這個模式才具備包裹內容
的特性 。這個wrap_content
模式本身并不是已經具備包裹內容
的特性
如果不對上面的onMeasure()
方法進行重寫式散,無論是warp_content
還是match_parent
筋遭,SpecSzie
的大小都是窗口的大小,也就是自定義View
所在的布局大小暴拄,而布局的寬高一般默認是match_parent
漓滔,顯示在屏幕上就是DecorView
中的ContentView
的大小。
重寫了onMeasure()
方法揍移,在AT_MOST
也就是wrap_content
時次和,確定了300
,也就是父容器給指定了SpecSize
為300px
不曉得整個敘述清不清楚那伐,回頭以后自己還能不能看明白踏施。石蔗。。
雖然指定了wrap_content
的大小畅形,但還會有問題养距,padding
并未處理
2. 處理Padding <p>
先根據上面的onMeasure()
方法,創(chuàng)建一個針對可以處理warp_content
過的PaddingView
代碼很簡單日熬,就是繪制一個Bitmap
棍厌,以Bitmap
的寬高作為wrap_content
最大值
public class PaddingView extends View {
private Paint mPaint;
public PaddingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
//畫筆
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
}
/**
* 繪制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制底色 輔助用
canvas.drawColor(Color.CYAN);
//繪制Bitmap
final float left = 0;
final float top = 0;
canvas.drawBitmap(mBitmap, left, top, null);
}
/**
* 測量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth();
resultHeight = mBitmap.getHeight();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight();
}
// 取Bitmap寬高和窗口寬高的較小的一個
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
setMeasuredDimension(resultWidth, resultHeight);
}
}
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.szlk.customview.custom.PaddingView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
測試的圖片有點巧合,寬度和手機屏幕差不多
簡單修改代碼竖席, 在布局文件加入android:padding="50dp"
耘纱,發(fā)現并沒有效果。因為測量的時候毕荐,并沒有加入padding
的大小的考慮束析。
對onMeasure()
和onDraw()
進行簡單修改:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
}
// 取Bitmap寬高和窗口寬高的較小的一個
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
setMeasuredDimension(resultWidth, resultHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制底色 輔助用
canvas.drawColor(Color.CYAN);
//繪制Bitmap
final float left = getPaddingLeft();
final float top = getPaddingTop();
canvas.drawBitmap(mBitmap, left, top, null);
}
padding
的效果是有了,可照片太大了憎亚,控件resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight()
的大小员寇,超過了手機屏幕寬度,導致控件內容顯示不全第美〉妫可以考慮使用Matrix
進行優(yōu)化
2.1 使用Matrix進行優(yōu)化 <p>
主要是利用Matrix.setRectToRect()
方法,不了解的同學什往,可以去Android 自定義View學習(八)——Matrix知識學習看一下 : )
public class PaddingView extends View {
private Bitmap mBitmap;
private Matrix mMatrix ;
private RectF srcRectF;
private RectF dstRectF;
public PaddingView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化
*/
private void init() {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mMatrix = new Matrix();
//截取用來顯示的Bitmap有效區(qū)域
srcRectF = new RectF(0,0,mBitmap.getWidth(),mBitmap.getHeight());
//顯示Bitmap的底板
dstRectF = new RectF();
}
/**
* 繪制
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪制底色 輔助用
canvas.drawColor(Color.CYAN);
//繪制Bitmap
final float left = getPaddingLeft();
final float top = getPaddingTop();
//設置Bitmap的縮放模式
mMatrix.setRectToRect(srcRectF,dstRectF, Matrix.ScaleToFit.FILL);
//利用后乘平移對開始繪制位置進行改變 這里不可以使用setTranslate()
mMatrix.postTranslate(left,top);
canvas.drawBitmap(mBitmap, mMatrix, null);
}
/**
* 測量
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//將最終結果寬高設置為窗口大小 這樣省的判斷MeasureSpec.EXACTLY模式的情況
int resultWidth = wSpecSize;
int resultHeight = hSpecSize;
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
} else if (wSpecMode == MeasureSpec.AT_MOST) {
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = hSpecSize;
} else if (hSpecMode == MeasureSpec.AT_MOST) {
resultWidth = wSpecSize;
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
}
//取Bitmap寬高和窗口寬高的較小的一個
resultWidth = Math.min(resultWidth, wSpecSize);
resultHeight = Math.min(resultHeight, hSpecSize);
//確定dstRectF的范圍
dstRectF.left= dstRectF.top = 0;
dstRectF.right = resultWidth-getPaddingRight()-getPaddingLeft();
dstRectF.bottom = resultHeight-getPaddingBottom()-getPaddingTop();
setMeasuredDimension(resultWidth, resultHeight);
}
}
雖然圖片有些變形扳缕,這樣無論多大的圖片都可以填充滿整個控件。有點類似ImageView
的android:scaleType="centerCrop"
這個屬性恶守,當然想要完全實現這個屬性的特點第献,還需要對srcRectF
進行優(yōu)化贡必,截取Bitmap
的中間正方形部分來顯示就可以了
Margin
外邊距兔港,封裝在LayoutParams內交由父容器統(tǒng)一處理
3. 最后 <p>
主要是學習1.1
原因分析那里。理解UI界面架構圖
仔拟,這個圖看起來并不復雜衫樊,但涉及的東西卻非常多。算是先了解一下View
的工作原理利花,View
最復雜的過程就是測量科侈。
本人很菜,有錯誤炒事,請指出
共勉 : )