|  |  | 
 |  |  | 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 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) | 
 |  |  |     } | 
 |  |  | } |