/* * Copyright 2017. nekocode (nekocode.cn@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cn.nekocode.badge; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.SpannableString; import android.text.Spanned; import android.text.style.ImageSpan; /** * @author nekocode (nekocode.cn@gmail.com) */ public class BadgeDrawable extends Drawable { public static final int TYPE_NUMBER = 1; public static final int TYPE_ONLY_ONE_TEXT = 1 << 1; public static final int TYPE_WITH_TWO_TEXT = 1 << 2; public static final int TYPE_WITH_TWO_TEXT_COMPLEMENTARY = 1 << 3; @IntDef({TYPE_NUMBER, TYPE_ONLY_ONE_TEXT, TYPE_WITH_TWO_TEXT, TYPE_WITH_TWO_TEXT_COMPLEMENTARY}) public @interface BadgeType {} private static class Config { private int badgeType = TYPE_NUMBER; private int number = 0; private String text1 = ""; private String text2 = ""; private float textSize = spToPixels(12); private int badgeColor = 0xffCC3333; private int textColor = 0xffFFFFFF; private Typeface typeface = Typeface.DEFAULT_BOLD; private float cornerRadius = dipToPixels(2); private float paddingLeft = dipToPixels(2); private float paddingTop = dipToPixels(2); private float paddingRight = dipToPixels(2); private float paddingBottom = dipToPixels(2); private float paddingCenter = dipToPixels(3); private int storkeWidth = (int) dipToPixels(1); } private Config config; private ShapeDrawable backgroundDrawable; private ShapeDrawable backgroundDrawableOfText2; private ShapeDrawable backgroundDrawableOfText1; private int badgeWidth; private int badgeHeight; private float[] outerR = new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f}; private float[] outerROfText1 = new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f}; private float[] outerROfText2 = new float[]{0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f}; private Paint paint; private Paint.FontMetrics fontMetrics; private int text1Width, text2Width; public static class Builder { private Config config; public Builder() { config = new Config(); } public Builder type(@BadgeType int type) { config.badgeType = type; return this; } public Builder number(int number) { config.number = number; return this; } public Builder text1(String text1) { config.text1 = text1; return this; } public Builder text2(String text2) { config.text2 = text2; return this; } public Builder textSize(float size) { config.textSize = size; return this; } public Builder badgeColor(int color) { config.badgeColor = color; return this; } public Builder textColor(int color) { config.textColor = color; return this; } public Builder typeFace(Typeface typeface) { config.typeface = typeface; return this; } public Builder cornerRadius(float radius) { config.cornerRadius = radius; return this; } public Builder padding(float l, float t, float r, float b, float c) { config.paddingLeft = l; config.paddingTop = t; config.paddingRight = r; config.paddingBottom = b; config.paddingCenter = c; return this; } public Builder strokeWidth(int width) { config.storkeWidth = width; return this; } public BadgeDrawable build() { return new BadgeDrawable(config); } } private BadgeDrawable(Config config) { paint = new Paint(); paint.setAntiAlias(true); paint.setTypeface(config.typeface); paint.setTextAlign(Paint.Align.CENTER); paint.setStyle(Paint.Style.FILL); paint.setAlpha(255); this.config = config; setCornerRadius(config.cornerRadius); RoundRectShape shape = new RoundRectShape(outerR, null, null); backgroundDrawable = new ShapeDrawable(shape); shape = new RoundRectShape(outerROfText1, null, null); backgroundDrawableOfText1 = new ShapeDrawable(shape); shape = new RoundRectShape(outerROfText2, null, null); backgroundDrawableOfText2 = new ShapeDrawable(shape); setTextSize(config.textSize); measureBadge(); } public void setBadgeType(@BadgeType int type) { config.badgeType = type; measureBadge(); } public void setNumber(int number) { config.number = number; } public int getNumber() { return config.number; } public void setText1(String text1) { config.text1 = text1; measureBadge(); } public String getText1() { return config.text1; } public void setText2(String text2) { config.text2 = text2; measureBadge(); } public String getText2() { return config.text2; } public void setTextSize(float textSize) { config.textSize = textSize; paint.setTextSize(textSize); fontMetrics = paint.getFontMetrics(); measureBadge(); } public float getTextSize() { return config.textSize; } public void setBadgeColor(int color) { config.badgeColor = color; } public int getBadgeColor() { return config.badgeColor; } public void setTextColor(int color) { config.textColor = color; } public int getTextColor() { return config.textColor; } public void setCornerRadius(float radius) { config.cornerRadius = radius; outerR[0] = outerR[1] = outerR[2] = outerR[3] = outerR[4] = outerR[5] = outerR[6] = outerR[7] = radius; outerROfText1[0] = outerROfText1[1] = outerROfText1[6] = outerROfText1[7] = radius; outerROfText1[2] = outerROfText1[3] = outerROfText1[4] = outerROfText1[5] = 0f; outerROfText2[0] = outerROfText2[1] = outerROfText2[6] = outerROfText2[7] = 0f; outerROfText2[2] = outerROfText2[3] = outerROfText2[4] = outerROfText2[5] = radius; } public void setPadding(float l, float t, float r, float b, float c) { config.paddingLeft = l; config.paddingTop = t; config.paddingRight = r; config.paddingBottom = b; config.paddingCenter = c; measureBadge(); } public void setStrokeWidth(int width) { config.storkeWidth = width; } private void measureBadge() { badgeHeight = (int) (config.textSize + config.paddingTop + config.paddingBottom); switch (config.badgeType) { case TYPE_ONLY_ONE_TEXT: text1Width = (int) paint.measureText(config.text1); badgeWidth = (int) (text1Width + config.paddingLeft + config.paddingRight); setCornerRadius(config.cornerRadius); break; case TYPE_WITH_TWO_TEXT: text1Width = (int) paint.measureText(config.text1); text2Width = (int) paint.measureText(config.text2); badgeWidth = (int) (text1Width + text2Width + config.paddingLeft + config.paddingRight + config.paddingCenter); setCornerRadius(config.cornerRadius); break; case TYPE_WITH_TWO_TEXT_COMPLEMENTARY: text1Width = (int) paint.measureText(config.text1); text2Width = (int) paint.measureText(config.text2); badgeWidth = (int) (text1Width + text2Width + config.paddingLeft + config.paddingRight + config.paddingCenter); setCornerRadius(config.cornerRadius); break; default: badgeWidth = (int) (config.textSize + config.paddingLeft + config.paddingRight); setCornerRadius(badgeHeight); } int boundsWidth = getBounds().width(); if (boundsWidth > 0) { // If the bounds has been set, adjust the badge size switch (config.badgeType) { case TYPE_ONLY_ONE_TEXT: if(boundsWidth < badgeWidth) { text1Width = (int) (boundsWidth - config.paddingLeft - config.paddingRight); text1Width = text1Width > 0 ? text1Width : 0; badgeWidth = boundsWidth; } break; case TYPE_WITH_TWO_TEXT: case TYPE_WITH_TWO_TEXT_COMPLEMENTARY: if(boundsWidth < badgeWidth) { if (boundsWidth < (text1Width + config.paddingLeft + config.paddingRight)) { text1Width = (int) (boundsWidth - config.paddingLeft - config.paddingRight); text1Width = text1Width > 0 ? text1Width : 0; text2Width = 0; } else { text2Width = (int) (boundsWidth - text1Width - config.paddingLeft - config.paddingRight - config.paddingCenter); text2Width = text2Width > 0 ? text2Width : 0; } badgeWidth = boundsWidth; } break; } } } @Override public void setBounds(int left, int top, int right, int bottom) { super.setBounds(left, top, right, bottom); measureBadge(); } @Override public void draw(@NonNull Canvas canvas) { Rect bounds = getBounds(); int marginTopAndBottom = (int) ((bounds.height() - badgeHeight) / 2f); int marginLeftAndRight = (int) ((bounds.width() - badgeWidth) / 2f); backgroundDrawable.setBounds( bounds.left + marginLeftAndRight, bounds.top + marginTopAndBottom, bounds.right - marginLeftAndRight, bounds.bottom - marginTopAndBottom); backgroundDrawable.getPaint().setColor(config.badgeColor); backgroundDrawable.draw(canvas); float textCx = bounds.centerX(); float textCy = bounds.centerY() - (fontMetrics.bottom + fontMetrics.top) / 2f; switch (config.badgeType) { case TYPE_ONLY_ONE_TEXT: paint.setColor(config.textColor); canvas.drawText( cutText(config.text1, text1Width), textCx, textCy, paint); break; case TYPE_WITH_TWO_TEXT_COMPLEMENTARY: paint.setColor(config.textColor); canvas.drawText( config.text1, marginLeftAndRight + config.paddingLeft + text1Width / 2f, textCy, paint); backgroundDrawableOfText2.setBounds( (int) (bounds.left + marginLeftAndRight + config.paddingLeft + text1Width + config.paddingCenter / 2f), bounds.top + marginTopAndBottom+ config.storkeWidth, bounds.width() - marginLeftAndRight - config.storkeWidth, bounds.bottom - marginTopAndBottom - config.storkeWidth); backgroundDrawableOfText2.getPaint().setColor(config.textColor); backgroundDrawableOfText2.draw(canvas); paint.setColor(config.badgeColor); canvas.drawText( cutText(config.text2, text2Width), bounds.width() - marginLeftAndRight - config.paddingRight - text2Width / 2f, textCy, paint); break; case TYPE_WITH_TWO_TEXT: backgroundDrawableOfText1.setBounds( bounds.left + marginLeftAndRight + config.storkeWidth, bounds.top + marginTopAndBottom+ config.storkeWidth, (int) (bounds.left + marginLeftAndRight + config.paddingLeft + text1Width + config.paddingCenter / 2f - config.storkeWidth / 2f), bounds.bottom - marginTopAndBottom - config.storkeWidth); backgroundDrawableOfText1.getPaint().setColor(0xffFFFFFF); backgroundDrawableOfText1.draw(canvas); paint.setColor(config.badgeColor); canvas.drawText( config.text1, text1Width / 2f + marginLeftAndRight + config.paddingLeft, textCy, paint); backgroundDrawableOfText2.setBounds( (int) (bounds.left + marginLeftAndRight + config.paddingLeft + text1Width + config.paddingCenter / 2f + config.storkeWidth / 2f), bounds.top + marginTopAndBottom + config.storkeWidth, bounds.width() - marginLeftAndRight - config.storkeWidth, bounds.bottom - marginTopAndBottom - config.storkeWidth); backgroundDrawableOfText2.getPaint().setColor(0xffFFFFFF); backgroundDrawableOfText2.draw(canvas); paint.setColor(config.badgeColor); canvas.drawText( cutText(config.text2, text2Width), bounds.width() - marginLeftAndRight - config.paddingRight - text2Width / 2f, textCy, paint); break; default: paint.setColor(config.textColor); canvas.drawText( cutNumber(config.number, badgeWidth), textCx, textCy, paint); } } @Override public int getIntrinsicWidth() { return badgeWidth; } @Override public int getIntrinsicHeight() { return badgeHeight; } @Override public void setAlpha(int alpha) { paint.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter colorFilter) { paint.setColorFilter(colorFilter); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } private String cutNumber(int number, int width) { String text = String.valueOf(number); if (paint.measureText(text) < width) return text; return "…"; } private String cutText(String text, int width) { if (paint.measureText(text) <= width) return text; String suffix = "..."; while(paint.measureText(text + suffix) > width) { if(text.length() > 0) text = text.substring(0, text.length() - 1); if(text.length() == 0) { suffix = suffix.substring(0, suffix.length() - 1); if(suffix.length() == 0) break; } } return text + suffix; } public SpannableString toSpannable() { final SpannableString spanStr = new SpannableString(" "); spanStr.setSpan(new ImageSpan(this, ImageSpan.ALIGN_BOTTOM), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); setBounds(0, 0, getIntrinsicWidth(), getIntrinsicHeight()); return spanStr; } private static float dipToPixels(float dipValue) { final float scale = Resources.getSystem().getDisplayMetrics().density; return dipValue * scale + 0.5f; } private static float spToPixels(float spValue) { final float fontScale = Resources.getSystem().getDisplayMetrics().scaledDensity; return spValue * fontScale + 0.5f; } }