Android自定义View-在Tab上添加红点消息提示数字 动态刷新切换显示椭圆和圆

背景

最近一个老项目里,在tab上有一个数量提示数字,类似于微信和QQ上的未读消息提示那样的效果,不过是用Android自己的基本控件实现的,不是太好动态刷新控制和复用,所以就想通过自定义View来实现这一功能

代码下载

实现未读消息提示

其实主要工作就是画个圆,然后在圆上面画个字,说起来很简单,看看实际操作

第一步:定义MsgHintView继承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文件,里面内容如下

<declare-styleable name="MsgHintView">
    <!--提示字颜色-->
    <attr name="textColor" format="color"/>
    <!--提示字大小-->
    <attr name="textSize" format="integer"/>
    <!--圆背景色-->
    <attr name="backgroundColor" format="color"/>
    <!--圆半径-->
    <attr name="radius" format="integer" />
</declare-styleable>

在布局文件中声明这个View需要先定义命名空间
在这里插入图片描述
然后才可以使用自定义属性

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:mango="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white">

    <RelativeLayout
        android:layout_centerInParent="true"
        android:layout_width="100dp"
        android:layout_height="50dp">

        <TextView
            android:id="@+id/bt_encrypt"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/yellow"
            android:gravity="center"
            android:text="我的"/>

        <com.mango.view.MsgHintView
            android:id="@+id/tv_hint"
            android:layout_marginTop="9dp"
            android:layout_marginRight="17dp"
            android:layout_alignParentRight="true"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            mango:textColor="@color/white"
            mango:textSize="@integer/hint_size"
            mango:backgroundColor="@color/red"
            mango:radius="@integer/hint_circle_radius"/>

    </RelativeLayout>



</RelativeLayout>


dimens文件

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="hint_size">11</integer>
    <integer name="hint_circle_radius">9</integer>
</resources>

第三步:获取自定义属性

接下来就是在自定义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给出

第四步:计算整个View的大小

画笔和属性都弄好了,但是往哪画呢?肯定是画在画布上啊,但是这个画布多大呢?我们需要确定下,重写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;

  • 如果设置LEFT,这也是默认设置,那么绘制文字所在矩形就从x坐标开始往右绘制,也就是这个矩形在x坐标的右侧,或者说矩形的四条边的左边的x坐标就是上面说的x坐标,自然文字也就绘制在x坐标的右边,如图
    在这里插入图片描述
  • 如果设置RIGHT,那么绘制文字所在矩形就从x坐标开始往左绘制,也就是这个矩形在x坐标的左侧,自然文字也就绘制在x坐标的左边
    在这里插入图片描述
  • 如果设置CENTER,这个就比较好理解了,x处于矩形水平方向的中间位置,如图
    在这里插入图片描述

所以这个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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值