這是我重讀《Android 群英傳》的時候做的讀書筆記,在 View 這塊,醫(yī)生講解真的非常深入淺出,非常值得一讀,并且多次重讀。
架構(gòu):
- PhoneWindow 將一個 DecorView 設(shè)置為整個應(yīng)用窗口的根 View,這里面所有 View 的監(jiān)聽事件,都通過 WindowManagerService 來接收。DecorView 分為 TitleView 和 ContentView,ContentView 是一個 ID 為 content 的 FrameLayout
- 在
onCreate()
方法中調(diào)用setContentView()
方法后,ActivityManagerService 會回調(diào)onResume()
方法,此時系統(tǒng)才會把整個 DecorView 添加到 PhoneWindow 中,并讓其顯示出來,從而完成最終的界面繪制。
View 的測量:
測量 View 的類:MeasureSpec 類,它是一個32位的 int 值,高兩位為測量模式,低30位為測量大小,使用位運(yùn)算提高并優(yōu)化效率。
重寫 onMeasure()
后,最終要做的是把測量后的寬高值作為參數(shù)設(shè)置給 setMeasureDimension()
方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
setMeasureDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
//可作為模板代碼!
private int measureWidth(int measureSpec){
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY){//精確值模式,指定具體數(shù)值
result = specSize;
}else{
result = 200;//先設(shè)置一個默認(rèn)大小
//最大值模式,layout_width 或 layout_height 為 wrap_content 時,控件大小隨控件的內(nèi)容變化而變化,此時控件尺寸只要不超過父控件允許的最大尺寸即可。
if(specMode == MeasureSpec.AT_MOST){
result = Math.min(result, specSize);//取出我們指定的大小和 specSize 中最小的一個來作為最后的測量值
}
//MeasureSpec.UNSPECIFIED 不指定其大小,View 想多大就多大
}
return result;
}
即,如果不重寫 onMeasure()
方法,系統(tǒng)則會不知道該默認(rèn)多大尺寸,就會默認(rèn)填充整個父布局,所以,重寫 onMeasure()
方法的目的,就是為了能夠給 View 一個 wrap_content 屬性下的默認(rèn)大小。
View 的繪制
onDraw()
中的參數(shù),就是 Canvas 對象,使用該對象進(jìn)行繪圖,而在其他地方,則需要 new 出該對象:
Canvas canvas = new Canvas(bitmap);
傳進(jìn)去的 bitmap 是與這個 bitmap 創(chuàng)建的 Canvas 畫布緊密聯(lián)系的,這個過程稱為裝載畫布。該 bitmap 用來存儲所有繪制在 Canvas 上的像素信息。所有的 Canvas.drawXXX 方法都發(fā)生在這個 bitmap 上。
@Override
protected void onDraw(Canvas canvas){
//...
//在 onDraw 方法中繪制兩個 bitmap
canvas.drawBitmap(bitmap1, 0, 0, null);
canvas.drawBitmap(bitmap2, 0, 0, null);
//...
}
private void otherMethod(){
//將 bitmap2 裝載到另一個 Canvas 對象中
Canvas mCanvas = new Canvas(bitmap2);
//其他地方使用 Canvas 對象的繪圖方法在裝載 bitmap2 的 Canvas 對象上進(jìn)行繪圖
mCanvas.drawXXX
}
通過 mCanvas 將繪制效果作用在了 bitmap2 上,再刷新 View 的時候,就會發(fā)現(xiàn)通過 onDraw()
方法畫出來的 bitmap2 已經(jīng)改變,因?yàn)?bitmap2 承載了在 mCanvas 上所進(jìn)行的繪圖操作。我們沒有將圖形直接繪制在 onDraw()
方法制定的那塊畫布上,而是通過改變 bitmap,讓 View 重繪,從而顯示改變之后的 bitmap。
ViewGroup 的測量
當(dāng) ViewGroup 的大小為 wrap_content 時,ViewGroup 需要對子 View 進(jìn)行遍歷,以便獲得所有子 View 大小從而決定自己的大小,即調(diào)用子 View 的 Measure 方法來獲得每一個子 View 的測量結(jié)果。
子 View 測量完畢后,ViewGroup 執(zhí)行 Layout 過程時,同樣是遍歷調(diào)用子 View 的 Layout 方法,并指定其具體顯示的位置,從而來決定其布局位置。
自定義 View
@Override
protected void onDraw(Canvas canvas){
//在回調(diào)父類方法前,對 TextView 來說是在繪制文本內(nèi)容之前,實(shí)現(xiàn)邏輯
super.onDraw(canvas);
//之后,繪制文本之后
}
在 attrs.xml 中通過使用<declar-styleable>
標(biāo)簽聲明使用了自定義屬性,使用如下代碼獲得在布局文件中自定義的那些屬性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
mLeftColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
//完成資源回收,避免重新創(chuàng)建的時候的錯誤
ta.recycle();
自定義 ViewGroup
重寫 onMeasure()
來對子 View 進(jìn)行測量,重寫 onLayout()
確定子 View 位置,重寫 onTouchEvent()
增加響應(yīng)事件。
實(shí)例需求:自定義 ViewGroup 實(shí)現(xiàn)類似 ScrollView 上下滑動,同時增加粘性效果。即,當(dāng)一個子 View 向上滑動大于一定距離后,松開將自動上滑,顯示下一個子 View,否則回到原始位置。
步驟一:先實(shí)現(xiàn)類似 ScrollView 功能:
@override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int count = getChildCount();
//遍歷通知子 View 對其自身進(jìn)行測量
for(int i = 0;i < count; ++i){
View childView = getChildAt(i);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
步驟二:再對子 View 進(jìn)行放置位置設(shè)定,讓每個子 View 都顯示完整的一屏。所以,本例中ViewGroup 的高度就是子 View 的個數(shù)乘以屏幕高度,然后遍歷設(shè)定每個子 View 放置的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
int childCount = getChildCount();
//設(shè)置 ViewGroup 高度
MarginLayoutParams mlp = (MarginLayoutParams)getLayoutParams();
mlp.height = mScreenHeight.childCount;
setLayoutParams(mlp);
//修改子 View 的 top 和 bottom 屬性,使它們依次排列
for(int i=0; i<childCount; i++){
View child = getChildAt(t);
if(child.getVisibility() != View.GONE){
child.layout(l, i*mScreenHeight, r, (i+1)*mScreenHeight);
}
}
}
步驟三:
@Override
public boolean onTouchEvent(MotionEvent event){
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y;
mStart = getScrollY();//記錄按下位置
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y;
if(getScrollY() < 0){
dy = 0;
}
if(getScrollY() >getHeight - mScreenHeight){
dy = 0;
}
scrollBy(0, dy);//隨手指滾動 dy
mLastY = y;
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();
int dScrollY = mEnd - mStart;
if(dScrollY > 0){//上滑
if(dScrollY < mScreenHeight / 3){//小于一定距離, 滾回去
mScroller.startScroll(0,getScrollY(), 0, -dScrollY);
}else{//大于,則滾動完剩余的距離
mScroller.startScroll(0,getScrollY(), 0, mScreenHeight-dScrollY);
}
}else{//同理
if(-dScrollY < mScreenHeight /3){
mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
}else{
mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight - dScrollY);
}
}
break;
}
postInvalidata();
return true;
}
/**
*Called by a parent to request that a child update its values for mScrollX and mScrollY if necessary. This will typically be done if the child is animating a scroll using a Scroller object.
**/
@Override
public void computeScroll(){
super.computeScroll();
if(mScroller.computeScrollOffset()){
scrollTo(0, mScroller.getCurrY());
postInvalidate();
}
}
事件攔截機(jī)制
點(diǎn)擊 View 的 log:
ViewGroupA dispatchTouchEvent
ViewGroupA onInterceptTouchEvent
ViewGroupB dispatchTouchEvent
ViewGroupB onInterceptTouchEvent
View dispatchTouchEvent
View onTouchEvent //last event, will back to parent
ViewGroupB onTouchEvent
ViewGroupA onTouchEvent
所以事件傳遞順序是:先執(zhí)行 dispatchTouchEvent()
然后是 onInterceptTouchEvent()
。返回值:True,攔截,不繼續(xù);False,不攔截,繼續(xù)流程。初始返回是 false。
事件處理順序是:onTouchEvent()
。返回值:True,處理了,不審核;False,給上級處理。初始返回是 false。
即:
分發(fā)、攔截:如果某個 ViewGroup 直接使用
dispatchTouchEvent()
返回了 true ,則分發(fā)攔截結(jié)束,不再向其子 View 傳遞,則,直接執(zhí)行該 ViewGroup 的onTouchEvent()
,然后繼續(xù)向上處理對應(yīng) ViewGroup 的onTouchEvent()
。處理:如果某個 View 直接在
onTouchEvent()
中返回了 true。則上級不再執(zhí)行onTouchEvent()
。所有的處理在此結(jié)束。