自定义View —— FlowLayout
摘要
最近研究了一下 View 和 ViewGroup 这个东东, 然后想着动手写点什么。
于是实现了一下 FlowLayout —— 自动换行的标签布局。先上效果图:
背景知识
关于自定义 view,你需要了解 view 的构造流程。
我总结了一下,大体是这样的 onMeasure -> onLayout -> onDraw。
我们通过重写这三个方法来自定义控件。
1. onMeasure: 自定义宽高
2. onLayout: 自定义子控件的排列方式
3. onDraw: 自定义绘图方式,只有你想自己画圆圈、正方形之类的,才需要重写它
思路
那么我们怎么实现这个自动换行的控件呢?其实思路已经很清晰了。
1. 在 onMeasure 的时候,计算总行高 —— 每次宽度到头, 就另起一行,最后 set 一下计算出来的宽高。
2. 在 onLayout 的时候,调整子控件的起始位置 —— 也就是到头的时候要调整一下,另起一行。
实现
- onMeasure:
搞了一个类 MeasureSpecEntry 保存一下 measureSpec,会方便一点:
private static final int MOD_ON_MEASURE = 0;
private static final int MOD_ON_LAYOUT = 1;
MeasureSpecEntry mWidthEntry = new MeasureSpecEntry(),
mHeightEntry = new MeasureSpecEntry();
...
private class MeasureSpecEntry{
public int mMeasureSpec;
public int mMode;
public int mSize;
public MeasureSpecEntry(){}
public MeasureSpecEntry(int measureSpec) {
init(measureSpec);
}
public void init(int measureSpec){
this.mMeasureSpec = measureSpec;
this.mMode = MeasureSpec.getMode(measureSpec);
this.mSize = MeasureSpec.getSize(measureSpec);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidthEntry.init(widthMeasureSpec);
mHeightEntry.init(heightMeasureSpec);
manageChild(MOD_ON_MEASURE); // 关于换行的处理
}
我们主要看 manageChild 这个函数。因为 measure 和 layout 的时候,关于换行处理是一致的,
于是我弄了一个 mod 来切换: manageChild(int mod)。
// 我注释掉了 mod == MOD_ON_LAYOUT 的部分
public void manageChild(int mod){
int widthStart=0, heightStart=0;
for(int i=0; i<getChildCount(); i++){
View child = getChildAt(i);
// measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
// 请不要用 getWidth 和 getHeight, 它们此时为0
if(mod == MOD_ON_MEASURE){
measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
mlastChildHeight = child.getMeasuredHeight();
}
// 超出边界, 换行
if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
widthStart = 0; // 重置起始 width
heightStart += child.getMeasuredHeight() + mItemSpace;
// 换行后, 高度增加 child.getHeight() + mItemSpace
}
// layout
//if(mod == MOD_ON_LAYOUT) {
// child.layout(widthStart, heightStart,
// widthStart + child.getMeasuredWidth(),
// heightStart + child.getMeasuredHeight());
//}
// 更新起点
widthStart += child.getMeasuredWidth() + mItemSpace;
}
if(mod == MOD_ON_MEASURE){
// TODO: match_parent 失效, 需做一些特殊处理
setMeasuredDimension(mWidthEntry.mSize, heightStart + (widthStart==0? 0: mlastChildHeight));
}
}
大体上维护一个 widthStart 和 heightStart,遍历 child 来计算宽高
最后用 setMeasuredDimension( newWidth, newHeight)把宽高设上去。
代码很清晰,不多做解释。
- onLayout
跟 onMeasure 基本一致,就切换一下 mod, 直接看代码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
manageChild(MOD_ON_LAYOUT); // 还是调用 manageChild,切换一下 mod
}
// 我注释掉了 mod == MOD_ON_MEASURE 的部分
public void manageChild(int mod){
int widthStart=0, heightStart=0;
for(int i=0; i<getChildCount(); i++){
View child = getChildAt(i);
// measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
// 请不要用 getWidth 和 getHeight, 它们此时为0
//if(mod == MOD_ON_MEASURE){
// measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
// mlastChildHeight = child.getMeasuredHeight();
//}
// 超出边界, 换行
if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
widthStart = 0; // 重置起始 width
heightStart += child.getMeasuredHeight() + mItemSpace;
// 换行后, 高度增加 child.getHeight() + mItemSpace
}
// layout
if(mod == MOD_ON_LAYOUT) {
child.layout(widthStart, heightStart,
widthStart + child.getMeasuredWidth(),
heightStart + child.getMeasuredHeight());
}
// 更新起点
widthStart += child.getMeasuredWidth() + mItemSpace;
}
//if(mod == MOD_ON_MEASURE){
// TODO: match_parent 失效, 需做一些特殊处理
// setMeasuredDimension(mWidthEntry.mSize,
// heightStart + (widthStart==0? 0: mlastChildHeight));
//}
}
跟之前一样,在遍历 child 的时候,用 child.layout( left, top, right, bottom) 来放置我们的 child。
附录
最后放上完整的代码,感兴趣的童鞋可以拷过去把玩一下。如图,用它包裹一坨 TextView 或是别的。。。
完整代码:
package com.example.jinliangshan.littlezhihu.home.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by jinliangshan on 16/9/1.
*/
public class MyFlowLayout extends ViewGroup {
private static final String TAG = "MyFlowLayout";
private static final int MOD_ON_MEASURE = 0;
private static final int MOD_ON_LAYOUT = 1;
private int mItemSpace = 30;
MeasureSpecEntry mWidthEntry = new MeasureSpecEntry(),
mHeightEntry = new MeasureSpecEntry();
private int mlastChildHeight;
public MyFlowLayout(Context context) {
super(context);
}
public MyFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MyFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidthEntry.init(widthMeasureSpec);
mHeightEntry.init(heightMeasureSpec);
manageChild(MOD_ON_MEASURE);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
manageChild(MOD_ON_LAYOUT);
}
public void manageChild(int mod){
int widthStart=0, heightStart=0;
for(int i=0; i<getChildCount(); i++){
View child = getChildAt(i);
// measure 之后, getMeasuredWidth 和 getMeasuredHeight 才有值,
// 请不要用 getWidth 和 getHeight, 它们此时为0
if(mod == MOD_ON_MEASURE){
measureChild(child, mWidthEntry.mMeasureSpec, mHeightEntry.mMeasureSpec);
mlastChildHeight = child.getMeasuredHeight();
}
// 超出边界, 换行
if(widthStart + child.getMeasuredWidth() + mItemSpace > mWidthEntry.mSize) {
widthStart = 0; // 重置起始 width
heightStart += child.getMeasuredHeight() + mItemSpace;
// 换行后, 高度增加 child.getHeight() + mItemSpace
}
// layout
if(mod == MOD_ON_LAYOUT) {
child.layout(widthStart, heightStart,
widthStart + child.getMeasuredWidth(),
heightStart + child.getMeasuredHeight());
}
// 更新起点
widthStart += child.getMeasuredWidth() + mItemSpace;
}
if(mod == MOD_ON_MEASURE){
// TODO: match_parent 失效, 需做一些特殊处理
setMeasuredDimension(mWidthEntry.mSize, heightStart + (widthStart==0? 0: mlastChildHeight));
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
private class MeasureSpecEntry{
public int mMeasureSpec;
public int mMode;
public int mSize;
public MeasureSpecEntry(){}
public MeasureSpecEntry(int measureSpec) {
init(measureSpec);
}
public void init(int measureSpec){
this.mMeasureSpec = measureSpec;
this.mMode = MeasureSpec.getMode(measureSpec);
this.mSize = MeasureSpec.getSize(measureSpec);
}
}
}