Custom view: Category selection function similar to today's headlines

Keywords: xml Mobile

It's a long time since the last article. In fact, I wrote some interesting view s in the middle. But because I'm too lazy to put them on, the new year 2017 started. The company's project is not tense, so I will fill in some articles when I am free.

I usually prefer to brush headlines, see the headlines above many categories, in the category selection page there is a category selection page, as follows

It feels that if you use gridview, you should be able to achieve this function, but the next two lists are also troublesome to use, or data linkage problem, and then I think about making this whole in a view.

This is how it works. Next, we will complete the custom view step by step.

Initialization information

 private void init() {
        //Initialization list
        wait = new ArrayList<>();
        select = new ArrayList<>();
        waitList = new ArrayList<>();
        selectList = new ArrayList<>();
        //Initialization Brush
        paint = new Paint();
        paint.setTextAlign(Paint.Align.CENTER);
        paint.setAntiAlias(true);
        paint.setTextSize(36);
        paint.setColor(Color.parseColor("#008080"));
        //Define the width and height of each category entry, where the width is set to the width of four words
        Rect rect = new Rect();
        paint.getTextBounds("Four characters", 0, 4, rect);
        textHeight = rect.height();
        singleHeight = rect.height() + textPadding * 2;
        singleWidth = rect.width() + textPadding * 2;
        Log.d(TAG, "init: singleHeight: " + singleHeight + "singleWidth: " + singleWidth);
    }

This part is used to do some initialization, focusing on the three variable values obtained in the third part: textHeight / singleHeight / singleWidth.
These three variables represent respectively:
1.textHeight: When the font size is 36, the height of the text.
2. Single Height: The height of the items displayed on the mobile phone increases two textPadding sizes relative to textHeight, mainly in order to make the border not directly adjacent to the font when drawing the back border, in order to look good.
3.singleWidth: Ibid., width of entries.

On Measure

 @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) {
            //The width set must be at least the width of an entry
            throw new RuntimeException("the width too small");
        }
        //Number of entries per line
        countOfLine = widthSize / singleWidth;
        //Few entries fit exactly the entire line, so divide the remaining space into two parts and place it at both ends.
        viewMargin = (widthSize - countOfLine * singleWidth) / 2;
        Log.d(TAG, "onMeasure: countOfLine: " + countOfLine + "viewMargin: " + viewMargin);
        int measuredHeight;

        //1. Calculate the height of the selected area
        if (selectList.size() == 0) {//Unselected content
            selectHeight = singleHeight;
        } else {
            selectHeight = selectList.size() % countOfLine == 0 ? selectList.size() / countOfLine * singleHeight : (selectList.size() / countOfLine + 1) * singleHeight;
        }
        //2. Calculate the height of the area to be selected
        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;//Total height of content (for high scrolling)
        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = Math.min(heightSize, contentHeight);
        }
        initData();
        Log.d(TAG, "onMeasure: widthSize: " + widthSize + "measuredHeight: " + measuredHeight);
        setMeasuredDimension(widthSize, measuredHeight);
    }

    //Converting incoming strings into objects
    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);
            }
        }
    }

This part mainly determines the size of the view and the location of each item to draw.

1. Determine the width and height of the view

1. determine width

If the width is determined in xml, it is determined by the settings in xml. If XML gives fill_parent or wrap_content, it is treated according to fill_parent. That is to say, the width recommended by the parent control is taken, but both of them have one principle: the width can tolerate at least the next entry, otherwise the error will be thrown.

2. determine the height

The determination of height is relatively troublesome. If a certain value is given in xml, it is used to determine height. Otherwise, it is processed according to wrap_content. Through calculation: the height of selection area + partition area + alternative area height = the total height, the height of content is not necessarily the height of view. The maximum height of view is the value obtained by fill parent, so when there is no value in xml. Given a given value to the height, the parent control suggests a smaller one for the height heightSize and the content height Height.

2. Determine the location of item for each entry

The code to determine the location of each item is in the initData function. The main logic is to determine the location information of each item by calculating left,top,right,bottom, and then set the four values to the corresponding item object so that they can be drawn on the corresponding position in ondraw.

Start drawing view(onDraw)

Once you have determined the width of the view and the location of each item, you can start drawing the view.

 @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw: selectList size: " + selectList.size() + "  waitList size : " + waitList.size());
        //1.Selected content of the painting
        paint.setStyle(Paint.Style.FILL);
        if (selectList.size() == 0) {
            canvas.drawText("Please click on the category to add.", 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);
            }
        }

        //Draw spacing lines
        paint.setStrokeWidth(5);
        canvas.drawLine(0, selectHeight + dividerHeight / 2, widthSize, selectHeight + dividerHeight / 2, paint);
        paint.setStrokeWidth(1);

        //2.Picture alternatives
        if (waitList.size() == 0) {
            paint.setStyle(Paint.Style.FILL);
            canvas.drawText("No alternatives.", 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);
            }
        }
    }

This part is not too explanatory, because we have determined the location information of item in onMeasure. At this time, we only need to extract the corresponding information from the corresponding object to draw. The related drawing api is not difficult, and detailed explanation can be found on the internet.

event processing

There are two events to be dealt with here: 1. Click on the alternative entries to add to the selection area (deleting the alternative from the selection is not implemented, the principle is the same); 2. When the length of the content is longer than the height of the view, handle the rolling event.

 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: Pre modification 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: Revised 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;
        }
    });

Specific code as above, in the click event first to determine which alternative item is in the point, and then add it to the selection area, and if set to listen, trigger the corresponding listen. In the sliding event, mainly deal with when need to slide, when can slide up, when can slide down and when to slide to a fixed point special processing, logic is not difficult.

At this point, a complete category selection view is completed (delete the backup function from the selection area, and you can refer to it if you are interested).

Click Download Source Code

Posted by minorgod on Tue, 26 Mar 2019 23:12:30 -0700