自定义view, 图片右上角显示数字

先上效果图

自定义view

复制代码
arduino 复制代码
/**
 * Description : java类作用描述
 *

 * @since : 2026/1/7
 */


import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.ImageView;

/**
 * 实现在图片右上角显示消息条数(无需设置padding)
 */
@SuppressLint("AppCompatCustomView")
public class PointImageView extends ImageView {

    /**
     * 默认模式
     */
    private int pointMode = NUMBER_POINT;

    // 1.不显示红点
    public static final int NO_POINT = 1;
    // 2.只显示一个红点,表示有新消息
    public static final int ONLY_POINT = 2;
    // 3.显示一个红点,红点中间还有消息的数量
    public static final int NUMBER_POINT = 3;

    // 消息的数量
    private int number = 0;

    // 记录当前是否有新消息(自动根据number判定)
    private boolean isHaveMessage = false;

    /**
     * 画圆的画笔
     */
    private Paint paint;

    /**
     * 画消息条数的画笔
     */
    private TextPaint paintText;

    // 角标半径(固定15dp,无需依赖padding)
    private float badgeRadius;
    // 文字大小(固定12sp)
    private float textSize;

    public PointImageView(Context context) {
        super(context);
        init(context);
    }

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

    /**
     * 初始化数据(适配dp/sp,移除padding依赖)
     */
    private void init(Context context) {
        // 转换尺寸:dp转px、sp转px(适配不同设备)
        badgeRadius = dp2px(context, 30); // 角标半径15dp(核心:固定大小,不依赖padding)
        textSize = sp2px(context, 30);    // 文字大小12sp

        // 初始化红点画笔
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL); // 实心
        paint.setColor(0xffff0000);       // 红色
        paint.setAntiAlias(true);         // 抗锯齿

        // 初始化文字画笔
        paintText = new TextPaint();
        paintText.setColor(0xffffffff);   // 白色
        paintText.setTextSize(textSize);  // 设置文字大小
        paintText.setAntiAlias(true);
        paintText.setTextAlign(Paint.Align.CENTER); // 文字水平居中(简化计算)
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 无消息则不绘制
        if (!isHaveMessage) {
            return;
        }

        // 计算角标中心坐标:View右上角向内偏移radius(避免超出View)
        float badgeCenterX = getWidth() - badgeRadius;  // 右边缘 - 半径
        float badgeCenterY = badgeRadius;               // 上边缘 + 半径

        switch (pointMode) {
            case NO_POINT: // 不显示红点
                break;
            case ONLY_POINT: // 只显示红点
                canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, paint);
                break;
            case NUMBER_POINT: // 显示红点且带消息条数
                // 1. 绘制红色圆点
                canvas.drawCircle(badgeCenterX, badgeCenterY, badgeRadius, paint);
                // 2. 处理显示的文字
                String showText;
                if (number > 0 && number < 100) {
                    showText = String.valueOf(number);
                } else if (number >= 100) {
                    showText = "99+"; // 优化显示:99+ 更符合通用设计
                } else {
                    showText = "";
                }
                // 3. 计算文字垂直居中的基准线(核心:避免文字偏移)
                FontMetrics fm = paintText.getFontMetrics();
                // 垂直居中公式:中心Y坐标 - (字体上边界+下边界)/2
                float textBaseline = badgeCenterY - (fm.top + fm.bottom) / 2;
                // 4. 绘制文字(水平居中+垂直居中)
                canvas.drawText(showText, badgeCenterX, textBaseline, paintText);
                break;
        }
    }

    /**
     * 设置消息条数(自动判定是否有消息,无需手动调用setHaveMessage)
     */
    public void setMessageNum(int number) {
        this.number = number;
        // 自动更新:数字>0则显示红点/数字,否则不显示
        this.isHaveMessage = number > 0;
        invalidate(); // 触发重绘
    }

    /**
     * 手动控制是否显示(可选,优先用setMessageNum自动控制)
     */
    public void setHaveMessage(boolean isHaveMessage) {
        this.isHaveMessage = isHaveMessage;
        invalidate();
    }

    /**
     * 设置显示模式
     */
    public void setPointMode(int mode) {
        if (mode > 0 && mode <= 3) {
            pointMode = mode;
        } else {
            throw new RuntimeException("设置的模式有误,仅支持1-3");
        }
        invalidate(); // 模式变化后重绘
    }

    // 工具方法:dp转px
    private float dp2px(Context context, float dp) {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP,
                dp,
                context.getResources().getDisplayMetrics()
        );
    }

    // 工具方法:sp转px
    private float sp2px(Context context, float sp) {
        return TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_SP,
                sp,
                context.getResources().getDisplayMetrics()
        );
    }

    // 兼容旧方法名(避免调用方报错)
    @Deprecated
    public void setHaveMesage(boolean isHaveMesage) {
        setHaveMessage(isHaveMesage);
    }
}

使用

ini 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <com.amap.apis.cluster.testview.PointImageView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/test"
        android:id="@+id/imageView" />

</LinearLayout>

在Activity 中加载

scss 复制代码
private PointImageView mPointImageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_customer_view_main);
    mPointImageView = findViewById(R.id.imageView);
    mPointImageView.setImageResource(R.drawable.test);
    mPointImageView.setMessageNum(10);
    mPointImageView.setPointMode(3);

    int sizeDp = 200;
    int sizePx = dp2px(sizeDp); // dp 转 px
    ViewGroup.LayoutParams params = new LinearLayout.LayoutParams(sizePx, sizePx);
    mPointImageView.setLayoutParams(params);
    mPointImageView.setPadding(50,50,50,50);

}
private int dp2px(float dp) {
    float density = getResources().getDisplayMetrics().density;
    return (int) (dp * density + 0.5f);
}
相关推荐
千里马学框架6 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台6 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone6 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc6 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo6 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077006 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼6 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone6 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen6 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone6 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui