Android自定义控件--流式布局(FlowLayout)--自动适配

原文链接:

FlowLayout流式布局


在android开发中,随着开发需求的不断提升,android原生的控件在很大程度上已不能满足开发者以及用户的需求,为了更好的增加用户体验,更有利的维护UI,在一个完整的程序中,自定义控件往往是不可或缺的知识,我根据自己的学习经验,现在对自定义控件的分类,以及自定义控件的流程,然后根据FlowLayout案例进行简单分析

一.自定义控件的流程

//1.第一步,测量
    onMeasure();
//2.第二步,布局
    onLayout();
//3.第三部,绘制
    onDraw()

二.自定义控件的分类

  1. 继承View重写onDraw方法 

        用于实现一些不规则的效果,不方便通过组合的方式达到,需要通过静态或者动态的显示一些不规则的图形的,需要通过绘制的方式实现,这种方法需要手动的填写支持padding和wrap_content方法  
    
  2. 继承ViewGroup派生特殊的Layout

    这也是我们这此文章介绍的一种自定义布局,即除LinearLayout,RelativeLayout,FrameLayout这几种系统的布局以外的布局,需要稍微的处理元素和子元素的测量和绘制过程,
    
  3. 继承特定的View(比如TextView)

      这种方法用来扩展已经有的View的功能,这种方法相对比较简单
    
  4. 继承特定的ViewGroup(比如LinearLayout)

     这种方法比较普遍,当某种效果比较像很多种View组合在一起的时候,可以采用这种方法来实现,采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程.
    
    上次的文章中介绍了一个PullRefresh(下拉刷新,与加载更多按钮)
    

    下拉刷新,加载很多的地址PullRefresh

三.此次的布局控件时第二种类型,继承ViewGrop派生特殊的Layout

(一).具体作用

    FlowLayout是一种流式布局,主要根据自控件的加入顺序进行依次排序,当每一行排满时,进行换行操作,然后根据每一行的未使用空间对View进行屏幕的适配,实例图如下图

Android自定义控件--流式布局(FlowLayout)--自动适配_第1张图片

(二).原理分析

自定义流式布局的原理大概分为以下几点

Android自定义控件--流式布局(FlowLayout)--自动适配_第2张图片

(三).具体的代码实现

第一步.首先需要先自定义一个类然后继承ViewGroup

public class FlowLayout extends ViewGroup {
    public FlowLayout(Context context) {
        super(context);
    }
    public FlowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    //onMeasure()方法,用于View以及自身的测量,是本次自定义控件需要重写的重要的方法之一,
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
    //onLayout()方法,主要对View进行布局
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    }
}

第二步.我们现在着手看看onMeasure()方法的实现

1.对ViewGrop进行测量,首先要拿到ViewGrop的尺寸,以及测量模式    

        MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
        iMeasureSpec.getMode(widthMeasureSpec);
        MeasureSpec.getMode(heightMeasureSpec);

2.对所有的子控件进行遍历测量,根据相同方式拿到每个View的尺寸以及测量模式,按照上面的原理分析进行处理,代码如下

1.计算每一行的已经使用的高度
useWidth += childWidth;
            if(mLine == null){
                mLine = new Line();
            }
            if(useWidth < width){
2.如果宽度足够加入下一个View则,将View加入到行中
                    mLine.addView(childView);
                    useWidth += HorizonytalSpace;
3.第一种情况:当加入View后,再加入间距超出测量范围,必须进行换行操作
                if(useWidth >= width){
                    if(!newLine()){
                        break;
                    }
                }
            }else {
4.第二种情况,剩余控件不足与添加此View,有两种处理方法
    //1)但前行中没有其它的元素,此单个View尺寸,超出他的父控件,则必须加入这一行,然后进行换行操作
                if(mLine.getLineCount() == 0){
                    mLine.addView(childView);
                    LineList.add(mLine);
                    if(!newLine()){
                        break;
                    }
                }else {
        //2)但前行已经有其他View剩余控件不够View的放置,新建一行,然后将View加入新的行中
                    if(!newLine()){
                        break;
                    }
                    mLine.addView(childView);
                    useWidth += HorizonytalSpace + childWidth;
                }
            }
        }

5.判断最后一行,如果最后一行有子View,并且没有存储,则存储起来
if (mLine != null && mLine.getViewCount() > 0&& !mLines.contains(mLine)) {
            mLines.add(mLine);
        }
6.宽度设置好后,对ViewGroup的高度进行测量计算
    //高度 = 列间距 + 每一行中最大的MaxHeight的和

        for (int i = 0; i size() ; i++) {
            TotalHeight += LineList.get(i).MaxHeight;
        }
        TotalHeight += (LineList.size() - 1)*VerticalSpaace + getPaddingTop() + getPaddingBottom();

第三步.对每一行的封装

//在封装的类中,实现两个方法,一个是addView,另一个时对每一行的View进行处理,布局
 class Line{
        public void addView(View view)
        public void layoutView(int l,int t){
1.使用循环处理每一个View 
        for(int i=0;i<mLines.Size();i++)
2.首先对Width来说,计算出剩余的宽度,根据每一行的View的数量进行平均分配,计算得出每一个空间的增加量(widthOffSet)
               childWidth += widthOffSet;
3.针对高度,根据MaxHeight,最大高度,计算出每一个View应该增加多少Top直,才能保证View相对于此行直居中.
               TopOffSet = TopOffSet>0?TopOffSet:0;
               view.layout(l,t+ TopOffSet,l +childWidth,t + TopOffSet + childHeight);
               Left += HorizonytalSpace + childWidth;
        }

第四步.onLayout()的实现过程

因为在上一步Line的封装中已经对,每一行的View已经进行了布局,所以这里只需要调用即可

@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //遍历行集合(LineList),
        int Top = getPaddingTop();
        int Left = getPaddingLeft();
        for (int i = 0; i < LineList.size(); i++) {
            Line line = LineList.get(i);
            line.layoutView(Left,Top);
            //每一行的唯一的差别就是首个View的Top不同,动态的改变Top的值
            Top += line.MaxHeight + VerticalSpaace;
        }
    }

> 到目前为止,整个FlowLayout流式布局打大概知识就全部介绍完毕了,有不足的地方还请大家指正,谢谢了

package com.example.orchid.googleplatstore.ui.View;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import java.util.ArrayList;

/**
 * Created by orchid
 * on 16-11-2.
 */

public class MyFlowLayout extends ViewGroup {

    private int useWidth;

    private int MaxHeight;

    private int HorizonytalSpace = 5;

    private int VerticalSpaace = 5;

    private Line mLine;

    private int MaxLine = 100;

    private ArrayList LineList = new ArrayList();
//    private  int

    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);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //获取控件的具体尺寸
        int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();//获取空间的宽度
        int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();//获取控件的高度
        //获取控件的测量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);//宽度的测量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);//高度的测量模式

        //开始遍历所有的子控件
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            //获取子控件的尺寸,与测量模式
            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,widthMode == MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:widthMode);
            int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,heightMode == MeasureSpec.EXACTLY?MeasureSpec.AT_MOST:heightMode);
            //测量子控件
            childView.measure(childWidthMeasureSpec,childHeightMeasureSpec);

            int childWidth = childView.getMeasuredWidth();//子控件的宽度
            int childHeight = childView.getMeasuredHeight();//子控件的高度

            useWidth += childWidth;

            if(mLine == null){
                mLine = new Line();
            }

            if(useWidth < width){
                //未超过最大限度,可以添加到当前行
                    mLine.addView(childView);
                    useWidth += HorizonytalSpace;

                if(useWidth >= width){
                    if(!newLine()){
                        break;//创建失败,结束for循环
                    }
                }
            }else {
                //2.但前行没有控件,必须加入到当前行,然后换行
                if(mLine.getLineCount() == 0){
                    //添加到当前行,然后换行
                    mLine.addView(childView);
                    LineList.add(mLine);
                    if(!newLine()){
                        break;
                    }

                }else {
                    //超过最大高度,
                    //1.当前行有控件,需要新建一行
                    if(!newLine()){
                        break;
                    }
                    mLine.addView(childView);
                    useWidth += HorizonytalSpace + childWidth;
                }
            }
            if (mLine != null && mLine.getLineCount() > 0
                    && !LineList.contains(mLine)) {
                // 由于前面采用判断长度是否超过最大宽度来决定是否换行,则最后一行可能因为还没达到最大宽度,所以需要验证后加入集合中
                LineList.add(mLine);
            }

        }
        //为控件设置宽度,高度
        int Totalwidth = MeasureSpec.getSize(widthMeasureSpec);
        int TotalHeight = 0;
        for (int i = 0; i 1)*VerticalSpaace + getPaddingTop() + getPaddingBottom();
        setMeasuredDimension(Totalwidth,TotalHeight);
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //遍历行集合(LineList),
        int Top = getPaddingTop();
        int Left = getPaddingLeft();
        for (int i = 0; i < LineList.size(); i++) {
            Line line = LineList.get(i);

            line.layoutView(Left,Top);

            Top += line.MaxHeight + VerticalSpaace;
        }
    }

    private boolean newLine(){
        //判断是否超过最大行数
        if(LineList.size() < MaxLine){
            //将上一行添加到,LineList中
            LineList.add(mLine);
            mLine = new Line();//创建一个新的行
            //新的一行,使用的数据为0
            useWidth = 0;
            MaxHeight = 0;
            return true;//创建成功返回true
        }
        return false;
    }

    //创建一个类,用来处理每一行的数据
    class Line{

        private int mLineWidth = 0;

        private int MaxHeight = 0;

        private ArrayList viewlist = new ArrayList();

        public void addView(View view){

            viewlist.add(view);
            mLineWidth += view.getMeasuredWidth();
            int childHeight = view.getMeasuredHeight();
            MaxHeight = MaxHeight < childHeight?childHeight:MaxHeight;

        }

        public int getLineCount(){
            return viewlist.size();
        }

        public void layoutView(int l,int t){
            //对此行的数据进行布局
            int Left = l;
            int Top = t;

            int childCount = viewlist.size();
            int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight() -(childCount-1) * HorizonytalSpace;

            //计算剩余宽度
            int surplusWidth  = width - mLineWidth;
            if(surplusWidth > 0){
                //计算每个布局的添加量
                int widthOffSet = (int) (surplusWidth * 1.0f/viewlist.size() + 0.5f);

                for (int i = 0; i < viewlist.size(); i++) {

                    View view = viewlist.get(i);
                    int childWidth = view.getMeasuredWidth();
                    int childHeight = view.getMeasuredHeight();

                    childWidth += widthOffSet;//重新分配控件的高度

                    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth,MeasureSpec.EXACTLY);
                    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight,MeasureSpec.EXACTLY);

                    view.measure(childWidthMeasureSpec,childHeightMeasureSpec);
                    //分配布局控件时的偏移量
                    int TopOffSet = (MaxHeight - childHeight) / 2;
                    TopOffSet = TopOffSet>0?TopOffSet:0;//如果TopOffSet(竖直方向的偏移量)小于0,则设置为0;
                    view.layout(Left,Top+ TopOffSet,Left +childWidth,Top + TopOffSet + childHeight);

                    Left += HorizonytalSpace + childWidth;
                }
            }else{
            }
        }
    }

    public void setHorizontalSpacing(int horizonytalSpace) {
        HorizonytalSpace = horizonytalSpace;
    }

    public void setVerticalSpacing(int verticalSpaace) {
        VerticalSpaace = verticalSpaace;
    }
}

你可能感兴趣的:(Android)