目錄:
1屯吊、前言
2、View原理
3赞辩、ViewRoot
4雌芽、自定義view
1、前言
在Android應用開發(fā)中辨嗽,經(jīng)常會用到以下3點世落,自定義View、動畫、Touch事件分發(fā)屉佳。自定義View谷朝,可以寫出非常漂亮的界面。良好的動畫武花,會提升app的質感圆凰。Touch事件分發(fā),影響著與用戶的互動体箕。
如需要寫自定義view专钉,最重要的是理解view原理,本文今天嘗試從源碼角度解析View原理累铅。
2跃须、View原理
從本文標題結合內(nèi)容,部分同學在看完后可能會覺得博主在裝13娃兽,View原理是什么菇民?應該比較高深。其實View原理所有人都懂投储。
如上第练,view原理就是measure、layout玛荞、draw的三個過程娇掏。measure,確定view的大小冲泥。layout確定view的位置驹碍,draw,繪制view凡恍。
3志秃、ViewRoot
當Activity執(zhí)行onResume后,界面就是可見的了嚼酝,為什么是這樣呢浮还?本文跟蹤這條線索來查看view是怎么被添加的?view是如何被刷新的闽巩?
調用時序圖:
上代碼
//ActivityThread的handleResumeActivity方法钧舌,將Activity的DecorView通過WindowManager添加,所以界面可見了
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
追蹤wm.addView方法涎跨,最終調用WindowManagerGlobal類的addView方法
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//如果View的窗口類型是子窗口類型洼冻,則找出其父View
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
//初始化ViewRoot
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//將view和ViewRoot保存到列表中
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
try {
//ViewRoot設置View
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
//View添加出錯,則刪除此View
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}
WindowManagerGlobal的addView方法中隅很,初始化了ViewRoot對象撞牢,并且調用了setView方法。ViewRoot可以理解為View的管理者,View的刷新屋彪、繪制等都是通過ViewRoot調用的所宰,且View與WMS之間的跨進程交互,也是通過ViewRoot實現(xiàn)的畜挥。繼續(xù)查看setView方法仔粥。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
int res; /* = WindowManagerImpl.ADD_OKAY; */
//請求界面刷新,要執(zhí)行measure蟹但、layout躯泰、draw那套流程了
requestLayout();
try {
//通過WindowSession與WMS交互,告訴WMS矮湘,這個窗口需要被添加斟冕,需要被顯示了
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
} catch (RemoteException e) {
}
//WindowSession.addToDisplay的結果值,如果返回值不等于add_ok缅阳,則添加失敗,拋出異常
if (res < WindowManagerGlobal.ADD_OKAY) {
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
}
}
}
ViewRoot與WMS使用WindowSession跨進程交互景描。從以上代碼中可以看出十办,一個Activity中只有一個ViewRoot,并不是一個View對應著一個View超棺。當然向族,如果是類似狀態(tài)欄這種直接通過WindowManager添加的View,這類View也會對應著一個ViewRoot棠绘。
ViewRoot的requestLayout方法比較簡單件相,一路跟蹤,最后會執(zhí)行ViewRoot的performTraversals方法氧苍,此方法非常復雜夜矗,非常長刁笙。
private void performTraversals() {
final View host = mView;
//被添加view的期望寬高
int desiredWindowWidth;
int desiredWindowHeight;
//可見性是否變化
boolean viewVisibilityChanged = mViewVisibility != viewVisibility || mNewSurfaceNeeded;
//是否需要重新布局
boolean layoutRequested = mLayoutRequested && !mStopped;
//窗口是否需要重新確定大小
boolean windowShouldResize = layoutRequested && windowSizeMayChange
&& ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight())
|| (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.width() < desiredWindowWidth && frame.width() != mWidth)
|| (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT &&
frame.height() < desiredWindowHeight && frame.height() != mHeight));
//計算view的大小
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
//是否要layout
final boolean didLayout = layoutRequested && !mStopped;
if (didLayout) {
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
}
//如果沒有取消繪制圃阳,則繪制view
if (!cancelDraw && !newSurface) {
performDraw();
}
}
performTraversals方法中,根據(jù)各種條件憨募,計算是否需要measure赡突、layout以及draw对扶,view的刷新完成。至此惭缰,Activity從onResume之后發(fā)生的故事浪南,都解釋清楚了。
選擇一個方法重新看看漱受,performMeasure的具體實現(xiàn)具體是什么:
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
performMeasure方法中直接調用view的measure方法络凿,measure方法是個final方法,無法被子類重寫,measure方法中調用onMeasure方法喷众,調用子view的measure方法各谚,完成整個view樹的measure操作。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//現(xiàn)在的MeasureSpec與老的MeasureSpec不相同時到千,則需要檢測判斷是否調用onMeasure
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure方法昌渤,直接調用setMeasuredDimension方法,確定view的寬和高憔四,所以在自定義View中膀息,一定要對自己調用setMeasuredDimension方法,確定自己的寬和高了赵。同時只需要調用子view的measure方法即可潜支。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在performMeasure方法中,調用了Trace.traceBegin方法柿汛,只有調用此方法冗酿,才能在SysTrace工具中看到對應的方法的執(zhí)行時間。
4络断、自定義view
自定義view是一個系統(tǒng)性的工作裁替,必須對view原理、元素繪制等都有一定掌握才行貌笨。本博中對canvas繪制以及camera使用等進行過相關總結弱判,不再復述。自定義view中文字的繪制較為特殊锥惋,本文以兩行文字控件舉例昌腰。
查看canvas.drawText接口說明:
y值意義是,被繪制文字的baseline的y坐標膀跌,baseline究竟是什么呢遭商?
Baseline是基線,在Android中淹父,文字的繪制都是從Baseline處開始的株婴,Baseline往上至字符“最高處”的距離我們稱之為ascent(上坡度),Baseline往下至字符“最低處”的距離我們稱之為descent(下坡度)暑认;
leading(行間距)則表示上一行字符的descent到該行字符的ascent之間的距離困介;
top和bottom文檔描述地很模糊,其實這里我們可以借鑒一下TextView對文本的繪制蘸际,TextView在繪制文本的時候總會在文本的最外層留出一些內(nèi)邊距座哩,為什么要這樣做?因為TextView在繪制文本的時候考慮到了類似讀音符號粮彤,下圖中的A上面的符號就是一個拉丁文的類似讀音符號的東西:
top的意思其實就是根穷,除了Baseline到字符頂端的距離外還應該包含這些符號的高度姜骡,bottom的意思也是一樣。一般情況下我們極少使用到類似的符號屿良,所以往往會忽略掉這些符號的存在圈澈,但是Android依然會在繪制文本的時候在文本外層留出一定的邊距,這就是為什么top和bottom總會比ascent和descent大一點的原因尘惧。而在TextView中我們可以通過xml設置其屬性android:includeFontPadding="false"去掉一定的邊距值但是不能完全去掉康栈。
本文將自定義一個顯示兩行文字的控件,文字居中顯示喷橙,效果如下圖:
直接上代碼啥么,查看draw方法
public void draw(Canvas canvas){
//Log.i("okunu"," firsttext = " + mFirstText + " msecond = " + mSecondText);
int totalTextHeight = mFirstTextHeight + mGap + mSecondTextHeight;
TextPaint paint = getPaint();
paint.setTextSize(mFirstSize);
paint.setColor(mFirstColor);
paint.setTypeface(mFirsTypeface);
float x1 = (mWidth - mFirstTextWidth)/2;
float y1 = (mHeight - totalTextHeight) - paint.ascent();
//float y1 = (mHeight - totalTextHeight);
canvas.drawText(mFirstText, 0, mFirstText.length(), x1, y1, paint);
paint.setTextSize(mSecondSize);
paint.setColor(mSecondColor);
paint.setTypeface(mSecondTypeface);
float x2 = (mWidth - mSecondTextWidth)/2;
float y2 = (mHeight - totalTextHeight) + mFirstTextHeight + mGap - paint.ascent();
canvas.drawText(mSecondText, 0, mSecondText.length(), x2, y2, paint);
}
x坐標的處理很容易理解,中間位置即可贰逾。y坐標的計算比較特殊悬荣,從效果圖上看,文字的繪制起點就是view的頂點處疙剑,y坐標應該是0氯迂,但根據(jù)canvas.drawText接口的分析,此處傳遞的y坐標真實意義是baseline坐標值言缤,而不是文字的頂點坐標值囚戚。所以y1值計算時需要減去ascent值。如果去掉這一步驟轧简,那么第一行文字就看不見了。
ps:在計算y值時匾二,頂點是0哮独,結合前文對baseline的介紹,baseline和頂點之間相差一個ascent察藐,那么baseline的值就是頂點坐標加上ascent即可皮璧。由于ascent值為負,所以加負號即可分飞。計算baseline值可由頂點值推導得到悴务。
計算baseline的位置,首先我們得知道文字的top位置譬猫,如果文字在view的正中心讯檐,top位置也可以確定,就是view的正中心和文字高度相關染服。如果文字在頂部别洪,top位置就在view的頂部,確定了top位置之后柳刮,再來計算baseline的位置就相當容易了挖垛,baseline和top之前相隔的距離就是 (-top)
痒钝,于是就可以確定文字的繪制位置了
再次總結下相關點:
- 文字的寬可以由paint計算得出
- 文字的高可以由paint計算得出,為求精確痢毒,一般要求是
bottom - top
- 先確定文字的top位置送矩,再來計算baseline位置
所有代碼均已上傳至本人的github,歡迎訪問哪替。