Android學(xué)習(xí)感悟之View繪制流程

簡介

說到View橄浓,只要是接觸過Android的朋友份氧,應(yīng)該都知道是什么東西,他就是承載各種數(shù)據(jù)顯示的控件诗茎,比如文本常常使用TextView來顯示工坊,圖片->ImageView,列表->RecyclerView敢订;在這篇文章中會從以下幾個方面來介紹我所感悟的View王污。

本文包括:

  • 如何自定義View;

自定義View

首先我們需要了解為什么要自定義View楚午?在我看來昭齐,比較常見的其目的有以下兩種:

  • 解決系統(tǒng)提供的View難以滿足界面實(shí)現(xiàn)的需求;
  • 封裝某一特定View組合矾柜,實(shí)現(xiàn)代碼的復(fù)用阱驾;

那要如何實(shí)現(xiàn)自定義View,下邊會用一些簡單的例子來說明怪蔑;當(dāng)然實(shí)現(xiàn)方式也是多種多樣里覆,我認(rèn)為可以分為以下幾種情況:

  • 自定義View:

    • 繼承View并進(jìn)一步實(shí)現(xiàn);
    • 繼承已有的View并進(jìn)一步實(shí)現(xiàn)缆瓣。
  • 自定義ViewGroup:

    • 繼承ViewGroup并進(jìn)一步實(shí)現(xiàn)喧枷;
    • 繼承已有的ViewGroup并實(shí)現(xiàn);

接下來借用例子逐一實(shí)現(xiàn),首先從自定義View開始割去,繼承View并實(shí)現(xiàn)窟却,接下來要實(shí)現(xiàn)的是以左上角為直角的等腰直角三角形;

第一步:創(chuàng)建一個View取名叫TriangleView繼承自View并實(shí)現(xiàn)其構(gòu)造方法呻逆,如下:

public class TriangleView extends View {
    public TriangleView(Context context) {
        this(context, null);
    }

    public TriangleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TriangleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

這一步中有一個細(xì)節(jié)和一個要點(diǎn)夸赫。

細(xì)節(jié)就是構(gòu)造函數(shù)之間的調(diào)用,如果在不清楚你所繼承的View構(gòu)造方法的調(diào)用中是否有其他配置的話(例如主題樣式)咖城,最好還是直接調(diào)用對應(yīng)參數(shù)的super構(gòu)造函數(shù)茬腿;

要點(diǎn)就是必須重寫帶有Attibuteset參數(shù)的構(gòu)造參數(shù),這樣我們才能拿到在布局的xml文件中的參數(shù)宜雀。

第二步:想好自定義View所包含的屬性切平,這里我們只需要三角形的填充色,實(shí)現(xiàn)如下:

首先在資源文件中創(chuàng)建一個declare-styleable資源辐董,一般寫在values下的attrs文件中悴品,如下:

<declare-styleable name="TriangleView">
    <attr name="fillColor" format="color" />
</declare-styleable>

這里需要注意的是名字最好與自定義View的名字一致,方便查找简烘,也更加規(guī)范苔严;

attr標(biāo)簽中就是自定義的屬性,包含了名稱和類型孤澎,其中類型有很多種届氢,下邊也整理了一個表格,描述各個屬性的含義覆旭,如下:

format meaning
color 顏色值退子,如:"#ff0000"或"R.color.colorAccent"
dimension 尺寸,如:單位為sp,dp型将,px的值
reference 資源id寂祥,如:"R.drawable.img_loading"
boolean 布爾類型,如:true or false
float 浮點(diǎn)小數(shù)七兜,如:0.1
integer 整數(shù)壤靶,如:10
string 字符串,如:"你好啊"或"R.string.app_name"
fraction 百分比惊搏,如:20%
enum 枚舉,具體使用請參照LinearLayout的orientation屬性
flag 標(biāo)記忧换,即某個屬性可以有多個標(biāo)記恬惯,相對于枚舉而言,枚舉只能選一個值亚茬,這個可以選多個酪耳,例如layout_gravity屬性

定義好了,接下來就是設(shè)置屬性和獲取屬性了,屬性的設(shè)置可在xml布局文件中設(shè)置碗暗,例如:

<com.arvin.example.allibraryexample.TriangleView
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:fillColor="@color/colorAccent" />

注意颈将,在使用自定義屬性是需要該View或者其父View中加上

xmlns:app="http://schemas.android.com/apk/res-auto"

其作用就是能自動找到該View所定義的屬性,使用方式就是

app:自定義屬性

然后就是獲取屬性言疗,如下:

super(context, attrs, defStyleAttr);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TriangleView);
fillColor = typedArray.getColor(R.styleable.TriangleView_fillColor, Color.RED);
typedArray.recycle();

attrs中包含了晴圾,在xml布局文件中所有的屬性,系統(tǒng)自帶屬性可通過調(diào)用父類的構(gòu)造函數(shù)獲得噪奄,自定義屬性死姚,即可通過TypedArray獲得,每種自定義屬性的類型都在TypedArray中對應(yīng)著一個get方法勤篮,等我們?nèi)⊥晁阉鲗傩院蠖级荆浀靡猺ecycler掉,原因就是為了復(fù)用這個TypedArray碰缔,具體原因在這里就不再細(xì)說了账劲,總之這個模式這樣用很好。

第三步金抡,定義View的大小

這其中就涉及到一個比較重要的方法了瀑焦,需要重寫

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
}

方法,然后設(shè)置寬度和高度竟终,這里有一點(diǎn)小常識蝠猬,Android中所有的View外形都是一個矩形,只是繪制時统捶,不同的方法導(dǎo)致形狀不同而已榆芦。言歸正傳,這時候就需要講解一下計算大小的規(guī)則了喘鸟,Android提供了一個MeasureSpec匆绣,包含了測量模式,以及測量尺寸什黑,測量模式包括:

  • UNSPECIFIED
    父視圖不對子視圖有任何約束崎淳,它可以達(dá)到所期望的任意尺寸。比如 ListView愕把、ScrollView拣凹,一般自定義 View 中用不到

  • EXACTLY
    父視圖為子視圖指定一個確切的尺寸,而且無論子視圖期望多大恨豁,它都必須在該指定大小的邊界內(nèi)嚣镜,對應(yīng)的屬性為 match_parent 或具體值,比如 100dp橘蜜,父控件可以通過MeasureSpec.getSize(measureSpec)直接得到子控件的尺寸菊匿。

  • AT_MOST
    父視圖為子視圖指定一個最大尺寸。子視圖必須確保它自己所有子視圖可以適應(yīng)在該尺寸范圍內(nèi),對應(yīng)的屬性為 wrap_content跌捆,這種模式下徽职,父控件無法確定子 View 的尺寸,只能由子控件自己根據(jù)需求去計算自己的尺寸佩厚,這種模式就是我們自定義視圖需要實(shí)現(xiàn)測量邏輯的情況姆钉。

簡單的了解過后我們參考o(jì)nMeasure的默認(rèn)實(shí)現(xiàn),如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

public int getMinimumHeight() {
    return mMinHeight;
}

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

代碼很簡單可款,可以發(fā)現(xiàn)育韩,其實(shí)就是通過調(diào)用setMeasuredDimension方法,設(shè)置了寬度和高度闺鲸,默認(rèn)情況下筋讨,wrap_content時,size為0摸恍,而我希望有一點(diǎn)高度和寬度悉罕,所以,我們自己寫一個getSize方法立镶,并讓測量模式為wrap_content時壁袄,有一個默認(rèn)的大小,具體如下:

private int getSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
            result = dp2px(DEFAULT_SIZE);
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result;
}

private int dp2px(float dp) {
    return (int) (dp * getResources().getDisplayMetrics().density + 0.5f);
}

然后畫出來的三角形媚媒,我希望是等邊直角三角形嗜逻,所以要讓寬和高一樣大,這樣簡單處理一下后缭召,大小就設(shè)置完了栈顷,具體的onMeasure實(shí)現(xiàn)如下:

@SuppressWarnings("SuspiciousNameCombination")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = getSize(getSuggestedMinimumWidth(), widthMeasureSpec);
    int height = getSize(getSuggestedMinimumWidth(), heightMeasureSpec);

    if (width > height) {
        width = height;
    } else {
        height = width;
    }

    setMeasuredDimension(width, height);
}

第四步,重寫onDraw方法嵌巷,這里就是繪制三角形萄凤,代碼很簡單,就不解釋了搪哪,如下:

//在構(gòu)造函數(shù)中調(diào)用靡努,初始化畫筆
private void init() {
    mPaint = new Paint();
    mPaint.setAntiAlias(true);
    mPaint.setColor(fillColor);
}

@SuppressLint("DrawAllocation")
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    Path path = new Path();
    path.moveTo(0, 0);
    path.lineTo(getWidth(), 0);
    path.lineTo(0, getHeight());
    path.close();

    canvas.drawPath(path, mPaint);
}

這樣,以左上角為直角的等邊直角三角形就畫好了晓折。

對于繼承特定View來實(shí)現(xiàn)自定義View惑朦,也不外乎上邊四步,所以就不再介紹了漓概;

接下來就是實(shí)現(xiàn)一個自定義ViewGroup行嗤,其中屬性的創(chuàng)建和獲取以及繪制與自定義View一致,就不再贅述垛耳,而更為重要的就是onMeasure以及onLayout方法,ViewGroup在我看來更像一個管理者,把自己的子View管理起來堂鲜,管理其大小栈雳,安排其位置。

下面缔莲,我們就來做一個自定義的ViewGroup哥纫,功能就是流布局,從上到下痴奏,從左到右蛀骇,把子View依次排列;我們可以想象一下實(shí)現(xiàn)思路读拆,首先擅憔,其ViewGroup的大小如何設(shè)置,很明顯檐晕,需要分別考慮AT_MOST和EXACTLY兩種模式(UNSPECIFIED這里不作考慮)下寬度和高度如何計量暑诸,思路如下:

  • AT_MOST

    寬度:只要等于寬度最大的一行的寬度加上paddingLeft和paddingRight后與父容器能給的最大塊度比較,較小的就是最終的寬度辟灰;

    高度:把每一行的高度加起來再加上paddingTop和paddingBottom后與父容器能給的最大高度比較个榕,較小的就是最終的高度;

  • EXACTLY

    寬度:其父容器能給的最大寬度芥喇;

    高度:其父容器能給的最大高度西采;

其中有一個邏輯就是什么時候換行,其實(shí)很明顯继控,就是當(dāng)這一行累積的子View的寬度大于容器的寬度時械馆,最后一個累積的View需要換行。換行后的高度湿诊,就在上一行的下邊開始狱杰。

下邊就是具體的代碼:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int horizontalPadding = getPaddingLeft() + getPaddingRight();
    int verticalPadding = getPaddingTop() + getPaddingBottom();

    int width = 0;
    int height = 0;

    int lineWidth = 0;
    int lineHeight = 0;

    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();

        measureChild(child, widthMeasureSpec, heightMeasureSpec);

        int childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
        int childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;

        if (childWidth + lineWidth > widthSize - horizontalPadding) {
            width = Math.max(width, lineWidth);
            height += lineHeight;

            lineWidth = childWidth;
            lineHeight = childHeight;
        } else {
            lineWidth += childWidth;
            lineHeight = Math.max(childHeight, lineHeight);
        }

        if (i == getChildCount() - 1) {
            width = Math.max(width, lineWidth) + horizontalPadding;
            height += lineHeight + verticalPadding;
        }
    }

    setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width,
            heightMode == MeasureSpec.EXACTLY ? heightSize : height);
 }

首先獲取到父容器為該view分配的最大測量尺寸以及測量模型,目的就是限制最大的寬度厅须,這里高度就沒有做限制仿畸,子View有多少都給顯示出來;

定義變量朗和,width和height表示最大的寬度和高度错沽;lineWidth和lineHeight為了記錄當(dāng)前行最大寬度和高度;horizontalPadding和verticalPadding就是pading眶拉,很簡單就不解釋了千埃。

然后就是遍歷所有子View,通過measureChild方法忆植,測量一下子View放可,然后就能獲取到子View的大小了(這里我們還支持了margin)谒臼;

接下來就是最重要的邏輯了,就是是否換行的邏輯耀里,即當(dāng)加上當(dāng)前子View后寬度是否比內(nèi)容最大寬度大蜈缤,大則換行,小則繼續(xù)累加冯挎;換行后底哥,行寬從新計算,行高累積房官,遍歷完后趾徽,就能得到一個承載搜索子View的寬高,然后再看測量模型翰守,來決定如何設(shè)值孵奶,MeasureSpec.EXACTLY則就是父容器所設(shè)定的值,MeasureSpec.AT_MOST則就是計算出來的寬高潦俺,這樣就把這個子View的大小測量完畢了拒课。

其中又一點(diǎn)需要注意,是讓View具有margin事示,則需要生成一個LayoutParams早像,如下:

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}

最后就是把子View排位,這一步和測量時邏輯幾乎相同肖爵,唯一不同就是卢鹦,需要讓子View調(diào)用layout方法,安排自己的位置劝堪,代碼如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int left = getPaddingLeft();
    int top = getPaddingTop();

    int horizontalPadding = getPaddingLeft() + getPaddingRight();

    int lineWidth = 0;
    int lineHeight = 0;

    for (int i = 0; i < getChildCount(); i++) {
        View child = getChildAt(i);
        if (child.getVisibility() == View.GONE) {
            continue;
        }
        MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();

        int childWidth = child.getMeasuredWidth() + params.leftMargin + params.rightMargin;
        int childHeight = child.getMeasuredHeight() + params.topMargin + params.bottomMargin;

        int cl, ct, cr, cb;

        if (childWidth + lineWidth > getWidth() - horizontalPadding) {

            left = getPaddingLeft();
            top += lineHeight;

            cl = left + params.leftMargin;
            ct = top + params.topMargin;
            cr = cl + child.getMeasuredWidth();
            cb = ct + child.getMeasuredHeight();

            left += childWidth;

            lineWidth = childWidth;
            lineHeight = childHeight;
        } else {
            cl = left + params.leftMargin;
            ct = top + params.topMargin;
            cr = cl + child.getMeasuredWidth();
            cb = ct + child.getMeasuredHeight();

            left += childWidth;

            lineWidth += childWidth;
            lineHeight = Math.max(childHeight, lineHeight);
        }

        child.layout(cl, ct, cr, cb);
    }
}

變量解釋:
left就是每一個View的最左邊冀自,top就是每一個View的最上邊;cl,ct,cr,cb分別表示當(dāng)前子View的左上右下的位置秒啦;

邏輯結(jié)構(gòu)都是一樣熬粗,就是在每一個View判斷完是否換行后,計算出其坐標(biāo)余境,并layout驻呐,計算過程也很簡單,如果不換行就改變left的值芳来,保持top不變含末,以及根據(jù)子View設(shè)置右下的位置,換行時即舌,初始化left佣盒,top值在上一行的下邊,這一步layout后記得更改left的值顽聂,這樣子View的布局就搞定了肥惭。測試運(yùn)行盯仪,發(fā)現(xiàn)剛剛好。

還有其他的一些自定義ViewGroup蜜葱,但是掌握了最核心的原理后磨总,發(fā)現(xiàn)每一個都是這樣,所以還有一些實(shí)現(xiàn)方式就不再距離說明了笼沥。

這樣自定義View就完成了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末娶牌,一起剝皮案震驚了整個濱河市奔浅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌诗良,老刑警劉巖汹桦,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鉴裹,居然都是意外死亡舞骆,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門径荔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來督禽,“玉大人,你說我怎么就攤上這事总处”繁梗” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵鹦马,是天一觀的道長胧谈。 經(jīng)常有香客問我,道長荸频,這世上最難降的妖魔是什么菱肖? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮旭从,結(jié)果婚禮上稳强,老公的妹妹穿的比我還像新娘。我一直安慰自己遇绞,他們只是感情好键袱,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著摹闽,像睡著了一般蹄咖。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上付鹿,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天澜汤,我揣著相機(jī)與錄音蚜迅,去河邊找鬼。 笑死俊抵,一個胖子當(dāng)著我的面吹牛谁不,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播徽诲,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼刹帕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谎替?” 一聲冷哼從身側(cè)響起偷溺,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钱贯,沒想到半個月后挫掏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秩命,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年尉共,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弃锐。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡袄友,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拿愧,到底是詐尸還是另有隱情杠河,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布浇辜,位于F島的核電站券敌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏柳洋。R本人自食惡果不足惜待诅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望熊镣。 院中可真熱鬧卑雁,春花似錦、人聲如沸绪囱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鬼吵。三九已至扣甲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間齿椅,已是汗流浹背琉挖。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工启泣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人示辈。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓寥茫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親矾麻。 傳聞我的和親對象是個殘疾皇子纱耻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

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