先上效果图
自定义view
/**
* 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);
}
}
使用
<?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 中加载
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);
}