想全局統一個小紅點樣式,總是改了這個忘了其他的,而且小紅點格式各樣,總是滿足不了自己的需求,所以心血來潮自己自定義View onDraw了一個。
可前往查看GitHub源碼.
效果就是這樣....
BadgeView_preview.gif
一定要記得在attrs.xml
項目中添加
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="BadgeView">
<attr name="iconSrc" format="reference"/>
<attr name="iconWidth" format="dimension"/>
<attr name="iconHeight" format="dimension"/>
<!--若是icon是正方形的,可直接設置這個參數-->
<attr name="iconSize" format="dimension"/>
<attr name="text" format="string"/>
<attr name="textSize" format="dimension"/>
<attr name="textColor" format="color"/>
<attr name="badgeNum" format="integer"/>
<!--是否顯示數字, 為false時只顯示小紅點; 沒有數字時,小紅點的大小通過badgeSize設置-->
<attr name="showNum" format="boolean"/>
<attr name="badgeBackgroundColor" format="color"/>
<!--限制設置小紅點的大小不能超過數字顯示模式(代碼中也做了限制); 顯示在文字模式大小的左下角;-->
<!-- 不顯示數字時, 小紅點的大小, 不包括邊線-->
<attr name="badgeRedSize" format="dimension"/>
<attr name="badgeNumSize" format="dimension"/>
<attr name="badgeNumColor" format="color"/>
<!--若小紅點有邊緣線,加上邊緣線-->
<attr name="badgeBorderColor" format="color"/>
<attr name="badgeBorderWidth" format="dimension"/>
<!--badge相對于主體右上角的相對位置, 重疊的部分的大小; 可以設置負值-->
<!--默認是( badgeHeight/2 ), 正好覆蓋一個角-->
<attr name="badgeBottom" format="dimension"/>
<attr name="badgeLeft" format="dimension"/>
<!-- 有些設計要求未讀前面加"+", (至少我們設計師這么設計) 顯示成 +1/+34/+99-->
<attr name="badgeNumPre" format="string"/>
</declare-styleable>
</resources>
主要代碼如下:
public class BadgeView extends View {
////可設置部分 start///////////////////////////////////////////////
// 主體部分的設置 icon
private int iconSrc;
private float iconWidth;
private float iconHeight;
// 沒有icon 就是文字描述了; icon的優先級比text高
private String text;
private int textColor;
private float textSize;
// 未讀數; 在顯示的時候 未讀數默認顯示形式9/23/99+
private int badgeNum;
private int badgeBackgroundColor;
private int badgeNumColor;
private float badgeNumSize;
// 是否顯示數字, 默認顯示小紅點
private boolean showNum;
// 不顯示數字時, 小紅點的大小, 不包括邊線
private float badgeRedSize;
// 邊線, 有些小紅點外邊有白邊, 若是設置了寬度,則會添加邊線; 邊線算在Badge整個的大小當中
private float badgeBorderWidth;
private int badgeBorderColor;
// 有些設計要求未讀前面加"+", (至少我們設計師這么設計) 顯示成 +1/+34/+99
private String badgeNumPre;
// badge的左下角 相對于 text/icon 右上角的相對位置,
// 默認是( badgeHeight/2 ), 正好覆蓋一個角
private float badgeBottom;
private float badgeLeft;
// 是否自己設置了
private boolean hasBadgeBottomAttr;
private boolean hasBadgeLeftAttr;
// view設置的padding
private float viewPaddingLeft;
private float viewPaddingTop;
private float viewPaddingRight;
private float viewPaddingBootom;
////可設置部分 end///////////////////////////////////////////////
// 小紅點真實大小 比 文本 的margin(不包括白邊)
private static final int BADGE_TEXT_MARGIN_LEFT = 10;
private static final int BADGE_TEXT_MARGIN_TOP = 6;
private static final int BADGE_TEXT_MARGIN_RIGHT = 10;
private static final int BADGE_TEXT_MARGIN_BOOTOM = 6;
// 可以設置padding
private static final int VIEW_PADDING = 0;
////以下是輔助變量///////////////////////////////////////////////
// 整個View的真實大小
private float viewHeight;
private float viewWidth;
// 內容所占的大小, 內容居中
private float viewMinHeight;
private float viewMinWidth;
// 小紅點有向右突出部分,為保證主體部分水平居中, 需要設置兩邊的margin
private float mainMarginHorizontal;
// 小紅點有向上突出部分,就算沒有未讀數,也需要預留出位置, 設置Top即可
private float mainMarginTop;
// 描述文字或者icon的寬高
private float mainWidth;
private float mainHeight;
// badge的整體寬高
private float badgeHeight;
private float badgeWidth;
// badgeNum/小紅點 的真實寬高
private float badgeNumHeight;
private float badgeNumWidth;
// icon
private Bitmap iconBitmap;
// 未讀數顯示的文案; 未讀數默認顯示形式9/23/99+
private String showUneadText;
// 畫筆
private Paint contentPaint;
private TextPaint textPaint;
private TextPaint badgeNumPaint;
public BadgeView(Context context) {
this(context, null);
}
public BadgeView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public BadgeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BadgeView);
iconSrc = array.getResourceId(R.styleable.BadgeView_iconSrc, 0);
float iconSize = array.getDimension(R.styleable.BadgeView_iconSize, dip2px(30));
iconWidth = array.getDimension(R.styleable.BadgeView_iconWidth, iconSize);
iconHeight = array.getDimension(R.styleable.BadgeView_iconHeight, iconSize);
text = array.getString(R.styleable.BadgeView_text);
if (TextUtils.isEmpty(text)) {
text = "Hello World";
}
textColor = array.getColor(R.styleable.BadgeView_textColor, Color.BLACK);
textSize = array.getDimension(R.styleable.BadgeView_textSize, sp2px(16));
badgeNum = array.getInteger(R.styleable.BadgeView_badgeNum, 0);
badgeBackgroundColor = array.getColor(R.styleable.BadgeView_badgeBackgroundColor, Color.rgb(0xFF, 0x76, 0x90));
badgeNumColor = array.getColor(R.styleable.BadgeView_badgeNumColor, Color.WHITE);
badgeNumSize = array.getDimension(R.styleable.BadgeView_badgeNumSize, sp2px(10));
badgeNumSize = array.getDimension(R.styleable.BadgeView_badgeNumSize, sp2px(10));
showNum = array.getBoolean(R.styleable.BadgeView_showNum, true);
badgeRedSize = array.getDimension(R.styleable.BadgeView_badgeRedSize, dip2px(8));
badgeBorderColor = array.getColor(R.styleable.BadgeView_badgeBorderColor, Color.WHITE);
badgeBorderWidth = array.getDimension(R.styleable.BadgeView_badgeBorderWidth, 0);
if (badgeBorderWidth < 0) {
badgeBorderWidth = 0;
}
badgeNumPre = array.getString(R.styleable.BadgeView_badgeNumPre);
// 初始化badgeNum的畫筆
badgeNumPaint = new TextPaint();
badgeNumPaint.setAntiAlias(true);
badgeNumPaint.setColor(badgeNumColor);
badgeNumPaint.setTextSize(badgeNumSize);
badgeNumPaint.setTextAlign(Paint.Align.CENTER);
// 計算 未讀數的高度
String minBadge = getUnreadText(0);
Rect minBadgeRect = new Rect();
badgeNumPaint.getTextBounds(minBadge, 0, minBadge.length(), minBadgeRect);
// 計算badge的高度
badgeNumHeight = minBadgeRect.height();
badgeHeight = badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM + badgeBorderWidth * 2;
// 限制設置小紅點的大小不能超過數字顯示模式; 顯示在文字模式大小的左下角
if (badgeRedSize > badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM) {
badgeRedSize = badgeNumHeight + BADGE_TEXT_MARGIN_TOP + BADGE_TEXT_MARGIN_BOOTOM;
}
// 獲取位置
hasBadgeBottomAttr = array.hasValue(R.styleable.BadgeView_badgeBottom);
hasBadgeLeftAttr = array.hasValue(R.styleable.BadgeView_badgeLeft);
badgeBottom = array.getDimension(R.styleable.BadgeView_badgeBottom, 0);
badgeLeft = array.getDimension(R.styleable.BadgeView_badgeLeft, 0);
//關閉清空TypedArray
array.recycle();
// 初始化主體文字描述的畫筆
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
textPaint.setTextAlign(Paint.Align.CENTER);
contentPaint = new Paint();
contentPaint.setAntiAlias(true);
}
public void setBadgeNum(int badgeNum) {
this.badgeNum = badgeNum;
}
public void setShowNum(boolean isShow) {
this.showNum = isShow;
}
public void setIconSrc(int res) {
this.iconSrc = res;
}
public void setBadgeLocation(float bottom, float left) {
this.badgeBottom = bottom;
this.badgeLeft = left;
hasBadgeBottomAttr = true;
hasBadgeLeftAttr = true;
}
/**
* 重新計算繪制這個View
*/
public void redraw() {
// 需要重新計算高寬,所以用這個
requestLayout();
// invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (viewWidth != viewMinWidth || viewHeight != viewMinHeight) {
canvas.save();
// 若是設置的高寬大于所需要的高寬, 對畫布進行操作
float paddingLeft = viewPaddingLeft + (viewWidth - viewPaddingLeft - viewPaddingRight - viewMinWidth) / 2;
float paddingTop = viewPaddingTop + (viewHeight -viewPaddingTop -viewPaddingBootom - viewMinHeight) / 2;
// 移動布局, 改變原點
canvas.translate(paddingLeft, paddingTop);
}
onDrawContent(canvas);
if (viewWidth != viewMinWidth || viewHeight != viewMinHeight) {
canvas.restore();
}
}
/**
* 繪制整個內容
* @param canvas
*/
private void onDrawContent(Canvas canvas) {
if (iconSrc != 0) {
// 畫icon
canvas.drawBitmap(iconBitmap, mainMarginHorizontal + (mainWidth - iconWidth) / 2, mainMarginTop + (mainHeight - iconHeight) / 2, contentPaint);
} else {
// 寫text, 文字是居中的
canvas.drawText(text, viewMinWidth / 2, viewMinHeight, textPaint);
}
if (badgeNum > 0) {
canvas.save();
// 移動布局, 改變原點
canvas.translate(viewMinWidth - badgeWidth, 0);
oDrawBadge(canvas);
canvas.restore();
}
}
private void oDrawBadge(Canvas canvas) {
// 若有小紅點有邊緣線, 畫邊緣線
if (badgeBorderWidth > 0) {
contentPaint.setStyle(Paint.Style.STROKE);
contentPaint.setColor(badgeBorderColor);
contentPaint.setStrokeWidth(badgeBorderWidth);
if (!showNum) {
// 不顯示數字
canvas.drawCircle(badgeWidth / 2, badgeHeight - badgeRedSize / 2 - badgeBorderWidth, badgeRedSize / 2, contentPaint);
} else if (badgeWidth == badgeHeight) {
// 顯示是字符串長度為1時, 為正圓
canvas.drawCircle(badgeWidth / 2, badgeHeight / 2, badgeWidth / 2, contentPaint);
} else {
// 橢圓
Path borderPath = new Path();
borderPath.addArc(new RectF(0, 0, badgeHeight, badgeHeight), 90, 180);
borderPath.lineTo(badgeWidth - badgeHeight / 2, 0);
borderPath.addArc(new RectF(badgeWidth - badgeHeight, 0, badgeWidth, badgeHeight), 270, 180);
borderPath.lineTo(badgeHeight / 2, badgeHeight);
canvas.drawPath(borderPath, contentPaint);
}
}
contentPaint.setColor(badgeBackgroundColor);
contentPaint.setStyle(Paint.Style.FILL);
if (showNum) {
// 繪制紅色背景圖
Path path = new Path();
path.addArc(new RectF(badgeBorderWidth, badgeBorderWidth, badgeHeight - badgeBorderWidth, badgeHeight - badgeBorderWidth), 90, 180);
path.lineTo(badgeWidth - badgeHeight / 2 + badgeBorderWidth, badgeBorderWidth);
path.addArc(new RectF(badgeWidth - badgeHeight + badgeBorderWidth, badgeBorderWidth, badgeWidth - badgeBorderWidth, badgeHeight - badgeBorderWidth), 270, 180);
path.lineTo(badgeHeight / 2 - badgeBorderWidth, badgeHeight - badgeBorderWidth);
canvas.drawPath(path, contentPaint);
// 寫上數字
canvas.drawText(showUneadText, badgeWidth / 2, badgeHeight - BADGE_TEXT_MARGIN_BOOTOM - badgeBorderWidth, badgeNumPaint);
} else {
// 畫實心圓
canvas.drawCircle(badgeRedSize / 2 + badgeBorderWidth, badgeHeight - badgeRedSize / 2 - badgeBorderWidth, badgeRedSize / 2, contentPaint);
}
}
private void intParams() {
// 初始化主體的一些數據
if (iconSrc != 0) {
mainHeight = iconHeight;
mainWidth = iconWidth;
if (iconBitmap == null) {
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), iconSrc);
// 縮放圖片
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 保證icon的scaleType="fitCenter"
// 獲取圖片的長邊
float length = width > height ? width : height;
// 獲取外框的最小邊
float size = iconWidth > iconHeight ? iconHeight : iconWidth;
// 讓圖片按照長邊進行縮放
float scale = size / length;
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
iconBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
}
// 因為icon是fitCenter, 所以有真實大小
iconWidth = iconBitmap.getWidth();
iconHeight = iconBitmap.getHeight();
} else {
// 字符描述文字的大小
Rect descRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), descRect);
mainWidth = descRect.width();
mainHeight = descRect.height();
}
// 初始化Badge的數據
if (showNum) {
showUneadText = getUnreadText(badgeNum);
Rect badgeRect = new Rect();
badgeNumPaint.getTextBounds(showUneadText, 0, showUneadText.length(), badgeRect);
badgeNumWidth = badgeRect.width();
if (showUneadText.length() == 1) {
// 當長度為1的時候,顯示正圓
badgeWidth = badgeHeight;
} else {
badgeWidth = badgeNumWidth + BADGE_TEXT_MARGIN_LEFT + BADGE_TEXT_MARGIN_RIGHT + badgeBorderWidth * 2;
}
} else {
badgeWidth = badgeRedSize + badgeBorderWidth * 2;
}
// badgeHeight在構造方法中初始化了, 全部使用數字模式的高度
// Badge位置設置的范圍做一個限制
if (!hasBadgeLeftAttr || badgeLeft > mainWidth) {
badgeLeft = getBadgeDefaultLocation();
}
if (!hasBadgeBottomAttr || badgeBottom > mainHeight) {
badgeBottom = getBadgeDefaultLocation();
}
// 計算整體內容的大小
mainMarginHorizontal = badgeWidth - badgeLeft;
mainMarginTop = badgeHeight - badgeBottom;
viewMinWidth = mainWidth + mainMarginHorizontal * 2;
viewMinHeight = mainHeight + mainMarginTop;
}
/**
* 獲取默認的位置
* @return
*/
private float getBadgeDefaultLocation() {
// 文字的時候默認往上些, 蓋住文字了
return iconSrc != 0 ? (showNum ? badgeHeight / 2 : badgeRedSize / 2 + badgeBorderWidth) : badgeRedSize / 2 + badgeBorderWidth - 3;
}
/**
* 構造未讀數顯示的文本
* 1) 未讀數默認顯示形式9/23/99+
* 2) 有些設計要求未讀前面加"+", (至少我們設計師這么設計) 顯示成 +1/+34/+99, 取配置badgeNumPre
* @param unread
* @return
*/
private String getUnreadText(int unread) {
String text = String.valueOf(unread);
if (TextUtils.isEmpty(badgeNumPre)) {
if (unread > 99) {
text = "99+";
}
} else {
if (unread > 99) {
text = badgeNumPre + "99";
} else if (unread >= 0) {
text = badgeNumPre + unread;
}
}
return text;
}
private int dip2px(int dpValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, getResources().getDisplayMetrics());
}
private int sp2px(int spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getResources().getDisplayMetrics());
}
/**
* android-自定義View解決wrap_content無效的問題
* see https://my.oschina.net/ccqy66/blog/616662
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 計算高寬
intParams();
viewPaddingLeft = getPaddingLeft();
viewPaddingTop = getPaddingTop();
viewPaddingRight = getPaddingRight();
viewPaddingBootom = getPaddingBottom();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//Measure Width
if (widthMode == MeasureSpec.EXACTLY) {
// 設置的大小不能比內容還小
viewWidth = widthSize < viewMinWidth ? viewMinWidth : widthSize;
} else {
viewWidth = viewMinWidth;
}
viewWidth += viewPaddingLeft + viewPaddingRight;
//Measure Height
if (heightMode == MeasureSpec.EXACTLY) {
// 設置的大小不能比內容還小
viewHeight = heightSize < viewMinHeight ? viewMinHeight : heightSize;
} else {
viewHeight = viewMinHeight;
}
if (viewHeight < viewMinHeight + VIEW_PADDING * 2) {
viewHeight = viewMinHeight + VIEW_PADDING * 2;
}
viewHeight += viewPaddingTop + viewPaddingBootom;
//MUST CALL THIS
setMeasuredDimension((int) Math.ceil(viewWidth), (int) Math.ceil(viewHeight));
}
}