管灌系统巡查员智能手机App
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
package com.example.expand_button
 
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
        )
    }
}