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