第4章 View的工作原理

本章代碼GitHub地址:https://github.com/LittleFogCat/AndroidBookNote/tree/master/chapter04_view

4.0 要點(diǎn)

View的繪制流程
measure -> layout -> draw

常用回調(diào)
onAttach onVisibilityChanged onDetach

滑動處理

4.1 ViewRoot DecorView

首先是這張Android的窗口層級圖

窗口層級

可以看到掀亩,在一個界面中疾忍,包含了一個Window,Window中包含了一個DecorView忧陪。DecorView其實(shí)是一個FrameLayout馋艺,一般包含了且僅包含一個豎直的LinearLayout,這個LinearLayout中又包含了一個TitleView和一個ContentView迈套。我們調(diào)用setContentView(id)的時候捐祠,設(shè)置的就是這個ContentView的布局。

ViewRoot是WindowManager和DecorView之間的紐帶桑李。View的三大流程其實(shí)都是通過ViewRoot來完成的踱蛀。
關(guān)于ViewRoot的來歷,又是怎么成為WindowManager和DecorView的紐帶的芙扎,書中只是一筆帶過星岗,這里來一探究竟。(更新:在第8章中有詳細(xì)分析)

4.1.*

順著源碼追蹤戒洼,只看有用部分:

當(dāng)ActivityThread收到一個Activity啟動消息時俏橘,會調(diào)用handleLaunchActivity(ActivityClientRecord, Intent, String)方法,handleLaunchActivity方法中有以下幾句:

WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);

handleResumeActivity(r.token, false, r.isForward, 
!r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

可以看到圈浇,handleLaunchActivity中調(diào)用了performLaunchActivityhandleResumeActivity兩個方法寥掐。

performLaunchActivity()中,Activity被創(chuàng)建(onCreate也是在這里調(diào)用的)磷蜀,并且通過Activity.attach()方法將Window和Activity綁定召耘。

activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);

activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);

handleResumeActivity()方法中,DecorView會被添加到Window中褐隆。(同時這個方法里面也有一個performResumeActivity()方法污它,在這里調(diào)用Activity.onResume())。

最后庶弃,我們會調(diào)用Activity的makeVisible()方法衫贬,并通知AMS我們的Activity已經(jīng)resume了。

ViewManager wm = a.getWindowManager();
// 將DecorView添加到Window中歇攻,但此時其是不可見的
wm.addView(decor, l);
// ==> mDecor.setVisibility(View.VISIBLE);
r.activity.makeVisible();
ActivityManager.getService().activityResumed(token);

繼續(xù)跟蹤wm.addView(View, ViewGroup.LayoutParams)固惯,會在WindowManagerGlobal這個單例類中找到:

ViewRootImpl root;
root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);

這里的view即是decorView。至此缴守,我們成功的建立起了WindowManager -> ViewRootImpl -> DecorView的關(guān)系葬毫。

4.2 MeasureSpec

4.2.1 MeasureSpec

網(wǎng)上講MeasureSpec的有很多。簡單的來講屡穗,MeasureSpec就是這個View的大刑瘛(不準(zhǔn)確,但是可以這樣簡化理解)村砂。它是一個32位的整型烂斋,高2位代表SpecMode,低30位代表SpecSize。
SpecMode有三種:

  • UNSPECIFIED 父容器不對View做限制源祈。
  • EXACTLY 精確測量模式,即View的最終大小色迂。在View中設(shè)置具體數(shù)字大小香缺,或者match_parent都是這個模式。
  • AT_MOST 可用大小模式歇僧,View的大小不會超過這個值图张。對應(yīng)的是wrap_content。

4.2.2 MeasureSpec和LayoutParams的對應(yīng)關(guān)系

我們在LayoutParams中會定義View的寬高诈悍,即布局xml中的android:layout_widthandroid:layout_height屬性祸轮。一般來講,我們會設(shè)置match_parent侥钳,wrap_content或者具體的數(shù)值适袜。
同時,我們會通過View的measure方法向其傳遞MeasureSpec舷夺。綜合父布局的MeasureSpec和View的LayoutParam苦酱,我們可以計(jì)算出這個View的MeasureSpec。
拋開UNSPECIFIED不談(一般不用)给猾,以下表格表示了如何通過二者確定View具體MeasureSpec的:

LP \ 父SpecMode EXACTLY AT_MOST
具體數(shù)值 specMode: EXACTLY
specSize: View定義的size
specMode: EXACTLY
specSize: View定義的size
wrap_content specMode: AT_MOST
specSize: 父specSize
specMode: AT_MOST
specSize: 父specSize
match_parent specMode: EXACTLY
specSize: 父specSize
specMode: AT_MOST
specSize: 父specSize

可以看出疫萤,除非將View的寬高設(shè)定為確定的數(shù)值,否則其是受到父容器的影響的敢伸。具體的measure過程在下一節(jié)講到扯饶。

4.3 View的工作流程

View的工作流程主要指measuer、layout池颈、draw尾序。
measure測量View的寬高,layout確定View的位置和大小饶辙,draw將View繪制在屏幕上蹲诀。

4.3.1 measure

View通過measure來測量大小。同時弃揽,ViewGroup除了測量自己脯爪,還會遍歷子View并調(diào)用其measure方法。
之前我們已經(jīng)知道矿微,View的大小由MeasureSpec來決定痕慢,而MeasureSpec又是通過父布局的MeasureSpec和LayoutParam共同決定的。
通過查看源碼涌矢,我們可以看到掖举,View的measure過程主要是通過在measure(int, int)方法中調(diào)用onMeasure(int, int)進(jìn)行的。而onMeasure()的默認(rèn)實(shí)現(xiàn)只有一句:

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

我們在重寫onMeasure方法的時候娜庇,必須要調(diào)用setMeasuredDimension(int measuredWidth, int measuredHeight)方法塔次,否則系統(tǒng)會拋出異常方篮。這個方法的主要目的是給View的mMeasuredWidthmMeasuredWidth變量賦值。
也就是說励负,measure的結(jié)果就是藕溅,通過調(diào)用measure(int, int)方法,最終給View的mMeasuredWidthmMeasuredWidth變量賦值继榆,使得接下來的layout和draw流程順利進(jìn)行巾表。
measure(int, int)方法的兩個參數(shù)是從何而來的呢?
在4.2.2中我們知道了略吨,View的MeasureSpec是通過父布局的MeasureSpec和自身的LayoutParam來進(jìn)行計(jì)算的集币,而這個過程是在父ViewGroup中就已經(jīng)完成了的,如4.2.2中表格所示翠忠。也就是說鞠苟,事實(shí)上,measure過程絕大多數(shù)工作是在父容器里面就已經(jīng)完成了的负间。
在ViewGroup類中有一個getChildMeasureSpec(int spec, int padding, int childDimension)方法偶妖,在這里我們可以看到ViewGroup是怎么確定子View的measureSpec的,截取其中一段:

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }

特別地政溃,當(dāng)View的寬(或者高)設(shè)置為wrap_content的時候趾访,查看4.2.2的表格,我們可以看到董虱,View的SpecMode是AT_MOST扼鞋,而SpecSize是父布局剩余的尺寸。也就是說愤诱,我們最后給這個View賦值的測量大小云头,也是父布局剩余尺寸,這跟match_parent是一樣的效果淫半,不符合我們的預(yù)期溃槐。造成這個結(jié)果的原因是,父布局并不知道這個View應(yīng)該是多大科吭,所以只能傳遞父布局的SpecSize昏滴。所以當(dāng)我們自定義View的時候,需要重寫onMeasure方法对人,并在其中加入當(dāng)View的SpecMode是AT_MOST時谣殊,我們期望的測量結(jié)果。例如牺弄,我們想設(shè)置wrap_content時的寬高是100px:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST) {
            setMeasuredDimension(100, 100);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(100, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 100);
        }
    }

而ViewGroup的measure過程姻几,除了要測量自身以外,還要測量各個子View,測量完之后再計(jì)算出ViewGroup最終的大小蛇捌。而這個過程根據(jù)ViewGroup的不同抚恒,最終測量出來的大小也可能是不一樣的,例如LinearLayout和RelativeLayout络拌,他們的測量過程顯然不可能相同柑爸,所以ViewGroup并沒有默認(rèn)實(shí)現(xiàn)measure過程,在自定義ViewGroup的時候盒音,必須重寫onMeasure方法,否則會導(dǎo)致無法顯示馅而。雖然ViewGroup提供了measureChildren(int, int)和measureChild(View, int, int)方法祥诽,可以簡便的對子元素進(jìn)行測量,

4.3.2 layout

在計(jì)算好了尺寸之后瓮恭,我們需要把View挨個放進(jìn)ViewGroup里雄坪,如同搭積木一般。這個過程就是layout屯蹦。所以我們可以簡單的認(rèn)為维哈,layout的過程是為ViewGroup“量身定制”的。
layout過程跟measure很類似登澜,ViewGroup遍歷所有的子View阔挠,計(jì)算出其應(yīng)在的位置。如同measure的最終結(jié)果是將mMeasuredWidthmMeasuredWidth變量賦值一般脑蠕,layout的最終結(jié)果是給View的mLeft mTop mRight mBottom四個變量賦值购撼。
在ViewGroup中,onLayout是一個抽象方法谴仙,需要我們自己實(shí)現(xiàn)迂求,在其中放置我們的子View。舉個簡單的例子晃跺,我們要做個子元素豎直排列的布局揩局,并且每個子元素間隔10px,重寫ViewGroup的onLayout方法:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int top = t;
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            int childLeft = l;
            int childTop = top;
            int childRight = childLeft + child.getMeasuredWidth();
            int childBottom = childTop + child.getMeasuredHeight();
            child.layout(childLeft, childTop, childRight, childBottom);

            top += child.getMeasuredHeight() + 10;
        }
    }

效果如下:


layout

可以看到符合預(yù)期掀虎。

4.3.3 draw

一般情況下凌盯,draw分為以下幾步:

  1. 繪制背景(drawBackground)
  2. 繪制自身(onDraw)
  3. 繪制子元素(dispatchDraw)
  4. 繪制裝飾(scrollbars)

我們一般只關(guān)心自身的繪制,也就是說涩盾,重寫onDraw方法就可以了十气。對于自定義View的繪制,最重要的莫過于Canvas和Paint的使用春霍。

4.3.4 小結(jié)*

View的三大流程砸西,不是并列關(guān)系,而是依賴、遞進(jìn)的關(guān)系芹枷。也就是說衅疙,對于父布局,必須先測量好每個子元素的大小鸳慈,再確定他們每個的位置饱溢,最后才能繪制出他們的圖像。即:
measure -> layout -> draw

4.4 自定義View和ViewGroup

最后來根據(jù)本章內(nèi)容做一下自定義View走芋、ViewGroup绩郎。


預(yù)想圖

我的目標(biāo)是這樣的:

  1. 自定義View:外圈圓形,包裹了一個五角星翁逞±哒龋可以自定義圓形和五角星的顏色,以及五角星的旋轉(zhuǎn)角度挖函。(其實(shí)旋轉(zhuǎn)可以使用android:rotation屬性的)
  2. 自定義ViewGroup:將所有的子View從左到右状植,從上到下,依次排列怨喘。

4.4.1 自定義View:StarView

4.4.1.0 定義屬性

首先創(chuàng)建包含五角星各項(xiàng)數(shù)據(jù)的實(shí)體類Star

public class Star {
    /**
     * 五角星從中心到頂點(diǎn)的距離
     */
    private double mCVLength;

    /**
     * 中心點(diǎn)的坐標(biāo)
     */
    private Point mCenter;

    /**
     * 五角星旋轉(zhuǎn)的角度
     */
    private double mRotate;

    /**
     * 五角星5個頂點(diǎn)坐標(biāo)津畸,順序?yàn)椋簭淖钌戏巾旤c(diǎn)開始,順時針旋轉(zhuǎn)的所有頂點(diǎn)必怜。
     */
    private Point[] mPoints = new Point[5];

    // ...
}

略去其他部分肉拓,這里主要保存了五角星從中心到頂點(diǎn)的距離(大小)梳庆、中心點(diǎn)的坐標(biāo)(位置)帝簇、五角星旋轉(zhuǎn)的角度(角度),以及五個頂點(diǎn)的坐標(biāo)(前三個值計(jì)算得到)靠益。而我們等下在繪制圖形的過程中丧肴,主要用到的就是這五個點(diǎn)的坐標(biāo)。(至于是怎么求到的胧后,則是高中知識芋浮,過程充滿了血淚不表)

現(xiàn)在開始自定義StarView。新建StarView.java壳快,繼承自View纸巷。在style.xml中加入如下屬性:

    <declare-styleable name="StarView">
        <attr name="star_color" format="color" />
        <attr name="star_scale" format="float" />
        <attr name="star_rotate" format="float" />
    </declare-styleable>

分別代表五角星的顏色、五角星占圈內(nèi)的比例眶痰、五角星的旋轉(zhuǎn)角度瘤旨。而圓圈背景則直接從background屬性獲取,然后再把背景設(shè)置成透明:

        Drawable bgDrawable = getBackground();
        if (bgDrawable instanceof ColorDrawable) {
            mBgColor = ((ColorDrawable) bgDrawable).getColor();
        } else {
            mBgColor = Color.RED;
        }
        setBackgroundColor(0);

是不是很粗暴竖伯?

4.4.1.1 onMeasure

在onMeasure中存哲,我們只是處理了對于wrap_content的判斷:如果長(寬)是wrap_content因宇,那么就將其設(shè)置為與寬(長)相等,即正方形(實(shí)際繪圖區(qū)域祟偷,即去掉了padding之后的真實(shí)繪圖區(qū)域)察滑。如果二者皆是wrap_content,那么就均設(shè)為默認(rèn)大小修肠。

4.4.1.2 onDraw

首先贺辰,我們?nèi)コ烁鞣Npadding之后,得到了真實(shí)的圓心坐標(biāo)(cx, cy)嵌施、半徑r饲化。半徑的值為真實(shí)繪圖區(qū)域短邊的一半。然后調(diào)用canvas.drawCircle()方法繪制出背景圓形吗伤。
然后滓侍,我們定義的Star類就登場了。
回顧一下牲芋,我們創(chuàng)建了Star對象之后,就可以獲取它的5個頂點(diǎn)坐標(biāo)捺球。知道了坐標(biāo)缸浦,我們就可以通過Path + canvas.DrawPath()來繪圖了。先使用Path對象氮兵,按我們平時手工的方法畫一個五角星裂逐,然后再drawPath填充顏色。代碼如下:

        // draw star
        Star star = getStar(mStarScale * r, cx, cy, mStarRotate);
        Star.Point points[] = star.getPoints();

        mPath.setFillType(Path.FillType.WINDING);
        mPath.moveTo(points[0].x, points[0].y);
        mPath.lineTo(points[3].x, points[3].y);
        mPath.lineTo(points[1].x, points[1].y);
        mPath.lineTo(points[4].x, points[4].y);
        mPath.lineTo(points[2].x, points[2].y);
        mPath.close();

        mPaint.setColor(mStarColor);
        canvas.drawPath(mPath, mPaint);

其中g(shù)etStar()方法是為了避免在onDraw中創(chuàng)建對象泣栈。mPath.setFillType(Path.FillType.WINDING)允許我們完全填充這個路徑內(nèi)部卜高。具體可以參考相關(guān)文章:https://blog.csdn.net/qq_30889373/article/details/78793086

4.4.1.3 完整代碼

/**
 * 圓形背景,五角星圖案的自定義View南片。
 */
public class StarView extends View {
    private static final String TAG = "StarView";
    private static final int DEFAULT_SIZE_PX = 128;
    private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private int mStarColor;
    private int mBgColor;
    private float mStarScale;
    private float mStarRotate;
    private Path mPath;

    public StarView(Context context) {
        this(context, null, 0);
    }

    public StarView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StarView);
        mStarColor = a.getColor(R.styleable.StarView_star_color, Color.YELLOW);
        mStarScale = a.getFloat(R.styleable.StarView_star_scale, 0.8f);
        mStarRotate = a.getFloat(R.styleable.StarView_star_rotate, 0);
        a.recycle();

        init();
    }

    private void init() {
        Drawable bgDrawable = getBackground();
        if (bgDrawable instanceof ColorDrawable) {
            mBgColor = ((ColorDrawable) bgDrawable).getColor();
        } else {
            mBgColor = Color.RED;
        }
        setBackgroundColor(0);
        mPath = new Path();
    }

    @Override
    @SuppressWarnings("SuspiciousNameCombination")
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        // 對長寬為wrap_content的判斷
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(DEFAULT_SIZE_PX, DEFAULT_SIZE_PX);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            int drawSize = heightSpecSize - getPaddingTop() - getPaddingBottom();
            setMeasuredDimension(drawSize + getPaddingLeft() + getPaddingRight(), heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            int drawSize = widthSpecSize - getPaddingLeft() - getPaddingRight();
            setMeasuredDimension(widthSpecSize, drawSize + getPaddingTop() + getPaddingBottom());
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        int realWidth = width - paddingLeft - paddingRight;
        int realHeight = height - paddingTop - paddingBottom;
        if (realWidth <= 0 || realHeight <= 0) {
            return;
        }

        float cx, cy, r; // 圓心坐標(biāo)x掺涛,圓心坐標(biāo)y,半徑

        r = Math.min(realWidth, realHeight) / 2.0f;
        cx = paddingLeft + r;
        cy = paddingTop + r;

        // draw background
        if (mBgColor != Color.TRANSPARENT) {
            mPaint.setColor(mBgColor);
            canvas.drawCircle(cx, cy, r, mPaint);
        }

        // draw star
        Star star = getStar(mStarScale * r, cx, cy, mStarRotate);
        Star.Point points[] = star.getPoints();

        mPath.setFillType(Path.FillType.WINDING);
        mPath.moveTo(points[0].x, points[0].y);
        mPath.lineTo(points[3].x, points[3].y);
        mPath.lineTo(points[1].x, points[1].y);
        mPath.lineTo(points[4].x, points[4].y);
        mPath.lineTo(points[2].x, points[2].y);
        mPath.close();

        mPaint.setColor(mStarColor);
        canvas.drawPath(mPath, mPaint);
    }

    /**
     * 由于onDraw中最好不要頻繁的創(chuàng)建對象薪缆,所以使用臨時的成員來保存Star。
     */
    private Star mStarTemp;

    private Star getStar(double a, double cx, double cy, double rotate) {
        if (mStarTemp == null) {
            mStarTemp = new Star(a, cx, cy, rotate);
        } else {
            mStarTemp.setStar(a, cx, cy, rotate);
        }
        return mStarTemp;
    }

}
/**
 * 通過輸入五角星的中心點(diǎn)坐標(biāo)和頂點(diǎn)到中心的長度伞广,計(jì)算出五角星每個頂點(diǎn)的坐標(biāo)拣帽。
 * <p>
 * Created by LittleFogCat on 2019/1/26.
 */
@SuppressWarnings("WeakerAccess")
public class Star {

    /**
     * 一些計(jì)算中常用的常數(shù)
     */
    private static final double sin18 = sin(18);
    private static final double sin36 = sin(36);
    private static final double cos18 = cos(18);
    private static final double cos36 = cos(36);

    /**
     * 五角星從中心到頂點(diǎn)的距離
     */
    private double mCVLength;

    /**
     * 中心點(diǎn)的坐標(biāo)
     */
    private Point mCenter;

    /**
     * 五角星旋轉(zhuǎn)的角度
     */
    private double mRotate;

    /**
     * 五角星5個頂點(diǎn)坐標(biāo),順序?yàn)椋簭淖钌戏巾旤c(diǎn)開始嚼锄,順時針旋轉(zhuǎn)的所有頂點(diǎn)减拭。
     */
    private Point[] mPoints = new Point[5];

    /**
     * 構(gòu)造函數(shù),構(gòu)造出一個正置無旋轉(zhuǎn)的五角星区丑。
     *
     * @param a  五角星中心到頂點(diǎn)的距離
     * @param cx 五角星中心坐標(biāo)x
     * @param cy 五角星中心坐標(biāo)y
     */
    public Star(double a, double cx, double cy) {
        this(a, cx, cy, 0);
    }

    /**
     * 主要構(gòu)造函數(shù)拧粪。根據(jù)五角星中心坐標(biāo)和中心到頂點(diǎn)的距離修陡,計(jì)算出每個頂點(diǎn)的長度。
     *
     * @param a      五角星中心到頂點(diǎn)的距離
     * @param cx     五角星中心坐標(biāo)x
     * @param cy     五角星中心坐標(biāo)y
     * @param rotate 五角星旋轉(zhuǎn)角度既们,0度為正置五角星
     */
    public Star(double a, double cx, double cy, double rotate) {
        mCVLength = a;
        mCenter = new Point(cx, cy);
        mRotate = rotate;
        makeCoordinate();
    }

    public void setStar(double a, double cx, double cy, double rotate) {
        mCVLength = a;
        mCenter.x = (float) cx;
        mCenter.y = (float) cy;
        mRotate = rotate;
        makeCoordinate();
    }

    /**
     * 計(jì)算頂點(diǎn)坐標(biāo)濒析。
     */
    private void makeCoordinate() {
        Point p[] = getPoints();
        final double x = mCenter.x;
        final double y = mCenter.y;
        final double a = mCVLength;

        if (mRotate == 0) {
            p[0] = new Point(x, y - a);
            p[1] = new Point(x + a * cos18, y - a * sin18);
            p[2] = new Point(x + a * sin36, y + a * cos36);
            p[3] = new Point(x - a * sin36, y + a * cos36);
            p[4] = new Point(x - a * cos18, y - a * sin18);
        } else {
            final double r = mRotate;
            for (int i = 0; i < 5; i++) {
                p[i] = new Point(x + a * sin(r + 72 * i), y - a * cos(r + 72 * i));
            }
        }
    }

    /**
     * 獲取五角星的頂點(diǎn)坐標(biāo)
     *
     * @return 五角星的頂點(diǎn)坐標(biāo)
     */
    public Point[] getPoints() {
        if (mPoints == null || mPoints.length != 5) {
            mPoints = new Point[5];
        }
        return mPoints;
    }

    /**
     * {@link Math#sin(double)} 參數(shù)是弧度,這里轉(zhuǎn)換為以度數(shù)為參數(shù)的函數(shù)
     *
     * @param a degree
     * @return sin(a)
     */
    private static double sin(double a) {
        return Math.sin(Math.toRadians(a));
    }

    /**
     * {@link Math#cos(double)} 參數(shù)是弧度啥纸,這里轉(zhuǎn)換為以度數(shù)為參數(shù)的函數(shù)
     *
     * @param a degree
     * @return cos(a)
     */
    private static double cos(double a) {
        return Math.cos(Math.toRadians(a));
    }

    public static class Point {
        public float x, y;

        public Point() {
        }

        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }

        public Point(double x, double y) {
            this.x = (float) x;
            this.y = (float) y;
        }

    }
}

4.4.2 自定義ViewGroup:FlowLayout

我們給他取了一個很好聽的名字号杏,F(xiàn)lowLayout流布局。實(shí)際上就是把子View挨個放斯棒。雖然寫的時候感覺挺麻煩的盾致,但是其實(shí)思路上面很簡單,沒什么復(fù)雜的地方荣暮。

4.4.2.0 onMeasure

只需要處理長(寬)是wrap_content的情況庭惜。思路很簡單,挨個取出所有的子View:

  1. 如果寬是wrap_content穗酥,那么用變量保存最長行的寬度护赊,本行寬度和本行剩余寬度;如果本行剩余寬度比這個子View小砾跃,那么就到下一行繼續(xù)排骏啰,比較本行寬度和最長寬度;最后哪一行的寬度最寬抽高,setMeasuredDimension的width就是它了(當(dāng)然判耕,不能超過parent的寬度);
  2. 如果高是wrap_content翘骂,那么和1中相同的排法壁熄,不同的就是記錄每行最高的View高度,然后把他們?nèi)悠饋硖季梗玫降木褪强偟母叨攘耍ó?dāng)然草丧,不能超過parent的高度);
  3. 如果寬高都是wrap_content莹桅,那么就是1和2的結(jié)合方仿。

4.4.2.1 onLayout

排布方式已經(jīng)在onMeasure中說過了,所以onLayout只需要簡單的算一下子View的上下左右坐標(biāo)即可统翩。
需要注意的是仙蚜,為了支持margin屬性,我們需要自定義LayoutParams厂汗,繼承自ViewGroup.MarginLayoutParams委粉,然后重寫generateLayoutParams()方法。

4.4.2.2 完整代碼

public class FlowLayout extends ViewGroup {
    public FlowLayout(Context context) {
        super(context);
        init();
    }

    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    private void init() {

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        final int childCount = getChildCount();
        measureChildrenWithMargins(widthMeasureSpec, heightMeasureSpec);

        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            int totalWidth = getPaddingLeft() + getPaddingRight();
            int totalHeight = getPaddingTop() + getPaddingBottom();
            int rowWidth = getPaddingLeft() + getPaddingRight();
            int rowHeight = 0;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                if (widthSize - rowWidth < childWidth) { // 行剩余空間不足娶桦,需要換行
                    totalHeight += childHeight;
                    rowHeight = childHeight;
                    rowWidth = childWidth + getPaddingLeft() + getPaddingRight();

                } else {
                    rowWidth += childWidth;
                    if (childHeight > rowHeight) {
                        rowHeight = childHeight;
                        totalHeight += childHeight - rowHeight;
                    }
                }
                if (totalWidth < rowWidth) {
                    totalWidth = rowWidth;
                }
            }
            setMeasuredDimension(totalWidth, totalHeight);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            int totalWidth = getPaddingLeft() + getPaddingRight();
            int rowWidth = getPaddingLeft() + getPaddingRight();
            int rowHeight = getPaddingTop() + getPaddingBottom();
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                if (widthSize - rowWidth < childWidth) { // 行剩余空間不足贾节,需要換行
                    rowHeight = childHeight;
                    rowWidth = childWidth + getPaddingLeft() + getPaddingRight();
                } else {
                    rowWidth += childWidth;
                    if (childHeight > rowHeight) {
                        rowHeight = childHeight;
                    }

                }
                if (totalWidth < rowWidth) {
                    totalWidth = rowWidth;
                }
            }
            setMeasuredDimension(totalWidth, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            int totalHeight = getPaddingTop() + getPaddingBottom();
            int rowWidth = getPaddingLeft() + getPaddingRight();
            int rowHeight = 0;
            for (int i = 0; i < childCount; i++) {
                View child = getChildAt(i);
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                if (widthSize - rowWidth < childWidth) { // 行剩余空間不足汁汗,需要換行
                    totalHeight += childHeight;
                    rowHeight = childHeight;
                    rowWidth = childWidth + getPaddingLeft() + getPaddingRight();
                } else {
                    rowWidth += childWidth;
                    if (childHeight > rowHeight) {
                        rowHeight = childHeight;
                        totalHeight += childHeight - rowHeight;
                    }
                }
            }
            setMeasuredDimension(widthSize, totalHeight);
        }

    }

    protected void measureChildrenWithMargins(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int childCount = getChildCount();
        int widthUsed = getPaddingLeft() + getPaddingRight();
        int heightUsed = getPaddingTop() + getPaddingBottom();
        int rowHeight = 0;
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (child.getVisibility() == View.GONE) {
                continue;
            }
            if (widthUsed + lp.width + lp.leftMargin + lp.rightMargin > widthSpecSize) {
                widthUsed = getPaddingLeft() + getPaddingRight();
                rowHeight = 0;
            }
            measureChildWithMargins(child, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed);
            int measuredWidth = child.getMeasuredWidth();
            int measuredHeight = child.getMeasuredHeight();
            widthUsed += measuredWidth;
            if (measuredHeight > rowHeight) {
                rowHeight = measuredHeight;
                heightUsed += measuredHeight - rowHeight;
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();

        final int childCount = getChildCount();
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int rowHeight = 0;
        int childTop = paddingTop;
        int childLeft = paddingLeft;

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();
                LayoutParams lp = (LayoutParams) child.getLayoutParams();

                int left, top, right, bottom;
                if (childLeft + childWidth > width) { // 換行
                    childTop += rowHeight;

                    left = paddingLeft + lp.leftMargin;
                    top = childTop + lp.topMargin;
                    right = left + childWidth;
                    bottom = top + childHeight;
                    child.layout(left, top, right, bottom);
                    childLeft = right + lp.rightMargin;

                    rowHeight = childHeight + lp.topMargin + lp.bottomMargin;
                } else {
                    left = childLeft + lp.leftMargin;
                    top = childTop + lp.topMargin;
                    right = left + childWidth;
                    bottom = top + childHeight;
                    child.layout(left, top, right, bottom);
                    childLeft = right + lp.rightMargin;

                    rowHeight = Math.max(rowHeight, childHeight + lp.topMargin + lp.bottomMargin);
                }
            }
        }
    }

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

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

最后編輯于
?著作權(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)容