Android 学习笔记 自定义控件之排行控件

作者: maoqitian | 来源:发表于2017-02-08 22:21 被阅读395次

  • 自定义控件,从字面意思来看我的理解是根据自己的想法定义控件。
    自定义控件一般有三种类型:

  • 组合原生控件实现自己想要的效果

  • 继承原生控件实现自定义

  • 完全自定义控件(继承View 、ViewGroup)

  • 本文的排行控件就属于完全自定义控件,来一发效果图:

控件效果图.png
  • 完全自定义控件中继承View或者ViewGroup,而至于你要继承那个类,决定权在于你想做的控件中是否有子控件,有子控件则继承ViewGroup,没有 子控件则继承View.很显然我们要做的这个排行控件是有一行一行的子View,所以是继承ViewGroup.
  • 自定义控件中一般有三个方法 onMeasure() 、onLayout() 、onDraw() 三个方法,分别表示测量,子View的摆放和绘制内容。这个三个方法顺序执行就是Android界面绘制的流程。
  • 该控件目的为让大小长度不一的小格子排放整齐,所以控件中的每一行相当于控件的子View,而每一行的每一个格子又相当于每一行的子View.根据这个思路我们可以把每一行也封装成一个对象,也在该对象中写一个onLayout()方法来设置每个小格子的摆放位置。

  • 下面开始撸这个自定义控件
    首先写个类MyFlowLayout继承ViewGroup,继承ViewGroup必须实现onLayout() 方法,该控件的子View如何摆放可以在该方法中实现。
/**
 * Created by 毛麒添 on 2017/2/7 0007.
 * 自定义排行控件
 */

public class MyFlowLayout extends ViewGroup {

    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 onLayout(boolean changed, int l, int t, int r, int b) {
        }
    }
  • 然后我们需要先实现测量的方法,要把地方都给测量好了,才能摆放控件。onMeasure() 方法的思路为:
  • 首先获取获取控件的宽高,当然是去掉上下左右padding后的实际有效宽高,并获取他们的测量模式(一般有三种模式,MeasureSpec.EXACTLY(确定模式)MeasureSpec.AT_MOST(包裹内容模式,父容器有多大就是多大)MeasureSpec.UNSPECIFIED(没有确定的模式));
  • 遍历所有子控件,也就是每一行,重新测量并获取他的宽度
  • 判断子控件的宽度是否大于上面获取的实际有效宽度,如果没有超出,则可以添加每一行的子View,而此时如果新加入子View后宽度大于实际有效宽度,则换行;如果第一次判断已经超出,而且该行没有任何控件,一旦添加子控件,就超出宽度,则强制加入,否则先换行再加入新的每一行的子View
  • 最后根据最新的高度来测量整体布局的大小

下面上代码:

    private int usedWidth;//每一行行子控件已经使用的宽度

    private int horizontalSpace= ToolUtils.dipToPx(6);//每行每个子View水平间距
    private int verticalSpace= ToolUtils.dipToPx(8);//每一行竖直间距

    private Line mLine;//当前行对象
    private static final int MAX_LINE=100;//控件拥有的最大行数
    private ArrayList<Line> lineList=new ArrayList<MyFlowLayout.Line>();//保存每一行对象的List

//测量
    @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);

            //如果父控件模式是确定模式EXACTLY,则子控件包裹内容AT_MOST,否则等于原本的模式
            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();

            //如果当前行对象为空。初始化一个
            if(mLine==null){
                mLine=new Line();
            }

            usedWidth+=childWidth;//已经使用的宽度加上一个子控件的宽度
            //是否超出最大宽度
            if(usedWidth<width){//没有超出

                mLine.addView(childView);//当前行添加子控件
                usedWidth+=horizontalSpace;//没有超出,增加一个水平的间距
                if(usedWidth>width){//如果增加间距后超出最大宽度则需要换行

                    if(!newLine()){//换行
                        break;//退出循环
                    }
                }

            }else {//已经超出
                //该行没有任何控件,一旦添加子控件,就超出宽度
                if(mLine.getChildSize()==0){
                    //强制将其加入到这一行,
                    mLine.addView(childView);
                    if (!newLine()) {//换行
                        break;
                    }
                }else {
                    //该行有其他控件,一旦添加新控件就超出宽度,先换行
                    if(!newLine()){//换行
                        break;//退出循环
                    }
                    mLine.addView(childView);

                    usedWidth+=childWidth+horizontalSpace;//更新已经使用的宽度
                }
            }
        }

        //保存最后一行的数据
        if(mLine!=null&&mLine.getChildSize()!=0&&!lineList.contains(mLine)){
              lineList.add(mLine);
        }
        //获取控件整体宽高度
        int totalWidth = MeasureSpec.getSize(widthMeasureSpec);
        int totalHeight=0;
        for (int i = 0; i <lineList.size() ; i++) {
            Line line = lineList.get(i);
            totalHeight+=line.maxChildHeight;
        }

        //增加竖直的间距,上下边距
        totalHeight+=(lineList.size()-1)*verticalSpace+getPaddingTop()+getPaddingBottom();

        //根据最新的高度来测量整体布局的大小
        setMeasuredDimension(totalWidth,totalHeight);
    }
  • 为了屏幕适配,将dip转换成像素的工具类
/**
 * Created by 毛麒添 on 2017/1/18 0018
 */

public class ToolUtils {
   /**
     * @param dip  dp值
     * @return 返回dp转换成的像素值
     */
    public  static  int dipToPx(float dip){
        float density = getContext().getResources().getDisplayMetrics().density;//像素密度
        //dp=px/像素密度 px=dp*像素密度
        int px= (int) (dip*density+0.5f);//四舍五入
        return px;
    }
}
  • 每一行对象的封装,根据上面的思路,每一行里面的小格子也是子View,所以也需要给每一行对象写一个onLayout()方法,每个格子左上角的坐标就可以确定其摆放的位置,摆放小格子的思路为:
  • 首先获取每一行的实际有效宽度,然后在获取每一行除去已有子控件剩余的宽度
  • 如果有剩余的宽度,则遍历该行的所有子控件,测量好宽度,将剩余的宽度平均分配给已有的子View,
  • 当一个子控件比较高度比其他的子控件高度小的时候,让其竖直位置居中
  • 如果没有剩余空间(子控件宽度超过本身宽度,占满整行),强行将其设置进入该行
 //每一行对象的封装
    class Line{

        public ArrayList<View> childViewList=new ArrayList<View>();//当前行所有子控件的集合

        public int totalChildWidth;//当前行所有子控件的总宽度

        public int maxChildHeight;//当前行中所有子控件中最高的控件的高度

        //添加一个子控件
        public void addView(View view){
            childViewList.add(view);

            //获取总宽度的值
            totalChildWidth+=view.getMeasuredWidth();

            //最高控件的高度
            int height=view.getMeasuredHeight();
            //如果当前加入的控件高度大于之前保存的高度则改变最大高度的值,否则最大高度的值保持不变
            maxChildHeight=maxChildHeight<height?height:maxChildHeight;

        }

        //获取子控件的个数
        public int getChildSize(){
            return childViewList.size();
        }

        //每一行设置好子view的位置
        public void layout(int left,int top){
            int count=getChildSize();
            //如果这一行放不下要添加的控件,则将该行剩余的位置平均分配给已经存在的子控件
            //屏幕的有效宽度
            int valiaWidth=getMeasuredWidth()-getPaddingLeft()-getPaddingRight();
            //屏幕的剩余可分配宽度
            int surplusWidth=valiaWidth-totalChildWidth-(count-1)*horizontalSpace;

            if(surplusWidth>=0){//如果有剩余空间

                //将剩余控件平均分配给每个子控件
                //每个子控件可以分配到的空间
                int space= (int) (surplusWidth/count+0.5f);
                //遍历每个子控件
                for (int i = 0; i <count ; i++) {
                    View childView = childViewList.get(i);
                    int measuredWidth = childView.getMeasuredWidth();
                    int measuredHeight = childView.getMeasuredHeight();
                    //将空间分配给每个子控件
                    measuredWidth+=space;
                    int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY);
                    int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY);
                    //重新测量
                    childView.measure(widthMeasureSpec,heightMeasureSpec);

                    //当一个子控件比较高度比其他的子控件高度小的时候,让其竖直位置居中
                    //高度较小的子控件高度偏移量
                    int Topoffset= (int) ((maxChildHeight-measuredHeight)/2+0.5f);

                    if(Topoffset<0){
                        Topoffset=0;
                    }

                    //设置其位置
                    childView.layout(left,top+Topoffset,left+measuredWidth,top+Topoffset+measuredHeight);
                    //更新left值
                    left+=measuredWidth+horizontalSpace;
                }

            }else {//没有剩余空间(子控件宽度超过本身宽度,占满整行)
                View childView = childViewList.get(0);
                //设置位置
                childView.layout(left,top,left+childView.getMeasuredWidth(),top+childView.getMeasuredHeight());
            }
        }

    }
  • 换行方法,只要调用该方法,就先保存上一行的数据,并且将保存每一行已经使用的宽度变量清零并且新建下一行的对象
 /**
     * 换行方法
     * @return ture 创建新的一行成功 false 创建新的一行失败
     */
    private boolean newLine(){
         //保存上一行的数据
        lineList.add(mLine);

        //如果此时的最大行数没有超过控件最大行数限制
        if(lineList.size()<MAX_LINE){
            mLine=new Line();
            //已经使用的宽度清零
            usedWidth=0;
            return true;
        }
      return false;

    }
  • 最后在onLayout()中设置每一行的位置
 //设置每一行的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

            int left=getPaddingLeft();
            int top=getPaddingTop();
            //遍历所有行对象,设置位置
            for (int i = 0; i < lineList.size(); i++) {
                Line line = lineList.get(i);
                line.layout(left,top);

                //每设置一行,更新top的值
                top+=line.maxChildHeight+verticalSpace;

        }
    }

到此,这个自定义的排行控件已经完成,上面成果图为设置一个String类型的List,将字数不等的汉字设置给TextView,背景设置的是随机颜色和圆角,这里就不贴代码了。如果有哪些地方写得不对,请大家指出,让我们一起共同进步!

相关文章

网友评论

本文标题:Android 学习笔记 自定义控件之排行控件

本文链接:https://www.haomeiwen.com/subject/hfymittx.html