管灌系统巡查员智能手机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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
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 data class LegendItem(
        val selectedIcon: Drawable,
        val unselectedIcon: Drawable,
        val description: String,
        var isSelected: Boolean = true
    )
    
    private var legendItems: List<LegendItem> = listOf()
    private var itemSpacing: Float = context.resources.displayMetrics.density * 16 // 图例项之间的水平间距
    private var iconSize: Int = (24 * context.resources.displayMetrics.density).toInt() // 图标大小
    private var iconTextSpacing: Float = context.resources.displayMetrics.density * 4 // 图标和文字之间的垂直间距
 
    // 展开时显示的完整文字
    private var expandedText: String = "标注点: 当前位置\n区域: 配送范围"
    // 收起时显示的单个字符
    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
 
    // 添加新属性
    private var textLines: List<String> = listOf()
 
    // 添加点击回调接口
    interface OnLegendItemClickListener {
        fun onLegendItemClick(position: Int, isSelected: Boolean)
    }
    
    private var legendItemClickListener: OnLegendItemClickListener? = null
 
    // 修改图例项的总高度计算
    private val legendItemHeight: Int
        get() = iconSize + iconTextSpacing.toInt() + paint.textSize.toInt() + paint.descent().toInt() - paint.ascent().toInt()
 
    // 添加展开后的字体大小属性
    private var expandedTextSize: Float = textSize
 
    // 添加一个变量保存默认字体大小
    private var defaultTextSize: Float = 0f
 
    // 添加一个属性定义三角形图标的点击区域扩展范围
    private val triangleClickPadding: Float = 15f * context.resources.displayMetrics.density // 20dp
 
    // 添加一个标识符,用于区分不同的 ExpandButton 实例
    private var buttonId: String = "default"
 
    companion object {
        private const val PREFS_NAME = "expand_button_prefs"
        private const val KEY_LEGEND_STATES = "legend_states"
    }
 
    init {
        // 保存 XML 中设置的默认字体大小
        defaultTextSize = textSize
 
        // 读取自定义属性
        context.theme.obtainStyledAttributes(
            attrs,
            R.styleable.ExpandButton,
            defStyleAttr,
            0
        ).apply {
            try {
                customLetterSpacing = getDimension(R.styleable.ExpandButton_letterSpacing, customLetterSpacing)
                expandedText = getString(R.styleable.ExpandButton_expandedText) ?: "标注点: 当前位置\n区域: 配送范围"
                collapsedText = getString(R.styleable.ExpandButton_collapsedText) ?: "图例"
                animationDuration = getInteger(R.styleable.ExpandButton_animDuration, 300).toLong()
                triangleMargin = getDimension(R.styleable.ExpandButton_triangleMargin, triangleMargin)
                itemSpacing = getDimension(R.styleable.ExpandButton_itemSpacing, itemSpacing)
                iconSize = getDimension(R.styleable.ExpandButton_iconSize, iconSize.toFloat()).toInt()
                iconTextSpacing = getDimension(R.styleable.ExpandButton_iconTextSpacing, iconTextSpacing)
                expandedTextSize = getDimension(R.styleable.ExpandButton_expandedTextSize, defaultTextSize)
            } finally {
                recycle()
            }
        }
 
        // 设置初始文本和宽度
        if (collapsedText.isNotEmpty()) {
            text = collapsedText
            post {
                // 确保初始宽度为收起状态的宽度
                layoutParams = layoutParams.apply {
                    width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
                }
            }
        }
 
        // 修改触摸事件处理
        setOnTouchListener(null) // 移除原有的触摸监听器
        
        // 移除原有的点击监听器
        setOnClickListener(null)
 
        // 设置左边距,为图标留出空间
        compoundDrawablePadding = triangleMargin.toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 左边距增加,为图标留空间
            paddingTop,
            paddingRight,
            paddingBottom
        )
 
        // 设置单行显示,防止高度变化
        maxLines = if (isExpanded) Int.MAX_VALUE else 1
        isSingleLine = !isExpanded
        
        // 设置文字垂直居中
        gravity = Gravity.CENTER_VERTICAL
 
        // 设置默认的内边距
        val defaultPadding = (8 * context.resources.displayMetrics.density).toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + triangleMargin).toInt(), // 左边距增加,为图标留空间
            defaultPadding, // 上边距
            defaultPadding, // 右边距
            defaultPadding  // 下边距
        )
    }
 
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        
        if (isExpanded) {
            // 展开状态下的高度计算
            val desiredHeight = legendItemHeight + paddingTop + paddingBottom
            setMeasuredDimension(measuredWidth, desiredHeight)
        }
    }
 
    override fun onDraw(canvas: Canvas) {
        // 绘制展开/收起图标
        drawTriangle(canvas)
        
        if (!isExpanded) {
            // 收起状态使用默认字体大小
            paint.textSize = defaultTextSize
            super.onDraw(canvas)
            return
        }
        
        // 展开状态使用展开后的字体大小
        paint.textSize = expandedTextSize
        
        // 计算所有图例项中最宽的宽度
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        
        // 计算总宽度(添加首尾的边距)
        val totalWidth = legendItems.size * maxWidth + 
            (legendItems.size + 1) * itemSpacing  // 修改这里,添加一个额外的间距
        
        // 计算起始x坐标,使整体水平居中
        var x = (width - totalWidth) / 2 + itemSpacing  // 添加起始边距
        
        // 计算垂直居中的y坐标,考虑上下边距
        val centerY = height / 2f
        
        legendItems.forEachIndexed { index, item ->
            // 计算当前图例项的起始位置(移除index > 0的判断)
            val itemStartX = x
            
            // 计算图标的水平位置(居中于图例项)
            val iconLeft = itemStartX + (maxWidth - iconSize) / 2
            
            // 计算文字的位置
            val textWidth = paint.measureText(item.description)
            val textX = itemStartX + (maxWidth - textWidth) / 2
            
            // 绘制图标,根据选中状态选择不同的图标
            val iconTop = paddingTop + (height - legendItemHeight) / 2
            val currentIcon = if (item.isSelected) item.selectedIcon else item.unselectedIcon
            currentIcon.setBounds(
                iconLeft.toInt(),
                iconTop.toInt(),
                (iconLeft + iconSize).toInt(),
                (iconTop + iconSize).toInt()
            )
            currentIcon.draw(canvas)
            
            // 绘制文字,根据选中状态使用不同的颜色
            paint.color = if (item.isSelected) 
                currentTextColor 
            else 
                0xFF999999.toInt() // 灰色
            val textY = iconTop + iconSize + iconTextSpacing - paint.ascent()
            canvas.drawText(item.description, textX, textY, paint)
            
            // 更新下一个图例项的起始位置
            x = itemStartX + maxWidth + itemSpacing
        }
        
        // 恢复画笔颜色
        paint.color = currentTextColor
    }
 
    // 将原来的onDraw中的三角形绘制逻辑提取出来
    private fun drawTriangle(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()
    }
 
    /**
     * 设置字间距
     * @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
    }
 
    /**
     * 切换展开/收起状态
     */
    private fun toggleExpand() {
        isExpanded = !isExpanded
 
        // 计算收起和展开状态的宽度
        val collapsedWidth = run {
            paint.textSize = defaultTextSize
            val width = paint.measureText(collapsedText).toInt() + paddingLeft + paddingRight
            paint.textSize = if (isExpanded) expandedTextSize else defaultTextSize
            width
        }
        
        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) {
            paint.textSize = expandedTextSize
        } else {
            text = collapsedText
            paint.textSize = defaultTextSize
            setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, defaultTextSize)
        }
 
        invalidate()
    }
 
    /**
     * 计算展开后的总宽度
     */
    private fun calculateExpandedWidth(): Int {
        // 临时保存当前字体大小
        val currentTextSize = paint.textSize
        // 设置为展开状态的字体大小
        paint.textSize = expandedTextSize
        
        try {
            // 计算所有图例项中最宽的宽度
            val maxWidth = legendItems.maxOf { item ->
                maxOf(paint.measureText(item.description), iconSize.toFloat())
            }
            
            // 计算总宽度 = 所有图例项的宽度 + 所有间距(包括首尾) + 左右内边距
            return (legendItems.size * maxWidth + 
                (legendItems.size + 1) * itemSpacing +  
                paddingLeft + paddingRight).toInt()
        } finally {
            // 恢复原来的字体大小
            paint.textSize = currentTextSize
        }
    }
 
    /**
     * 设置展开后的可点击文本
     */
    private fun setExpandedClickableText() {
        val builder = SpannableStringBuilder()
        
        // 计算所有图例项中最宽的宽度
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        
        // 计算总宽度
        val totalWidth = legendItems.size * maxWidth + 
            (legendItems.size - 1) * itemSpacing
        
        // 计算整体水平居中需要的起始空格
        val startPadding = ((width - totalWidth) / 2 / paint.measureText(" ")).toInt()
        if (startPadding > 0) {
            builder.append(" ".repeat(startPadding))
        }
        
        // 添加垂直空间,为图标预留位置
        val verticalSpaces = ((iconSize + iconTextSpacing) / paint.textSize).toInt()
        builder.append("\n".repeat(verticalSpaces))
        
        legendItems.forEachIndexed { index, item ->
            if (index > 0) {
                // 在图例项之间添加水平间距
                builder.append(" ".repeat((itemSpacing / paint.measureText(" ")).toInt()))
            }
            
            // 计算水平居中所需的空格数
            val textWidth = paint.measureText(item.description)
            val paddingSpaces = ((maxWidth - textWidth) / 2 / paint.measureText(" ")).toInt()
            
            // 添加左侧空格实现居中
            if (paddingSpaces > 0) {
                builder.append(" ".repeat(paddingSpaces))
            }
            
            // 添加描述文本
            val startPosition = builder.length
            builder.append(item.description)
            
            // 添加右侧空格以确保宽度一致
            val remainingSpaces = ((maxWidth - textWidth) / paint.measureText(" ")).toInt() - paddingSpaces
            if (remainingSpaces > 0) {
                builder.append(" ".repeat(remainingSpaces))
            }
            
            // 为文字设置点击事件
            val clickableSpan = object : ClickableSpan() {
                override fun onClick(view: View) {
                    onCharClickListener?.invoke(item.description[0], index)
                }
 
                override fun updateDrawState(ds: android.text.TextPaint) {
                    super.updateDrawState(ds)
                    ds.isUnderlineText = false
                    ds.color = currentTextColor
                }
            }
            
            builder.setSpan(
                clickableSpan,
                startPosition,
                startPosition + item.description.length,
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
            )
        }
        
        // 设置文本对齐方式为居中
        gravity = Gravity.CENTER
        
        text = builder
        movementMethod = android.text.method.LinkMovementMethod.getInstance()
    }
 
    /**
     * 判断点击是否在三角形图标区域内
     */
    private fun isClickOnTriangle(x: Float): Boolean {
        val iconSize = triangleDrawable.intrinsicWidth
        val iconLeft = paddingLeft - iconSize - compoundDrawablePadding
        
        // 扩大点击区域:左右各增加 triangleClickPadding
        return x <= (paddingLeft + triangleClickPadding) && 
               x >= (iconLeft - triangleClickPadding)
    }
 
    /**
     * 设置三角形图标与文字的间距
     * @param margin 间距值(像素)
     */
    fun setTriangleMargin(margin: Float) {
        this.triangleMargin = margin
        compoundDrawablePadding = margin.toInt()
        setPadding(
            (16 * context.resources.displayMetrics.density + margin).toInt(),
            paddingTop,
            paddingRight,
            paddingBottom
        )
    }
 
    /**
     * 设置按钮的唯一标识符
     * @param id 标识符
     */
    fun setButtonId(id: String) {
        this.buttonId = id
        // 加载保存的状态
 
    }
 
 
    /**
     * 设置图例内容
     */
    @JvmName("setLegendsList")
    fun setLegends(items: List<Quadruple<Drawable, Drawable, String, Boolean>>) {
        legendItems = items.map { (selectedIcon, unselectedIcon, description, isSelected) ->
            selectedIcon.setBounds(0, 0, iconSize, iconSize)
            unselectedIcon.setBounds(0, 0, iconSize, iconSize)
            LegendItem(selectedIcon, unselectedIcon, description, isSelected)
        }
        
        if (!isExpanded) {
            text = collapsedText
        } else {
            invalidate()
        }
        requestLayout()
    }
 
    // 添加一个 Java 友好的方法
    @JvmName("setLegendsArray")
    fun setLegends(vararg items: Quadruple<Drawable, Drawable, String, Boolean>) {
        setLegends(items.toList())
    }
 
    // 添加一个数据类来表示四元组
    data class Quadruple<A, B, C, D>(
        val first: A,
        val second: B,
        val third: C,
        val fourth: D
    )
 
    // 添加一个便捷的扩展函数来创建 Quadruple
    fun <A, B, C, D> quadrupleOf(first: A, second: B, third: C, fourth: D): Quadruple<A, B, C, D> {
        return Quadruple(first, second, third, fourth)
    }
 
    /**
     * 添加设置监听器的方法
     */
    fun setOnLegendItemClickListener(listener: OnLegendItemClickListener) {
        legendItemClickListener = listener
    }
 
    /**
     * 处理触摸事件
     */
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                // 检查点击是否在三角形图标区域内
                if (isClickOnTriangle(event.x)) {
                    toggleExpand()
                    return true
                }
                
                // 如果是展开状态,检查是否点击了图例项
                if (isExpanded) {
                    val clickedIndex = getClickedItemIndex(event.x, event.y)
                    if (clickedIndex != -1) {
                        toggleItemSelection(clickedIndex)
                        return true
                    }
                } else if (!isExpanded && event.x > paddingLeft) {
                    // 在收起状态下,点击非三角形区域也展开
                    toggleExpand()
                    return true
                }
            }
        }
        return super.onTouchEvent(event)
    }
 
    /**
     * 获取点击位置对应的图例项索引
     */
    private fun getClickedItemIndex(x: Float, y: Float): Int {
        if (!isExpanded) return -1
 
        val maxWidth = legendItems.maxOf { item ->
            maxOf(paint.measureText(item.description), iconSize.toFloat())
        }
        
        val totalWidth = legendItems.size * maxWidth + 
            (legendItems.size - 1) * itemSpacing
        
        val startX = (width - totalWidth) / 2
        val iconTop = paddingTop + (height - legendItemHeight) / 2
        val iconBottom = iconTop + legendItemHeight
 
        // 检查垂直方向是否在图例项范围内
        if (y < iconTop || y > iconBottom) return -1
 
        // 检查水平方向点击的是哪个图例项
        legendItems.forEachIndexed { index, _ ->
            val itemStartX = startX + index * (maxWidth + itemSpacing)
            val itemEndX = itemStartX + maxWidth
            if (x >= itemStartX && x <= itemEndX) {
                return index
            }
        }
        return -1
    }
 
    /**
     * 切换图例项的选中状态
     */
    private fun toggleItemSelection(index: Int) {
        if (index < 0 || index >= legendItems.size) return
        
        legendItems[index].isSelected = !legendItems[index].isSelected
        legendItemClickListener?.onLegendItemClick(
            index, 
            legendItems[index].isSelected
        )
 
        invalidate()
    }
 
    /**
     * 设置展开后的字体大小
     * @param size 字体大小(像素)
     */
    fun setExpandedTextSize(size: Float) {
        this.expandedTextSize = size
        if (isExpanded) {
            invalidate()
        }
    }
 
    /**
     * 设置展开后的字体大小(SP)
     * @param sp 字体大小(SP)
     */
    fun setExpandedTextSizeSp(sp: Float) {
        setExpandedTextSize(sp * context.resources.displayMetrics.scaledDensity)
    }
}