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

来源:互联网 发布:linux配置web服务器 编辑:程序博客网 时间:2024/05/17 23:23

距离上一篇文章时间已经有点久远了,其实中间也写了几个好玩的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
原创粉丝点击