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的实现原理

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

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用以下步骤为Android App中的TextView中的Drawable添加点击事件: 1. 创建一个自定义DrawableClickListener类,该类实现了View.OnClickListener接口。 ```java public class DrawableClickListener implements View.OnClickListener { public enum DrawablePosition { TOP, BOTTOM, LEFT, RIGHT }; DrawablePosition drawablePosition; public DrawableClickListener(DrawablePosition drawablePosition) { super(); this.drawablePosition = drawablePosition; } @Override public void onClick(View v) { if (drawableClickListener != null) { drawableClickListener.onClick(drawablePosition); } } public interface DrawableClickListener { void onClick(DrawablePosition target); } private DrawableClickListener drawableClickListener; public void setDrawableClickListener(DrawableClickListener listener) { this.drawableClickListener = listener; } } ``` 2. 在TextView中添加drawable,并设置DrawableClickListener。 ```java Drawable drawable = ContextCompat.getDrawable(this, R.drawable.ic_launcher_background); drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); textView.setCompoundDrawables(null, drawable, null, null); DrawableClickListener drawableClickListener = new DrawableClickListener(DrawableClickListener.DrawablePosition.LEFT); drawableClickListener.setDrawableClickListener(new DrawableClickListener.DrawableClickListener() { @Override public void onClick(DrawableClickListener.DrawablePosition target) { Toast.makeText(MainActivity.this, "Drawable clicked", Toast.LENGTH_SHORT).show(); } }); textView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getRawX() >= (textView.getRight() - textView.getCompoundDrawables()[2].getBounds().width())) { drawableClickListener.onClick(DrawableClickListener.DrawablePosition.RIGHT); return true; } } return false; } }); ``` 上述代码中,我们首先创建了一个Drawable,并将其设置为TextView中的左侧Drawable。然后我们创建了一个DrawableClickListener,并将其设置为TextView的OnTouchListener。在OnTouchListener中,我们检查触摸事件的位置是否在Drawable上,并在这种情况下触发DrawableClickListener的onClick事件。 这样,每当用户单击TextView中的Drawable时,将会触发DrawableClickListener的onClick事件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值