From 49732ac79a321e058f59ef0881e3134d1ddc7aee Mon Sep 17 00:00:00 2001 From: dongliang Date: Thu, 7 May 2026 12:04:26 +0930 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E4=B8=8A=E6=8A=A5?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E6=98=BE=E7=A4=BA=E5=9B=BE=E7=89=87=E9=99=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新建 PicItem 数据类,uploadPic 改为强类型 - 布局加 picContainer 横排展示区 - showPictures 方法:最多3张缩略图横排,HTTP 加载 - 三个状态页面都显示图片(和语音同级) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../com/xiaoqu/watch/data/task/PicItem.kt | 12 ++++ .../com/xiaoqu/watch/data/task/TaskDetail.kt | 7 ++- .../xiaoqu/watch/ui/task/TaskListFragment.kt | 61 +++++++++++++++++++ .../main/res/layout/fragment_task_list.xml | 9 +++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt diff --git a/app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt b/app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt new file mode 100644 index 0000000..bb40df9 --- /dev/null +++ b/app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt @@ -0,0 +1,12 @@ +package com.xiaoqu.watch.data.task + +import com.google.gson.annotations.SerializedName + +/** + * 图片附件数据类 + * 用户上报任务可能包含图片描述 + */ +data class PicItem( + /** 图片 URL */ + @SerializedName("url") val url: String = "" +) diff --git a/app/src/main/java/com/xiaoqu/watch/data/task/TaskDetail.kt b/app/src/main/java/com/xiaoqu/watch/data/task/TaskDetail.kt index 75b9d32..41c58e5 100644 --- a/app/src/main/java/com/xiaoqu/watch/data/task/TaskDetail.kt +++ b/app/src/main/java/com/xiaoqu/watch/data/task/TaskDetail.kt @@ -54,8 +54,8 @@ data class TaskDetail( @SerializedName("standard") val standard: String = "", /** 任务要求(黄色提示) */ @SerializedName("taskRequire") val taskRequire: String? = null, - /** 图片附件 */ - @SerializedName("uploadPic") val uploadPic: List? = null, + /** 图片附件(用户上报任务可能包含图片) */ + @SerializedName("uploadPic") val uploadPic: List? = null, /** 语音附件(用户上报任务可能包含语音描述) */ @SerializedName("voice") val voice: List? = null, /** 打卡标志 */ @@ -67,6 +67,9 @@ data class TaskDetail( /** 是否有语音附件(用户上报任务) */ val hasVoice: Boolean get() = !voice.isNullOrEmpty() + /** 是否有图片附件(用户上报任务) */ + val hasPictures: Boolean get() = !uploadPic.isNullOrEmpty() + /** 地点显示文字(多个用逗号分隔) */ val positionText: String get() = taskPositions?.joinToString(",") ?: "" diff --git a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt index 44ba819..8cfe449 100644 --- a/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt +++ b/app/src/main/java/com/xiaoqu/watch/ui/task/TaskListFragment.kt @@ -260,6 +260,8 @@ class TaskListFragment : BaseFragment() { binding.tvPosition.visibility = View.GONE binding.tvTimeInfo.visibility = View.GONE binding.tvPoints.visibility = View.GONE + binding.picContainer.visibility = View.GONE + binding.picContainer.removeAllViews() binding.tvNote.visibility = View.GONE binding.btnVoice.visibility = View.GONE binding.blockGoWhere.visibility = View.GONE @@ -284,6 +286,7 @@ class TaskListFragment : BaseFragment() { // 备注/描述 showNote(detail) showVoice(detail) + showPictures(detail) } // ===== 待打卡:地点最优先,其他次要 ===== @@ -314,6 +317,7 @@ class TaskListFragment : BaseFragment() { // 备注和语音放在指引块后面(次要信息) showNote(detail) showVoice(detail) + showPictures(detail) } // ===== 进行中/待完成:地点+打卡时间+完成指引 ===== @@ -331,6 +335,7 @@ class TaskListFragment : BaseFragment() { } showNote(detail) showVoice(detail) + showPictures(detail) if (!detail.confirmTime.isNullOrEmpty()) { binding.tvCheckinTime.text = "${detail.confirmTime} 已打卡" binding.tvCheckinTime.visibility = View.VISIBLE @@ -404,6 +409,62 @@ class TaskListFragment : BaseFragment() { } } + /** + * 显示图片缩略图(用户上报任务 taskType=3/4 可能有图片附件) + * 横排展示,最多3张,点击查看大图 + */ + private fun showPictures(detail: TaskDetail) { + if (!detail.hasPictures) return + + val pics = detail.uploadPic!!.filter { it.url.isNotEmpty() } + if (pics.isEmpty()) return + + binding.picContainer.visibility = View.VISIBLE + binding.picContainer.removeAllViews() + + // 计算每张图片尺寸:容器宽度 / 最多3列,留 gap + val maxCount = minOf(pics.size, 3) + val gap = 8 // dp + val containerWidth = resources.displayMetrics.widthPixels - + (21 * 2 * resources.displayMetrics.density).toInt() // 减去 safe area padding + val imgSize = (containerWidth - gap * (maxCount - 1) * + resources.displayMetrics.density.toInt()) / maxCount + + for ((index, pic) in pics.take(3).withIndex()) { + val imageView = android.widget.ImageView(requireContext()).apply { + layoutParams = android.widget.LinearLayout.LayoutParams(imgSize, imgSize).apply { + if (index > 0) marginStart = (gap * resources.displayMetrics.density).toInt() + } + scaleType = android.widget.ImageView.ScaleType.CENTER_CROP + setBackgroundColor(requireContext().getColor(R.color.card_background)) + } + // 用 HTTP 加载图片(和语音一样的 TLS 问题) + val httpUrl = pic.url.replace("https://", "http://") + loadImage(imageView, httpUrl) + binding.picContainer.addView(imageView) + } + } + + /** 异步加载图片到 ImageView */ + private fun loadImage(imageView: android.widget.ImageView, url: String) { + viewLifecycleOwner.lifecycleScope.launch { + try { + val bitmap = kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + val request = Request.Builder().url(url).build() + val response = downloadClient.newCall(request).execute() + if (response.isSuccessful) { + response.body?.byteStream()?.let { + android.graphics.BitmapFactory.decodeStream(it) + } + } else null + } + bitmap?.let { imageView.setImageBitmap(it) } + } catch (e: Exception) { + Timber.w(e, "图片加载失败: $url") + } + } + } + /** 更新语音按钮的图标和背景 */ private fun updateVoiceUI(playing: Boolean) { if (playing) { diff --git a/app/src/main/res/layout/fragment_task_list.xml b/app/src/main/res/layout/fragment_task_list.xml index 5f610ed..1446ac1 100644 --- a/app/src/main/res/layout/fragment_task_list.xml +++ b/app/src/main/res/layout/fragment_task_list.xml @@ -179,6 +179,15 @@ + + +