Android自定义小红点BadgeView

想全局统一个小红点样式,总是改了这个忘了其他的,而且小红点格式各样,总是满足不了自己的需求,所以心血来潮自己自定义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));
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容