管灌系统巡查员智能手机App
zuoxiao
2025-02-21 092bf21368ea824e9dc22467166960219165dc00
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
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
    }