起源
很多时候,我们需要一个图标加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的实现原理
响应区域计算方法(非原创,仅做备份)