自定义view:类似今日头条的类别选择功能

距离上一篇文章时间已经有点久远了,其实中间也写了几个好玩的view,但是由于自己太懒也没放上来,2017年新年开始了,公司的项目不紧张,闲下来就来补几篇.

自己平时也比较喜欢刷头条,看到头条上面好多个类别,在类别选择页面有一个类别选择的页面,如下图
这里写图片描述
感觉这个如果用gridview应该也能够实现功能,但是上下两个列表用起来也挺麻烦,还是数据联动问题,后来我就考虑把这个整体做在一个view里面.

这里写图片描述

做出来之后的效果是这个样子的.下面就来一步一步完成这个自定义view.

初始化信息

 private void init() {
        //初始化列表
        wait = new ArrayList<>();
        select = new ArrayList<>();
        waitList = new ArrayList<>();
        selectList = new ArrayList<>();
        //初始化画笔
        paint = new Paint();
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setAntiAlias(true);
        paint.setTextSize(36);
        paint.setColor(Color.parseColor("#008080"));
        //定义每一个类别条目所占的宽度和高度,这里宽度设置的是四个文字的宽度
        Rect rect = new Rect();
        paint.getTextBounds("四个文字", 0, 4, rect);
        textHeight = rect.height();
        singleHeight = rect.height() + textPadding * 2;
        singleWidth = rect.width() + textPadding * 2;
        Log.d(TAG, "init: singleHeight: " + singleHeight + "singleWidth: " + singleWidth);
    }

这一部分是用来做一些初始化的才做的,重点在里面的第三部分获取到的三个变量值:textHeight / singleHeight / singleWidth.
这三个变量分别代表的意思是:
1.textHeight:字号为36时,文字的高度.
2.singleHeight:显示在手机上的条目的高度,相对于textHeight增加了两个textPadding的大小,主要是为了让后面画边框的时候不让边框直接挨着字体,为了好看.
3.singleWidth:原理同上,条目的宽度.

获取测量数据(onMeasure)

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (widthSize < singleWidth) {
            //设置的宽度必须至少是一个条目的宽度
            throw new RuntimeException("the width too small");
        }
        //每一行的条目数
        countOfLine = widthSize / singleWidth;
        //放置的条目很少有正好放满整行的情况,这时把剩余空间分成两份,放置在两端
        viewMargin = (widthSize - countOfLine * singleWidth) / 2;
        Log.d(TAG, "onMeasure: countOfLine: " + countOfLine + "viewMargin: " + viewMargin);
        int measuredHeight;

        //1.计算已选择区域的高度
        if (selectList.size() == 0) {//没有被选中的内容
            selectHeight = singleHeight;
        } else {
            selectHeight = selectList.size() % countOfLine == 0 ? selectList.size() / countOfLine * singleHeight : (selectList.size() / countOfLine + 1) * singleHeight;
        }
        //2.计算待选区域的高度
        if (waitList.size() == 0) {
            waitHeight = singleHeight;
        } else {
            waitHeight = waitList.size() % countOfLine == 0 ? waitList.size() / countOfLine * singleHeight : (waitList.size() / countOfLine + 1) * singleHeight;
        }
        contentHeight = selectHeight + waitHeight + dividerHeight + 1;//内容的总高度(考虑高滚动时要用)
        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = Math.min(heightSize, contentHeight);
        }
        initData();
        Log.d(TAG, "onMeasure: widthSize: " + widthSize + "measuredHeight: " + measuredHeight);
        setMeasuredDimension(widthSize, measuredHeight);
    }

    //将传入的字符串转换成对象
    private void initData() {
        int currentSelectLine = -1;
        if (selectList.size() != 0) {
            for (int i = 0; i < selectList.size(); i++) {
                if (i % countOfLine == 0) {
                    currentSelectLine++;
                }
                selectList.get(i).setDAta(viewMargin + i % countOfLine * singleWidth, singleHeight * currentSelectLine, viewMargin + (i % countOfLine + 1) * singleWidth, singleHeight * (currentSelectLine + 1));
            }
        }

        int currentWaitLine = -1;
        if (waitList.size() != 0) {
            for (int i = 0; i < waitList.size(); i++) {
                if (i % countOfLine == 0) {
                    currentWaitLine++;
                }
                waitList.get(i).setDAta(viewMargin + i % countOfLine * singleWidth, singleHeight * currentWaitLine + selectHeight + dividerHeight, viewMargin + (i % countOfLine + 1) * singleWidth, singleHeight * (currentWaitLine + 1) + selectHeight + dividerHeight);
            }
        }
    }

这一部分主要是确定view的大小,以及确定了每一个条目要draw的位置.

1.确定view的宽高

1.确定宽

如果在xml中确定了宽,则按xml中设置的来定,如果xml给的是fill_parent或者wrap_content则都按照fill_parent处理,即取父控件建议宽度,但是这两个都有一个原则:宽度至少能容下一个条目,否则就抛错.

2.确定高

高度的确定就要相对麻烦一点,如果xml中给了确定的值,则使用该值来确定高度,否则就按照wrap_content处理,通过计算:选择区高度+分割区高度+备选区高度的=内从总高度,内容的高度并不一定就是view的高度,view的高度最大就是采用fill_parent方式获取到的值,所以当xml中没有给定确定的值给高度时,取父控件建议高度heightSize和内容高度contentHeight两者的较小的一个.

2.确定每个条目item的位置

确定每个item位置的代码在initData函数中,主要逻辑是通过计算确定出每个item的位置信息left,top,right,bottom,然后将这四个值设置到对应的item对象中去,以便在ondraw中将其画到对应位置上.

开始绘制view(onDraw)

确定好了view的宽高以及每个item的位置之后,就可以开始绘制view了.

 @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw: selectList size: " + selectList.size() + "  waitList size : " + waitList.size());
        //1.画已选内容
        paint.setStyle(Paint.Style.FILL);
        if (selectList.size() == 0) {
            canvas.drawText("请点击待选区类别进行添加.", widthSize / 2, selectHeight / 2, paint);
        } else {
            for (int i = 0; i < selectList.size(); i++) {
                TextItem textItem = selectList.get(i);
                paint.setColor(Color.BLACK);
                paint.setStyle(Paint.Style.FILL);
                canvas.drawText(textItem.getText(), textItem.getLeft() + singleWidth / 2, textItem.getTop() + (singleHeight + textHeight) / 2, paint);
                paint.setStyle(Paint.Style.STROKE);
                paint.setColor(Color.parseColor("#008080"));
                canvas.drawRoundRect(textItem.getRectF(), singleHeight / 2, singleHeight / 2, paint);
            }
        }

        //画间隔线
        paint.setStrokeWidth(5);
        canvas.drawLine(0, selectHeight + dividerHeight / 2, widthSize, selectHeight + dividerHeight / 2, paint);
        paint.setStrokeWidth(1);

        //2.画备选内容
        if (waitList.size() == 0) {
            paint.setStyle(Paint.Style.FILL);
            canvas.drawText("没有备选项.", widthSize / 2, selectHeight + waitHeight / 2 + dividerHeight, paint);
        } else {
            for (int i = 0; i < waitList.size(); i++) {
                TextItem textItem = waitList.get(i);
                paint.setColor(Color.BLACK);
                paint.setStyle(Paint.Style.FILL);
                canvas.drawText(textItem.getText(), textItem.getLeft() + singleWidth / 2, textItem.getTop() + (singleHeight + textHeight) / 2, paint);
                paint.setStyle(Paint.Style.STROKE);
                paint.setColor(Color.parseColor("#008080"));
                canvas.drawRoundRect(textItem.getRectF(), singleHeight / 2, singleHeight / 2, paint);
            }
        }
    }

这部分没有太对需要解释的,因为在onMeasure中我们已经确定好了item的位置信息,这时候只需要从对应对象中取出相应信息去进行绘制即可,相关的绘制的api都不难,网上也能找到详细解释.

事件处理

这里要处理的事件有两个:1.点击备选区条目添加到选择区(从选择删除回备选区未实现,原理一样);2.当内容的长度大于view的高度时,处理滚动事件

 GestureDetector detector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {

        private int dy;

        @Override
        public boolean onDown(MotionEvent e) {
            Log.d(TAG, "onDown: ");
            return true;
        }

        @Override
        public void onShowPress(MotionEvent e) {
            Log.d(TAG, "onShowPress: ");
        }

        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            Log.d(TAG, "onSingleTapUp: ");
            int position = checkTouchPoint(e);
            if (position == -1) {
                return true;
            }
            TextItem textItem = waitList.remove(position);
            selectList.add(textItem);
            if (waitItemClickListener != null) {
                waitItemClickListener.onWaitItemClick(textItem.getText());
            }
            requestLayout();
            invalidate();
            return true;
        }

        private int checkTouchPoint(MotionEvent e) {
            int x = (int) e.getX();
            int y = (int) e.getY();
            Region region = new Region();
            for (int i = 0; i < waitList.size(); i++) {
                TextItem textItem = waitList.get(i); 
                region.set(textItem.getLeft(), textItem.getTop(), textItem.getRight(), textItem.getBottom());
                if (region.contains(x, y)) {
                    return i;
                }
            }
            return -1;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            Log.d(TAG, "onScroll: " + distanceY);
            if (contentHeight <= getMeasuredHeight()) {
                return false;
            }
            if (offset == 0 && distanceY < 0) {
                return false;
            } else if (offset == contentHeight - getMeasuredHeight() && distanceY > 0) {
                return false;
            }
            dy = (int) distanceY;
            Log.d(TAG, "onScroll: 修改前的dy" + dy);
            offset += dy;
            if (offset < 0) {
                dy += Math.abs(offset);
                offset = 0;
            } else if (offset > contentHeight - getMeasuredHeight()) {
                dy -= offset - contentHeight + getMeasuredHeight();
                offset = contentHeight - getMeasuredHeight();
            }
            Log.d(TAG, "onScroll: 修改后的dy" + dy);
            scrollBy(0, dy);

            return false;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            Log.d(TAG, "onLongPress: ");
        }

        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            Log.d(TAG, "onFling: ");
            return false;
        }
    });

具体的代码如上,在点击事件中先去判断点中的是哪个备选的item,然后将其添加到选择区,同时如果被设置里监听,会触发对应监听.而在滑动事件中,主要处理了何时需要滑动,何时可以上滑,何时可以下滑以及滑动到定点时的特殊处理,逻辑都不难.

到这里,一个完整的类别选择view就完成了(从选择区删除回备选区功能,有兴趣可以自己参照着去实现下).

点击下载源码

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值