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>