使用 Drawable 实现图标角标

我在去年曾经写过一篇类似的《使用 Drawable 实现小红点》,但是小红点的具体实现是在这个类里面的。这次是在其思路上进行扩展,使得小红点或者说是角标的样式更加灵活。

在一些图标的右上角添加小红点,是我们开发中很常见的场景,比如下图所示,底部 TAB 会有小红点,上面的功能图标也会有小红点。
小红点界面

对于这种需求,我们以往的解决方式通常是使用一个 RelativeLayout,里面再放置一个 TextView,一个 ImageView 和一个表示小红点的普通的 View,并且,我们还需要根据小红点的大小,写死这个 View 的外边距参数以让它的中心能够“正好”位于图标的右上角。

这种实现它有如下缺点:

  • 增加了布局的层次,原本只需要一个 TextView 的 Tab 或 item,现在需要用一个布局容器和三个 View 的组合。
  • 需要根据小红点的大小去设置布局参数,当它扩展成更大的带数字的小红点时,我们还需要去修改布局。

Drawable 正是解决这些问题的利器。

Drawable 是对可绘制对象的抽象,再具体下来会有画图片的 BitmapDrawable 或者是纯色的 ColorDrawable,而且 Drawable 会在回调方法里提供一个 Canvas,这使得对它的利用有了很大的可能性。所以,我们可以继承自 Drawable 类,对原本的图标包装一下,并实现自己对小红点的绘制。
与我之前所写的《使用 Drawable 实现小红点》不同的是,这里的小红点,并不由我们的 Drawable 本身来绘制,它是可以从外面传入的。

这也就是说:我们只需要在 Drawable 里实现将小红点的 Drawable 画到我们图标上的对应位置上即可。具体的小红点的表现形式,是一个小红点,还是外面有一层描边,还是带数字,我们都不需要关心,由调用者处理。而且,这个小红点的 Drawable,可以通过 xml 定义,也可以自己再继承 Drawable 类来实现绘制(如带数字的小红点),这使得它也有很大的扩展性。我们只需要提供设置是否显示小红点以及设置小红点的位置的方法即可。完整代码如下:

class BadgeDrawable(private val mOrigin: Drawable, val badge: Drawable) : Drawable() {
    var showBadge: Boolean = false
        set(show) {
            field = show
            invalidateSelf()
        }

    var gravity = Gravity.CENTER

    init {
        badge.setBounds(0, 0, badge.intrinsicWidth, badge.intrinsicHeight)
    }

    override fun draw(canvas: Canvas) {
        mOrigin.draw(canvas)
        if (showBadge) {
            val radiusX = badge.intrinsicWidth / 2.0f
            val radiusY = badge.intrinsicHeight / 2.0f
            var x = bounds.right - radiusX
            var y = bounds.top - radiusY
            if (Gravity.LEFT.and(gravity) == Gravity.LEFT) {
                x -= radiusX
            } else if (Gravity.RIGHT.and(gravity) == Gravity.RIGHT) {
                x += radiusX
            }
            if (Gravity.TOP.and(gravity) == Gravity.TOP) {
                y -= radiusY
            } else if (Gravity.BOTTOM.and(gravity) == Gravity.BOTTOM) {
                y += radiusY
            }
            canvas.save()
            canvas.translate(x, y)
            badge.draw(canvas)
            canvas.restore()
        }
    }

    override fun getState(): IntArray {
        return mOrigin.state
    }

    override fun setState(stateSet: IntArray?): Boolean {
        return mOrigin.setState(stateSet)
    }

    override fun getConstantState(): ConstantState {
        return mOrigin.constantState
    }

    override fun isStateful(): Boolean {
        return mOrigin.isStateful
    }

    override fun jumpToCurrentState() {
        mOrigin.jumpToCurrentState()
    }

    override fun setAlpha(alpha: Int) {
        mOrigin.alpha = alpha
    }

    override fun getOpacity(): Int {
        return mOrigin.opacity
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        mOrigin.colorFilter = colorFilter
    }

    override fun getIntrinsicHeight(): Int {
        return mOrigin.intrinsicHeight
    }

    override fun getIntrinsicWidth(): Int {
        return mOrigin.intrinsicWidth
    }

    override fun setBounds(bounds: Rect?) {
        super.setBounds(bounds)
        mOrigin.bounds = bounds
    }

    override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
        super.setBounds(left, top, right, bottom)
        mOrigin.setBounds(left, top, right, bottom)
    }
}

那么,如何使用呢?
以上面的图片的 Tab 为例。我们的 Tab 自定义布局如下:

<com.githang.drawablewidget.DrawableTextView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:skin="http://schemas.android.com/android/skin"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/tab_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:drawablePadding="2dp"
    android:gravity="center"
    android:paddingBottom="6dp"
    android:paddingTop="8dp"
    android:textColor="@color/skin_tab_text"
    android:textSize="10sp"
    app:drawableHeight="22dp"
    skin:enable="true"
    tools:drawableTop="@drawable/ic_username"
    tools:ignore="MissingPrefix"
    tools:text="123123" />

然后我们在 kotlin 代码中创建对应的 Tab,并以一个变量保存这个包装了小红点的 Drawable,代码如下:

private lateinit var mOperatingBadge: BadgeDrawable
//... 中间代码省略

private fun initTabs() {
    mOperatingBadge = getBadgeDrawable(resources, R.drawable.ic_tab_operation)
    val operation = newTab(mOperatingBadge, R.string.operation)
    mTabLayout.addTab(operation)
// ... 其他Tab,代码省略
}

private fun getBadgeDrawable(resources: Resources, @DrawableRes drawableId: Int): BadgeDrawable {
    // 获取原来的图标
    val icon = ResourcesCompat.getDrawable(resources, drawableId, null)!!
    // 获取我们在 xml 定义的小红点
    val badge = ResourcesCompat.getDrawable(resources, R.drawable.badge_stroke_white, null)!!
    // 包装成我们的 BadgeDrawable 对象
    return BadgeDrawable(icon, badge)
}

private fun newTab(drawable: Drawable, @StringRes textId: Int): TabLayout.Tab {
    val tab = mTabLayout.newTab()
    tab.setCustomView(R.layout.widget_tab_view)
    tab.customView?.let {
        val textView = it.findViewById<TextView>(R.id.tab_text)
        // 将包装的 Drawable 对象设置到 TextView 的 drawableTop 上
        textView.setCompoundDrawablesWithIntrinsicBounds(null, drawable, null, null)
        textView.setText(textId)
    }
    return tab
}

xml 定义的小红点也很简单,它的形状是这样的:
描边小红点

代码如下:

<?xml version="1.0" encoding="utf-8"?>
<shape
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size android:width="11dp" android:height="11dp"/>
    <solid android:color="@color/text_red"/>
    <stroke android:width="2dp" android:color="@android:color/white"/>
</shape>

那么,当我们需要显示或隐藏这个小红点的时候需要怎么做呢?这很简单,调用一下我们的 BadgeDrawable 对象就可以了:

mOperatingBadge.showBadge = true

然后它就会更新自身的绘制。不会带来布局的变化,没有布局代码的入侵,而且小红点可以扩展成其他任意的角标实现,不管是三角还是数字,看你传进来的 Drawable 的具体表现。

本文相关项目地址:
badge-drawable:https://github.com/msdx/badge-drawable

猜你喜欢

转载自blog.csdn.net/maosidiaoxian/article/details/81216334