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閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容