Android自定義控件(十三)——實現CSDN搜索框文字提示容器

分析CSDN搜索文字佈局控件

首先,我們需要來分析一下,CSDN搜素框文字提示控件的佈局,下圖爲最新CSDN的搜索框佈局提示圖:
在這裏插入圖片描述
經過前面的自定義控件,佈局等學習後,我們來簡單的來分析上面這個控件,可以看到文字的提示是由很多的文字(TextView)組成,而每排的文字(TextView)數量不固定,根據長度自適應每行多少個文字,同時他們有上間距,也有下間距,也就是佈局中常用的Margin。

ok,我們看到的也就這些,主要實現的就是這個容器規則,但是我們前面只介紹瞭如果佈局容器,並沒有介紹如何使用Margin,所以我們先來了解一下ViewGroup中是如何獲取,以及設置Margin的。

ViewGroup獲取子控件的Margin

我們先來看看,ViewGroup需要重寫獲取設置Margin值的三個方法:

@Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
    }

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

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

每個自定義控件都需要這樣寫,至於原因,我們還需要來看一段源代碼,比如new MarginLayoutParams(getContext(),attrs),你可以通過Android Studio開發工具點進去看下,就會發現其實跟前面獲取自定義屬性的代碼基本一樣,代碼如下:

    public MarginLayoutParams(Context c, AttributeSet attrs) {
        super();

        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                R.styleable.ViewGroup_MarginLayout_layout_width,
                R.styleable.ViewGroup_MarginLayout_layout_height);

        int margin = a.getDimensionPixelSize(
                com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
        if (margin >= 0) {
            leftMargin = margin;
            topMargin = margin;
            rightMargin= margin;
            bottomMargin = margin;
        } else {
            int horizontalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
            int verticalMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);

            if (horizontalMargin >= 0) {
                leftMargin = horizontalMargin;
                rightMargin = horizontalMargin;
            } else {
                leftMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
                        UNDEFINED_MARGIN);
                if (leftMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                    leftMargin = DEFAULT_MARGIN_RESOLVED;
                }
                rightMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                        UNDEFINED_MARGIN);
                if (rightMargin == UNDEFINED_MARGIN) {
                    mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                    rightMargin = DEFAULT_MARGIN_RESOLVED;
                }
            }

            startMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                    DEFAULT_MARGIN_RELATIVE);
            endMargin = a.getDimensionPixelSize(
                    R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                    DEFAULT_MARGIN_RELATIVE);

            if (verticalMargin >= 0) {
                topMargin = verticalMargin;
                bottomMargin = verticalMargin;
            } else {
                topMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                        DEFAULT_MARGIN_RESOLVED);
                bottomMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                        DEFAULT_MARGIN_RESOLVED);
            }

            if (isMarginRelative()) {
               mMarginFlags |= NEED_RESOLUTION_MASK;
            }
        }

        final boolean hasRtlSupport = c.getApplicationInfo().hasRtlSupport();
        final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
        if (targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport) {
            mMarginFlags |= RTL_COMPATIBILITY_MODE_MASK;
        }

        // Layout direction is LTR by default
        mMarginFlags |= LAYOUT_DIRECTION_LTR;

        a.recycle();
    }

簡單的來理解,這段代碼先是提取了layout_margin的值並進行設置,然後,如果用戶沒有設置layout_margin,而是單獨設置的,就一個一個提取。

需要注意的是,之所以需要重寫這些函數,是因爲默認的ViewGroup方法只會獲取layout_width和layout_height,只有MarginLayoutParams()方法才具有提取margin值的功能。

實現CSDN搜索文字提示佈局

今天講解的Margin加上前面講解的自定義佈局的知識,就可以實現CSDN搜索文字提示佈局,首先,我們還是來分析一下博文開始的圖片,是不是發現所有的TextView的style除了文字不一樣,其他的都一樣呢?所以我爲了減少代碼額冗餘,我們先自定義一個style,代碼如下:

<style name="textview">
    <item name="android:layout_width">wrap_content</item>
    <item name="android:layout_height">wrap_content</item>
    <item name="android:layout_margin">4dp</item>
    <item name="android:background">@drawable/textviewshap</item>
    <item name="android:textColor">#FF1E90FF</item>
    <item name="android:textSize">16sp</item>
</style>

接着,我們設置我們的佈局文件,主Activity的XML佈局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.liyuanjinglyj.csdnserachapplication.CSDNSerachView
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            style="@style/textview"
            android:text="Python"/>

        <TextView
            style="@style/textview"
            android:text="Java"/>

        <TextView
            style="@style/textview"
            android:text="Spring Boot"/>

        <TextView
            style="@style/textview"
            android:text="PHP"/>

        <TextView
            style="@style/textview"
            android:text="Vue"/>

        <TextView
            style="@style/textview"
            android:text="Flutter"/>

        <TextView
            style="@style/textview"
            android:text="Python基礎教程"/>

        <TextView
            style="@style/textview"
            android:text="Java學習路線"/>

        <TextView
            style="@style/textview"
            android:text="C語言"/>
		<!--省略其他TextView-->

    </com.liyuanjinglyj.csdnserachapplication.CSDNSerachView>

然後就是常規的自定義ViewGroup步驟,測量onMeasure(),佈局onLayout(),首先肯定是測量所有子控件的大小,計算整個自定義ViewGroup的寬高,代碼如下(還是常規套路):

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth=MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight=MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode=MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode=MeasureSpec.getMode(heightMeasureSpec);
        
        int width=0;
        int height=0;
        
        //中間代碼
        
        setMeasuredDimension((measureWidthMode==MeasureSpec.EXACTLY)?measureWidth:width,
                (measureHeightMode==MeasureSpec.EXACTLY)?measureHeight:height);
    }

最重要的還是中間計算的代碼,但是我們現在增加了Margin值,所以我們需要多申請2個變量代碼如下:

int lineWidth=0;//記錄每一行的寬度
int lineHeight=0;//記錄每一行的高度

定義變量後,我們需要遍歷每個子控件,然後根據子空間的大小是否還能放在當前行,不行的話跳轉到下一行,並記錄當前行使用的寬度,以便後續判斷其他子控件是否裝的下,完整代碼如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
        int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
        int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        int width = 0;//整體寬度
        int height = 0;//整體高度
        int lineWidth = 0;//記錄每一行的寬度
        int lineHeight = 0;//記錄每一行的高度
        int count = getChildCount();//獲取子控件數量
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            measureChild(child, widthMeasureSpec, heightMeasureSpec);

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

            if (lineWidth + childWidth > measureWidth) {
                //如果當前行佔位Textview+新佔位如果當前行佔位Textview大於整體寬度,就需要換行設置控件
                width = Math.max(lineWidth, width);//獲取最大值的寬度值
                height += lineHeight;//換行後直接加上寬度就是整體寬度
                //因爲當前行放不下控件,所以寬高直接等於新換行的控件寬高
                lineWidth = childWidth;
                lineHeight = childHeight;
            } else {
                //否則
                lineWidth += childWidth;//如果控件沒有換行,就是之前的寬度加上新TextView寬度
                lineHeight = Math.max(lineHeight, childHeight);//而如果之前高度高於當前子控件高度,那麼該行還是之前的寬度,否則爲新控件高度
            }
            //因爲最後一行不管填不填滿,高度都需要加上最後一行所以,沒經過最後換行的操作,上面是不會計算加最後一行的高度的,所以必須單獨寫出來
            if (i == count - 1) {
                height += lineHeight;
                width = Math.max(width, lineWidth);
            }
        }

        setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
                (measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
    }

上面的註釋應該夠詳細了,這裏就不再贅述了,不過需要說明的是子控件的寬度是自身寬度加上margin_right和margin_left,子空間高度是自身高度加上margin_top和margin_bottom,上面自定義ViewGroup也就這個值的獲取與之前博文不同,其他的都是一些常規操作與判斷。
在這裏插入圖片描述
測量onMeasure()寫完了,我們還需要進行佈局代碼的編寫,因爲我們在控件中加入了margin屬性,所以我們需要標記當前子控件的top座標和left座標,爲什麼不需要bottom,以及right呢?因爲定位一個控件是其左上角座標(如上圖所示,看完就會明白),具體的代碼如下:

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        int lineWidth = 0;
        int lineHeight = 0;
        int top = 0, left = 0;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            if (childWidth + lineWidth > getMeasuredWidth()) {
                //如果換行,與onMeasure基本類同
                top += lineHeight;
                left = 0;
                lineHeight = childHeight;
                lineWidth = childWidth;
            } else {
                lineHeight = Math.max(lineHeight, childHeight);
                lineWidth += childWidth;
            }
            //計算子空間的left,top,right,bottom
            int lc = left + lp.leftMargin;
            int tc = top + lp.topMargin;
            int rc = lc + child.getMeasuredWidth();
            int bc = tc + child.getMeasuredHeight();
            child.layout(lc, tc, rc, bc);
            left += childWidth;//將left更改爲下個子空間的起始點
        }
    }

這樣我們就是實現了CSDN搜索框下文字佈局的容器控件效果,當然,我這裏還設置了邊框等樣式,本文主要介紹自定義ViewGroup,邊框樣式屬於基礎,這裏就不再介紹了。(邊框效果最後源代碼裏面有),實現效果如下:

在這裏插入圖片描述

本文GitHub下載地址:點擊下載

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