package com.example.expand_button
|
|
import android.animation.ValueAnimator
|
import android.content.Context
|
import android.graphics.Canvas
|
import android.graphics.Paint
|
import android.graphics.RectF
|
import android.graphics.drawable.Drawable
|
import android.text.InputType
|
import android.util.AttributeSet
|
import android.view.Gravity
|
import android.view.MotionEvent
|
import android.view.ViewGroup
|
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.core.content.ContextCompat
|
import android.util.TypedValue
|
import androidx.core.animation.addListener
|
import android.util.Log
|
|
class ExpandSearchView @JvmOverloads constructor(
|
context: Context,
|
attrs: AttributeSet? = null,
|
defStyleAttr: Int = 0
|
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
|
// 搜索图标
|
private val searchIcon: Drawable = ContextCompat.getDrawable(
|
context,
|
R.drawable.ic_search
|
)!!.mutate()
|
|
// 搜索按钮文字
|
private var searchButtonText: String = "搜索"
|
|
// 搜索按钮点击监听器
|
private var onSearchClickListener: (() -> Unit)? = null
|
|
// 当前是否处于展开状态
|
private var isExpanded: Boolean = false
|
// 动画持续时间,默认300毫秒
|
private var animationDuration: Long = 300
|
// 搜索图标与文字的间距,默认为8dp
|
private var iconMargin: Float = 8 * context.resources.displayMetrics.density
|
// 搜索按钮与输入框的间距,默认为8dp
|
private var searchButtonMargin: Float = 8 * context.resources.displayMetrics.density
|
// 提示文字
|
private var hintText: String = "请输入取水口或分水房名称"
|
|
// 将defaultHeight移到类的属性中
|
private val defaultHeight = (40 * context.resources.displayMetrics.density).toInt()
|
|
// 添加展开方向的属性
|
private var expandDirection: Int = EXPAND_LEFT
|
|
companion object {
|
const val EXPAND_LEFT = 0
|
const val EXPAND_RIGHT = 1
|
}
|
|
init {
|
// 设置单行输入
|
maxLines = 1
|
isSingleLine = true
|
|
// 设置搜索图标的大小
|
val iconSize = (24 * context.resources.displayMetrics.density).toInt()
|
// 不在这里设置图标边界,而是在onDraw中设置
|
|
// 设置提示文字
|
hint = hintText
|
|
// 设置文字垂直居中
|
gravity = Gravity.CENTER_VERTICAL or Gravity.START
|
|
// 设置固定高度
|
layoutParams = ViewGroup.LayoutParams(
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
defaultHeight
|
)
|
|
// 初始状态下禁用输入
|
inputType = InputType.TYPE_NULL
|
|
// 初始状态下隐藏输入框
|
if (!isExpanded) {
|
hint = ""
|
setText("")
|
isEnabled = false
|
|
// 确保初始宽度为收起状态的宽度
|
post {
|
layoutParams = layoutParams?.apply {
|
width = defaultHeight // 使用相同的高度作为宽度,保持正方形
|
height = defaultHeight // 保持固定高度
|
}
|
invalidate() // 添加这行确保图标重绘
|
}
|
}
|
}
|
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
// 设置固定高度
|
val defaultHeight = (40 * context.resources.displayMetrics.density).toInt()
|
|
if (!isExpanded) {
|
// 收缩状态下的宽度设为正方形
|
setMeasuredDimension(defaultHeight, defaultHeight)
|
} else {
|
// 展开状态下保持指定的宽度,但高度固定
|
setMeasuredDimension(measuredWidth, defaultHeight)
|
}
|
}
|
|
override fun onDraw(canvas: Canvas) {
|
if (!isExpanded) {
|
// 计算图标位置并绘制
|
val centerX = width / 2
|
val centerY = height / 2
|
val iconSize = (24 * context.resources.displayMetrics.density).toInt()
|
val iconHalfWidth = iconSize / 2
|
val iconHalfHeight = iconSize / 2
|
searchIcon.setBounds(
|
centerX - iconHalfWidth,
|
centerY - iconHalfHeight,
|
centerX + iconHalfWidth,
|
centerY + iconHalfHeight
|
)
|
searchIcon.draw(canvas)
|
} else {
|
val iconSize = searchIcon.intrinsicWidth
|
val centerY = height / 2
|
val iconLeftMargin = (5 * context.resources.displayMetrics.density).toInt()
|
|
// 绘制图标
|
searchIcon.setBounds(
|
iconLeftMargin,
|
centerY - iconSize / 2,
|
iconLeftMargin + iconSize,
|
centerY + iconSize / 2
|
)
|
searchIcon.draw(canvas)
|
|
// 绘制分隔线
|
paint.apply {
|
color = 0xFFFFFFFF.toInt()
|
strokeWidth = (1 * context.resources.displayMetrics.density)
|
}
|
val dividerX = iconLeftMargin + iconSize + (iconMargin / 2)
|
canvas.drawLine(
|
dividerX,
|
height * 0.2f,
|
dividerX,
|
height * 0.8f,
|
paint
|
)
|
|
// 记录文本绘制区域的信息
|
val buttonWidth = paint.measureText(searchButtonText)
|
val textLeft = compoundPaddingLeft.toFloat() // 使用复合内边距
|
val textRight = width - compoundPaddingRight.toFloat() // 使用复合内边距
|
|
// 只在实际绘制文本时记录一次区域信息
|
if (text!!.isNotEmpty() && !text.toString().endsWith("\n")) {
|
Log.d("ExpandSearchView", "文本绘制区域信息: 控件尺寸[宽度:$width, 高度:$height], " +
|
"文本区域[左边界:$textLeft, 右边界:$textRight, 可用宽度:${textRight - textLeft}], " +
|
"内边距[左:$paddingLeft, 右:$paddingRight, 复合左:$compoundPaddingLeft, 复合右:$compoundPaddingRight], " +
|
"文本信息[当前文本:'${text}', 长度:${text?.length}, 光标位置:$selectionStart], " +
|
"绘制参数[图标大小:$iconSize, 图标左边距:$iconLeftMargin, 按钮宽度:$buttonWidth, 按钮边距:$searchButtonMargin], " +
|
"位置信息[translationX:$translationX, scrollX:$scrollX, 文本偏移:${textLeft - compoundPaddingLeft}]")
|
}
|
|
// 画文本
|
super.onDraw(canvas)
|
|
// 绘制按钮文本
|
paint.apply {
|
color = currentTextColor
|
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16f, context.resources.displayMetrics)
|
textAlign = Paint.Align.CENTER
|
}
|
val buttonText = searchButtonText
|
val fontMetrics = paint.fontMetrics
|
val textY = height / 2f + (fontMetrics.bottom - fontMetrics.top) / 2f - fontMetrics.bottom
|
val buttonX = width - buttonWidth / 2 - searchButtonMargin
|
|
// 记录搜索按钮位置信息
|
Log.d("ExpandSearchView", "搜索按钮信息: 位置[X:$buttonX, Y:$textY], " +
|
"区域[左:${buttonX - buttonWidth/2}, 右:${buttonX + buttonWidth/2}, 上:0, 下:$height], " +
|
"尺寸[宽度:$buttonWidth, 高度:$height]")
|
|
canvas.drawText(
|
buttonText,
|
buttonX,
|
textY,
|
paint
|
)
|
}
|
}
|
|
override fun onTextChanged(
|
text: CharSequence?,
|
start: Int,
|
lengthBefore: Int,
|
lengthAfter: Int
|
) {
|
super.onTextChanged(text, start, lengthBefore, lengthAfter)
|
// 只在展开状态且实际有新字符输入时才输出日志
|
if (isExpanded && lengthAfter > lengthBefore) {
|
val newChar = text?.subSequence(start, start + lengthAfter - lengthBefore)
|
Log.d("ExpandSearchView", "新字符输入信息: 输入内容[新字符:'$newChar', 完整文本:'$text'], " +
|
"位置信息[输入位置:$start, 光标位置:$selectionStart], " +
|
"文本区域[左内边距:$paddingLeft, 右内边距:$paddingRight, 可见宽度:${width - paddingLeft - paddingRight}], " +
|
"滚动状态[scrollX:$scrollX, translationX:$translationX]")
|
}
|
}
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
if (event.action == MotionEvent.ACTION_UP) {
|
if (isExpanded) {
|
// 展开状态下,检查是否点击了搜索按钮
|
val buttonText = searchButtonText
|
val buttonWidth = paint.measureText(buttonText)
|
val buttonArea = RectF(
|
width - buttonWidth - searchButtonMargin * 2,
|
0f,
|
width.toFloat(),
|
height.toFloat()
|
)
|
|
if (buttonArea.contains(event.x, event.y)) {
|
onSearchClickListener?.invoke()
|
post { toggleExpand() }
|
return true
|
}
|
} else {
|
toggleExpand()
|
return true
|
}
|
}
|
return super.onTouchEvent(event)
|
}
|
|
override fun performClick(): Boolean {
|
// 确保可访问性服务正常工作
|
super.performClick()
|
return true
|
}
|
|
/**
|
* 切换展开/收起状态
|
*/
|
private fun toggleExpand() {
|
isExpanded = !isExpanded
|
|
val startWidth = width
|
val screenWidth = context.resources.displayMetrics.widthPixels
|
val location = IntArray(2)
|
getLocationInWindow(location)
|
|
// 获取父布局信息
|
val parent = parent as? ViewGroup
|
val parentWidth = parent?.width ?: screenWidth
|
|
// 获取或设置边距
|
var params = layoutParams as? ViewGroup.MarginLayoutParams
|
if (params == null) {
|
params = ViewGroup.MarginLayoutParams(layoutParams)
|
layoutParams = params
|
}
|
val margin = params.rightMargin.takeIf { it > 0 }
|
?: (45 * context.resources.displayMetrics.density).toInt() // 默认45dp的边距
|
|
// 计算目标宽度,考虑两边的边距
|
val endWidth = if (isExpanded) {
|
val iconSize = searchIcon.intrinsicWidth
|
val iconMarginPx = (5 * context.resources.displayMetrics.density).toInt()
|
val buttonWidth = paint.measureText(searchButtonText).toInt()
|
val hintWidth = paint.measureText(hintText)
|
val minRequiredWidth = iconMarginPx + iconSize + iconMargin.toInt() + // 左侧图标区域
|
hintWidth.toInt() + // 提示文字宽度
|
buttonWidth + (searchButtonMargin * 2).toInt() + // 按钮区域
|
(16 * context.resources.displayMetrics.density).toInt() // 额外边距
|
|
maxOf(minRequiredWidth, parentWidth - (margin * 2)) // 取所需宽度和父容器宽度的较大值
|
} else {
|
defaultHeight
|
}
|
|
val initialTranslationX = translationX
|
ValueAnimator.ofFloat(0f, 1f).apply {
|
duration = animationDuration
|
addUpdateListener { animator ->
|
val fraction = animator.animatedValue as Float
|
val currentWidth = if (isExpanded) {
|
startWidth + ((endWidth - startWidth) * fraction).toInt()
|
} else {
|
startWidth - ((startWidth - endWidth) * fraction).toInt()
|
}
|
|
(layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
|
width = currentWidth
|
rightMargin = margin
|
leftMargin = if (isExpanded) margin else 0 // 展开时添加左边距
|
}?.also { params ->
|
layoutParams = params
|
}
|
|
// 修正位移,当控件展开时translationX设置为0
|
if (isExpanded) {
|
translationX = 0f
|
} else {
|
translationX = initialTranslationX * (1 - fraction)
|
}
|
|
requestLayout()
|
invalidate()
|
}
|
|
addListener(onStart = {
|
if (!isExpanded) {
|
setText("")
|
}
|
}, onEnd = {
|
if (!isExpanded) {
|
translationX = 0f
|
setPadding(
|
defaultHeight / 4,
|
defaultHeight / 4,
|
defaultHeight / 4,
|
defaultHeight / 4
|
)
|
hint = ""
|
isEnabled = false
|
clearFocus()
|
inputType = InputType.TYPE_NULL
|
isFocusable = false
|
isFocusableInTouchMode = false
|
|
// 收起时移除左边距
|
(layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
|
leftMargin = 0
|
}?.also { params ->
|
layoutParams = params
|
}
|
}
|
if (isExpanded) {
|
hint = hintText
|
isEnabled = true
|
inputType = InputType.TYPE_CLASS_TEXT
|
isFocusable = true
|
isFocusableInTouchMode = true
|
|
val iconSize = searchIcon.intrinsicWidth
|
val iconMarginPx = (5 * context.resources.displayMetrics.density).toInt()
|
val verticalPadding = (8 * context.resources.displayMetrics.density).toInt()
|
val buttonWidth = paint.measureText(searchButtonText).toInt()
|
val hintWidth = paint.measureText(hintText)
|
|
// 记录提示文字信息
|
Log.d("ExpandSearchView", "提示文字信息: 文本['$hintText'], " +
|
"宽度信息[文本宽度:$hintWidth, 可用宽度:${width - iconMarginPx - iconSize - iconMargin - buttonWidth - searchButtonMargin * 2}], " +
|
"内边距[左:${iconMarginPx + iconSize + iconMargin.toInt()}, 右:${buttonWidth + (searchButtonMargin * 2).toInt()}]")
|
|
setPadding(
|
iconMarginPx + iconSize + iconMargin.toInt(), // 左边距包含图标和间距
|
verticalPadding,
|
buttonWidth + (searchButtonMargin * 2).toInt(), // 右边距包含按钮和间距
|
verticalPadding
|
)
|
requestFocus()
|
}
|
})
|
start()
|
}
|
}
|
|
|
/**
|
* 设置搜索图标
|
*/
|
fun setSearchIcon(icon: Drawable) {
|
val size = (24 * context.resources.displayMetrics.density).toInt()
|
icon.setBounds(0, 0, size, size)
|
searchIcon.bounds = icon.bounds
|
invalidate()
|
}
|
|
/**
|
* 设置搜索图标与文字的间距
|
*/
|
fun setIconMargin(margin: Float) {
|
iconMargin = margin
|
invalidate()
|
}
|
|
/**
|
* 设置提示文字
|
*/
|
fun setHintText(text: String) {
|
hintText = text
|
if (isExpanded) {
|
hint = hintText
|
}
|
}
|
|
/**
|
* 设置动画时长
|
*/
|
fun setAnimationDuration(duration: Long) {
|
animationDuration = duration
|
}
|
|
/**
|
* 设置搜索按钮与输入框的间距
|
*/
|
fun setSearchButtonMargin(margin: Float) {
|
searchButtonMargin = margin
|
invalidate()
|
}
|
|
/**
|
* 设置搜索按钮点击监听器
|
*/
|
fun setOnSearchClickListener(listener: () -> Unit) {
|
onSearchClickListener = listener
|
}
|
|
/**
|
* 设置搜索按钮文字
|
*/
|
fun setSearchButtonText(text: String) {
|
searchButtonText = text
|
if (isExpanded) {
|
invalidate()
|
}
|
}
|
|
/**
|
* 设置展开方向
|
*/
|
fun setExpandDirection(direction: Int) {
|
require(direction == EXPAND_LEFT || direction == EXPAND_RIGHT) {
|
"Direction must be either EXPAND_LEFT or EXPAND_RIGHT"
|
}
|
expandDirection = direction
|
}
|
}
|