Android自定义View实现流式布局(热门标签效果)

BetFarnell 8年前
   <p><strong>效果图</strong></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d930d6cfd2f392beeeb94eb2ede84310.gif"></p>    <p style="text-align:center">实现效果图</p>    <p>思维导图</p>    <p><img src="https://simg.open-open.com/show/ad584714dbe324578ac3f471d150d9d8.png"></p>    <p>思维导图</p>    <h2><strong>一、流式布局的实现</strong></h2>    <p>实现原理:采用面向对象思想将整个布局分为很多行的对象,每个行对象管理自己行内的孩子,这里通过集合来管理。</p>    <p><strong>1. 内部类Line的实现</strong></p>    <p><strong>1.1 定义行的基本属性</strong></p>    <ul>     <li>List<View>:管理行中的孩子</li>     <li>maxWidth:行的最大宽度</li>     <li>usedWidth:使用的宽度</li>     <li>height:行的高度</li>     <li>space:孩子之间的间距</li>     <li> <p>构造初始化maxWidth和space</p> <pre>  <code class="language-java">public Line(int maxWidth, int horizontalSpace) {        this.maxWidth = maxWidth;        this.space = horizontalSpace;    }</code></pre> </li>    </ul>    <p><strong>1.2 addView(View view)方法实现</strong></p>    <ul>     <li> <p>往行的集合里添加View,更新行的使用宽度和高度</p> <pre>  <code class="language-java">/**     * 往集合里添加孩子     */    public void addView(View view) {        int childWidth = view.getMeasuredWidth();        int childHeight = view.getMeasuredHeight();          // 更新行的使用宽度和高度        if (views.size() == 0) {            // 集合里没有孩子的时候            if (childWidth > maxWidth) {                usedWidth = maxWidth;                height = childHeight;            } else {                usedWidth = childWidth;                height = childHeight;            }        } else {            usedWidth += childWidth + space;            height = childHeight > height ? childHeight : height;        }          // 添加孩子到集合        views.add(view);    }</code></pre> </li>    </ul>    <p><strong>1.3 canAddView(View view)方法实现</strong></p>    <ul>     <li> <p>判断是否能往行里添加孩子,如果孩子的宽度大于剩余宽度就不能</p> <pre>  <code class="language-java">/**     * 判断当前的行是否能添加孩子     *     * @return     */    public boolean canAddView(View view) {        // 集合里没有数据可以添加        if (views.size() == 0) {            return true;        }          // 最后一个孩子的宽度大于剩余宽度就不添加        if (view.getMeasuredWidth() > (maxWidth - usedWidth - space)) {            return false;        }          // 默认可以添加        return true;    }</code></pre> </li>    </ul>    <p><strong>2. 对容器进行测量(onMeasure方法的实现)</strong></p>    <p><strong>2.1 获取宽度,计算maxWidth,构造传入Line</strong></p>    <ul>     <li> <p>总宽度减去左右边距就是行的最大宽度</p> <pre>  <code class="language-java">// 获取总宽度    int width = MeasureSpec.getSize(widthMeasureSpec);    // 计算最大的宽度    mMaxWidth = width - getPaddingLeft() - getPaddingRight();</code></pre> </li>    </ul>    <p><strong>2.2 循环获取孩子进行测量</strong></p>    <ul>     <li> <p>获取孩子总数,遍历获取每一个孩子,然后进行测量,测量完之后还需要将孩子添加到行集合里,然后将行添加到管理行的集合里</p> <pre>  <code class="language-java">// ******************** 测量孩子 ********************    // 遍历获取孩子    int childCount = this.getChildCount();    for (int i = 0; i < childCount; i++) {        View childView = getChildAt(i);        // 测量孩子        measureChild(childView, widthMeasureSpec, heightMeasureSpec);          // 测量完需要将孩子添加到管理行的孩子的集合中,将行添加到管理行的集合中          if (mCurrentLine == null) {            // 初次添加第一个孩子的时候            mCurrentLine = new Line(mMaxWidth, HORIZONTAL_SPACE);              // 添加孩子            mCurrentLine.addView(childView);            // 添加行            mLines.add(mCurrentLine);          } else {            // 行中有孩子的时候,判断时候能添加            if (mCurrentLine.canAddView(childView)) {                // 继续往该行里添加                mCurrentLine.addView(childView);            } else {                //  添加到下一行                mCurrentLine = new Line(mMaxWidth, HORIZONTAL_SPACE);                mCurrentLine.addView(childView);                mLines.add(mCurrentLine);            }        }    }</code></pre> </li>    </ul>    <p><strong>2.3 测量自己</strong></p>    <ul>     <li> <p>由于宽度肯定是填充整个屏幕,这里只需要处理行的高度,累加所有的行高和竖直边距算出高度</p> <pre>  <code class="language-java">// ******************** 测量自己 *********************    // 测量自己只需要计算高度,宽度肯定会被填充满的    int height = getPaddingTop() + getPaddingBottom();    for (int i = 0; i < mLines.size(); i++) {        // 所有行的高度        height += mLines.get(i).height;    }    // 所有竖直的间距    height += (mLines.size() - 1) * VERTICAL_SPACE;      // 测量    setMeasuredDimension(width, height);</code></pre> </li>    </ul>    <p><strong>3. 指定孩子的显示位置(onLayout方法的实现)</strong></p>    <p>实现思路:指定孩子的位置,孩子给了行管理,所以这里具体孩子的位置应该交给行去指定。容器只需要指定行的位置就可以。</p>    <ul>     <li> <p>遍历获取所有的行,让行去指定孩子的位置,指定行的高度</p> <pre>  <code class="language-java">@Override    protected void onLayout(boolean changed, int l, int t, int r, int b) {        // 这里只负责高度的位置,具体的宽度和子孩子的位置让具体的行去管理        l = getPaddingLeft();        t = getPaddingTop();        for (int i = 0; i < mLines.size(); i++) {            // 获取行            Line line = mLines.get(i);            // 管理            line.layout(t, l);              // 更新高度            t += line.height;            if (i != mLines.size() - 1) {                // 不是最后一条就添加间距                t += VERTICAL_SPACE;            }        }    }</code></pre> </li>    </ul>    <p><strong>4. Line中layout方法的实现(指定孩子的位置)</strong></p>    <ul>     <li> <p>遍历获取每一个孩子,获取孩子的宽度和高度,计算上下左右的大小,指定孩子的位置,之后还需要更新孩子左边的大小</p> <pre>  <code class="language-java">// 循环指定孩子位置    for (View view : views) {        // 获取宽高        int measuredWidth = view.getMeasuredWidth();        int measuredHeight = view.getMeasuredHeight();        // 重新测量        view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),                MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));        // 重新获取宽度值        measuredWidth = view.getMeasuredWidth();          int top = t;        int left = l;        int right = measuredWidth + left;        int bottom = measuredHeight + top;        // 指定位置        view.layout(left, top, right, bottom);          // 更新数据        l += measuredWidth + space;    }</code></pre> </li>    </ul>    <p><strong>5. 细节处理</strong></p>    <ul>     <li> <p>第一次测量之后,行管理器中就有了行的对象,之后每次测量都会去创建下一行,这样就会出现很多空行出来,所以需要在测量之前将集合清空。</p> <pre>  <code class="language-java">mLines.clear();    mCurrentLine = null;</code></pre> </li>     <li> <p>每一行的最后一个孩子放不下就放到下一行,这样每一行就都会有空格,这里将这些空格平分给行里的每一个孩子,重新指定其宽度。</p> <pre>  <code class="language-java">// 平分剩下的空间    int avg = (maxWidth - usedWidth) / views.size();      // 重新测量    view.measure(MeasureSpec.makeMeasureSpec(measuredWidth + avg, MeasureSpec.EXACTLY),            MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY));    // 重新获取宽度值    measuredWidth = view.getMeasuredWidth();</code></pre> </li>    </ul>    <p><strong>6. 使用自定义属性,将水平间距和竖直间距做成属性,在布局中指定,增强扩展性</strong></p>    <ul>     <li> <p>attrs文件指定属性名</p> <pre>  <code class="language-java"><declare-styleable name="FlowLayout">        <attr name="width_space" format="dimension"/>        <attr name="height_space" format="dimension"/>   </declare-styleable></code></pre> </li>     <li> <p>构造中获取属性</p> <pre>  <code class="language-java">// 获取自定义属性    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);    horizontal_space = array.getDimension(R.styleable.FlowLayout_width_space,0);    vertical_space =  array.getDimension(R.styleable.FlowLayout_height_space,0);    array.recycle();</code></pre> </li>     <li> <p>布局中使用属性</p> <pre>  <code class="language-java">app:width_space="10dp"    app:height_space="10dp"</code></pre> </li>    </ul>    <p>经过以上步骤之后,FlowLayout基本就已经实现了,接下来就是使用了。</p>    <h2><strong>二、流式布局的使用</strong></h2>    <ul>     <li> <p>布局中申明</p> <pre>  <code class="language-java"><ScrollView        xmlns:android="http://schemas.android.com/apk/res/android"        xmlns:tools="http://schemas.android.com/tools"        android:layout_width="match_parent"        android:layout_height="match_parent"        xmlns:app="http://schemas.android.com/apk/res-auto"        android:fillViewport="true"        tools:context="com.pinger.sample.MainActivity">          <com.pinger.library.FlowLayout            app:width_space="10dp"            app:height_space="10dp"            android:id="@+id/flow_layout"            android:layout_width="match_parent"            android:layout_height="match_parent"            android:padding="5dp"/>    </ScrollView></code></pre> </li>     <li> <p>代码中使用</p> </li>     <li> <p>其实就是循环遍历数据的长度,不断的创建TextView,然后设置TextView的属性和背景,包括五彩背景等,最后将TextView添加到FlowLayout中就可以。</p> </li>    </ul>    <p> </p>    <p> </p>    <p> </p>    <p> </p>