管灌系统巡查员智能手机App
expand_button/src/main/java/com/example/expand_button/ExpandButton.kt
@@ -1,14 +1,319 @@
package com.example.expand_button
class ExpandButton {
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ClickableSpan
import android.util.AttributeSet
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.ContextCompat
/**
 * 可展开的按钮控件
 * 初始状态显示单个字符,点击后展开显示完整文字
 * 展开后的每个字符都可以单独点击
 */
class ExpandButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {
    // 展开时显示的完整文字
    private var expandedText: String = ""
    // 收起时显示的单个字符
    private var collapsedText: String = ""
    // 当前是否处于展开状态
    private var isExpanded: Boolean = false
    // 动画持续时间,默认300毫秒
    private var animationDuration: Long = 300
    // 字符点击事件监听器
    private var onCharClickListener: ((Char, Int) -> Unit)? = null
    // 字间距,默认为2dp
    private var customLetterSpacing: Float = context.resources.displayMetrics.density * 2
    // 三角形图标
    private val triangleDrawable: Drawable = ContextCompat.getDrawable(
        context,
        R.drawable.ic_triangle
    )!!.mutate()
    // 图标旋转角度
    private var triangleRotation = 0f
    // 三角形图标与文字的间距,默认为8dp
    private var triangleMargin: Float = 3 * context.resources.displayMetrics.density
    init {
        // 读取自定义属性
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.ExpandButton,
            defStyleAttr,
            0
        ).apply {
            try {
                customLetterSpacing = getDimension(R.styleable.ExpandButton_letterSpacing, customLetterSpacing)
                expandedText = getString(R.styleable.ExpandButton_expandedText) ?: ""
                collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: ""
                animationDuration = getInteger(R.styleable.ExpandButton_animDuration, 300).toLong()
                triangleMargin = getDimension(R.styleable.ExpandButton_triangleMargin, triangleMargin)
            } finally {
                recycle()
            }
        }
        // 设置初始文本和宽度
        if (collapsedText.isNotEmpty()) {
            text = collapsedText
            post {
                // 确保初始宽度为收起状态的宽度
                layoutParams = layoutParams.apply {
                    width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
                }
            }
        }
        // 设置文本可点击,仅在收起状态时响应点击展开
        setOnClickListener {
            if (!isExpanded) {
                toggleExpand()
            }
        }
        // 添加触摸事件处理
        setOnTouchListener { _, event ->
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    // 检查点击是否在三角形图标区域内
                    if (isClickOnTriangle(event.x)) {
                        toggleExpand()
                        return@setOnTouchListener true
                    }
                }
            }
            false
        }
        // 设置左边距,为图标留出空间
        compoundDrawablePadding = triangleMargin.toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 左边距增加,为图标留空间
            paddingTop,
            paddingRight,
            paddingBottom
        )
        // 设置单行显示,防止高度变化
        maxLines = 1
        isSingleLine = true
        // 设置文字垂直居中
        gravity = Gravity.CENTER_VERTICAL
    }
    override fun onDraw(canvas: Canvas) {
        // 保存画布状态
        canvas.save()
        // 计算图标位置
        val iconSize = triangleDrawable.intrinsicWidth
        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
        val iconTop = (height - iconSize) / 2
        // 设置图标边界
        triangleDrawable.setBounds(
            iconLeft,
            iconTop,
            iconLeft + iconSize,
            iconTop + iconSize
        )
        // 旋转画布
        canvas.rotate(
            triangleRotation,
            (iconLeft + iconSize / 2).toFloat(),
            (iconTop + iconSize / 2).toFloat()
        )
        // 绘制图标
        triangleDrawable.draw(canvas)
        // 恢复画布状态
        canvas.restore()
        super.onDraw(canvas)
    }
    /**
     * 设置字间距
     * @param spacing 间距值(像素)
     */
    fun setCustomLetterSpacing(spacing: Float) {
        this.customLetterSpacing = spacing
        if (isExpanded) {
            setExpandedClickableText()
        }
    }
    /**
     * 设置展开和收起时显示的文字
     * @param expanded 展开时显示的完整文字
     * @param collapsed 收起时显示的单个字符
     */
    fun setExpandText(expanded: String, collapsed: String) {
        this.expandedText = expanded
        this.collapsedText = collapsed
        text = collapsedText
    }
    /**
     * 设置单个字符点击监听器
     * @param listener 点击回调,参数为被点击的字符和位置
     *                char: 被点击的字符
     *                position: 字符在文本中的位置(从0开始)
     */
    fun setOnCharClickListener(listener: (char: Char, position: Int) -> Unit) {
        this.onCharClickListener = listener
    }
    /**
     * 设置展开/收起动画的持续时间
     * @param duration 动画持续时间(毫秒)
     */
    fun setAnimationDuration(duration: Long) {
        this.animationDuration = duration
    }
    /**
     * 切换展开/收起状态
     * 使用ValueAnimator实现宽度动画和图标旋转
     */
    private fun toggleExpand() {
        isExpanded = !isExpanded
        // 计算收起和展开状态的宽度
        val collapsedWidth = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
        val expandedWidth = calculateExpandedWidth()
        // 创建宽度动画
        ValueAnimator.ofInt(
            if (isExpanded) collapsedWidth else expandedWidth,
            if (isExpanded) expandedWidth else collapsedWidth
        ).apply {
            duration = animationDuration
            addUpdateListener { animator ->
                layoutParams = layoutParams.apply {
                    width = animator.animatedValue as Int
                }
                requestLayout()
            }
            start()
        }
        // 创建图标旋转动画
        ValueAnimator.ofFloat(
            if (isExpanded) 0f else 180f,
            if (isExpanded) 180f else 0f
        ).apply {
            duration = animationDuration
            addUpdateListener { animator ->
                triangleRotation = animator.animatedValue as Float
                invalidate() // 重绘以更新图标旋转
            }
            start()
        }
        // 更新文本
        if (isExpanded) {
            setExpandedClickableText()
        } else {
            text = collapsedText
        }
    }
    /**
     * 计算展开后的总宽度
     */
    private fun calculateExpandedWidth(): Int {
        val spaceWidth = paint.measureText(" ") * (customLetterSpacing / 10)
        // 计算所有字符的总宽度
        val textWidth = expandedText.fold(0f) { acc, char ->
            acc + paint.measureText(char.toString())
        }
        // 计算间距的总宽度(字符数量减1个间距)
        val spacesWidth = spaceWidth * (expandedText.length - 1)
        return (textWidth + spacesWidth).toInt() + paddingLeft + paddingRight
    }
    /**
     * 设置展开后的可点击文本
     */
    private fun setExpandedClickableText() {
        val builder = SpannableStringBuilder()
        expandedText.forEachIndexed { index, char ->
            // 添加字符
            builder.append(char)
            // 为字符设置点击事件
            val clickableSpan = object : ClickableSpan() {
                override fun onClick(view: View) {
                    onCharClickListener?.invoke(char, index)
                }
                override fun updateDrawState(ds: android.text.TextPaint) {
                    super.updateDrawState(ds)
                    // 移除下划线
                    ds.isUnderlineText = false
                    // 保持原始文字颜色
                    ds.color = currentTextColor
                }
            }
            builder.setSpan(
                clickableSpan,
                builder.length - 1,
                builder.length,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            )
            // 只在非最后一个字符后添加空格作为间距
            if (index < expandedText.length - 1) {
                builder.append(" ".repeat((customLetterSpacing / 10).toInt()))
            }
        }
        text = builder
        // 启用LinkMovementMethod以响应ClickableSpan的点击事件
        movementMethod = android.text.method.LinkMovementMethod.getInstance()
    }
    /**
     * 判断点击是否在三角形图标区域内
     */
    private fun isClickOnTriangle(x: Float): Boolean {
        val iconSize = triangleDrawable.intrinsicWidth
        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
        return x <= paddingLeft && x >= iconLeft
    }
    /**
     * 设置三角形图标与文字的间距
     * @param margin 间距值(像素)
     */
    fun setTriangleMargin(margin: Float) {
        this.triangleMargin = margin
        compoundDrawablePadding = margin.toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + margin).toInt(),
            paddingTop,
            paddingRight,
            paddingBottom
        )
    }
}