Android开发,自定义控件实现角标标签

前言: 在Android开发中,经常会遇到给某条数据右上角添加一个角标,来突出重点。实现这种角标做法有两种:1. 简单粗暴的做法就是让UI直接切图即可。2. 自定义控件来实现

1. 控件概述

CornerLabelView 是一个用于在视图的四个角落显示角标的自定义控件。角标可以显示文字,并且可以自定义角标的位置、大小、颜色、文字大小、文字颜色等属性。

2. 控件拆分

为了便于理解和维护,我们将 CornerLabelView 拆分为以下几个部分:

  1. 属性定义与初始化
  2. 测量与布局
  3. 绘制角标背景
  4. 绘制文字
  5. 属性设置方法

3. 属性定义与初始化

首先,我们需要定义控件的属性,并在构造函数中进行初始化

public class CornerLabelView extends View {
    
    
    private int mHalfWidth; // View宽度的一半
    private Paint mPaint; // 角标画笔
    private TextPaint mTextPaint; // 文字画笔
    private Path mPath; // 角标路径

    private int position; // 角标位置,0:右上角、1:右下角、2:左下角、3:左上角
    private int sideLength; // 角标的显示边长
    private int textSize; // 字体大小
    private int textColor; // 字体颜色
    private String text; // 角标文字
    private int bgColor; // 角标背景颜色
    private int marginLeanSide; // 文字到斜边的距离

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

    public CornerLabelView(Context context, AttributeSet attrs) {
    
    
        super(context, attrs);
        initAttrs(context, attrs);
        init();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
    
    
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CornerLabelView, 0, 0);
        for (int i = 0; i < ta.getIndexCount(); i++) {
    
    
            int attr = ta.getIndex(i);
            if (attr == R.styleable.CornerLabelView_position) {
    
    
                position = ta.getInt(attr, 0);
            } else if (attr == R.styleable.CornerLabelView_side_length) {
    
    
                sideLength = ta.getDimensionPixelSize(attr, sideLength);
            } else if (attr == R.styleable.CornerLabelView_text_size) {
    
    
                textSize = ta.getDimensionPixelSize(attr, textSize);
            } else if (attr == R.styleable.CornerLabelView_text_color) {
    
    
                textColor = ta.getColor(attr, textColor);
            } else if (attr == R.styleable.CornerLabelView_text) {
    
    
                text = ta.getString(attr);
            } else if (attr == R.styleable.CornerLabelView_bg_color) {
    
    
                bgColor = ta.getColor(attr, bgColor);
            } else if (attr == R.styleable.CornerLabelView_margin_lean_side) {
    
    
                marginLeanSide = ta.getDimensionPixelSize(attr, marginLeanSide);
            }
        }
        ta.recycle();
    }

    private void init() {
    
    
        mPath = new Path();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(bgColor);

        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(textColor);
        mTextPaint.setTextSize(textSize);
    }
}

注意事项: 需要在res下的values中新建attrs.xml,来自定义属性

<declare-styleable name="CornerLabelView">
        <attr name="side_length" format="dimension" />
        <attr name="margin_lean_side" format="dimension" />
        <attr name="bg_color" format="color" />
        <attr name="text_color" format="color" />
        <attr name="text_size" format="dimension" />
        <attr name="text" format="string" />
        <attr name="position">
            <enum name="right_top" value="0" />
            <enum name="right_bottom" value="1" />
            <enum name="left_bottom" value="2" />
            <enum name="left_top" value="3" />
        </attr>
    </declare-styleable>

4. 测量与布局

onMeasure 方法中,我们根据控件的宽高模式来设置控件的尺寸

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

    if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    
    
        setMeasuredDimension(sideLength * 2, sideLength * 2);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
    
    
        setMeasuredDimension(heightSpecSize, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
    
    
        setMeasuredDimension(widthSpecSize, widthSpecSize);
    } else if (widthSpecSize != heightSpecSize) {
    
    
        int size = Math.min(widthSpecSize, heightSpecSize);
        setMeasuredDimension(size, size);
    }
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    
    super.onSizeChanged(w, h, oldw, oldh);
    mHalfWidth = Math.min(w, h) / 2;
}

5. 绘制角标背景

onDraw 方法中,我们首先绘制角标的背景。

@Override
protected void onDraw(Canvas canvas) {
    
    
    super.onDraw(canvas);
    // 将原点移动到画布中心
    canvas.translate(mHalfWidth, mHalfWidth);
    // 根据角标位置旋转画布
    canvas.rotate(position * 90);

    if (sideLength > mHalfWidth * 2) {
    
    
        sideLength = mHalfWidth * 2;
    }

    // 绘制角标背景
    mPath.moveTo(-mHalfWidth, -mHalfWidth);
    mPath.lineTo(sideLength - mHalfWidth, -mHalfWidth);
    mPath.lineTo(mHalfWidth, mHalfWidth - sideLength);
    mPath.lineTo(mHalfWidth, mHalfWidth);
    mPath.close();
    canvas.drawPath(mPath, mPaint);
}

6. 绘制文字

接下来,我们绘制角标中的文字

// 绘制文字前画布旋转45度
canvas.rotate(45);
// 角标实际高度
int h1 = (int) (Math.sqrt(2) / 2.0 * sideLength);
int h2 = (int) -(mTextPaint.ascent() + mTextPaint.descent());
// 文字绘制坐标
int x = (int) -mTextPaint.measureText(text) / 2;
int y;
if (marginLeanSide >= 0) {
    
     // 使用clv:margin_lean_side属性时
    if (position == 1 || position == 2) {
    
    
        if (h1 - (marginLeanSide - mTextPaint.ascent()) < (h1 - h2) / 2) {
    
    
            y = -(h1 - h2) / 2;
        } else {
    
    
            y = (int) -(h1 - (marginLeanSide - mTextPaint.ascent()));
        }
    } else {
    
    
        if (marginLeanSide < mTextPaint.descent()) {
    
    
            marginLeanSide = (int) mTextPaint.descent();
        }

        if (marginLeanSide > (h1 - h2) / 2) {
    
    
            marginLeanSide = (h1 - h2) / 2;
        }
        y = -marginLeanSide;
    }
} else {
    
     // 默认情况下
    if (sideLength > mHalfWidth) {
    
    
        sideLength = mHalfWidth;
    }
    y = (int) (-Math.sqrt(2) / 2.0 * sideLength + h2) / 2;
}

// 如果角标在右下、左下则进行画布平移、翻转,已解决绘制的文字显示问题
if (position == 1 || position == 2) {
    
    
    canvas.translate(0, (float) (-Math.sqrt(2) / 2.0 * sideLength));
    canvas.scale(-1, -1);
}
// 绘制文字
canvas.drawText(text, x, y, mTextPaint);

7. 属性设置方法

最后,我们提供了一些方法,用于动态设置控件的属性

public CornerLabelView setBgColorId(int bgColorId) {
    
    
    this.bgColor = getResources().getColor(bgColorId);
    mPaint.setColor(bgColor);
    invalidate();
    return this;
}

public CornerLabelView setBgColor(int bgColor) {
    
    
    mPaint.setColor(bgColor);
    invalidate();
    return this;
}

public CornerLabelView setTextColorId(int colorId) {
    
    
    this.textColor = getResources().getColor(colorId);
    mTextPaint.setColor(textColor);
    invalidate();
    return this;
}

public CornerLabelView setTextColor(int color) {
    
    
    mTextPaint.setColor(color);
    invalidate();
    return this;
}

public CornerLabelView setText(int textId) {
    
    
    this.text = getResources().getString(textId);
    invalidate();
    return this;
}

public CornerLabelView setText(String text) {
    
    
    this.text = text;
    invalidate();
    return this;
}

8. 总结

通过以上拆分,我们将 CornerLabelView 的实现分为了属性定义与初始化、测量与布局、绘制角标背景、绘制文字以及属性设置方法五个部分。这种拆分方式不仅使代码更易于理解和维护,也方便我们在后续的开发中进行扩展和修改。以下是整个类的全部代码实现过程

public class CornerLabelView extends View {
    
    
    private int mHalfWidth;//View宽度的一半
    private Paint mPaint;//角标画笔
    private TextPaint mTextPaint;//文字画笔
    private Path mPath;//角标路径

    private int position;//角标位置,0:右上角、1:右下角、2:左下角、3:左上角
    //角标的显示边长
    private int sideLength = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());
    //字体大小
    private int textSize = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics());
    //字体颜色
    private int textColor = Color.WHITE;
    private String text;
    //角标背景
    private int bgColor = Color.RED;
    //文字到斜边的距离
    private int marginLeanSide = -1;

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

    public CornerLabelView(Context context, AttributeSet attrs) {
    
    
        super(context, attrs);
        initAttrs(context, attrs);
        init();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
    
    
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CornerLabelView, 0, 0);
        for (int i = 0; i < ta.getIndexCount(); i++) {
    
    
            int attr = ta.getIndex(i);
            if (attr == R.styleable.CornerLabelView_position) {
    
    
                position = ta.getInt(attr, 0);
            } else if (attr == R.styleable.CornerLabelView_side_length) {
    
    
                sideLength = ta.getDimensionPixelSize(attr, sideLength);
            } else if (attr == R.styleable.CornerLabelView_text_size) {
    
    
                textSize = ta.getDimensionPixelSize(attr, textSize);
            } else if (attr == R.styleable.CornerLabelView_text_color) {
    
    
                textColor = ta.getColor(attr, textColor);
            } else if (attr == R.styleable.CornerLabelView_text) {
    
    
                text = ta.getString(attr);
            } else if (attr == R.styleable.CornerLabelView_bg_color) {
    
    
                bgColor = ta.getColor(attr, bgColor);
            } else if (attr == R.styleable.CornerLabelView_margin_lean_side) {
    
    
                marginLeanSide = ta.getDimensionPixelSize(attr, marginLeanSide);
            }
        }
        ta.recycle();
    }

    private void init() {
    
    
        mPath = new Path();

        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(bgColor);

        mTextPaint = new TextPaint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(textColor);
        mTextPaint.setTextSize(textSize);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
    
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(sideLength * 2, sideLength * 2);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(heightSpecSize, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
    
    
            setMeasuredDimension(widthSpecSize, widthSpecSize);
        } else if (widthSpecSize != heightSpecSize) {
    
    
            int size = Math.min(widthSpecSize, heightSpecSize);
            setMeasuredDimension(size, size);
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    
    
        super.onSizeChanged(w, h, oldw, oldh);
        mHalfWidth = Math.min(w, h) / 2;
    }

    @Override
    protected void onDraw(Canvas canvas) {
    
    
        super.onDraw(canvas);
        //将原点移动到画布中心
        canvas.translate(mHalfWidth, mHalfWidth);
        //根据角标位置旋转画布
        canvas.rotate(position * 90);

        if (sideLength > mHalfWidth * 2) {
    
    
            sideLength = mHalfWidth * 2;
        }

        //绘制角标背景
        mPath.moveTo(-mHalfWidth, -mHalfWidth);
        mPath.lineTo(sideLength - mHalfWidth, -mHalfWidth);
        mPath.lineTo(mHalfWidth, mHalfWidth - sideLength);
        mPath.lineTo(mHalfWidth, mHalfWidth);
        mPath.close();
        canvas.drawPath(mPath, mPaint);

        //绘制文字前画布旋转45度
        canvas.rotate(45);
        //角标实际高度
        int h1 = (int) (Math.sqrt(2) / 2.0 * sideLength);
        int h2 = (int) -(mTextPaint.ascent() + mTextPaint.descent());
        //文字绘制坐标
        int x = (int) -mTextPaint.measureText(text) / 2;
        int y;
        if (marginLeanSide >= 0) {
    
     //使用clv:margin_lean_side属性时
            if (position == 1 || position == 2) {
    
    
                if (h1 - (marginLeanSide - mTextPaint.ascent()) < (h1 - h2) / 2) {
    
    
                    y = -(h1 - h2) / 2;
                } else {
    
    
                    y = (int) -(h1 - (marginLeanSide - mTextPaint.ascent()));
                }
            } else {
    
    
                if (marginLeanSide < mTextPaint.descent()) {
    
    
                    marginLeanSide = (int) mTextPaint.descent();
                }

                if (marginLeanSide > (h1 - h2) / 2) {
    
    
                    marginLeanSide = (h1 - h2) / 2;
                }
                y = -marginLeanSide;
            }
        } else {
    
     //默认情况下
            if (sideLength > mHalfWidth) {
    
    
                sideLength = mHalfWidth;
            }
            y = (int) (-Math.sqrt(2) / 2.0 * sideLength + h2) / 2;
        }

        //如果角标在右下、左下则进行画布平移、翻转,已解决绘制的文字显示问题
        if (position == 1 || position == 2) {
    
    
            canvas.translate(0, (float) (-Math.sqrt(2) / 2.0 * sideLength));
            canvas.scale(-1, -1);
        }
        //绘制文字
        canvas.drawText(text, x, y, mTextPaint);
    }

    /**
     * 设置角标背景色
     *
     * @param bgColorId
     * @return
     */

    public CornerLabelView setBgColorId(int bgColorId) {
    
    
        this.bgColor = getResources().getColor(bgColorId);
        mPaint.setColor(bgColor);
        invalidate();
        return this;
    }

    /**
     * 设置角标背景色
     *
     * @param bgColor
     * @return
     */
    public CornerLabelView setBgColor(int bgColor) {
    
    
        mPaint.setColor(bgColor);
        invalidate();
        return this;
    }

    /**
     * 设置文字颜色
     *
     * @param colorId
     * @return
     */
    public CornerLabelView setTextColorId(int colorId) {
    
    
        this.textColor = getResources().getColor(colorId);
        mTextPaint.setColor(textColor);
        invalidate();
        return this;
    }

    /**
     * 设置文字颜色
     *
     * @param color
     * @return
     */
    public CornerLabelView setTextColor(int color) {
    
    
        mTextPaint.setColor(color);
        invalidate();
        return this;
    }

    /**
     * 设置文字
     *
     * @param textId
     * @return
     */
    public CornerLabelView setText(int textId) {
    
    
        this.text = getResources().getString(textId);
        invalidate();
        return this;
    }

    /**
     * 设置文字
     *
     * @param text
     * @return
     */
    public CornerLabelView setText(String text) {
    
    
        this.text = text;
        invalidate();
        return this;
    }
}

CornerLabelView相关方法

方法名 描述
setBgColorId(int bgColorId) 设置角标背景色(资源id)
setBgColor(int bgColor) 设置角标背景色
setTextColorId(int colorId) 设置文字颜色(资源id)
setTextColor(int color) 设置文字颜色
setText(int textId) 设置文字(资源id)
setText(String text) 设置文字

CornerLabelView的自定义属性

属性名 格式 描述 默认值
side_length dimension 角标水平显示宽度(当该值和CornerLabelView宽度相等时,角标显示为三角形) 40dp
bg_color color 角标背景色 RED
text_color color 文字颜色 WHITE
text_size dimension 文字尺寸 14sp
text string 文字
position enum 角标位置:right_top、right_bottom、left_bottom、left_top right_top
margin_lean_side dimension 文字到角最长标斜边的距离(因为有默认距离,则此属性可选) -1

9. 运行效果图

在这里插入图片描述
本文章来自开源项目:https://github.com/shehuan/CornerLabelView