Android View - 实现流式布局

11 篇文章 0 订阅
6 篇文章 0 订阅

流式布局,就是一个容器(ViewGroup),往里面添加元素(子View),元素会一直跟在前一个元素的左边,如果超过容器的边界,就把元素放在下一行的第一个位置。Like This:

我们自己来实现一下这么一种布局,在实现之前,你需要理解关于自定义ViewGroup相关的知识,可以参考 Android 手把手教您自定ViewGroup;如果没问题,接着往下看。

网上已经有很多人实现流式布局了,不过大部分都是静态设置数据,也就是说直接在xml添加子View,像这样:

<com.zhy.zhy_flowlayout02.FlowLayout  
    android:layout_width="fill_parent"  
    android:layout_height="wrap_content" >  
    <TextView  
        style="@style/text_flag_01"  
        android:text="Welcome" />  
    <TextView  
        style="@style/text_flag_01"  
        android:text="IT工程师" />  
    <TextView  
        style="@style/text_flag_01"  
        android:text="学习ing" />  
    ... 
</com.zhy.zhy_flowlayout02.FlowLayout>

出自Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
下面我实现的动态设置子View,像ListView和GridView一样,直接上码!!

流式布局FlowLayout

新建类,继承ViewGroup
// FlowLayout类
public class FlowLayout extends ViewGroup {
    /** 子View之间水平距离 */
    private int horizontalSpace = 0;
    /** 子View之间垂直距离 */
    private int verticalSpace = 0;
    // 构造
    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        horizontalSpace = array.getDimensionPixelOffset(R.styleable.FlowLayout_horizontal_space, 0);
        verticalSpace = array.getDimensionPixelOffset(R.styleable.FlowLayout_vertical_space, 0);
        array.recycle();
    }
}

支持xml定义水平距离和垂直距离。

计算所有子View的高宽(测量)
// FlowLayout类
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 取出宽高的模式
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    // 取出宽高的大小
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    // 子View的个数
    int childCount = getChildCount();
    // 计算宽度
    int calculateWidth = 0;
    // 计算高度
    int calculateHeight = 0;
    // 行宽度
    int lineWidth = 0;
    // 行高度
    int lineHeight = 0;
    // 计算,遍历所有子View
    for (int index = 0; index < childCount; index++) {
        View childView = getChildAt(index);
        // 测量子View
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        // 子View宽度
        int childWidth = childView.getMeasuredWidth() + horizontalSpace;
        // 子View高度
        int childHeight = childView.getMeasuredHeight();
        // 如果 当前行宽 + 当前子View宽度 > 最大宽度
        if (childWidth + lineWidth > widthSize) {
            // 换行
            // 更新计算宽度,如果当前行宽大于之前计算宽度,则计算宽度=当前行宽
            calculateWidth = Math.max(lineWidth, calculateWidth);
            // 计算高度累加当前行高
            calculateHeight += lineHeight + (calculateHeight == 0 ? 0 : verticalSpace);
            // 更新当前行宽
            lineWidth = childWidth;
            // 更新当前行高
            lineHeight = childHeight;
        } else {
            // 不换行
            // 累加当前行宽
            lineWidth += childWidth;
            // 更新当前行高,如果当前子View总高度大于当前行高,则当前行高=当前子View总高度
            lineHeight = Math.max(childHeight, lineHeight);
        }
        // 判断是否最后一个
        if (index == childCount - 1) {
            // 更新计算宽度,如果当前行宽大于之前计算宽度,则计算宽度=当前行宽
            calculateWidth = Math.max(lineWidth, calculateWidth);
            // 计算高度累加当前行高
            calculateHeight += lineHeight + (calculateHeight == 0 ? 0 : verticalSpace);
        }
    }
    // 如果是EXACTLY模式,直接设置用户指定的宽度
    int measureWidth = widthMode == MeasureSpec.EXACTLY ? widthSize : calculateWidth;
    // 如果是EXACTLY模式,直接设置用户指定的高度
    int measureHeight = heightMode == MeasureSpec.EXACTLY ? heightSize : calculateHeight;
    // 设置宽高
    setMeasuredDimension(measureWidth, measureHeight);
}
设置所有子View的位置(布局)
// FlowLayout类
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    // 子View的个数
    int childCount = getChildCount();
    // 获取最大宽度
    int maxWidth = getWidth();
    // 行宽
    int lineWidth = 0;
    // 行高
    int lineHeight = 0;
    // 记录高度
    int recordHeight = 0;
    // 布局,遍历所有子View
    for (int index = 0; index < childCount; index++) {
        View childView = getChildAt(index);
        int childWidth = childView.getMeasuredWidth() + horizontalSpace;
        int childHeight = childView.getMeasuredHeight();
        // 如果 当前行宽 + 当前子View总宽度 > 最大宽度
        if (lineWidth + childWidth > maxWidth) {
            // 换行
            // 记录高度累加当前行高,从第2行起,都要加上垂直距离
            recordHeight += lineHeight + (recordHeight == 0 ? 0 : verticalSpace);
            // 布局
            int childLeft = 0;
            int childTop = recordHeight + verticalSpace;
            int childRight = childWidth;
            int childBottom = recordHeight + childHeight + verticalSpace;
            childView.layout(childLeft, childTop, childRight, childBottom);
            // 更新行宽
            lineWidth = childWidth;
            // 更新行高
            lineHeight = childHeight;
            // 下一个
            continue;
        }
        // 不换行
        // 布局
        int childLeft = lineWidth;
        int childTop = recordHeight + (recordHeight == 0 ? 0 : verticalSpace);
        int childRight = lineWidth + childWidth;
        int childBottom = recordHeight + childHeight + (recordHeight == 0 ? 0 : verticalSpace);
        childView.layout(childLeft, childTop, childRight, childBottom);
        // 更新行宽
        lineWidth += childWidth;
        // 更新行高,取最大
        lineHeight = Math.max(childHeight, lineHeight);
    }
}

只要实现以上3步,这个Layout就可以正常使用了,我们看看怎么添加元素。

适配器FlowAdapter

适配器用于把Object数据转化成View,然后交给FlowLayout显示。至于动态更新流式布局,也是操作这个适配器,采用观察者模式。FlowAdapter为发布者,FlowLayout为观察者,通过调用FlowAdapter的更新方法,便会通知FlowLayout观察者更新数据。我们看实现:

// FlowAdapter类
public abstract class FlowAdapter <T> implements FlowPublisher {

    // 数据源
    private List<T> dataList;
    // 观察者
    private FlowObserver observer;

    public FlowAdapter(List<T> dataList) {
        this.dataList = dataList;
    }

    @Override
    public void register(FlowObserver observer) {
        this.observer = observer;
    }

    @Override
    public void unregister() {
        this.observer = null;
    }

    /**
     * 子类复写,由数据data获取View
     * @param position
     * @param data
     * @return
     */
    public abstract View getView(int position, T data);

    /**
     * 解析所有数据
     * @return
     */
    List<View> parseViews() {
        List<View> viewList = new ArrayList<>();
        for (int position = 0; position < dataList.size(); position++) {
            viewList.add(getView(position, dataList.get(position)));
        }
        return viewList;
    }

    /**
     * 通知更新
     */
    public void notifyChange() {
        if (observer != null) {
            observer.notifyChange(this);
        }
    }

}

observer便是我们的FlowLayout,我们看看notifyChange方法:

// FlowLayout类
/**
 * 观察者更新方法
 * @param publisher
 */
@Override
public void notifyChange(FlowPublisher publisher) {
    if (publisher instanceof FlowAdapter) {
        refresh((FlowAdapter)publisher);
    }
}
/**
 * 刷新
 */
private void refresh(FlowAdapter adapter) {
    // 移除所有子View
    removeAllViews();
    // 获取FlowAdapter所有子View
    List<View> childViews = adapter.parseViews();
    // 添加到FlowLayout中
    for (int index = 0; index < childViews.size(); index++) {
        final View childView = childViews.get(index);
        // 设置点击事件
        final int position = index;
        childView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onItemClickListener != null) {
                    onItemClickListener.onItemClick(position, childView);
                }
            }
        });
        // 添加
        addView(childView);
    }
    // 更新
    requestLayout();
}

很简单,就是移除所有子View,然后添加所有适配器的View。
当然还有setAdapter方法:

/**
 * 设置适配器
 * @param adapter
 */
public void setAdapter(FlowAdapter adapter) {
    adapter.register(this);
    refresh(adapter);
}

和ListView很像,有木有?!(虽然内部实现有点不一样)

使用实例

xml布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:flow_layout="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#9c9c9c">

    <com.johan.library.viewtoolkit.flowlayout.FlowLayout
        android:id="@+id/flow_layout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        flow_layout:horizontal_space="10dp"
        flow_layout:vertical_space="10dp"
        android:background="@android:color/white"
        />

    <!-- 设置了精确的宽度 -->
    <com.johan.library.viewtoolkit.flowlayout.FlowLayout
        android:id="@+id/flow_layout2"
        android:layout_width="150dp"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        flow_layout:horizontal_space="10dp"
        flow_layout:vertical_space="10dp"
        android:background="@android:color/white"
        />

</LinearLayout>

activity使用:

public class FlowLayoutActivity extends Activity {

    private List<String> dataList = new ArrayList<>();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flow_layout);
        dataList.add("Java");
        dataList.add("Swift");
        dataList.add("Html");
        dataList.add("CSS");
        dataList.add("Go");
        dataList.add("C#");
        dataList.add("PHP");
        FlowLayout flowLayout = (FlowLayout) findViewById(R.id.flow_layout);
        final FlowLayoutAdapter adapter = new FlowLayoutAdapter(dataList);
        flowLayout.setAdapter(adapter);
        flowLayout.setOnItemClickListener(new FlowLayout.OnItemClickListener() {
            @Override
            public void onItemClick(int position, View view) {
                dataList.remove(position);
                adapter.notifyChange();
            }
        });
        FlowLayout flowLayout2 = (FlowLayout) findViewById(R.id.flow_layout2);
        FlowLayoutAdapter adapter2 = new FlowLayoutAdapter(dataList);
        flowLayout2.setAdapter(adapter2);
    }

    public class FlowLayoutAdapter extends FlowAdapter <String>  {
        public FlowLayoutAdapter(List<String> dataList) {
            super(dataList);
        }
        @Override
        public View getView(int position, String data) {
            View layout = LayoutInflater.from(FlowLayoutActivity.this).inflate(R.layout.item_flow_layout, null);
            TextView contentView = (TextView) layout.findViewById(R.id.item_content);
            contentView.setText(data);
            return layout;
        }
    }

}

效果图:

完整代码已经上传到我的github库,地址:https://github.com/JohanMan/viewtoolkit

参考资料

Android 手把手教您自定ViewGroup
Android 自定义ViewGroup 实战篇 -> 实现FlowLayout

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值