4.3 自定義控件 之 繼承ViewGroup實現標籤雲

點此進入:從零快速構建APP系列目錄導圖
點此進入:UI編程系列目錄導圖
點此進入:四大組件系列目錄導圖
點此進入:數據網絡和線程系列目錄導圖

實現一個知識點的標籤顯示,每個標籤的長度未知,如下圖所示:

本篇的控件涉及到的內容比較多,所以先介紹下View的繪製流程、相關回調方法等,避免後面用到的時候不知道什麼意思。

一、View繪製流程

1、mesarue() 測量過程

主要作用:爲整個 View 樹計算實際的大小,即設置實際的高(對應屬性:mMeasuredHeight)和寬(對應屬性:mMeasureWidth),每個 View 的控件的實際寬高都是由父視圖和本身視圖決定的。

具體的調用鏈如下:ViewRoot 根對象的屬性 mView(其類型一般爲 ViewGroup 類型)調用 measure()方法去計算 View 樹的大小,回調 View/ViewGroup 對象的 onMeasure() 方法,該方法實現的功能如下:
1、設置本 View 視圖的最終大小,該功能的實現通過調用 setMeasuredDimension()方法去設置實際的高(對應屬性:mMeasuredHeight)和寬(對應屬性:mMeasureWidth)。
2 、如果該 View 對象是個 ViewGroup 類型,需要重寫該 onMeasure() 方法,對其子視圖進行遍歷的measure() 過 程 。 對 每 個 子 視 圖 的 measure() 過 程 , 是 通 過 調 用 父 類 ViewGroup.java 類 裏 的measureChildWithMargins() 方法去實現,該方法內部只是簡單地調用了 View 對象的 measure() 方法。

2、layout() 佈局過程

主要作用:爲將整個根據子視圖的大小以及佈局參數將 View 樹放到合適的位置上。
具體的調用鏈如下:
1、layout 方法會設置該 View 視圖位於父視圖的座標軸,即 mLeft,mTop,mLeft,mBottom(調用setFrame()函數去實現)接下來回調 onLayout()方法(如果該 View 是 ViewGroup 對象,需要實現該方法,對每個子視圖進行佈局)。
2、如果該 View 是個 ViewGroup 類型,需要遍歷每個子視圖 childView,調用該子視圖的 layout() 方法去設置它的座標值。
3、draw()繪圖過程
由 ViewRoot 對象的 performTraversals() 方法調用 draw() 方法發起繪製該 View 樹,值得注意的是每次發起繪圖時,並不會重新繪製每個 View 樹的視圖,而只會重新繪製那些“需要重繪”的視圖,View 類內部變量包含了一個標誌位 DRAWN,當該視圖需要重繪時,就會爲該 View 添加該標誌位。

調用流程 :

1 、繪製該 View 的背景
2 、爲顯示漸變框做一些準備操作(大多數情況下,不需要改漸變框)
3、調用 onDraw() 方法繪製視圖本身(每個 View 都需要重載該方法,ViewGroup 不需要實現該方法)
4、調用 dispatchDraw() 方法繪製子視圖(如果該 View 類型不爲 ViewGroup,即不包含子視圖,不需要重載該方法)

值得說明的是,ViewGroup 類已經爲我們重寫了 dispatchDraw() 的功能實現,應用程序一般不需要重寫該方法,但可以重載父類函數實現具體的功能。

另外,關於 invalidate() 方法的介紹,大家可以參照這篇:Android中View繪製流程以及invalidate()等相關方法分析

二、自定義標籤雲類的實現

1、自定義屬性

<declare-styleable name="TagsLayout">
        <attr name="tagVerticalSpace" format="dimension" />
        <attr name="tagHorizontalSpace" format="dimension" />
</declare-styleable>

2、構造函數中獲取自定義屬性值

    public TagsLayout(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public TagsLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mAttributeSet = attrs;
        init();
    }

    public TagsLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        mAttributeSet = attrs;
        init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public TagsLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mContext = context;
        mAttributeSet = attrs;
        init();
    }

    private void init() {
        TypedArray attrArray = mContext.obtainStyledAttributes(mAttributeSet, R.styleable.TagsLayout);
        if (attrArray != null) {
            mChildHorizontalSpace = attrArray.getDimensionPixelSize(R.styleable.TagsLayout_tagHorizontalSpace, 0);
            mChildVerticalSpace = attrArray.getDimensionPixelSize(R.styleable.TagsLayout_tagVerticalSpace, 0);
            attrArray.recycle();
        }
    }

3、onMeasure函數測量子控件大小,然後設置當前控件大小

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 獲得它的父容器爲它設置的測量模式和大小
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
        // 遍歷每個子元素
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE)
                continue;
            // 測量每一個child的寬和高
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            // 得到child的lp
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            // 當前子空間實際佔據的寬度
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + childHorizontalSpace;
            // 當前子空間實際佔據的高度
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + childVerticalSpace;
            /**
             * 如果加入當前child,則超出最大寬度,則的到目前最大寬度給width,類加height 然後開啓新行
             */
            if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {
                width = Math.max(lineWidth, childWidth);// 取最大的
                lineWidth = childWidth; // 重新開啓新行,開始記錄
                // 疊加當前高度,
                height += lineHeight;
                // 開啓記錄下一行的高度
                lineHeight = childHeight;
                child.setTag(new Location(left, top + height, childWidth + left - childHorizontalSpace, height + child.getMeasuredHeight() + top));
            } else {// 否則累加值lineWidth,lineHeight取最大高度
                child.setTag(new Location(lineWidth + left, top + height, lineWidth + childWidth - childHorizontalSpace + left, height + child.getMeasuredHeight() + top));
                lineWidth += childWidth;
                lineHeight = Math.max(lineHeight, childHeight);
            }
        }
        width = Math.max(width, lineWidth) + getPaddingLeft() + getPaddingRight();
        height += lineHeight;
        sizeHeight += getPaddingTop() + getPaddingBottom();
        height += getPaddingTop() + getPaddingBottom();
        setMeasuredDimension((modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : width, (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : height);
    }

通過遍歷所有子控件調用measureChild函數獲取每個子控件的大小,然後通過寬度疊加判斷是否換行,疊加控件的高度,同時記錄下當前子控件的座標,這裏記錄座標引用了自己寫的一個內部類Location.java。

4、onLayout函數對所有子控件重新佈局

    private class Location {
        private int left;
        private int top;
        private int right;
        private int bottom;
        private Location(int left, int top, int right, int bottom) {
            this.left = left;
            this.top = top;
            this.right = right;
            this.bottom = bottom;
        }
    }

三、自定義標籤雲的使用

1、在佈局文件中直接引用

    <com.wgh.willflowcloudtag.TagsLayout
        android:id="@+id/tagsLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal">
    </com.wgh.willflowcloudtag.TagsLayout>

2、在MainActivity中用代碼添加標籤

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TagsLayout tagsLayout = (TagsLayout) findViewById(R.id.tagsLayout);
        ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        String[] string={"鋤禾日當午","汗滴禾下土", "metacognition", "WillFlow","誰知盤中餐","粒粒皆辛苦"};
        for (int i = 0; i < string.length; i++) {
            TextView textView = new TextView(this);
            textView.setText(string[i]);
            textView.setTextColor(getResources().getColor(R.color.colorAccent));
            textView.setBackgroundResource(R.drawable.a);
            tagsLayout.addView(textView, lp);
        }
    }

至此有關簡單的自定義控件已經介紹的差不多了,項目中很複雜的控件現在涉及的比較少,以後用到之後再做記錄。

最後,給大家介紹一款開源3D標籤雲:3D標籤雲

點此進入:GitHub開源項目“愛閱”。“愛閱”專注於收集優質的文章、站點、教程,與大家共分享。下面是“愛閱”的效果圖:


聯繫方式:

簡書:WillFlow
CSDN:WillFlow
微信公衆號:WillFlow

微信公衆號:WillFlow

發佈了81 篇原創文章 · 獲贊 21 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章