最近一个老项目里,在tab上有一个数量提示数字,类似于微信和QQ上的未读消息提示那样的效果,不过是用Android自己的基本控件实现的,不是太好动态刷新控制和复用,所以就想通过自定义View来实现这一功能
代码下载
其实主要工作就是画个圆,然后在圆上面画个字,说起来很简单,看看实际操作
public class MsgHintView extends View {
public MsgHintView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public MsgHintView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs);
}
}
我们知道画一个圆需要圆心坐标和半径,同时还需要设置圆的颜色;画字需要知道字的大小,颜色;为了更方便开发使用,就需要使用自定义属性;那就在values目录下新建一个attrs.xml文件,里面内容如下
在布局文件中声明这个View需要先定义命名空间
然后才可以使用自定义属性
dimens文件
11
9
接下来就是在自定义View里获取这些属性
private void init(AttributeSet attrs){
TypedArray typedArray = getResources().obtainAttributes(attrs, R.styleable.MsgHintView);
mTextColor = typedArray.getColor(R.styleable.MsgHintView_textColor, ContextCompat.getColor(getContext(), R.color.white));
mTextSize = typedArray.getInteger(R.styleable.MsgHintView_textSize, 12);
mBackgroundColor = typedArray.getColor(R.styleable.MsgHintView_backgroundColor, ContextCompat.getColor(getContext(), R.color.red));
mCircleRadius = typedArray.getInteger(R.styleable.MsgHintView_radius, 6);
}
画一个圆和字总需要画笔才能画啊,所以需要在这个方法里继续实例化两只画笔
mHintPaint = new Paint();
mHintPaint.setColor(mTextColor);
/**
* 设置以x,y点为中心,两边等分开始绘制
*/
mHintPaint.setTextAlign(Paint.Align.CENTER);
mHintPaint.setTextSize(px2Dp(mTextSize > 14 ? 14:mTextSize));
mHintPaint.setAntiAlias(true);
mCirclePaint = new Paint();
mCirclePaint.setColor(mBackgroundColor);
/**
* Paint.Style.FILL是描边
* Paint.Style.STROKE是填充
*/
mCirclePaint.setStyle(Paint.Style.FILL);
/**
* 设置画笔粗细 单位px
*/
mCirclePaint.setStrokeWidth(10);
/**
* true 抗锯齿 边界变的模糊 平滑
*/
mCirclePaint.setAntiAlias(true);
/**
* true 防抖动 变的更加平滑和饱满,图像更加清晰
*/
mCirclePaint.setDither(true);
至于Paint类的API最后会在Github给出
画笔和属性都弄好了,但是往哪画呢?肯定是画在画布上啊,但是这个画布多大呢?我们需要确定下,重写onMeasure方法,计算View宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
//从约束规范中获取模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//从约束规范中获取尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
//在布局中设置了具体值
mViewWidth = widthSize;
} else if (widthMode == MeasureSpec.AT_MOST) {
//在布局中设置 wrap_content,控件就取能完全展示内容的宽度(同时需要考虑屏幕的宽度)
mViewWidth = Math.min(mViewWidth, widthSize);
}
if (heightMode == MeasureSpec.EXACTLY) {
mViewHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST){
mViewHeight = Math.min(mViewHeight, heightSize);
}
*/
/**
* 还是不允许用户自由设置整个View的宽高
* 以圆半径确定View的大小
*/
mViewHeight = mViewWidth = (int) px2Dp(mCircleRadius * 2);
//保存测量宽度和测量高度 一定要调用该方法,否则计算无效
setMeasuredDimension(mViewWidth, mViewHeight);
}
要知道整个View其实是一个矩形,开发者在布局里设置layout_width和layout_height属性就是指定这个矩形的宽高,如果设置的宽高和圆的半径差距太大,就会导致要么画出来的圆超过了这个矩形,要么就比矩形小很多;要是根据用户在布局文件中设置的宽高来定义矩形的大小,就会给开发者比较大的负担,因为他也不知道设置多少合适,而且容错率低;所以我就不管你怎么设置宽高,我只根据你给的圆的半径来计算整个矩形的大小,这样就能保证圆的上下左右四个90度切线刚好跟矩形的四条边贴合
这里就是重写onDraw方法,首先画一个圆,这个很简单,通过Canvas的drawCircle方法,根据圆心坐标、半径、画笔就能画出一个圆
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (TextUtils.isEmpty(mHintValue)) {
return;
}
canvas.drawCircle(mViewWidth/2,mViewHeight/2,px2Dp(mCircleRadius),mCirclePaint);
}
接下来就是画字了,通过Canvas.drawText(String text, float x, float y, Paint paint)方法绘制
其中第一个参数和第四个参数我们很好理解,就是要绘制的文字和画字的画笔;但是x和y到底代表什么呢?有的人可能以为想,文字所在的区域其实也是一个矩形,那xy就是矩形的中心点;或者有的人认为是跟绘制其它View一样是所在矩形的左上角的顶点坐标
其实都不是的,一般而言,(x,y)所代表的位置是所画图形对应的矩形的左上角点,但是在drawText的时候就变得比较特殊了;要知道我们小时候练习写字的时候练习本是这样的
每个字处于四条线之内,所以在Android里利用drawText绘制文字时,也有这样一些线来定义如何绘制文字的规则,如下(图是参考他人博客,博客地址在文章底部给出):
而(x,y)中的y表示的是基线的位置,这样只要知道x坐标、基线位置(y坐标)、文字大小,那这个文字的位置就基本确定了
注意: 为什么说是基本确定而不是完全确定呢?上面我们知道y坐标代表基线的位置,那x坐标代表什么呢?往下看
Paint类有一个方法setTextAlign,它接收三个值Paint.Align.CENTER,LEFT,RIGHT;
所以这个x坐标表示的是绘制矩形的相对坐标,这个矩形可能在x坐标的右边、左边,或者x坐标将矩形长度平分
我这里设置的是CENTER,所以需要计算坐标才能让文字在这个圆里处于居中的位置
/**
* 让文字在圆里居中显示
* @return
*/
private PointF calcTextXY(){
mPoint.x = mViewWidth/2;
//基线(baseline线)y坐标 drawText方法中的y就是基线的y坐标
float baseLine = px2Dp( (mCircleRadius*2) - ((mCircleRadius*2) - mTextSize)/2 );
/**
* FontMetrics包含ascent,descent,top,bottom这些线的位置
* top = top线的y坐标 - baseline线的y坐标;
* ascent = ascent线的y坐标 - baseline线的y坐标
* descent = descent线的y坐标 - baseline线的y坐标;
* bottom = bottom线的y坐标 - baseline线的y坐标;
*/
Paint.FontMetrics fontMetrics = mHintPaint.getFontMetrics();
//当前绘制顶线(ascent线)的y坐标 - baseline线的y坐标
float ascent = fontMetrics.ascent;
//可绘制最顶线(top线)的y坐标 - baseline线的y坐标
float top = fontMetrics.top;
mPoint.y = baseLine - (ascent - top);
return mPoint;
}
然后就可以绘制了
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (TextUtils.isEmpty(mHintValue)) {
return;
}
if (mPoint == null) {
mPoint = new PointF();
calcTextXY();
}
canvas.drawCircle(mViewWidth/2,mViewHeight/2,px2Dp(mCircleRadius),mCirclePaint);
canvas.drawText(mHintValue,mPoint.x,mPoint.y,mHintPaint);
}
当然了还需要提供一个方法,实现动态刷新
public void setHintValue(int mHintValue) {
if (mHintValue >= MAX_HINT) {
this.mHintValue = "99+";
} else {
this.mHintValue = mHintValue + "";
}
invalidate();
}
当提醒数字超过99时,我们通常以99+来显示,所以就需要更大的区域绘制圆,但是如果是圆,一旦半径变大,影响美观,所以就考虑绘制椭圆,只需要更改计算绘制text的baseLine线的坐标和绘制圆的api,同时当绘制椭圆时,加大绘制区域的宽高
/**
* 让文字在圆里居中显示
* @return
*/
private PointF calcTextXY(){
mTextPoint.x = ((float) mViewWidth)/2;
//基线(baseline线)y坐标 drawText方法中的y就是基线的y坐标
float baseLine = px2Dp( (mCircleRadius*2) - ((mCircleRadius*2) - mTextSize)/2 );
/**
* FontMetrics包含ascent,descent,top,bottom这些线的位置
* top = top线的y坐标 - baseline线的y坐标;
* ascent = ascent线的y坐标 - baseline线的y坐标
* descent = descent线的y坐标 - baseline线的y坐标;
* bottom = bottom线的y坐标 - baseline线的y坐标;
*/
Paint.FontMetrics fontMetrics = mHintPaint.getFontMetrics();
//当前绘制顶线(ascent线)的y坐标 - baseline线的y坐标
float ascent = fontMetrics.ascent;
//可绘制最顶线(top线)的y坐标 - baseline线的y坐标
float top = fontMetrics.top;
if (MULTIPLE == 2) {
mTextPoint.y = baseLine - (ascent - top) - px2Dp(0.5f);
} else {
mTextPoint.y = baseLine - (ascent - top)+mOvalPoint.top;
}
return mTextPoint;
}
public void setHintValue(int mHintValue) {
int mode;
if (mHintValue > MAX_HINT) {
this.mHintValue = "99+";
mode = MORE_THAN_100;
MULTIPLE = 2.5f;
mCirclePaint.setColor(mMorebackgroundColor);
} else {
mode = LESS_THAN_100;
this.mHintValue = mHintValue + "";
MULTIPLE = 2.0f;
mCirclePaint.setColor(mBackgroundColor);
}
if (lastMode != mode) {
requestLayout();
} else {
invalidate();
}
lastMode = mode;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (TextUtils.isEmpty(mHintValue)) {
return;
}
if (mTextPoint == null) {
mTextPoint = new PointF();
}
if (mOvalPoint == null) {
mOvalPoint = new RectF();
mOvalPoint.left = 0;
mOvalPoint.top = px2Dp(2);
mOvalPoint.right = mViewWidth;
mOvalPoint.bottom = mViewHeight - mOvalPoint.top;
}
calcTextXY();
if (mHintValue.equals("99+")) {
canvas.drawOval(mOvalPoint,mCirclePaint);
} else {
canvas.drawCircle(circleX,circleY,px2Dp(mCircleRadius),mCirclePaint);
}
canvas.drawText(mHintValue, mTextPoint.x, mTextPoint.y,mHintPaint);
}
参考文章:https://blog.csdn.net/harvic880925/article/details/50423762