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


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

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

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

  • 完全自定义控件(继承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,背景设置的是随机颜色和圆角,这里就不贴代码了。如果有哪些地方写得不对,请大家指出,让我们一起共同进步!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容