A tag layout TagLayout~

Posted by paqman on Mon, 10 Jan 2022 19:19:13 +0100

preface:

Boring Xiaobai wrote to play. The final name is like this because it has experienced several versions (after completing a small goal, write another one). Therefore, in the final version, you may not need so many useless things ~ delete them appropriately ~ or use the previous version ~ when there is no chat, you always fantasize. I would like to support many functions, but when I write it, I will find it very sad / (ㄒ o ㄒ) / ~ ~. The final version does not reach the fantasy version, and it is not very sound. Of course, there may be small bugs ~ (● '◡ ●) or let's play a game name and ask everyone to find bugs together ~ the game begins!

Origin of naming~

TagLayout: initial version, simple tag layout, from top to bottom, from left to right, even P adding is not supported...

TagAdapterLayout: This version is mainly because I want to try two ways to manage the relationship between data and tags. It's natural to think of listview, which uses the adapter. Subsequent versions are also based on this nonsense. The adapter is responsible for maintaining data and tagLayout is responsible for managing tags. Another is to maintain a data set by yourself. In fact, the essence is the same. It's a question of thinking. I want to try and see what the difference will be in the end, but the second one hasn't been written yet...

Optimized simpletagadapterlayout: This version does some optimization when adding and deleting data, instead of full updating. Then copy, yes, it's also copy, because I'm curious about how some operations of listview are done, because it's really like this tagLayout, but the layout is different. I even have the idea of inheriting listview and changing the layout rules, but obviously, I think it's too simple and ended in failure. If you have an idea, try it. Anyway, I'm not a big man and I'm not ashamed~

Extended optimized simpletadapterlayout: This version has some function extensions (the idea is this idea, but I only made gravity. I didn't expect any other extended functions. I wanted to make a layout direction. Later, I thought it seemed to overlap with gravity and gave up)

Scrollableextended optimized simpletagadapterlayout: just look at the name. This version only does sliding processing, although you probably don't need it at all...

So its final name is long-----------

To sum up:

1. Only horizontal layout is supported, not vertical layout

2. Only highly consistent labels are supported, and there is no problem with inconsistency, that is, the reality will be very messy. It is not the code that causes it to be messy, which makes its own typesetting very messy.. You can do the gravity of each line, but it's also very messy and didn't do it

3. The adapter manages data and manages its own tags

4. Support gravity (left center right)

5. Support sliding (sliding switch, sliding rebound)

6. Some minor optimizations

About these general directions, other details are annotated in the code. It seems that nothing has been done / (ㄒ o ㄒ)/~~

In fact, all the things that should be noted are marked, and all the things that should be annotated are annotated. Xiaobai gives consideration to Xiaobai. Big brother, please give me a touch to kill~

Some places have commented out another way of writing. If you are interested, you can try what effect it has~

Finally, the worm scrollableextended optimized simpletadapterlayout itself

package com.greentravel.smart.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Scroller;

import com.greentravel.smart.R;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static android.view.View.MeasureSpec.AT_MOST;

/**
 * @ProjectName: gate_app
 * @Package: com.greentravel.smart.view
 * @ClassName: TabLayout
 * @Description: Label layout for reference adapter management data
 * 1.At present, it is only applicable to label consistency (label layout of the same type and consistent height)
 * 2.At present, only horizontal layout is supported
 * 3.When adding or deleting data, it has been optimized (the initial index and all subsequent indexes have changed, and will not change before index)
 * a.Add child view again
 * b.Binding listening
 * Binding monitoring: here, all tags are subject to continuous click control, that is, clicking tag A cannot respond to the click event of tag B within the interval.
 * c.Measurement sub view
 * d.Layout subview
 * Found bug s: there is no problem with adding data in a single time, but when adding data multiple times in a short time, measure and layout will not be executed every time,
 * Instead, only the last scheduling is performed (measure is called several times at a time and layout is called once, which seems to have been handled by the system). Therefore, measurement and layout cannot be optimized.
 * Of course, if you are sure that your data operations are not so frequent, you can still do this optimization
 * 4.Label visibility and height inconsistencies are not considered
 * 5.It is forbidden to add sub views directly (manual layout redrawing)
 * 6.Support gravity
 * 7.Support sliding
 * a.Sliding boundary cache control
 * b.Boundary sliding springback scroller startScroll+override computeScroll()
 * c.Glider Fling + velocitytracker to make the sliding not stiff
 * d.No multi finger operation event findPointerIndex + event. getY(pointerIndex)
 * @Author: wangwei
 * @CreateDate: 2021/7/8 14:15
 * @UpdateUser:
 * @UpdateDate: 2021/7/8 14:15
 * @UpdateRemark:
 * @Version: 1.0
 */
public class ScrollableExtendedOptimizedSimpleTagAdapterLayout extends ViewGroup {

    private static final String TAG = " TagAdapterLayout ";

    private int start = 0;//Start flag bit when data is added or deleted

    private int doubleClickInterval = 200;//Label valid double click interval

    private int lineSpace = 10;//Row spacing

    private int itemSpace = 10;//Label spacing

    private int gravity = Gravity.RIGHT;

    private int contentHeight;//Total content height

    private int bufferDistance = 200;//Maximum buffer distance of boundary sliding out

    private boolean supportBufferDistance = true;//Whether buffer distance is supported

    private boolean scrollable = false;//Whether sliding is supported

    private Scroller scroller;
    private VelocityTracker velocityTracker;
    private boolean needScroll;

    private TagAdapter adapter;
    private OnTagClickListener onTagClickListener;
    private OnTagLongClickListener onTagLongClickListener;

    public ScrollableExtendedOptimizedSimpleTagAdapterLayout(Context context) {
        super(context);
    }

    public ScrollableExtendedOptimizedSimpleTagAdapterLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initFromAttributes(context, attrs, 0, 0);
//        init();
    }

    public ScrollableExtendedOptimizedSimpleTagAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initFromAttributes(context, attrs, defStyleAttr, 0);
    }

    public ScrollableExtendedOptimizedSimpleTagAdapterLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
    }

    private void initFromAttributes(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.TagLayout, defStyleAttr, defStyleRes);
        itemSpace = a.getDimensionPixelOffset(R.styleable.TagLayout_itemSpace, itemSpace);
        lineSpace = a.getDimensionPixelOffset(R.styleable.TagLayout_lineSpace, lineSpace);
        gravity = a.getInt(R.styleable.TagLayout_gravity, gravity);
        bufferDistance = a.getInt(R.styleable.TagLayout_bufferDistance, bufferDistance);
        scrollable = a.getBoolean(R.styleable.TagLayout_scrollable, scrollable);
        log(TAG + "gravity = " + gravity);
        a.recycle();
        init();
    }

    /*private String[] strings = new String[]{"This is a test text ",
            "Me Too!", "Coincidentally, I'm also a test text, and it's longer than you two "," then don't I want to be longer, see how long I am, is it the longest among you? I'm the longest test text among you "," how envious "," ah "," Oh, oh, oh, oh, oh, oh,
            "This is a test text ",
            "Me Too!", "Coincidentally, I'm also a test text, and it's longer than you two. "" then don't I want to be longer? See how long I am, is it the longest among you? I'm the longest test text among you. "," how envious "," ah "," Oh, oh, oh, oh "};*/
   

    private void init() {
        scroller = new Scroller(getContext());
        velocityTracker = VelocityTracker.obtain();
    }

    public void setAdapter(TagAdapter adapter) {
        this.adapter = adapter;
    }

    public void setLineSpacePx(int lineSpace) {
        this.lineSpace = lineSpace;
    }

    public void setLineSpaceDp(int lineSpace) {
        this.lineSpace = dip2px(lineSpace);
    }

    public void setItemSpacePx(int itemSpace) {
        this.itemSpace = itemSpace;
    }

    public void setItemSpaceDp(int itemSpace) {
        this.itemSpace = dip2px(itemSpace);
    }

    public void setDoubleClickInterval(int interval) {
        doubleClickInterval = interval;
    }

    public void setBufferDistance(int bufferDistance) {
        this.bufferDistance = bufferDistance;
    }

    public void setScrollable(boolean scrollable) {
        this.scrollable = scrollable;
    }

    public boolean isScrollable() {
        return scrollable;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        log("onMeasure");
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        int wLastSize = wSize;
        int hLastSize = hSize;

        int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        /*int maxWidth = 0;
        int maxHeight = 0;
        for (int i = 0; i < childCount; i++) {
            maxWidth = Math.max(maxWidth, getChildAt(i).getMeasuredWidth());
            maxHeight = Math.max(maxHeight, getChildAt(i).getMeasuredHeight());
        }*/

        if (childCount == 0) {
            setMeasuredDimension(0, 0);
            return;
        }

        calculateTotalHeight();
        needScroll = contentHeight > getMeasuredHeight();
        log("onMeasure needScroll = " + needScroll + " contentH = " + contentHeight + " height = " + getMeasuredHeight());

        log(TAG + hMode + " " + hSize);

        /**
         * If the width is WrapContent, wMode is at_ For most, the widest sub view width, plus the left and right padding values, and the minimum value in the upper limit wSize are taken as the final width, which is not considered by UNSPECIFIED
         * If the height is WrapContent, hMode is AT_MOST, calculate the total height after arrangement, plus the upper and lower padding values, and the minimum value in the upper limit hSize as the final width, which is not considered by UNSPECIFIED
         * The exact value and MatchParent, and if the mode is actual, it is the initial value
         */
        if (wMode == AT_MOST /*|| wMode == MeasureSpec.UNSPECIFIED*/) {
            wSize = Math.min(calculateMaxWidth() + getPaddingLeft() + getPaddingRight(), wSize);
        } else if (wMode == MeasureSpec.UNSPECIFIED) {//If not specified, it is calculated by its own size
            wSize = calculateMaxWidth() + getPaddingLeft() + getPaddingRight();
        }
        if (hMode == AT_MOST /*|| hMode == MeasureSpec.UNSPECIFIED*/) {
            hSize = Math.min(contentHeight, hSize);
        } else if (hMode == MeasureSpec.UNSPECIFIED) {//If not specified, it is calculated according to its own size
            hSize = contentHeight;
        }
        log(TAG + hMode + " " + hSize);
        setMeasuredDimension(wSize, hSize);
    }

    //Calculate maximum width when width WrapContent
    private int calculateMaxWidth() {
        int maxWidth = 0;
        for (int i = 0; i < getChildCount(); i++) {
            maxWidth = Math.max(maxWidth, getChildAt(i).getMeasuredWidth());
        }
        return maxWidth;
    }

    //Height WrapContent, and the maximum height is calculated when it is arranged horizontally
    //The arrangement direction has not been realized yet. This method is not used
    private int calculateMaxHeight() {
        int maxHeight = 0;
        for (int i = 0; i < getChildCount(); i++) {
            maxHeight = Math.max(maxHeight, getChildAt(i).getMeasuredHeight());
        }
        return maxHeight;
    }

    private int calculateTotalHeight() {
        int hSize = 0;
        int ll = getPaddingLeft();//X available starting position
        int lineHeight = 0;//Row height
        for (int i = 0; i < getChildCount(); i++) {
            View childAt = getChildAt(i);
            if (ll + childAt.getMeasuredWidth() > getMeasuredWidth() - getPaddingRight()) {
                ll = getPaddingLeft();//reset
                hSize += lineHeight + lineSpace;
                lineHeight = childAt.getMeasuredHeight();
                ll += childAt.getMeasuredWidth() + itemSpace;//record
            } else {
                ll += childAt.getMeasuredWidth() + itemSpace;//record
                lineHeight = Math.max(lineHeight, childAt.getMeasuredHeight());//The row height is the maximum height of the child View of the row
            }
        }
        hSize += lineHeight;//Height of last row
        contentHeight = hSize + getPaddingTop() + getPaddingBottom();
        return hSize;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        log("onSizeChanged");
        super.onSizeChanged(w, h, oldw, oldh);
        needScroll = contentHeight > h;
        log("onSizeChanged needScroll = " + needScroll + " contentH = " + contentHeight + " height = " + h);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        int ll = getPaddingLeft();//Gravity is the starting position of x-axis when it is left, which is separated for easy understanding
        int lr = getMeasuredWidth() - getPaddingRight();//The starting position of x-axis when Gravity is right, which is separated for easy understanding
        int lc = getPaddingLeft();//The starting position of x-axis when Gravity is center, which is separated for easy understanding
        int lc_lineCount = 0;//When Gravity is center, it is used to record the number of view s waiting for layout in this row
        int tt = getPaddingTop();//y-axis layout start position
        int lineHeight = 0;//Row height

        switch (gravity) {
            case Gravity.LEFT:
                //Calculate the line feed layout of the starting bit from left to right and the non line feed layout. Each cycle carries out the layout of the current view
                for (int i = 0; i < childCount; i++) {
                    View childAt = getChildAt(i);
                    if (ll + childAt.getMeasuredWidth() > getMeasuredWidth() - getPaddingRight()) {
                        ll = getPaddingLeft();//reset
                        tt += lineHeight + lineSpace;
                        lineHeight = childAt.getMeasuredHeight();//Line feed, reset line height
                        childAt.layout(ll, tt, ll + childAt.getMeasuredWidth(), tt + childAt.getMeasuredHeight());//The system solves the padding problem
                        ll += childAt.getMeasuredWidth() + itemSpace;//record
                    } else {
                        childAt.layout(ll, tt, ll + childAt.getMeasuredWidth(), tt + childAt.getMeasuredHeight());
                        ll += childAt.getMeasuredWidth() + itemSpace;//record
                        lineHeight = Math.max(lineHeight, childAt.getMeasuredHeight());//The row height is the maximum height of the child View of the row
                    }
                }
                break;
            case Gravity.RIGHT:
                //Calculate the line feed layout of the starting bit from right to left, and the non line feed layout. Each cycle carries out the layout of the current view
                for (int i = 0; i < childCount; i++) {
                    View childAt = getChildAt(i);
                    if (lr - childAt.getMeasuredWidth() < getPaddingLeft()) {
                        lr = getMeasuredWidth() - getPaddingRight();//reset
                        tt += lineHeight + lineSpace;
                        lineHeight = childAt.getMeasuredHeight();//Line feed, reset line height
                        childAt.layout(lr - childAt.getMeasuredWidth(), tt, lr, tt + childAt.getMeasuredHeight());//The system solves the padding problem
                        lr -= childAt.getMeasuredWidth() + itemSpace;//record
                    } else {
                        childAt.layout(lr - childAt.getMeasuredWidth(), tt, lr, tt + childAt.getMeasuredHeight());
                        lr -= childAt.getMeasuredWidth() + itemSpace;//record
                        lineHeight = Math.max(lineHeight, childAt.getMeasuredHeight());//The row height is the maximum height of the child View of the row
                    }
                }
                break;
            case Gravity.CENTER:
                //Calculate the starting position line feed layout from left to right, and only lay out all the sub view s of the previous row when line feed, and finally fill in the layout of the last row
                for (int i = 0; i < childCount; i++) {
                    View childAt = getChildAt(i);
                    if (lc + childAt.getMeasuredWidth() > getMeasuredWidth() - getPaddingLeft()) {
                        int var_l = (getMeasuredWidth() - lc - getPaddingRight()) / 2 + getPaddingLeft();//Actual layout start bit of the previous line
                        for (int before = i - lc_lineCount; before < i; before++) {//The child view s in the previous row are laid out one by one
                            View childBefore = getChildAt(before);
                            childBefore.layout(var_l, tt, var_l + childBefore.getMeasuredWidth(), tt + childBefore.getMeasuredHeight());
                            var_l += childBefore.getMeasuredWidth() + itemSpace;
                        }
                        lc_lineCount = 1;//Reset
                        lc = getPaddingLeft();//reset
                        tt += lineHeight + lineSpace;
                        lineHeight = childAt.getMeasuredHeight();//Line feed, reset line height
                        lc += childAt.getMeasuredWidth() + itemSpace;//record
                    } else {
                        lc += childAt.getMeasuredWidth() + itemSpace;//record
                        lineHeight = Math.max(lineHeight, childAt.getMeasuredHeight());//The row height is the maximum height of the child View of the row
                        lc_lineCount++;
                    }
                    if (i == childCount - 1) {
                        int var_l = (getMeasuredWidth() - lc - getPaddingRight()) / 2 + getPaddingLeft();
                        for (int before = childCount - lc_lineCount; before < childCount; before++) {
                            View childBefore = getChildAt(before);
                            childBefore.layout(var_l, tt, var_l + childBefore.getMeasuredWidth(), tt + childBefore.getMeasuredHeight());
                            var_l += childBefore.getMeasuredWidth() + itemSpace;
                        }
                    }
                }
                break;
            default:
                break;
        }
    }

    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
        }
    }

    private float lastY;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                log("down");
                //lastY = event.getY();
                return true;
            case MotionEvent.ACTION_MOVE:
                log("move");
                //scrollBy(0, (int) (lastY - event.getY()));
                scroll(0, (int) (lastY - event.getY()));
                lastY = event.getY();
                return true;
            case MotionEvent.ACTION_UP:
                log("up distance = " + (int) (lastY - event.getY()));
                lastY = event.getY();
                finishScrollBetter();
                break;
            default:
                break;
        }
        return false;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            //When the child view can respond to the down event, tagLayout's processing when the down event occurs is placed here
            case MotionEvent.ACTION_DOWN:
                lastY = ev.getY();
                if (!scroller.isFinished()) {
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //To a certain extent, it prevents the click event of the sub view from being unable to respond because the small-scale move event is intercepted by tagLayout when the sub view is clicked continuously
                if (Math.abs(lastY - ev.getY()) < 3) {
                    log("interceptMove false");
                    return false;
                }
                log("interceptMove true");
                return true;
            default:
                break;
        }
        return false;
    }

    private void finishScrollBetter() {
        if (canScroll()) {
            if (getScrollY() < 0) {
                scroller.startScroll(0, getScrollY(), 0, -getScrollY());
                invalidate();
            } else if (getScrollY() > contentHeight - getHeight()) {
                scroller.startScroll(0, getScrollY(), 0, (contentHeight - getHeight()) - getScrollY());
                invalidate();
            } else {
                //Calculate the last xy velocity
                velocityTracker.computeCurrentVelocity(1000);//It is unified with the speed unit required by fling and transmitted to 1000
                //Positive and negative velocity represent direction
                //scroller.fling(0, getScrollY(), 0, -(int) velocityTracker.getYVelocity(), 0, 0, -bufferDistance, contentHeight - getHeight()+bufferDistance);// Buffer rebound, but do not know how to rebound, change the speed interpolator?
                scroller.fling(0, getScrollY(), 0, -(int) velocityTracker.getYVelocity(), 0, 0, 0, contentHeight - getHeight());//Buffer not found
            }
        }
    }

    private void scroll(int x, int y) {
        if (canScroll()) {
            //Judge the sliding up and down according to the positive and negative of y, and optimize the boundary
            log(TAG + "y = " + y + " scrollY = " + getScrollY() + " tH = " + contentHeight + " h = " + getHeight());
            if (getScrollY() == 0 - bufferDistance && y <= 0) {
                scrollTo(0, 0 - bufferDistance);
                return;
            }
            if (getScrollY() == contentHeight - getHeight() + bufferDistance && y >= 0) {
                scrollTo(0, contentHeight - getHeight() + bufferDistance);
                return;
            }
//            y = getScrollY() + y < 0 - bufferDistance ?  0 : getScrollY() + y > contentHeight - getHeight() + bufferDistance ?  0 : y;// There are some errors
            y = getScrollY() + y < 0 - bufferDistance ? -bufferDistance - getScrollY() : getScrollY() + y > contentHeight - getHeight() + bufferDistance ? contentHeight - getHeight() + bufferDistance - getScrollY() : y;//More accurate
            log(TAG + "y1 = " + y + " scrollY = " + getScrollY() + " tH = " + contentHeight + " h = " + getHeight());

            //If the sliding is not smooth at the boundary, the y calculation is wrong, the boundary calculation is wrong, and the end is advanced
            scrollBy(x, y);
        }
    }

    private boolean canScroll() {
        if (!scrollable || (scrollable && !needScroll)) {
            return false;
        }
        return true;
    }

    @Override
    protected void onDetachedFromWindow() {
        if (velocityTracker != null) {
            velocityTracker.recycle();
        }
        velocityTracker = null;
        super.onDetachedFromWindow();
    }

    @Override
    public void addView(View child) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void addView(View child, int index) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void addView(View child, int width, int height) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void addView(View child, LayoutParams params) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void addView(View child, int index, LayoutParams params) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void removeAllViews() {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void removeView(View view) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void removeViewAt(int index) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Override
    public void removeViews(int start, int count) {
        throw new UnsupportedOperationException("this method is not supported in " + TAG);
    }

    @Deprecated
    private void addTags() {
        removeAllViewsInLayout();
        for (int i = 0; i < adapter.getCount(); i++) {
            addViewInLayout(adapter.getView(i, null, this), -1, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        }
        requestLayout();
        bindListener();
    }

    private void setTags() {
        removeAllViewsInLayout();
        for (int i = start; i < adapter.getCount(); i++) {
            addViewInLayout(adapter.getView(i, null, this), -1, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        }
        requestLayout();
        bindListener(0);
    }

    private void addTags(int start) {
        //removeViews(start, getChildCount() - start);
        for (int i = start; i < adapter.getCount(); i++) {
//            addView(adapter.getView(i, null, this));
            addViewInLayout(adapter.getView(i, null, this), -1, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
        }
        requestLayout();
        bindListener(start);
    }

    private void removeTags(int... indexes) {
        for (int i = indexes.length - 1; i >= 0; i--) {
//            removeViewAt(indexes[i]);
            View c = getChildAt(indexes[i]);
//            getLayoutTransition().removeChild(this,c);// Invalid. Listen for the delete action and execute the animation of all sub view s
//            removeViewsInLayout(indexes[i], 1);
            //getChildAt(indexes[i]).clearAnimation();
            removeViewInLayout(c);
            invalidate(c.getLeft(), c.getTop(), c.getRight(), c.getBottom());//Redraw delete area
            log(TAG + "tag remove at" + indexes[i] + " " + getChildCount());
        }
        requestLayout();
    }

    @Deprecated
    private void bindListener() {
        final long[] var1 = {0};
        for (int i = 0; i < getChildCount(); i++) {
            getChildAt(i).setOnClickListener(new OnClickListener() {
                //long var1 = 0;
                @Override
                public void onClick(View v) {
                    if (System.currentTimeMillis() - var1[0] > doubleClickInterval) {
                        v.setSelected(!v.isSelected());
                        if (onTagClickListener != null) {
                            onTagClickListener.onTagClick(indexOfChild(v));
                        }
                    }
                    var1[0] = System.currentTimeMillis();
                }
            });
            if (onTagLongClickListener != null) {
                getChildAt(i).setOnLongClickListener(new OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        onTagLongClickListener.onTagLongClick(indexOfChild(v));
                        return true;
                    }
                });
            }
        }
    }

    private void bindListener(int start) {
        final long[] var1 = {0};
        for (int i = start; i < getChildCount(); i++) {
            getChildAt(i).setOnClickListener(new OnClickListener() {
                //long var1 = 0;
                @Override
                public void onClick(View v) {
                    log("Tag inner click");
                    if (System.currentTimeMillis() - var1[0] > doubleClickInterval) {
                        v.setSelected(!v.isSelected());
                        if (onTagClickListener != null) {
                            onTagClickListener.onTagClick(indexOfChild(v));
                        }
                    }
                    var1[0] = System.currentTimeMillis();
                }
            });
            if (onTagLongClickListener != null) {
                getChildAt(i).setOnLongClickListener(new OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        onTagLongClickListener.onTagLongClick(indexOfChild(v));
                        return true;
                    }
                });
            }
        }
    }

    public int[] getSelectedTagsIndexes() {
        int[] r = new int[0];
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i).isSelected()) {
                r = Arrays.copyOf(r, r.length + 1);
                r[r.length - 1] = i;
            }
        }
        return r;
    }

    public <E> List<E> getSelectedTags() {
        /*Type type = adapter.getClass().getGenericSuperclass();
        final Object o = Array.newInstance(type.getClass(), 0);
        ArrayList r = new ArrayList(Arrays.asList(o));
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i).isSelected()) {
                r.add(adapter.getItem(i));
            }
        }*/
        ArrayList r = new ArrayList<E>();
        for (int i = 0; i < getChildCount(); i++) {
            if (getChildAt(i).isSelected()) {
                r.add(adapter.getItem(i));
            }
        }
        return r;
    }

    public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
        this.onTagClickListener = onTagClickListener;
    }

    public void setOnTagLongClickListener(OnTagLongClickListener onTagLongClickListener) {
        this.onTagLongClickListener = onTagLongClickListener;
    }

    public interface OnTagClickListener {
        void onTagClick(int index);
    }

    public interface OnTagLongClickListener {
        void onTagLongClick(int index);
    }

    /**
     * In principle, it is only responsible for data management and notification
     * Others are completed by the view itself, such as monitoring
     *
     * @param <T>
     */
    public abstract static class TagAdapter<T> extends BaseAdapter {

        private ScrollableExtendedOptimizedSimpleTagAdapterLayout tagLayout;

        private OnTagClickListener onTagClickListener;

        private List<T> datas = new ArrayList<T>();

        public TagAdapter(ScrollableExtendedOptimizedSimpleTagAdapterLayout tagLayout) {
            this.tagLayout = tagLayout;
        }

        @Override
        public int getCount() {
            return datas.size();
        }

        @Override
        public T getItem(int position) {
            return datas.get(position);
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public abstract View getView(int position, View convertView, ViewGroup parent);

        public void setDatas(List<T> ts) {
            this.datas = ts;
            notifyTagsChanged();
        }

        public void add(T t) {
            datas.add(t);
            notifyTagsAdded(getCount() - 1);
        }

        public void addAll(List<T> ts) {
            datas.addAll(ts);
            notifyTagsAdded(getCount() - ts.size());
        }

        public void remove(int index) {
            datas.remove(index);
            notifyTagsRemoved(index);
        }

        public void removeAll(int... indexes) {
            Arrays.sort(indexes);
            for (int i = indexes.length - 1; i >= 0; i--) {
                datas.remove(indexes[i]);
                tagLayout.log(TAG + "data remove at" + indexes[i] + " " + datas.size());
            }
            notifyTagsRemoved(indexes);
        }

        private void notifyTagsChanged() {
            tagLayout.setTags();
        }

        private void notifyTagsAdded(int start) {
            tagLayout.addTags(start);
        }

        private void notifyTagsRemoved(int... indexes) {
            tagLayout.removeTags(indexes);
        }

        //Follow the principle of only managing data and monitoring by the view itself, which is not recommended
        @Deprecated
        private void bindListener() {
            for (int i = 0; i < getCount(); i++) {
                int finalI = i;
                tagLayout.getChildAt(i).setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (onTagClickListener != null) {
                            onTagClickListener.onTagClick(finalI);
                        }
                    }
                });
                /*This will not do.*/
                /*getView(i,null,tagLayout).setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if(onTagClickListener!=null){
                            onTagClickListener.onTagClick(getItem(finalI));
                        }
                    }
                });*/
            }
        }

        //Follow the principle of only managing data and monitoring by the view itself, which is not recommended
        @Deprecated
        public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
            this.onTagClickListener = onTagClickListener;
        }

        //Follow the principle of only managing data and monitoring by the view itself, which is not recommended
        @Deprecated
        public interface OnTagClickListener {
            void onTagClick(int index);
        }
    }

    public int dip2px(float var1) {
        float var2 = getContext().getResources().getDisplayMetrics().density;
        return (int) (var1 * var2 + 0.5F);
    }

    public int sp2px(float var1) {
        float var2 = getContext().getResources().getDisplayMetrics().scaledDensity;
        return (int) (var1 * var2 + 0.5F);
    }

    /**************************Debug***********************/

    private boolean isDebug = true;

    public void setDebug(boolean isDebug) {
        this.isDebug = isDebug;
    }

    private void log(String msg) {
        if (isDebug) {
            System.out.println(TAG.concat(msg));//Log Tag string restricted
        }
    }
}

Properties file

<declare-styleable name="TagLayout">
        <attr name="lineSpace" format="dimension"></attr>
        <attr name="itemSpace" format="dimension"></attr>
        <attr name="gravity" format="integer">
            <flag name="left" value="3"/>
            <flag name="right" value="5"/>
            <flag name="center" value="17"/>
        </attr>
        <attr name="bufferDistance" format="dimension"></attr>
        <attr name="scrollable" format="boolean"></attr>
    </declare-styleable>

call

ScrollableExtendedOptimizedSimpleTagAdapterLayout tag = (ScrollableExtendedOptimizedSimpleTagAdapterLayout) findViewById(R.id.Tag);
        ScrollableExtendedOptimizedSimpleTagAdapterLayout.TagAdapter adapter = new ScrollableExtendedOptimizedSimpleTagAdapterLayout.TagAdapter(tag) {
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                TextView view = new TextView(MainActivity.this);
                view.setText(getItem(position).toString());
                view.setBackgroundColor(Color.GREEN);
                view.setBackgroundTintList(new ColorStateList(new int[][]{new int[]{android.R.attr.state_selected}, new int[]{-android.R.attr.state_selected}}, new int[]{Color.YELLOW, Color.GREEN}));
                return view;
            }
        };

        tag.setOnTagClickListener(new ScrollableExtendedOptimizedSimpleTagAdapterLayout.OnTagClickListener() {
            @Override
            public void onTagClick(int o) {
                ToastUtils.showShortToast(MainActivity.mContext, tag.<String>getSelectedTags().toString());
            }
        });
        tag.setAdapter(adapter);
        adapter.addAll(Arrays.asList(TagAdapterLayout.strings));

Topics: Java