Skip to content

Instantly share code, notes, and snippets.

@erickogi
Created April 23, 2019 02:50
Show Gist options
  • Save erickogi/6c3f0bb52bbd04eb2f5699d1578a0924 to your computer and use it in GitHub Desktop.
Save erickogi/6c3f0bb52bbd04eb2f5699d1578a0924 to your computer and use it in GitHub Desktop.

Revisions

  1. erickogi created this gist Apr 23, 2019.
    82 changes: 82 additions & 0 deletions Badge.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,82 @@
    package com.kogicodes.sokoni.utils.Badge;

    import android.graphics.PointF;
    import android.graphics.drawable.Drawable;
    import android.view.View;

    public interface Badge {

    int getBadgeNumber();

    Badge setBadgeNumber(int badgeNum);

    String getBadgeText();

    Badge setBadgeText(String badgeText);

    boolean isExactMode();

    Badge setExactMode(boolean isExact);

    boolean isShowShadow();

    Badge setShowShadow(boolean showShadow);

    Badge stroke(int color, float width, boolean isDpValue);

    int getBadgeBackgroundColor();

    Badge setBadgeBackgroundColor(int color);

    Badge setBadgeBackground(Drawable drawable, boolean clip);

    Drawable getBadgeBackground();

    Badge setBadgeBackground(Drawable drawable);

    int getBadgeTextColor();

    Badge setBadgeTextColor(int color);

    Badge setBadgeTextSize(float size, boolean isSpValue);

    float getBadgeTextSize(boolean isSpValue);

    Badge setBadgePadding(float padding, boolean isDpValue);

    float getBadgePadding(boolean isDpValue);

    boolean isDraggable();

    int getBadgeGravity();

    Badge setBadgeGravity(int gravity);

    Badge setGravityOffset(float offset, boolean isDpValue);

    Badge setGravityOffset(float offsetX, float offsetY, boolean isDpValue);

    float getGravityOffsetX(boolean isDpValue);

    float getGravityOffsetY(boolean isDpValue);

    Badge setOnDragStateChangedListener(OnDragStateChangedListener l);

    PointF getDragCenter();

    Badge bindTarget(View view);

    View getTargetView();

    void hide(boolean animate);

    interface OnDragStateChangedListener {
    int STATE_START = 1;
    int STATE_DRAGGING = 2;
    int STATE_DRAGGING_OUT_OF_RANGE = 3;
    int STATE_CANCELED = 4;
    int STATE_SUCCEED = 5;

    void onDragStateChanged(int dragState, Badge badge, View targetView);
    }
    }
    93 changes: 93 additions & 0 deletions BadgeAnimator.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,93 @@
    package com.kogicodes.sokoni.utils.Badge

    import android.animation.Animator
    import android.animation.AnimatorListenerAdapter
    import android.animation.ValueAnimator
    import android.graphics.Bitmap
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.PointF
    import java.lang.ref.WeakReference
    import java.util.Random

    class BadgeAnimator(badgeBitmap: Bitmap, center: PointF, badge: BadgeView) : ValueAnimator() {
    private val mFragments: Array<Array<BitmapFragment?>>
    private val mWeakBadge: WeakReference<BadgeView>

    init {
    mWeakBadge = WeakReference(badge)
    setFloatValues(0f, 1f)
    duration = 500
    mFragments = getFragments(badgeBitmap, center)
    addUpdateListener {
    val badgeView = mWeakBadge.get()
    if (badgeView == null || !badgeView.isShown) {
    cancel()
    } else {
    badgeView.invalidate()
    }
    }
    addListener(object : AnimatorListenerAdapter() {
    override fun onAnimationEnd(animation: Animator) {
    val badgeView = mWeakBadge.get()
    badgeView?.reset()
    }
    })
    }

    fun draw(canvas: Canvas) {
    for (i in mFragments.indices) {
    for (j in 0 until mFragments[i].size) {
    val bf = mFragments[i][j]
    val value = java.lang.Float.parseFloat(animatedValue.toString())
    bf?.updata(value, canvas)
    }
    }
    }

    private fun getFragments(badgeBitmap: Bitmap, center: PointF): Array<Array<BitmapFragment?>> {
    val width = badgeBitmap.width
    val height = badgeBitmap.height
    val fragmentSize = Math.min(width, height) / 6f
    val startX = center.x - badgeBitmap.width / 2f
    val startY = center.y - badgeBitmap.height / 2f
    var fragments = Array<Array<BitmapFragment?>>((height / fragmentSize).toInt()) { arrayOfNulls((width / fragmentSize)?.toInt()) }
    for (i in fragments.indices) {
    for (j in 0 until fragments[i].size) {
    val bf = BitmapFragment()
    bf.color = badgeBitmap.getPixel((j * fragmentSize).toInt(), (i * fragmentSize).toInt())
    bf.x = startX + j * fragmentSize
    bf.y = startY + i * fragmentSize
    bf.size = fragmentSize
    bf.maxSize = Math.max(width, height)
    fragments[i][j] = bf
    }
    }
    badgeBitmap.recycle()
    return fragments
    }

    private inner class BitmapFragment {
    internal var random: Random
    internal var x: Float = 0.toFloat()
    internal var y: Float = 0.toFloat()
    internal var size: Float = 0.toFloat()
    internal var color: Int = 0
    internal var maxSize: Int = 0
    internal var paint: Paint

    init {
    paint = Paint()
    paint.isAntiAlias = true
    paint.style = Paint.Style.FILL
    random = Random()
    }

    fun updata(value: Float, canvas: Canvas) {
    paint.color = color
    x = x + 0.1f * random.nextInt(maxSize).toFloat() * (random.nextFloat() - 0.5f)
    y = y + 0.1f * random.nextInt(maxSize).toFloat() * (random.nextFloat() - 0.5f)
    canvas.drawCircle(x, y, size - value * size, paint)
    }
    }
    }
    775 changes: 775 additions & 0 deletions BadgeView.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,775 @@
    package com.kogicodes.sokoni.utils.Badge

    import android.content.Context
    import android.graphics.Bitmap
    import android.graphics.Canvas
    import android.graphics.Color
    import android.graphics.Paint
    import android.graphics.Path
    import android.graphics.PointF
    import android.graphics.PorterDuff
    import android.graphics.PorterDuffXfermode
    import android.graphics.RectF
    import android.graphics.drawable.Drawable
    import android.os.Build
    import android.text.TextPaint
    import android.text.TextUtils
    import android.util.AttributeSet
    import android.view.Gravity
    import android.view.MotionEvent
    import android.view.View
    import android.view.ViewGroup
    import android.view.ViewParent
    import android.widget.FrameLayout
    import java.util.ArrayList

    class BadgeView private constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr), Badge {
    override var badgeBackgroundColor: Int = 0
    protected set(value: Int) {
    super.badgeBackgroundColor = value
    }
    protected var mColorBackgroundBorder: Int = 0
    override var badgeTextColor: Int = 0
    protected set(value: Int) {
    super.badgeTextColor = value
    }
    override var badgeBackground: Drawable? = null
    protected set(value: Drawable?) {
    super.badgeBackground = value
    }
    protected var mBitmapClip: Bitmap? = null
    protected var mDrawableBackgroundClip: Boolean = false
    protected var mBackgroundBorderWidth: Float = 0.toFloat()
    protected var mBadgeTextSize: Float = 0.toFloat()
    protected var mBadgePadding: Float = 0.toFloat()
    override var badgeNumber: Int = 0
    protected set(value: Int) {
    super.badgeNumber = value
    }
    override var badgeText: String? = null
    protected set(value: String?) {
    super.badgeText = value
    }
    override var isDraggable: Boolean = false
    protected set(value: Boolean) {
    super.isDraggable = value
    }
    protected var mDragging: Boolean = false
    override var isExactMode: Boolean = false
    protected set(value: Boolean) {
    super.isExactMode = value
    }
    override var isShowShadow: Boolean = false
    protected set(value: Boolean) {
    super.isShowShadow = value
    }
    override var badgeGravity: Int = 0
    protected set(value: Int) {
    super.badgeGravity = value
    }
    protected var mGravityOffsetX: Float = 0.toFloat()
    protected var mGravityOffsetY: Float = 0.toFloat()
    protected var mDefalutRadius: Float = 0.toFloat()
    protected var mFinalDragDistance: Float = 0.toFloat()
    protected var mDragQuadrant: Int = 0
    protected var mDragOutOfRange: Boolean = false
    protected var mBadgeTextRect: RectF
    protected var mBadgeBackgroundRect: RectF
    protected var mDragPath: Path
    protected var mBadgeTextFontMetrics: Paint.FontMetrics
    protected var mBadgeCenter: PointF
    protected var mDragCenter: PointF
    protected var mRowBadgeCenter: PointF
    protected var mControlPoint: PointF
    protected var mInnertangentPoints: MutableList<PointF>
    override var targetView: View
    protected set(value: View) {
    super.targetView = value
    }
    protected var mWidth: Int = 0
    protected var mHeight: Int = 0
    protected var mBadgeTextPaint: TextPaint
    protected var mBadgeBackgroundPaint: Paint
    protected var mBadgeBackgroundBorderPaint: Paint
    protected var mAnimator: BadgeAnimator? = null
    protected var mDragStateChangedListener: Badge.OnDragStateChangedListener? = null
    protected var mActivityRoot: ViewGroup? = null
    private val badgeCircleRadius: Float
    get() = if (badgeText!!.isEmpty()) {
    mBadgePadding
    } else if (badgeText!!.length == 1) {
    if (mBadgeTextRect.height() > mBadgeTextRect.width())
    mBadgeTextRect.height() / 2f + mBadgePadding * 0.5f
    else
    mBadgeTextRect.width() / 2f + mBadgePadding * 0.5f
    } else {
    mBadgeBackgroundRect.height() / 2f
    }
    override val dragCenter: PointF
    get() = if (isDraggable && mDragging) mDragCenter else null

    constructor(context: Context) : this(context, null) {}

    init {
    init()
    }

    private fun init() {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    mBadgeTextRect = RectF()
    mBadgeBackgroundRect = RectF()
    mDragPath = Path()
    mBadgeCenter = PointF()
    mDragCenter = PointF()
    mRowBadgeCenter = PointF()
    mControlPoint = PointF()
    mInnertangentPoints = ArrayList()
    mBadgeTextPaint = TextPaint()
    mBadgeTextPaint.isAntiAlias = true
    mBadgeTextPaint.isSubpixelText = true
    mBadgeTextPaint.isFakeBoldText = true
    mBadgeTextPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
    mBadgeBackgroundPaint = Paint()
    mBadgeBackgroundPaint.isAntiAlias = true
    mBadgeBackgroundPaint.style = Paint.Style.FILL
    mBadgeBackgroundBorderPaint = Paint()
    mBadgeBackgroundBorderPaint.isAntiAlias = true
    mBadgeBackgroundBorderPaint.style = Paint.Style.STROKE
    badgeBackgroundColor = -0x17b1c0
    badgeTextColor = -0x1
    mBadgeTextSize = DisplayUtil.dp2px(context, 11f).toFloat()
    mBadgePadding = DisplayUtil.dp2px(context, 5f).toFloat()
    badgeNumber = 0
    badgeGravity = Gravity.END or Gravity.TOP
    mGravityOffsetX = DisplayUtil.dp2px(context, 1f).toFloat()
    mGravityOffsetY = DisplayUtil.dp2px(context, 1f).toFloat()
    mFinalDragDistance = DisplayUtil.dp2px(context, 90f).toFloat()
    isShowShadow = true
    mDrawableBackgroundClip = false
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    translationZ = 1000f
    }
    }

    override fun bindTarget(targetView: View): Badge {
    if (targetView == null) {
    throw IllegalStateException("targetView can not be null")
    }
    if (parent != null) {
    (parent as ViewGroup).removeView(this)
    }
    val targetParent = targetView.parent
    if (targetParent != null && targetParent is ViewGroup) {
    this.targetView = targetView
    if (targetParent is BadgeContainer) {
    targetParent.addView(this)
    } else {
    val index = targetParent.indexOfChild(targetView)
    val targetParams = targetView.layoutParams
    targetParent.removeView(targetView)
    val badgeContainer = BadgeContainer(context)
    badgeContainer.id = targetView.id
    targetView.id = View.NO_ID
    targetParent.addView(badgeContainer, index, targetParams)
    badgeContainer.addView(targetView)
    badgeContainer.addView(this)
    }
    } else {
    throw IllegalStateException("targetView must have a parent")
    }
    return this
    }

    override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    if (mActivityRoot == null) findViewRoot(targetView)
    }

    private fun findViewRoot(view: View) {
    mActivityRoot = view.rootView as ViewGroup
    if (mActivityRoot == null) {
    findActivityRoot(view)
    }
    }

    private fun findActivityRoot(view: View) {
    if (view.parent != null && view.parent is View) {
    findActivityRoot(view.parent as View)
    } else if (view is ViewGroup) {
    mActivityRoot = view
    }
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
    when (event.actionMasked) {
    MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
    val x = event.x
    val y = event.y
    if (isDraggable && event.getPointerId(event.actionIndex) == 0
    && x > mBadgeBackgroundRect.left && x < mBadgeBackgroundRect.right &&
    y > mBadgeBackgroundRect.top && y < mBadgeBackgroundRect.bottom
    && badgeText != null) {
    initRowBadgeCenter()
    mDragging = true
    updataListener(Badge.OnDragStateChangedListener.STATE_START)
    mDefalutRadius = DisplayUtil.dp2px(context, 7f).toFloat()
    parent.requestDisallowInterceptTouchEvent(true)
    screenFromWindow(true)
    mDragCenter.x = event.rawX
    mDragCenter.y = event.rawY
    }
    }
    MotionEvent.ACTION_MOVE -> if (mDragging) {
    mDragCenter.x = event.rawX
    mDragCenter.y = event.rawY
    invalidate()
    }
    MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP, MotionEvent.ACTION_CANCEL -> if (event.getPointerId(event.actionIndex) == 0 && mDragging) {
    mDragging = false
    onPointerUp()
    }
    }
    return mDragging || super.onTouchEvent(event)
    }

    private fun onPointerUp() {
    if (mDragOutOfRange) {
    animateHide(mDragCenter)
    updataListener(Badge.OnDragStateChangedListener.STATE_SUCCEED)
    } else {
    reset()
    updataListener(Badge.OnDragStateChangedListener.STATE_CANCELED)
    }
    }

    protected fun createBadgeBitmap(): Bitmap {
    val bitmap = Bitmap.createBitmap(mBadgeBackgroundRect.width().toInt() + DisplayUtil.dp2px(context, 3f),
    mBadgeBackgroundRect.height().toInt() + DisplayUtil.dp2px(context, 3f), Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    drawBadge(canvas, PointF(canvas.width / 2f, canvas.height / 2f), badgeCircleRadius)
    return bitmap
    }

    protected fun screenFromWindow(screen: Boolean) {
    if (parent != null) {
    (parent as ViewGroup).removeView(this)
    }
    if (screen) {
    mActivityRoot!!.addView(this, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
    FrameLayout.LayoutParams.MATCH_PARENT))
    } else {
    bindTarget(targetView)
    }
    }

    private fun showShadowImpl(showShadow: Boolean) {
    var x = DisplayUtil.dp2px(context, 1f)
    var y = DisplayUtil.dp2px(context, 1.5f)
    when (mDragQuadrant) {
    1 -> {
    x = DisplayUtil.dp2px(context, 1f)
    y = DisplayUtil.dp2px(context, -1.5f)
    }
    2 -> {
    x = DisplayUtil.dp2px(context, -1f)
    y = DisplayUtil.dp2px(context, -1.5f)
    }
    3 -> {
    x = DisplayUtil.dp2px(context, -1f)
    y = DisplayUtil.dp2px(context, 1.5f)
    }
    4 -> {
    x = DisplayUtil.dp2px(context, 1f)
    y = DisplayUtil.dp2px(context, 1.5f)
    }
    }
    mBadgeBackgroundPaint.setShadowLayer((if (showShadow)
    DisplayUtil.dp2px(context, 2f)
    else
    0).toFloat(), x.toFloat(), y.toFloat(), 0x33000000)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    mWidth = w
    mHeight = h
    }

    override fun onDraw(canvas: Canvas) {
    if (mAnimator != null && mAnimator!!.isRunning) {
    mAnimator!!.draw(canvas)
    return
    }
    if (badgeText != null) {
    initPaints()
    val badgeRadius = badgeCircleRadius
    val startCircleRadius = mDefalutRadius * (1 - MathUtil.getPointDistance(mRowBadgeCenter, mDragCenter) / mFinalDragDistance)
    if (isDraggable && mDragging) {
    mDragQuadrant = MathUtil.getQuadrant(mDragCenter, mRowBadgeCenter)
    showShadowImpl(isShowShadow)
    if (mDragOutOfRange = startCircleRadius < DisplayUtil.dp2px(context, 1.5f)) {
    updataListener(Badge.OnDragStateChangedListener.STATE_DRAGGING_OUT_OF_RANGE)
    drawBadge(canvas, mDragCenter, badgeRadius)
    } else {
    updataListener(Badge.OnDragStateChangedListener.STATE_DRAGGING)
    drawDragging(canvas, startCircleRadius, badgeRadius)
    drawBadge(canvas, mDragCenter, badgeRadius)
    }
    } else {
    findBadgeCenter()
    drawBadge(canvas, mBadgeCenter, badgeRadius)
    }
    }
    }

    private fun initPaints() {
    showShadowImpl(isShowShadow)
    mBadgeBackgroundPaint.color = badgeBackgroundColor
    mBadgeBackgroundBorderPaint.color = mColorBackgroundBorder
    mBadgeBackgroundBorderPaint.strokeWidth = mBackgroundBorderWidth
    mBadgeTextPaint.color = badgeTextColor
    mBadgeTextPaint.textAlign = Paint.Align.CENTER
    }

    private fun drawDragging(canvas: Canvas, startRadius: Float, badgeRadius: Float) {
    val dy = mDragCenter.y - mRowBadgeCenter.y
    val dx = mDragCenter.x - mRowBadgeCenter.x
    mInnertangentPoints.clear()
    if (dx != 0f) {
    val k1 = (dy / dx).toDouble()
    val k2 = -1 / k1
    MathUtil.getInnertangentPoints(mDragCenter, badgeRadius, k2, mInnertangentPoints)
    MathUtil.getInnertangentPoints(mRowBadgeCenter, startRadius, k2, mInnertangentPoints)
    } else {
    MathUtil.getInnertangentPoints(mDragCenter, badgeRadius, 0.0, mInnertangentPoints)
    MathUtil.getInnertangentPoints(mRowBadgeCenter, startRadius, 0.0, mInnertangentPoints)
    }
    mDragPath.reset()
    mDragPath.addCircle(mRowBadgeCenter.x, mRowBadgeCenter.y, startRadius,
    if (mDragQuadrant == 1 || mDragQuadrant == 2) Path.Direction.CCW else Path.Direction.CW)
    mControlPoint.x = (mRowBadgeCenter.x + mDragCenter.x) / 2.0f
    mControlPoint.y = (mRowBadgeCenter.y + mDragCenter.y) / 2.0f
    mDragPath.moveTo(mInnertangentPoints[2].x, mInnertangentPoints[2].y)
    mDragPath.quadTo(mControlPoint.x, mControlPoint.y, mInnertangentPoints[0].x, mInnertangentPoints[0].y)
    mDragPath.lineTo(mInnertangentPoints[1].x, mInnertangentPoints[1].y)
    mDragPath.quadTo(mControlPoint.x, mControlPoint.y, mInnertangentPoints[3].x, mInnertangentPoints[3].y)
    mDragPath.lineTo(mInnertangentPoints[2].x, mInnertangentPoints[2].y)
    mDragPath.close()
    canvas.drawPath(mDragPath, mBadgeBackgroundPaint)
    //draw dragging border
    if (mColorBackgroundBorder != 0 && mBackgroundBorderWidth > 0) {
    mDragPath.reset()
    mDragPath.moveTo(mInnertangentPoints[2].x, mInnertangentPoints[2].y)
    mDragPath.quadTo(mControlPoint.x, mControlPoint.y, mInnertangentPoints[0].x, mInnertangentPoints[0].y)
    mDragPath.moveTo(mInnertangentPoints[1].x, mInnertangentPoints[1].y)
    mDragPath.quadTo(mControlPoint.x, mControlPoint.y, mInnertangentPoints[3].x, mInnertangentPoints[3].y)
    val startY: Float
    val startX: Float
    if (mDragQuadrant == 1 || mDragQuadrant == 2) {
    startX = mInnertangentPoints[2].x - mRowBadgeCenter.x
    startY = mRowBadgeCenter.y - mInnertangentPoints[2].y
    } else {
    startX = mInnertangentPoints[3].x - mRowBadgeCenter.x
    startY = mRowBadgeCenter.y - mInnertangentPoints[3].y
    }
    val startAngle = 360 - MathUtil.radianToAngle(MathUtil.getTanRadian(Math.atan((startY / startX).toDouble()),
    if (mDragQuadrant - 1 == 0) 4 else mDragQuadrant - 1)).toFloat()
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mDragPath.addArc(mRowBadgeCenter.x - startRadius, mRowBadgeCenter.y - startRadius,
    mRowBadgeCenter.x + startRadius, mRowBadgeCenter.y + startRadius, startAngle,
    180f)
    } else {
    mDragPath.addArc(RectF(mRowBadgeCenter.x - startRadius, mRowBadgeCenter.y - startRadius,
    mRowBadgeCenter.x + startRadius, mRowBadgeCenter.y + startRadius), startAngle, 180f)
    }
    canvas.drawPath(mDragPath, mBadgeBackgroundBorderPaint)
    }
    }

    private fun drawBadge(canvas: Canvas, center: PointF, radius: Float) {
    var radius = radius
    if (center.x == -1000f && center.y == -1000f) {
    return
    }
    if (badgeText!!.isEmpty() || badgeText!!.length == 1) {
    mBadgeBackgroundRect.left = center.x - radius.toInt()
    mBadgeBackgroundRect.top = center.y - radius.toInt()
    mBadgeBackgroundRect.right = center.x + radius.toInt()
    mBadgeBackgroundRect.bottom = center.y + radius.toInt()
    if (badgeBackground != null) {
    drawBadgeBackground(canvas)
    } else {
    canvas.drawCircle(center.x, center.y, radius, mBadgeBackgroundPaint)
    if (mColorBackgroundBorder != 0 && mBackgroundBorderWidth > 0) {
    canvas.drawCircle(center.x, center.y, radius, mBadgeBackgroundBorderPaint)
    }
    }
    } else {
    mBadgeBackgroundRect.left = center.x - (mBadgeTextRect.width() / 2f + mBadgePadding)
    mBadgeBackgroundRect.top = center.y - (mBadgeTextRect.height() / 2f + mBadgePadding * 0.5f)
    mBadgeBackgroundRect.right = center.x + (mBadgeTextRect.width() / 2f + mBadgePadding)
    mBadgeBackgroundRect.bottom = center.y + (mBadgeTextRect.height() / 2f + mBadgePadding * 0.5f)
    radius = mBadgeBackgroundRect.height() / 2f
    if (badgeBackground != null) {
    drawBadgeBackground(canvas)
    } else {
    canvas.drawRoundRect(mBadgeBackgroundRect, radius, radius, mBadgeBackgroundPaint)
    if (mColorBackgroundBorder != 0 && mBackgroundBorderWidth > 0) {
    canvas.drawRoundRect(mBadgeBackgroundRect, radius, radius, mBadgeBackgroundBorderPaint)
    }
    }
    }
    if (!badgeText!!.isEmpty()) {
    canvas.drawText(badgeText!!, center.x,
    (mBadgeBackgroundRect.bottom + mBadgeBackgroundRect.top
    - mBadgeTextFontMetrics.bottom - mBadgeTextFontMetrics.top) / 2f,
    mBadgeTextPaint)
    }
    }

    private fun drawBadgeBackground(canvas: Canvas) {
    mBadgeBackgroundPaint.setShadowLayer(0f, 0f, 0f, 0)
    val left = mBadgeBackgroundRect.left.toInt()
    val top = mBadgeBackgroundRect.top.toInt()
    var right = mBadgeBackgroundRect.right.toInt()
    var bottom = mBadgeBackgroundRect.bottom.toInt()
    if (mDrawableBackgroundClip) {
    right = left + mBitmapClip!!.width
    bottom = top + mBitmapClip!!.height
    canvas.saveLayer(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), null, Canvas.ALL_SAVE_FLAG)
    }
    badgeBackground!!.setBounds(left, top, right, bottom)
    badgeBackground!!.draw(canvas)
    if (mDrawableBackgroundClip) {
    mBadgeBackgroundPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN)
    canvas.drawBitmap(mBitmapClip!!, left.toFloat(), top.toFloat(), mBadgeBackgroundPaint)
    canvas.restore()
    mBadgeBackgroundPaint.xfermode = null
    if (badgeText!!.isEmpty() || badgeText!!.length == 1) {
    canvas.drawCircle(mBadgeBackgroundRect.centerX(), mBadgeBackgroundRect.centerY(),
    mBadgeBackgroundRect.width() / 2f, mBadgeBackgroundBorderPaint)
    } else {
    canvas.drawRoundRect(mBadgeBackgroundRect,
    mBadgeBackgroundRect.height() / 2, mBadgeBackgroundRect.height() / 2,
    mBadgeBackgroundBorderPaint)
    }
    } else {
    canvas.drawRect(mBadgeBackgroundRect, mBadgeBackgroundBorderPaint)
    }
    }

    private fun createClipLayer() {
    if (badgeText == null) {
    return
    }
    if (!mDrawableBackgroundClip) {
    return
    }
    if (mBitmapClip != null && !mBitmapClip!!.isRecycled) {
    mBitmapClip!!.recycle()
    }
    val radius = badgeCircleRadius
    if (badgeText!!.isEmpty() || badgeText!!.length == 1) {
    mBitmapClip = Bitmap.createBitmap(radius.toInt() * 2, radius.toInt() * 2,
    Bitmap.Config.ARGB_4444)
    val srcCanvas = Canvas(mBitmapClip!!)
    srcCanvas.drawCircle(srcCanvas.width / 2f, srcCanvas.height / 2f,
    srcCanvas.width / 2f, mBadgeBackgroundPaint)
    } else {
    mBitmapClip = Bitmap.createBitmap((mBadgeTextRect.width() + mBadgePadding * 2).toInt(),
    (mBadgeTextRect.height() + mBadgePadding).toInt(), Bitmap.Config.ARGB_4444)
    val srcCanvas = Canvas(mBitmapClip!!)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    srcCanvas.drawRoundRect(0f, 0f, srcCanvas.width.toFloat(), srcCanvas.height.toFloat(), srcCanvas.height / 2f,
    srcCanvas.height / 2f, mBadgeBackgroundPaint)
    } else {
    srcCanvas.drawRoundRect(RectF(0f, 0f, srcCanvas.width.toFloat(), srcCanvas.height.toFloat()),
    srcCanvas.height / 2f, srcCanvas.height / 2f, mBadgeBackgroundPaint)
    }
    }
    }

    private fun findBadgeCenter() {
    val rectWidth = if (mBadgeTextRect.height() > mBadgeTextRect.width())
    mBadgeTextRect.height()
    else
    mBadgeTextRect.width()
    when (badgeGravity) {
    Gravity.START or Gravity.TOP -> {
    mBadgeCenter.x = mGravityOffsetX + mBadgePadding + rectWidth / 2f
    mBadgeCenter.y = mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f
    }
    Gravity.START or Gravity.BOTTOM -> {
    mBadgeCenter.x = mGravityOffsetX + mBadgePadding + rectWidth / 2f
    mBadgeCenter.y = mHeight - (mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f)
    }
    Gravity.END or Gravity.TOP -> {
    mBadgeCenter.x = mWidth - (mGravityOffsetX + mBadgePadding + rectWidth / 2f)
    mBadgeCenter.y = mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f
    }
    Gravity.END or Gravity.BOTTOM -> {
    mBadgeCenter.x = mWidth - (mGravityOffsetX + mBadgePadding + rectWidth / 2f)
    mBadgeCenter.y = mHeight - (mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f)
    }
    Gravity.CENTER -> {
    mBadgeCenter.x = mWidth / 2f
    mBadgeCenter.y = mHeight / 2f
    }
    Gravity.CENTER or Gravity.TOP -> {
    mBadgeCenter.x = mWidth / 2f
    mBadgeCenter.y = mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f
    }
    Gravity.CENTER or Gravity.BOTTOM -> {
    mBadgeCenter.x = mWidth / 2f
    mBadgeCenter.y = mHeight - (mGravityOffsetY + mBadgePadding + mBadgeTextRect.height() / 2f)
    }
    Gravity.CENTER or Gravity.START -> {
    mBadgeCenter.x = mGravityOffsetX + mBadgePadding + rectWidth / 2f
    mBadgeCenter.y = mHeight / 2f
    }
    Gravity.CENTER or Gravity.END -> {
    mBadgeCenter.x = mWidth - (mGravityOffsetX + mBadgePadding + rectWidth / 2f)
    mBadgeCenter.y = mHeight / 2f
    }
    }
    initRowBadgeCenter()
    }

    private fun measureText() {
    mBadgeTextRect.left = 0f
    mBadgeTextRect.top = 0f
    if (TextUtils.isEmpty(badgeText)) {
    mBadgeTextRect.right = 0f
    mBadgeTextRect.bottom = 0f
    } else {
    mBadgeTextPaint.textSize = mBadgeTextSize
    mBadgeTextRect.right = mBadgeTextPaint.measureText(badgeText)
    mBadgeTextFontMetrics = mBadgeTextPaint.fontMetrics
    mBadgeTextRect.bottom = mBadgeTextFontMetrics.descent - mBadgeTextFontMetrics.ascent
    }
    createClipLayer()
    }

    private fun initRowBadgeCenter() {
    val screenPoint = IntArray(2)
    getLocationOnScreen(screenPoint)
    mRowBadgeCenter.x = mBadgeCenter.x + screenPoint[0]
    mRowBadgeCenter.y = mBadgeCenter.y + screenPoint[1]
    }

    protected fun animateHide(center: PointF) {
    if (badgeText == null) {
    return
    }
    if (mAnimator == null || !mAnimator!!.isRunning) {
    screenFromWindow(true)
    mAnimator = BadgeAnimator(createBadgeBitmap(), center, this)
    mAnimator!!.start()
    setBadgeNumber(0)
    }
    }

    fun reset() {
    mDragCenter.x = -1000f
    mDragCenter.y = -1000f
    mDragQuadrant = 4
    screenFromWindow(false)
    parent.requestDisallowInterceptTouchEvent(false)
    invalidate()
    }

    override fun hide(animate: Boolean) {
    if (animate && mActivityRoot != null) {
    initRowBadgeCenter()
    animateHide(mRowBadgeCenter)
    } else {
    setBadgeNumber(0)
    }
    }

    /**
    * @param badgeNumber equal to zero badge will be hidden, less than zero show dot
    */
    override fun setBadgeNumber(badgeNumber: Int): Badge {
    this.badgeNumber = badgeNumber
    if (this.badgeNumber < 0) {
    badgeText = ""
    } else if (this.badgeNumber > 99) {
    badgeText = if (isExactMode) this.badgeNumber.toString() else "99+"
    } else if (this.badgeNumber > 0 && this.badgeNumber <= 99) {
    badgeText = this.badgeNumber.toString()
    } else if (this.badgeNumber == 0) {
    badgeText = null
    }
    measureText()
    invalidate()
    return this
    }

    override fun setBadgeText(badgeText: String): Badge {
    this.badgeText = badgeText
    badgeNumber = 1
    measureText()
    invalidate()
    return this
    }

    override fun setExactMode(isExact: Boolean): Badge {
    isExactMode = isExact
    if (badgeNumber > 99) {
    setBadgeNumber(badgeNumber)
    }
    return this
    }

    override fun setShowShadow(showShadow: Boolean): Badge {
    isShowShadow = showShadow
    invalidate()
    return this
    }

    override fun setBadgeBackgroundColor(color: Int): Badge {
    badgeBackgroundColor = color
    if (badgeBackgroundColor == Color.TRANSPARENT) {
    mBadgeTextPaint.xfermode = null
    } else {
    mBadgeTextPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
    }
    invalidate()
    return this
    }

    override fun stroke(color: Int, width: Float, isDpValue: Boolean): Badge {
    mColorBackgroundBorder = color
    mBackgroundBorderWidth = if (isDpValue) DisplayUtil.dp2px(context, width) else width
    invalidate()
    return this
    }

    override fun setBadgeBackground(drawable: Drawable): Badge {
    return setBadgeBackground(drawable, false)
    }

    override fun setBadgeBackground(drawable: Drawable, clip: Boolean): Badge {
    mDrawableBackgroundClip = clip
    badgeBackground = drawable
    createClipLayer()
    invalidate()
    return this
    }

    override fun setBadgeTextColor(color: Int): Badge {
    badgeTextColor = color
    invalidate()
    return this
    }

    override fun setBadgeTextSize(size: Float, isSpValue: Boolean): Badge {
    mBadgeTextSize = if (isSpValue) DisplayUtil.dp2px(context, size) else size
    measureText()
    invalidate()
    return this
    }

    override fun getBadgeTextSize(isSpValue: Boolean): Float {
    return if (isSpValue) DisplayUtil.px2dp(context, mBadgeTextSize) else mBadgeTextSize
    }

    override fun setBadgePadding(padding: Float, isDpValue: Boolean): Badge {
    mBadgePadding = if (isDpValue) DisplayUtil.dp2px(context, padding) else padding
    createClipLayer()
    invalidate()
    return this
    }

    override fun getBadgePadding(isDpValue: Boolean): Float {
    return if (isDpValue) DisplayUtil.px2dp(context, mBadgePadding) else mBadgePadding
    }

    /**
    * @param gravity only support Gravity.START | Gravity.TOP , Gravity.END | Gravity.TOP ,
    * Gravity.START | Gravity.BOTTOM , Gravity.END | Gravity.BOTTOM ,
    * Gravity.CENTER , Gravity.CENTER | Gravity.TOP , Gravity.CENTER | Gravity.BOTTOM ,
    * Gravity.CENTER | Gravity.START , Gravity.CENTER | Gravity.END
    */
    override fun setBadgeGravity(gravity: Int): Badge {
    if (gravity == Gravity.START or Gravity.TOP ||
    gravity == Gravity.END or Gravity.TOP ||
    gravity == Gravity.START or Gravity.BOTTOM ||
    gravity == Gravity.END or Gravity.BOTTOM ||
    gravity == Gravity.CENTER ||
    gravity == Gravity.CENTER or Gravity.TOP ||
    gravity == Gravity.CENTER or Gravity.BOTTOM ||
    gravity == Gravity.CENTER or Gravity.START ||
    gravity == Gravity.CENTER or Gravity.END) {
    badgeGravity = gravity
    invalidate()
    } else {
    throw IllegalStateException("only support Gravity.START | Gravity.TOP , Gravity.END | Gravity.TOP , " +
    "Gravity.START | Gravity.BOTTOM , Gravity.END | Gravity.BOTTOM , Gravity.CENTER" +
    " , Gravity.CENTER | Gravity.TOP , Gravity.CENTER | Gravity.BOTTOM ," +
    "Gravity.CENTER | Gravity.START , Gravity.CENTER | Gravity.END")
    }
    return this
    }

    override fun setGravityOffset(offset: Float, isDpValue: Boolean): Badge {
    return setGravityOffset(offset, offset, isDpValue)
    }

    override fun setGravityOffset(offsetX: Float, offsetY: Float, isDpValue: Boolean): Badge {
    mGravityOffsetX = if (isDpValue) DisplayUtil.dp2px(context, offsetX) else offsetX
    mGravityOffsetY = if (isDpValue) DisplayUtil.dp2px(context, offsetY) else offsetY
    invalidate()
    return this
    }

    override fun getGravityOffsetX(isDpValue: Boolean): Float {
    return if (isDpValue) DisplayUtil.px2dp(context, mGravityOffsetX) else mGravityOffsetX
    }

    override fun getGravityOffsetY(isDpValue: Boolean): Float {
    return if (isDpValue) DisplayUtil.px2dp(context, mGravityOffsetY) else mGravityOffsetY
    }

    private fun updataListener(state: Int) {
    if (mDragStateChangedListener != null)
    mDragStateChangedListener!!.onDragStateChanged(state, this, targetView)
    }

    override fun setOnDragStateChangedListener(l: Badge.OnDragStateChangedListener): Badge {
    isDraggable = l != null
    mDragStateChangedListener = l
    return this
    }

    private inner class BadgeContainer(context: Context) : ViewGroup(context) {
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    for (i in 0 until childCount) {
    val child = getChildAt(i)
    child.layout(0, 0, child.measuredWidth, child.measuredHeight)
    }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    var targetView: View? = null
    var badgeView: View? = null
    for (i in 0 until childCount) {
    val child = getChildAt(i)
    if (child !is BadgeView) {
    targetView = child
    } else {
    badgeView = child
    }
    }
    if (targetView == null) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    } else {
    targetView.measure(widthMeasureSpec, heightMeasureSpec)
    badgeView?.measure(View.MeasureSpec.makeMeasureSpec(targetView.measuredWidth, View.MeasureSpec.EXACTLY),
    View.MeasureSpec.makeMeasureSpec(targetView.measuredHeight, View.MeasureSpec.EXACTLY))
    setMeasuredDimension(targetView.measuredWidth, targetView.measuredHeight)
    }
    }
    }
    }
    15 changes: 15 additions & 0 deletions DisplayUtil.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    package com.kogicodes.sokoni.utils.Badge

    import android.content.Context

    object DisplayUtil {
    fun dp2px(context: Context, dp: Float): Int {
    val scale = context.resources.displayMetrics.density
    return (dp * scale + 0.5f).toInt()
    }

    fun px2dp(context: Context, pxValue: Float): Int {
    val scale = context.resources.displayMetrics.density
    return (pxValue / scale + 0.5f).toInt()
    }
    }
    56 changes: 56 additions & 0 deletions MathUtil.kt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,56 @@
    package com.kogicodes.sokoni.utils.Badge

    import android.graphics.PointF

    object MathUtil {
    val CIRCLE_RADIAN = 2 * Math.PI
    fun getTanRadian(atan: Double, quadrant: Int): Double {
    var atan = atan
    if (atan < 0) {
    atan += CIRCLE_RADIAN / 4
    }
    atan += CIRCLE_RADIAN / 4 * (quadrant - 1)
    return atan
    }

    fun radianToAngle(radian: Double): Double {
    return 360 * (radian / CIRCLE_RADIAN)
    }

    fun getQuadrant(p: PointF, center: PointF): Int {
    if (p.x > center.x) {
    if (p.y > center.y) {
    return 4
    } else if (p.y < center.y) {
    return 1
    }
    } else if (p.x < center.x) {
    if (p.y > center.y) {
    return 3
    } else if (p.y < center.y) {
    return 2
    }
    }
    return -1
    }

    fun getPointDistance(p1: PointF, p2: PointF): Float {
    return Math.sqrt(Math.pow((p1.x - p2.x).toDouble(), 2.0) + Math.pow((p1.y - p2.y).toDouble(), 2.0)).toFloat()
    }

    fun getInnertangentPoints(circleCenter: PointF, radius: Float, slopeLine: Double?, points: MutableList<PointF>) {
    val radian: Float
    val xOffset: Float
    val yOffset: Float
    if (slopeLine != null) {
    radian = Math.atan(slopeLine).toFloat()
    xOffset = (Math.cos(radian.toDouble()) * radius).toFloat()
    yOffset = (Math.sin(radian.toDouble()) * radius).toFloat()
    } else {
    xOffset = radius
    yOffset = 0f
    }
    points.add(PointF(circleCenter.x + xOffset, circleCenter.y + yOffset))
    points.add(PointF(circleCenter.x - xOffset, circleCenter.y - yOffset))
    }
    }