feat: 用户上报任务显示图片附件
- 新建 PicItem 数据类,uploadPic 改为强类型 - 布局加 picContainer 横排展示区 - showPictures 方法:最多3张缩略图横排,HTTP 加载 - 三个状态页面都显示图片(和语音同级) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
12
app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt
Normal file
12
app/src/main/java/com/xiaoqu/watch/data/task/PicItem.kt
Normal file
@@ -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 = ""
|
||||||
|
)
|
||||||
@@ -54,8 +54,8 @@ data class TaskDetail(
|
|||||||
@SerializedName("standard") val standard: String = "",
|
@SerializedName("standard") val standard: String = "",
|
||||||
/** 任务要求(黄色提示) */
|
/** 任务要求(黄色提示) */
|
||||||
@SerializedName("taskRequire") val taskRequire: String? = null,
|
@SerializedName("taskRequire") val taskRequire: String? = null,
|
||||||
/** 图片附件 */
|
/** 图片附件(用户上报任务可能包含图片) */
|
||||||
@SerializedName("uploadPic") val uploadPic: List<Any>? = null,
|
@SerializedName("uploadPic") val uploadPic: List<PicItem>? = null,
|
||||||
/** 语音附件(用户上报任务可能包含语音描述) */
|
/** 语音附件(用户上报任务可能包含语音描述) */
|
||||||
@SerializedName("voice") val voice: List<VoiceItem>? = null,
|
@SerializedName("voice") val voice: List<VoiceItem>? = null,
|
||||||
/** 打卡标志 */
|
/** 打卡标志 */
|
||||||
@@ -67,6 +67,9 @@ data class TaskDetail(
|
|||||||
/** 是否有语音附件(用户上报任务) */
|
/** 是否有语音附件(用户上报任务) */
|
||||||
val hasVoice: Boolean get() = !voice.isNullOrEmpty()
|
val hasVoice: Boolean get() = !voice.isNullOrEmpty()
|
||||||
|
|
||||||
|
/** 是否有图片附件(用户上报任务) */
|
||||||
|
val hasPictures: Boolean get() = !uploadPic.isNullOrEmpty()
|
||||||
|
|
||||||
/** 地点显示文字(多个用逗号分隔) */
|
/** 地点显示文字(多个用逗号分隔) */
|
||||||
val positionText: String get() = taskPositions?.joinToString(",") ?: ""
|
val positionText: String get() = taskPositions?.joinToString(",") ?: ""
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,8 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
|||||||
binding.tvPosition.visibility = View.GONE
|
binding.tvPosition.visibility = View.GONE
|
||||||
binding.tvTimeInfo.visibility = View.GONE
|
binding.tvTimeInfo.visibility = View.GONE
|
||||||
binding.tvPoints.visibility = View.GONE
|
binding.tvPoints.visibility = View.GONE
|
||||||
|
binding.picContainer.visibility = View.GONE
|
||||||
|
binding.picContainer.removeAllViews()
|
||||||
binding.tvNote.visibility = View.GONE
|
binding.tvNote.visibility = View.GONE
|
||||||
binding.btnVoice.visibility = View.GONE
|
binding.btnVoice.visibility = View.GONE
|
||||||
binding.blockGoWhere.visibility = View.GONE
|
binding.blockGoWhere.visibility = View.GONE
|
||||||
@@ -284,6 +286,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
|||||||
// 备注/描述
|
// 备注/描述
|
||||||
showNote(detail)
|
showNote(detail)
|
||||||
showVoice(detail)
|
showVoice(detail)
|
||||||
|
showPictures(detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 待打卡:地点最优先,其他次要 =====
|
// ===== 待打卡:地点最优先,其他次要 =====
|
||||||
@@ -314,6 +317,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
|||||||
// 备注和语音放在指引块后面(次要信息)
|
// 备注和语音放在指引块后面(次要信息)
|
||||||
showNote(detail)
|
showNote(detail)
|
||||||
showVoice(detail)
|
showVoice(detail)
|
||||||
|
showPictures(detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 进行中/待完成:地点+打卡时间+完成指引 =====
|
// ===== 进行中/待完成:地点+打卡时间+完成指引 =====
|
||||||
@@ -331,6 +335,7 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
|||||||
}
|
}
|
||||||
showNote(detail)
|
showNote(detail)
|
||||||
showVoice(detail)
|
showVoice(detail)
|
||||||
|
showPictures(detail)
|
||||||
if (!detail.confirmTime.isNullOrEmpty()) {
|
if (!detail.confirmTime.isNullOrEmpty()) {
|
||||||
binding.tvCheckinTime.text = "${detail.confirmTime} 已打卡"
|
binding.tvCheckinTime.text = "${detail.confirmTime} 已打卡"
|
||||||
binding.tvCheckinTime.visibility = View.VISIBLE
|
binding.tvCheckinTime.visibility = View.VISIBLE
|
||||||
@@ -404,6 +409,62 @@ class TaskListFragment : BaseFragment<FragmentTaskListBinding>() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示图片缩略图(用户上报任务 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) {
|
private fun updateVoiceUI(playing: Boolean) {
|
||||||
if (playing) {
|
if (playing) {
|
||||||
|
|||||||
@@ -179,6 +179,15 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 图片展示区(用户上报任务附带图片) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/picContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginBottom="11dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- 指引块1:去哪里(橙色) -->
|
<!-- 指引块1:去哪里(橙色) -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/blockGoWhere"
|
android:id="@+id/blockGoWhere"
|
||||||
|
|||||||
Reference in New Issue
Block a user