自定义view之流式布局

前言

网上关于流式布局的代码已经很多了,但是我为什么又要去写呢,主要是在实际使用中发现定制化困难的缘故了.另一方面,凑文章数 ?

目前支持的功能:

  1. 最后一个文本点击的新增.
  2. 长按删除/恢复原样.
  3. 点击item切换背景和字体颜色,item的单击和长按回调.
  4. View的height随着内容自适应.

思路

一个文字列表,逐行排列,在每次开始的时候计算一下同一行剩余的width是否足够文字显示,如果不够,就增加高度,即移至下一行.(暂时未做一整行都显示不下的文字显示支持的逻辑处理.)

效果如下:

更新说明:

2019/1/13
增加view整体的高度自动适配(仅限于最后一个文本是"+")

代码参考如下:

public class FlowLayout extends View {
    private static final String TAG = "FlowLayout";
    private Paint mPaint;
    private List<String> textList = new ArrayList<>();
    private int minPadding = 100;
    private int textPadding = 20;
    private float textSize = 30;
    private int textPace = 35;                                //两个item的间隔
    private int textHeight = 40;
    private int distanceW = 1080;
    private List<int[]> pointList = new ArrayList<>();
    private int clickIndex = -1;
    private long touchDownTime;
    private boolean isLongClick;
    private int textNormalColor;
    private int textClickColor;
    private boolean isShowEndAdd;                            //最后一个字符串是否是+
    private FlowClickListener listener;
    private boolean paddingHasChanged;
    private int minHeight = 500;
    private int baseItemHeight;
    private int totalTextWidth;

    public FlowLayout(Context context) {
        this(context, null);
    }

    public FlowLayout(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FlowLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
        textNormalColor = a.getColor(R.styleable.FlowLayout_flow_text_normal_color, Color.BLUE);
        textClickColor = a.getColor(R.styleable.FlowLayout_flow_text_click_color, Color.WHITE);
        textSize = a.getDimension(R.styleable.FlowLayout_flow_text_size, context.getResources().getDimension(R.dimen.dimen_size_15));
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setTextSize(textSize);
        Rect rect = new Rect();
        mPaint.getTextBounds("1", 0, "1".length(), rect);
        textHeight = rect.height();
        textPadding = textHeight;
        textPace = textHeight * 3 / 2;
        baseItemHeight = textHeight + textPadding * 2 + textPace;
        mPaint.setAntiAlias(true);
    }

    public boolean isLongClick() {
        return isLongClick;
    }

    public void setData(List<String> textList) {
        setData(textList, false, -1);
    }

    public void setData(List<String> textList, boolean isShowEndAdd, int clickIndex) {
        this.isShowEndAdd = isShowEndAdd;
        this.clickIndex = clickIndex;
        this.textList.clear();
        this.textList.addAll(textList);
        resetMinHeight();
        postInvalidate();
    }

    //重设最小高度
    private void resetMinHeight() {
        int lastMinHeight = minHeight;
        minHeight = minPadding;
        int totalTextWidthC = 0;
        for (int i = 0; i < textList.size(); i++) {
            float width = mPaint.measureText(textList.get(i));
            totalTextWidthC += (width + textPadding * 2 + textPace);
            if (totalTextWidthC + textPace > distanceW) {  //判断width 是否够
                totalTextWidthC = (int) (width + textPadding * 2 + textPace);
                minHeight += baseItemHeight;
            }
            if (i == textList.size() - 1)
                this.totalTextWidth = totalTextWidthC;
        }
        minHeight += textPadding * 2;
        if (minHeight != lastMinHeight) {
            requestLayout();
        }
    }

    /**
     * @param addStr
     */
    public void addData(String addStr) {
        if (!isShowEndAdd) {    //新增加的字符串是否显示在末尾
            clickIndex = this.textList.size();
            this.textList.add(addStr);
            //TODO  待补充requestLayout逻辑
        } else {
            int lastListSize = textList.size() - 1;
            clickIndex = lastListSize;
            String endStr = textList.get(lastListSize);
            textList.set(lastListSize, addStr);
            textList.add(endStr);
            float lastWidth = mPaint.measureText(endStr);
            float addWidth = mPaint.measureText(addStr);
            int lastMinHeight = minHeight;
            int totalTextWidthC = (int) (totalTextWidth + (addWidth - lastWidth));
            for (int i = 0; i < 2; i++) {
                if (i > 0)
                    totalTextWidthC += (lastWidth + textPadding * 2 + textPace);
                if (totalTextWidthC + textPace > distanceW) {  //判断width 是否够
                    totalTextWidthC = (int) ((i == 0 ? addWidth : lastWidth) + textPadding * 2 + textPace);
                    minHeight += baseItemHeight;
                }
                if (i == 1)
                    this.totalTextWidth = totalTextWidthC;
            }
            if (minHeight != lastMinHeight) {
                requestLayout();
            }
        }
        postInvalidate();
    }

    public void clearDeleteType() {
        clickIndex = -1;
        isLongClick = false;
        postInvalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                if (paddingHasChanged) {
                    postInvalidate();
                    paddingHasChanged = false;
                }
                if (System.currentTimeMillis() - touchDownTime > 500) {
                    if (null != listener) {
                        clickIndex = -1;
                        isLongClick = !isLongClick;
                        postInvalidate();
                    }
                } else {
                    int i = 0;
                    float x = event.getX();
                    float y = event.getY();
                    for (int[] arr : pointList) {
                        if (x >= arr[0] && x <= arr[2] && y >= arr[1] && y <= arr[3]) {
                            clickIndex = i;
                            break;
                        }
                        i++;
                    }

                    if (null != listener && clickIndex >= 0) {
                        if (isLongClick)
                            listener.setOnClickLongItemListener(clickIndex);
                        else
                            listener.setOnClickItemListener(clickIndex);
                        postInvalidate();
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                paddingHasChanged = false;
                touchDownTime = System.currentTimeMillis();
                break;
        }
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (textList.size() <= 0)
            return;
        int startTextTop = minPadding;
        int startTextLeft = 0;
        int totalTextWidth = 0;
        pointList.clear();
        mPaint.setTextSize(textSize);
        for (int i = 0; i < textList.size(); i++) {
            mPaint.setTextAlign(Paint.Align.LEFT);
            float width = mPaint.measureText(textList.get(i));
            int lastWidth = totalTextWidth;
            totalTextWidth += (width + textPadding * 2 + textPace);
            if (totalTextWidth + textPace > distanceW) {  //判断width 是否够
                totalTextWidth = (int) (width + textPadding * 2 + textPace);
                startTextLeft = textPace;
                startTextTop += baseItemHeight;
            } else {
                startTextLeft = lastWidth + textPace;
            }

            mPaint.setColor(textNormalColor);
            boolean isShowClickType = i == clickIndex && null != listener && (!isShowEndAdd || i != textList.size() - 1) && !paddingHasChanged;
            mPaint.setStyle(isShowClickType ? Paint.Style.FILL : Paint.Style.STROKE);
            int[] array = {startTextLeft - textPadding, startTextTop - textPadding - textHeight,
                    (int) (startTextLeft + width + textPadding), startTextTop + textPadding};
            canvas.drawRect(array[0], array[1], array[2], array[3], mPaint);
            mPaint.setColor(isShowClickType ? textClickColor : textNormalColor);
            canvas.drawText(textList.get(i), startTextLeft, startTextTop, mPaint);
            if (isLongClick && null != listener && (!isShowEndAdd || i != textList.size() - 1) && !paddingHasChanged) {
                mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
                mPaint.setColor(textClickColor);
                canvas.drawCircle(startTextLeft + width + textPadding, startTextTop - textPadding * 2, textPadding, mPaint);
                mPaint.setColor(textNormalColor);
                mPaint.setStyle(Paint.Style.STROKE);
                canvas.drawCircle(startTextLeft + width + textPadding, startTextTop - textPadding * 2, textPadding, mPaint);
                mPaint.setTextAlign(Paint.Align.CENTER);
                canvas.drawText("x", startTextLeft + width + textPadding, startTextTop - textPadding * 2 + textHeight / 2, mPaint);
                int part = textPadding + 5;
                array[1] -= part;
                array[2] += part;
                array[0] = array[2] - part * 2;
                array[3] = array[1] + part * 2;
            }
            pointList.add(array);
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        Log.d(TAG, "onLayout: ");
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int resultWidth = sizeWidth;
        int resultHeight = sizeHeight;
        // 考虑内边距对尺寸的影响
        resultWidth += getPaddingLeft() + getPaddingRight();
        resultHeight += getPaddingTop() + getPaddingBottom();
        // 考虑父容器对尺寸的影响
        distanceW = resultWidth = resolveMeasure(sizeWidth, resultWidth);
        resultHeight = resolveMeasure(sizeHeight, resultHeight);
        Log.d(TAG, "onMeasure: minHeight: " + minHeight + ",resultHeight: " + resultHeight);
        setMeasuredDimension(resultWidth, minHeight);
    }

    /**
     * 根据传入的值进行测量
     */
    public int resolveMeasure(int measureSpec, int defaultSize) {
        int result = 0;
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (MeasureSpec.getMode(measureSpec)) {
            case MeasureSpec.AT_MOST:
            case MeasureSpec.EXACTLY:
                //设置warp_content时设置默认值
                result = Math.min(specSize, defaultSize);
                break;
            default:
                result = defaultSize;
        }
        return result;
    }

    public void removeItem(int index) {
        textList.remove(index);
        clickIndex = -1;
        resetMinHeight();
        postInvalidate();
    }

    public void setFlowClickListener(FlowClickListener clickListener) {
        this.listener = clickListener;
    }

    public interface FlowClickListener {
        void setOnClickItemListener(int index);

        void setOnClickLongItemListener(int index);
    }
}

attrs.xml文件内容

  <declare-styleable name="FlowLayout">
        <attr name="flow_text_normal_color" format="color"/>
        <attr name="flow_text_click_color" format="color"/>
        <attr name="flow_text_size" format="dimension"/>
    declare-styleable>

最后

Github项目地址

以上只是效果,实际还需要细节优化,有不对的地方欢迎留言指正,不胜感激.

你可能感兴趣的:(Android_View,自定义View)