Android 自定义View:四周Drawable可点击的TextView

起源

很多时候,我们需要一个图标加Text的UI。这时,可以使用setCompoundDrawables()
或者android:drawable系列属性给TextView的四周加上图标解决。但如果这个图标需要触发单独的点击事件,那么就没办法了。一般情况下,我们会独立图标为ImageView来添加点击事件,缺点是多一层布局,但有了这个自定义View,就可以完美解决这个问题。

源码

本源码基于BoBoMEe的进行完善,可以使用XML属性设置Drawable,也兼容了Relative相关的xml属性:drawableStartCompat与drawableEndCompat。

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View


/**
 * 四周Drawable可点击的TextView。
 * 参考来源:https://github.com/BoBoMEe/Android-Demos/blob/master/blogcodes/app/src/main/java/com/bobomee/blogdemos/view/compound/CompoundDrawablesTextView.java
 */
class DrawableClickableTextView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr),
    View.OnClickListener {
    
    
    private val drawableAmount = 4

    //各个方向的drawable,以left, top, right, bottom顺序存放
    private var drawables = Array<Drawable?>(drawableAmount) {
    
     null }

    //各个方向的drawable是否被touch,存放顺序同上
    private val drawablesTouch = BooleanArray(drawableAmount)

    //Drawable可响应的点击区域x方向允许的误差,表示图片x方向的此范围内的点击都被接受
    var lazyX = 0

    //Drawable可响应的点击区域y方向允许的误差,表示图片y方向的此范围内的点击都被接受
    var lazyY = 0

    //图片点击的监听器
    private var drawableClickListener: DrawableClickListener? = null

    init {
    
    
        //自己处理监听点击事件
        super.setOnClickListener(this)
        initDrawables()
    }

    /**
     * 获取xml文件中设置的Drawable
     * 为了兼容drawableStartCompat与drawableEndCompat属性,需要两次遍历进行赋值
     */
    private fun initDrawables() {
    
    
        drawables = super.getCompoundDrawablesRelative()
        super.getCompoundDrawables().forEachIndexed {
    
     index, drawable ->
            if (drawables[index] == null) {
    
    
                drawables[index] = drawable
            }
        }
    }

    inline fun setOnClickListener(crossinline listener: (DrawableClickableTextView, DrawableClickListener.Position) -> Unit) {
    
    
        setOnClickListener(object : DrawableClickListener {
    
    
            override fun onClick(
                view: DrawableClickableTextView,
                position: DrawableClickListener.Position
            ): Unit = listener(view, position)
        })
    }

    fun setOnClickListener(listener: DrawableClickListener?) {
    
    
        drawableClickListener = listener
    }

    override fun setOnClickListener(l: OnClickListener?) {
    
    
        throw UnsupportedOperationException("Please set DrawableClickListener!")
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent?): Boolean {
    
    
        // 在event为actionDown时标记用户点击是否在相应的图片范围内
        if (event != null) {
    
    
            if (event.action == MotionEvent.ACTION_DOWN) {
    
    
                if (drawableClickListener != null) {
    
    
                    resetTouchStatus()
                    repeat(4) {
    
     i ->
                        drawablesTouch[i] = isTouchDrawable(event, i)
                    }
                }
            }
        }
        return super.onTouchEvent(event)
    }

    /**
     * 计算图片点击可响应的范围并判断点击事件是否在点击范围内
     * 计算方法见http://trinea.iteye.com/blog/1562388
     */
    private fun isTouchDrawable(event: MotionEvent, position: Int): Boolean {
    
    
        val mLeftDrawable = drawables[position] ?: return false

        //是否是属于垂直位置,0代表左右,1代表上下
        val isVertical = position % 2


        //中间映射值,借此将adjacentDrawable1和2指向一垂直方向的两边,如取左或右,1和2分别对应上和下
        val mapValue = (isVertical) * 3
        val adjacentDrawable1 = drawables[(mapValue + 1) % 4]
        val adjacentDrawable2 = drawables[(mapValue + 3) % 4]

        val adjacentDrawable1Length = getDrawableLength(adjacentDrawable1, isVertical)
        val adjacentDrawable2Length = getDrawableLength(adjacentDrawable2, isVertical)
        val adjacentDrawablesDis: Int = adjacentDrawable1Length - adjacentDrawable2Length
        val viewLength = if (isVertical == 0) {
    
    
            height
        } else {
    
    
            width
        }
        val imageOneAxisCenter = 0.5 * (viewLength + adjacentDrawablesDis)

        val drawHeight: Int = mLeftDrawable.intrinsicHeight
        val drawWidth: Int = mLeftDrawable.intrinsicWidth
        val imageBounds = when (position) {
    
    
            0 -> {
    
    
                getLeftRect(imageOneAxisCenter, drawHeight, drawWidth, compoundDrawablePadding)
            }
            1 -> {
    
    
                getTopRect(imageOneAxisCenter, drawWidth, drawHeight, compoundDrawablePadding)
            }
            2 -> {
    
    
                getRightRect(
                    imageOneAxisCenter,
                    drawHeight,
                    drawHeight,
                    compoundDrawablePadding,
                    width
                )
            }
            3 -> {
    
    
                getBottomRect(
                    imageOneAxisCenter,
                    drawHeight,
                    drawHeight,
                    compoundDrawablePadding,
                    height
                )
            }
            else -> {
    
    
                throw IllegalStateException("position out of Range!")
            }
        }
        return imageBounds.contains(event.x.toInt(), event.y.toInt())
    }

    private fun getDrawableLength(
        drawable: Drawable?,
        isVertical: Int
    ) = if (drawable == null) {
    
    
        0
    } else if (isVertical == 0) {
    
    
        drawable.intrinsicHeight
    } else {
    
    
        drawable.intrinsicWidth
    }

    private fun getLeftRect(
        imageOneAxisCenter: Double,
        drawHeight: Int,
        drawWidth: Int,
        padding: Int
    ) = Rect(
        padding - lazyX,
        (imageOneAxisCenter - 0.5 * drawHeight - lazyY).toInt(),
        padding + drawWidth + lazyX,
        (imageOneAxisCenter + 0.5 * drawHeight + lazyY).toInt()
    )

    private fun getTopRect(
        imageOneAxisCenter: Double,
        drawWidth: Int,
        drawHeight: Int,
        padding: Int
    ) = Rect(
        (imageOneAxisCenter - 0.5 * drawWidth - lazyX).toInt(),
        padding - lazyY,
        (imageOneAxisCenter + 0.5 * drawWidth + lazyX).toInt(),
        padding + drawHeight + lazyY
    )

    private fun getRightRect(
        imageOneAxisCenter: Double,
        drawHeight: Int,
        drawWidth: Int,
        padding: Int,
        viewWidth: Int
    ) = Rect(
        viewWidth - padding - drawWidth - lazyX,
        (imageOneAxisCenter - 0.5 * drawHeight - lazyY).toInt(),
        viewWidth - padding + lazyX,
        (imageOneAxisCenter + 0.5 * drawHeight + lazyY).toInt()
    )

    private fun getBottomRect(
        imageOneAxisCenter: Double,
        drawHeight: Int,
        drawWidth: Int,
        padding: Int,
        viewHeight: Int
    ) = Rect(
        (imageOneAxisCenter - 0.5 * drawWidth - lazyX).toInt(),
        viewHeight - padding - drawHeight - lazyY,
        (imageOneAxisCenter + 0.5 * drawWidth + lazyX).toInt(),
        viewHeight - padding + lazyY
    )


    /**
     * 重置各个图片touch的状态
     */
    private fun resetTouchStatus() {
    
    
        repeat(4) {
    
     i ->
            drawablesTouch[i] = false
        }
    }


    override fun onClick(v: View?) {
    
    
        drawableClickListener?.apply {
    
    
            drawablesTouch.forEachIndexed {
    
     index, isTouch ->
                if (isTouch) {
    
    
                    onClick(
                        this@DrawableClickableTextView,
                        DrawableClickListener.Position.values()[index]
                    )
                    return
                }
            }
            onClick(this@DrawableClickableTextView, DrawableClickListener.Position.TEXT)
        }
    }

    @FunctionalInterface
    interface DrawableClickListener {
    
    
        /**
         * 点击相应位置的响应函数,点击文字也会进行响应。
         */
        fun onClick(view: DrawableClickableTextView, position: Position)

        /**
         * 点击的位置
         */
        enum class Position {
    
    
            /**
             * TextView左部的图片
             */
            LEFT,

            /**
             * TextView上部的图片
             */
            TOP,

            /**
             * TextView右部的图片
             */
            RIGHT,

            /**
             * TextView底部的图片
             */
            BOTTOM,

            /**
             * 文字
             */
            TEXT
        }
    }

    //代码调用时进行drawables更新

    override fun setCompoundDrawables(
        left: Drawable?,
        top: Drawable?,
        right: Drawable?,
        bottom: Drawable?
    ) {
    
    
        super.setCompoundDrawables(left, top, right, bottom)
        drawables = arrayOf(left, top, right, bottom)
    }

    override fun setCompoundDrawablesWithIntrinsicBounds(
        left: Drawable?,
        top: Drawable?,
        right: Drawable?,
        bottom: Drawable?
    ) {
    
    
        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)
        drawables = arrayOf(left, top, right, bottom)
    }

    override fun setCompoundDrawablesRelative(
        start: Drawable?,
        top: Drawable?,
        end: Drawable?,
        bottom: Drawable?
    ) {
    
    
        super.setCompoundDrawablesRelative(start, top, end, bottom)
        drawables = super.getCompoundDrawablesRelative()
    }

    override fun setCompoundDrawablesRelativeWithIntrinsicBounds(
        start: Drawable?,
        top: Drawable?,
        end: Drawable?,
        bottom: Drawable?
    ) {
    
    
        super.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom)
        drawables = super.getCompoundDrawablesRelative()
    }


}

使用方法

与普通TextView几乎完全一样,唯一不同就是点击Listener需要设置专属的DrawableClickListener。

参考资料

Android 可响应drawable点击事件的TextView
可以响应各个方向CompoundDrawables点击操作的TextView的实现原理

响应区域计算方法(非原创,仅做备份)

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Reven_L/article/details/126524121