Android應用開發(fā)三部曲 --- View原理

目錄:

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原理所有人都懂投储。

Paste_Image.png

如上第练,view原理就是measure、layout玛荞、draw的三個過程娇掏。measure,確定view的大小冲泥。layout確定view的位置驹碍,draw,繪制view凡恍。

3志秃、ViewRoot

當Activity執(zhí)行onResume后,界面就是可見的了嚼酝,為什么是這樣呢浮还?本文跟蹤這條線索來查看view是怎么被添加的?view是如何被刷新的闽巩?

調用時序圖:

Paste_Image.png

上代碼

            //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接口說明:


捕獲.PNG

y值意義是,被繪制文字的baseline的y坐標膀跌,baseline究竟是什么呢遭商?

221837171589523.png

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"去掉一定的邊距值但是不能完全去掉康栈。

本文將自定義一個顯示兩行文字的控件,文字居中顯示喷橙,效果如下圖:

Paste_Image.png

直接上代碼啥么,查看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,歡迎訪問哪替。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栋荸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子夷家,更是在濱河造成了極大的恐慌蒸其,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件库快,死亡現(xiàn)場離奇詭異摸袁,居然都是意外死亡,警方通過查閱死者的電腦和手機义屏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門靠汁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人闽铐,你說我怎么就攤上這事蝶怔。” “怎么了兄墅?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵踢星,是天一觀的道長。 經(jīng)常有香客問我隙咸,道長沐悦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任五督,我火速辦了婚禮藏否,結果婚禮上,老公的妹妹穿的比我還像新娘充包。我一直安慰自己副签,他們只是感情好,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布基矮。 她就那樣靜靜地躺著淆储,像睡著了一般。 火紅的嫁衣襯著肌膚如雪愈捅。 梳的紋絲不亂的頭發(fā)上遏考,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機與錄音蓝谨,去河邊找鬼灌具。 笑死青团,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的咖楣。 我是一名探鬼主播督笆,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诱贿!你這毒婦竟也來了娃肿?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤珠十,失蹤者是張志新(化名)和其女友劉穎料扰,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體焙蹭,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡晒杈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了孔厉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拯钻。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖撰豺,靈堂內(nèi)的尸體忽然破棺而出粪般,到底是詐尸還是另有隱情,我是刑警寧澤污桦,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布亩歹,位于F島的核電站,受9級特大地震影響凡橱,放射性物質發(fā)生泄漏捆憎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一梭纹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧致份,春花似錦变抽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滔蝉,卻和暖如春击儡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝠引。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工阳谍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛀柴,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓矫夯,卻偏偏與公主長得像鸽疾,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子训貌,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容