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