puzhibing
2026-08-21 5fa94b3718b4a78a4fcea4c97652b0ef20b7de3a
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
package com.xw.yunwei.ui.task.create
 
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.amap.api.location.AMapLocation
import com.blankj.utilcode.util.UriUtils
import com.luck.picture.lib.config.PictureMimeType
import com.luck.picture.lib.entity.LocalMedia
import com.luck.picture.lib.interfaces.OnResultCallbackListener
import com.xw.common.CommonDialog
import com.xw.common.R
import com.xw.common.base.FileSelectBean
import com.xw.common.base.ImageEventListener
import com.xw.common.base.ImageSelectAdapter
import com.xw.common.base.ShowImgListDialog
import com.xw.common.utils.GpsHelper
import com.xw.common.utils.KeyPreferences
import com.xw.common.utils.MMKVManager
import com.xw.common.utils.ToastUtilCommon
import com.xw.common.viewAdapter.ImageDefault
import com.xw.common.viewAdapter.ViewAdapter
import com.xw.yunwei.BR
import com.xw.yunwei.BaseBusinessActivity
import com.xw.yunwei.databinding.ActivityCreateTaskBinding
import com.xw.yunwei.databinding.DialogAccidentFilterBinding
import com.xw.yunwei.extension.colorIdToColorValue
import com.xw.yunwei.extension.dpToPx
import com.xw.yunwei.extension.spToPx
import com.xw.yunwei.model.CacheAccidentModel
import com.xw.yunwei.model.CarModel
import com.xw.yunwei.utils.DialogFactory
import com.xw.yunwei.utils.PictureSelectorFactory
import kotlin.math.ceil
 
/**
 * 事故上报页面
 *
 * 提供事故工单创建功能,包括:
 * - 车辆信息展示与选择(接收上一页传递的 [CarModel])
 * - 高德定位获取当前位置地址
 * - 事故类型多级筛选(左侧大类 + 右侧子类)
 * - 任务紧急等级选择(P0 ~ P3)
 * - 任务描述文字输入
 * - 相关照片上传(拍照 / 图库)
 * - 草稿缓存:返回时若已填写内容,提示是否保存到 MMKV
 *
 * 实现 [GpsHelper.OnMapLocation] 接口以接收定位结果。
 */
class CreateTaskActivity : BaseBusinessActivity<ActivityCreateTaskBinding, CreateTaskVM>(),
    GpsHelper.OnMapLocation {
    override fun getBindingVariable() = BR.viewModel
 
    /** 事故类型筛选弹窗 */
    private var filterDialog: MaterialDialog? = null
 
    /** 左侧事故大类 Adapter */
    private lateinit var leftAdapter: LeftCategoryAdapter
 
    /** 右侧事故子类 Adapter */
    private lateinit var rightAdapter: RightItemAdapter
    /** 高德定位辅助类 */
//    lateinit var gpsHelper : GpsHelper
    /** 从 MMKV 恢复的草稿数据 */
    var cacheData: CacheAccidentModel? = null
 
    //用于测量
    private val textPaint = TextPaint()
 
    override fun myCreate(savedInstanceState: Bundle?) {
        super.myCreate(savedInstanceState)
        // 尝试读取本地缓存的事故草稿数据
        cacheData = MMKVManager.getInstance().getModel(KeyPreferences.ACCIDENT)
        MMKVManager.getInstance().deleteModel(KeyPreferences.ACCIDENT)
//         gpsHelper = GpsHelper(
//            this,
//            this,
//            isOnceLocation = true,
//            isAutomatic = cacheData == null,
//            savedInstanceState = savedInstanceState
//        )
//        lifecycle.addObserver(gpsHelper)
    }
 
    override fun init() {
        super.init()
        initAdapter()
        val selectCar = intent.getParcelableExtra<CarModel>("car")
        // 若存在缓存草稿,则恢复相关字段;否则仅设置选中的车辆
        cacheData?.let {
//            viewModel.selectCar.value = it.car
            viewModel.selectCar.value = selectCar
//            viewModel.location.value = it.location
            viewModel.categories = it.categories ?: ArrayList()
            viewModel.taskLevel.value = it.taskLevel
            viewModel.taskDesc.value = it.taskDesc
            viewModel.adapter.setList(it.files)
            refreshFilterData()
        } ?: run { viewModel.selectCar.value = selectCar }
 
//        if (viewModel.selectCar.value == null) {
//            startActivityForResult(
//                SelectCarActivity::class.java,
//                110,
//                bundleOf("car" to null)
//            )
//        }
        // 有缓存但定位为空时,重新发起定位
//        if (cacheData != null && viewModel.location.value == null) {
//            gpsHelper.startLocation()
//        }
        viewModel.getVehicleLocation()
 
        // 检查任务等级选项文字是否重叠,重叠则将横向内边距减半
        binding.root.post { adjustLevelPaddingIfOverlap() }
    }
 
    override fun setObservable() {
        super.setObservable()
        // 监听选中车辆变化,更新车辆信息布局
        viewModel.selectCar.observe(this) {
            initCarLayout(it)
        }
        viewModel.carLocationInfo.observe(this) {
            binding.tvAddress.text = it?.address ?: "未知位置"
        }
    }
 
    override fun setListener() {
        super.setListener()
        binding.llFilters.setOnClickListener {
            viewModel.getTypeList {
                showFilterDialog()
            }
        }
        // 点击事故类型筛选按钮,先拉取类型列表再弹窗
        binding.tvAccidentFilter.setOnClickListener {
            viewModel.getTypeList {
                showFilterDialog()
            }
        }
    }
 
    /**
     * 初始化图片选择适配器
     *
     * 绑定单组图片 RecyclerView,最大数量由 [CreateTaskVM.maxSize] 控制。
     */
    private fun initAdapter() {
        val imageEvent = object : ImageEventListener<FileSelectBean>() {
            override fun addImage(item: FileSelectBean) {
                showTypeDialog()
            }
 
            override fun viewImage(item: FileSelectBean) {
                val imgList: MutableList<String> = ArrayList()
                for (item1 in viewModel.adapter.data) {
                    imgList.add(item1.localPath.ifEmpty { item1.url })
                }
                ShowImgListDialog(
                    this@CreateTaskActivity, imgList, imgList.indexOf(item.localPath)
                ).show()
            }
 
            override fun deleteImage(item: FileSelectBean) {
                viewModel.adapter.remove(item)
            }
        }
        viewModel.adapter = ImageSelectAdapter(this, viewModel.maxSize, 1, imageEvent)
        binding.rv.adapter = viewModel.adapter
        binding.rv.layoutManager = GridLayoutManager(this, 5)
    }
 
    /**
     * 显示图片选择方式弹窗(拍照 / 图库)
     */
    private fun showTypeDialog() {
        CommonDialog.showTypeDialog(this) {
            if (it == 0) takeAPhotograph() else if (it == 1) enterGallery()
        }
    }
 
    /**
     * 进入图库选择图片
     *
     * 使用 PictureSelector 打开相册,最大可选数量为剩余可上传数量。
     */
    // 进入图库
    @SuppressLint("SourceLockedOrientationActivity")
    private fun enterGallery() {
        PictureSelectorFactory.enterGallery(
            this, viewModel.maxSize - viewModel.adapter.data.size, resultCallbackListener
        )
    }
 
    /**
     * 调用系统相机拍照
     */
    // 拍照
    private fun takeAPhotograph() {
        PictureSelectorFactory.takeAPhotograph(this, resultCallbackListener)
    }
 
    /**
     * 图片选择/拍照结果回调
     *
     * 将选中的图片路径转为 [FileSelectBean] 并追加到现有数据列表中。
     */
    private var resultCallbackListener = object : OnResultCallbackListener<LocalMedia> {
        override fun onResult(result: ArrayList<LocalMedia>?) {
            result?.let { selectList ->
                val dataList: MutableList<FileSelectBean> = ArrayList()
                dataList.addAll(viewModel.adapter.data)
                for (item in selectList) {
                    val path: String = item.availablePath
                    var imgPath =
                        if (PictureMimeType.isContent(path) && !item.isCut && !item.isCompressed) {
                            UriUtils.uri2File(Uri.parse(path)).absolutePath
                        } else {
                            path
                        }
                    dataList.add(FileSelectBean(localPath = imgPath, type = "img"))
                }
                viewModel.adapter.setList(dataList)
            }
        }
 
        override fun onCancel() {}
    }
 
    /**
     * 显示事故类型筛选弹窗(底部 BottomSheet 样式)
     *
     * 左侧显示事故大类,右侧显示对应子类,支持多选。
     * 弹窗 dismiss 时更新筛选按钮文案。
     */
    private fun showFilterDialog() {
        if (filterDialog == null) {
            filterDialog = DialogFactory.produce(this).apply {
                customView(
                    com.xw.yunwei.R.layout.dialog_accident_filter,
                    noVerticalPadding = true,
                    horizontalPadding = false
                )
                cancelable(true)
                cornerRadius(12f)
                val bind: DialogAccidentFilterBinding? = DataBindingUtil.bind(getCustomView())
                bind?.let {
                    setupRecyclerViews(it)
                    it.close.setOnClickListener { dismiss() }
                    // 左侧按钮:仅重置选中状态,不关闭弹窗
                    it.btnLeft.setOnClickListener {
                        viewModel.categories.forEach {
                            it.items.forEach {
                                it.isSelected = false
                            }
                        }
                        leftAdapter.selectedPosition = 0
                        rightAdapter.updateData(viewModel.categories[0].items)
 
                    }
                    // 右侧按钮:确认选择并关闭弹窗
                    it.btnRight.setOnClickListener {
                        if (viewModel.getSelectType().isEmpty()) {
                            ToastUtilCommon.showWarn("还未选择事故类型")
                            return@setOnClickListener
                        }
                        dismiss()
                    }
                }
            }
 
            // 弹窗关闭时更新筛选按钮文案(已选则显示"选好了")
            filterDialog!!.setOnDismissListener { refreshFilterData() }
        }
 
        filterDialog?.show()
        // 关键:让软键盘把 BottomSheet 顶上去
        filterDialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        filterDialog?.window?.setLayout(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT
        )
        filterDialog?.window?.setGravity(Gravity.BOTTOM)
        filterDialog?.window?.setWindowAnimations(R.style.AnimUp)
 
    }
 
    /**
     * 初始化事故类型筛选弹窗中的左右 RecyclerView
     *
     * @param bind 弹窗视图绑定对象
     */
    private fun setupRecyclerViews(bind: DialogAccidentFilterBinding) {
        bind.leftRecyclerView.layoutManager = LinearLayoutManager(this)
        leftAdapter = LeftCategoryAdapter(viewModel.categories) { category, position ->
            rightAdapter.updateData(category.items)
        }
        bind.leftRecyclerView.adapter = leftAdapter
 
        bind.rightRecyclerView.layoutManager = LinearLayoutManager(this)
        rightAdapter = RightItemAdapter(viewModel.categories[0].items)
        bind.rightRecyclerView.adapter = rightAdapter
 
    }
 
    /**
     * 初始化车辆信息展示布局
     *
     * 根据 [CarModel] 加载车辆图片、车牌号、车身编码、运营状态等。
     * 运营状态对应不同颜色:
     * - 1 = 运营中(蓝色)
     * - 2 = 维修中(红色)
     * - 3 = 已收车(灰色)
     * - 4 = 车辆异常(橙色)
     *
     * @param item 当前选中的车辆信息
     */
    @SuppressLint("SetTextI18n")
    private fun initCarLayout(item: CarModel) {
        ViewAdapter.loadImage(
            binding.icon,
            ImageDefault(item.getShowVehiclePicture(), com.xw.yunwei.R.mipmap.img_work_car)
        )
        binding.name.text = item.plateNumber
        binding.color.text = "${item.vehicleColor} | 车身编码:${item.getShowVehicleCode(6)}"
        binding.status.setTextColor(item.getShowStatusTextColor().colorIdToColorValue(this))
        binding.status.setBackgroundResource(item.getShowStatusBgResource())
        binding.status.text=item.getShowStatus()
    }
 
    /**
     * 高德定位结果回调
     *
     * 定位失败时提示用户开启 GPS 权限;定位成功则将地址保存到 [CreateTaskVM.location]。
     *
     * @param pLocation 定位结果,null 表示定位失败
     */
    override fun location(pLocation: AMapLocation?) {
        if (pLocation == null) {
            CommonDialog.showLocationNotGrantedTipsDialog(this@CreateTaskActivity)
        } else {
            //viewModel.location.value = pLocation
        }
    }
 
//    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
//        super.onActivityResult(requestCode, resultCode, data)
//        if (requestCode == 110) {
//            if (resultCode == RESULT_OK) {
//                data?.getParcelableExtra<CarModel>("car")?.let {
//                    viewModel.selectCar.value = it
//                }
//            } else if (resultCode == RESULT_CANCELED) {
//                if (viewModel.selectCar.value == null) {
//                    finish()
//                }
//            }
//        }
//    }
 
    /**
     * 返回键拦截处理
     *
     * 若当前页面已填写事故相关内容,弹出确认框询问是否保存草稿到 MMKV;
     * 未填写则直接返回。
     */
    override fun onBackPressedHandel() {
        val cacheData = viewModel.getDataWrite()
        if (cacheData == null) {
            super.onBackPressedHandel()
        } else {
            CommonDialog.showTipsDialog(
                this,
                "保存当前已填写内容,更换车辆后将不用重新填写,是否保存?",
                "不保存",
                "保存",
                leftBlock = {
                    super.onBackPressedHandel()
                    true
                },
                rightBlock = {
                    MMKVManager.getInstance().saveModel(KeyPreferences.ACCIDENT, cacheData)
                    super.onBackPressedHandel()
                    true
                })
        }
    }
 
    //刷新筛选数据
    private fun refreshFilterData() {
        val selectTypeName = viewModel.getSelectTypeName()
        binding.llFilters.isVisible = selectTypeName.isNotEmpty()
        binding.tvAccidentFilter.isVisible = selectTypeName.isEmpty()
        binding.llFilters.removeAllViews()
        if (selectTypeName.isNotEmpty()) {
            binding.llFilters.post {
                fill(binding.llFilters, binding.llFilters.width, selectTypeName)
            }
        }
    }
 
    //填充数据
    private fun fill(parent: LinearLayout, parenWidth: Int, list: List<String>) {
        val itemWidths = list.map { calculationItemViewWidth(it, 8, 4) }
        val pair = findWrapIndex(parenWidth, itemWidths)
        val numItemWidth = if (pair.second) {
            calculationItemViewWidth("+${list.size - pair.first - 1}", 4, 0)
        } else {
            0
        }
        val needWeight =
            pair.second && pair.first == 0 && itemWidths.first() >= (parenWidth - numItemWidth)
        list.subList(0, pair.first + 1).forEach {
            parent.addView(getFilterItemView(it, 8, 4, if (needWeight) 1f else 0f))
        }
        if (pair.second) {
            parent.addView(getFilterItemView("+${list.size - pair.first - 1}", 4, 0))
        }
    }
 
    //查找合适的下标
    private fun findWrapIndex(
        parenWidth: Int,
        list: List<Int>,
        needLastNum: Boolean = true
    ): Pair<Int, Boolean> {
        val size = list.size
        var itemWidthCount = 0
        for ((index, itemWidth) in list.withIndex()) {
            if (itemWidthCount + itemWidth == parenWidth) {
                return Pair(index, false)
            }
            if (itemWidthCount + itemWidth > parenWidth) {
                if (needLastNum) {
                    if (index == 0) {
                        return Pair(index, size > 1)
                    }
                    //末尾的计数
                    val numberItemWidth =
                        calculationItemViewWidth("+${size - index}", 4, 0)
                    //剩余内容宽度
                    val surplusContentWidth = parenWidth - numberItemWidth
                    //将剩余内容宽度带入,重新找适合的下标
                    val wrapIndex = findWrapIndex(surplusContentWidth, list, false)
                    return Pair(wrapIndex.first, true)
                } else {
                    return Pair(index - 1, false)
                }
            }
            itemWidthCount += itemWidth
        }
        return Pair(size - 1, false)
    }
 
    //计算item控件宽度
    private fun calculationItemViewWidth(item: String, padHorizontal: Int, marginEnd: Int): Int {
        textPaint.textSize = 12f.spToPx(this@CreateTaskActivity)
        val sl = StaticLayout.Builder.obtain(item, 0, item.length, textPaint, Int.MAX_VALUE)
            .setIncludePad(false) // 看你期望要不要 TextView 默认那套 pad
            .build()
 
        val line0Width = sl.getLineWidth(0) // 这一行的“排版宽度”
        val padWidth = padHorizontal.dpToPx(this) * 2//两侧内间距宽度
        return ceil(line0Width + padWidth + marginEnd.dpToPx(this)).toInt()
    }
 
    //获取筛选item控件
    private fun getFilterItemView(
        item: String,
        padHorizontal: Int,
        marginEnd: Int,
        weight: Float = 0f,
    ): View {
        return TextView(this).apply {
            setBackgroundResource(com.xw.yunwei.R.drawable.shape_gray11_bg_4)
            layoutParams = LinearLayout.LayoutParams(
                if (weight > 0) 0 else LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT,
                weight
            ).apply {
                this.marginEnd = marginEnd.dpToPx(this@CreateTaskActivity)
            }
            setPadding(
                padHorizontal.dpToPx(this@CreateTaskActivity),
                4.dpToPx(this@CreateTaskActivity),
                padHorizontal.dpToPx(this@CreateTaskActivity),
                4.dpToPx(this@CreateTaskActivity),
            )
            setTextColor(R.color.gray_9.colorIdToColorValue(this@CreateTaskActivity))
            setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
            text = item
            maxLines = 1
            ellipsize = TextUtils.TruncateAt.END
        }
    }
 
    private fun adjustLevelPaddingIfOverlap() {
        val levels = listOf(binding.level0 /*, binding.level1, binding.level2*/)
        val hasOverlap = levels.any { level ->
            val leftTv = level.getChildAt(0) as TextView
            val rightTv = level.getChildAt(1) as TextView
            rightTv.left - leftTv.right < 5.dpToPx(this)
        }
        if (hasOverlap) {
            val halfPadding = 8.dpToPx(this)
            levels.forEach {
                it.setPadding(halfPadding, it.paddingTop, halfPadding, it.paddingBottom)
            }
        }
    }
}