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(), 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("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() { override fun addImage(item: FileSelectBean) { showTypeDialog() } override fun viewImage(item: FileSelectBean) { val imgList: MutableList = 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 { override fun onResult(result: ArrayList?) { result?.let { selectList -> val dataList: MutableList = 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("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) { 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, needLastNum: Boolean = true ): Pair { 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) } } } }