diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt new file mode 100644 index 0000000..08be23c --- /dev/null +++ b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt @@ -0,0 +1,163 @@ +package com.casic.br.app.view + +import android.app.ActionBar.LayoutParams +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.casic.br.app.adapter.EditableImageAdapter +import com.casic.br.app.databinding.ActivityAddHiddenTroubleBinding +import com.casic.br.app.extensions.combineFilePath +import com.casic.br.app.utils.GlideLoadEngine +import com.casic.br.app.vm.ImageFileViewModel +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets +import com.pengxh.kt.lite.extensions.createCompressImageDir +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.extensions.toJson +import com.pengxh.kt.lite.utils.LoadState +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.File + +class AddHiddenTroubleActivity : KotlinBaseActivity(), + Handler.Callback { + + private val kTag = "AddHiddenTroubleActivity" + private val selectedImages = ArrayList() + private val imagePaths = ArrayList() + private val recyclerViewImages = ArrayList() + private val marginOffset by lazy { 1.dp2px(this) } + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private lateinit var editableImageAdapter: EditableImageAdapter + private lateinit var imageFileViewModel: ImageFileViewModel + + override fun initViewBinding(): ActivityAddHiddenTroubleBinding { + return ActivityAddHiddenTroubleBinding.inflate(layoutInflater) + } + + override fun setupTopBarLayout() { + + } + + override fun observeRequestState() { + imageFileViewModel.loadState.observe(this) { + if (it == LoadState.Loading) { + "图片上传中,请稍后...".show(this) + } + } + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + val params = window.attributes + params.height = LayoutParams.WRAP_CONTENT + window.attributes = params + + imageFileViewModel = ViewModelProvider(this)[ImageFileViewModel::class.java] + imageFileViewModel.resultModel.observe(this) { + if (it.code == 200) { + val url = it.data.toString() + imagePaths.add(url) + recyclerViewImages.add(url.combineFilePath()) + editableImageAdapter.notifyDataSetChanged() + } + } + + //左外边距,左内边距,TextView宽度,内间距,右内边距,右外边距 + val viewWidth = getScreenWidth() - (15 + 15 + 65 + 10 + 15 + 15).dp2px(this) + editableImageAdapter = EditableImageAdapter(this, viewWidth, 3, 3) + editableImageAdapter.setupImage(recyclerViewImages) + binding.recyclerView.addItemDecoration( + RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) + ) + binding.recyclerView.adapter = editableImageAdapter + } + + override fun initEvent() { + editableImageAdapter.setOnItemClickListener(object : + EditableImageAdapter.OnItemClickListener { + override fun onAddImageClick() { + selectPicture() + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(view: View?, position: Int) { + editableImageAdapter.deleteImage(position) + selectedImages.removeAt(position) + } + }) + + binding.dialogConfirmButton.setOnClickListener { + Log.d(kTag, imagePaths.toJson()) + } + + binding.dialogCancelButton.setOnClickListener { finish() } + } + + private fun selectPicture() { + PictureSelector.create(this).openGallery(SelectMimeType.ofImage()).isGif(false) + .isMaxSelectEnabledMask(true).setFilterMinFileSize(100).setMaxSelectNum(3) + .isDisplayCamera(true).setImageEngine(GlideLoadEngine.get) + .setSelectedData(selectedImages) + .forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + // 如果设置了setSelectedData,它会将之前的结果作为最新的返回值,要注意重复问题 + // 线程控制图片压缩上传过程,防止速度过快导致压缩失败 + val sum = (result.size * 500).toLong() + object : CountDownTimer(sum, 500) { + override fun onTick(millisUntilFinished: Long) { + val i = millisUntilFinished / 500 + val message = weakReferenceHandler.obtainMessage() + message.obj = result[i.toInt()] + message.what = 2024042201 + weakReferenceHandler.handleMessage(message) + } + + override fun onFinish() { + + } + }.start() + } + + override fun onCancel() {} + }) + } + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == 2024042201) { + val result = msg.obj as LocalMedia + selectedImages.add(result) + //压缩图片 + Luban.with(this).load(result.realPath).ignoreBy(100) + .setTargetDir(createCompressImageDir().toString()) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File) { + //上传图片 + imageFileViewModel.uploadImage(file) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + } + }).launch() + } + return true + } +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt new file mode 100644 index 0000000..08be23c --- /dev/null +++ b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt @@ -0,0 +1,163 @@ +package com.casic.br.app.view + +import android.app.ActionBar.LayoutParams +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.casic.br.app.adapter.EditableImageAdapter +import com.casic.br.app.databinding.ActivityAddHiddenTroubleBinding +import com.casic.br.app.extensions.combineFilePath +import com.casic.br.app.utils.GlideLoadEngine +import com.casic.br.app.vm.ImageFileViewModel +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets +import com.pengxh.kt.lite.extensions.createCompressImageDir +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.extensions.toJson +import com.pengxh.kt.lite.utils.LoadState +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.File + +class AddHiddenTroubleActivity : KotlinBaseActivity(), + Handler.Callback { + + private val kTag = "AddHiddenTroubleActivity" + private val selectedImages = ArrayList() + private val imagePaths = ArrayList() + private val recyclerViewImages = ArrayList() + private val marginOffset by lazy { 1.dp2px(this) } + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private lateinit var editableImageAdapter: EditableImageAdapter + private lateinit var imageFileViewModel: ImageFileViewModel + + override fun initViewBinding(): ActivityAddHiddenTroubleBinding { + return ActivityAddHiddenTroubleBinding.inflate(layoutInflater) + } + + override fun setupTopBarLayout() { + + } + + override fun observeRequestState() { + imageFileViewModel.loadState.observe(this) { + if (it == LoadState.Loading) { + "图片上传中,请稍后...".show(this) + } + } + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + val params = window.attributes + params.height = LayoutParams.WRAP_CONTENT + window.attributes = params + + imageFileViewModel = ViewModelProvider(this)[ImageFileViewModel::class.java] + imageFileViewModel.resultModel.observe(this) { + if (it.code == 200) { + val url = it.data.toString() + imagePaths.add(url) + recyclerViewImages.add(url.combineFilePath()) + editableImageAdapter.notifyDataSetChanged() + } + } + + //左外边距,左内边距,TextView宽度,内间距,右内边距,右外边距 + val viewWidth = getScreenWidth() - (15 + 15 + 65 + 10 + 15 + 15).dp2px(this) + editableImageAdapter = EditableImageAdapter(this, viewWidth, 3, 3) + editableImageAdapter.setupImage(recyclerViewImages) + binding.recyclerView.addItemDecoration( + RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) + ) + binding.recyclerView.adapter = editableImageAdapter + } + + override fun initEvent() { + editableImageAdapter.setOnItemClickListener(object : + EditableImageAdapter.OnItemClickListener { + override fun onAddImageClick() { + selectPicture() + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(view: View?, position: Int) { + editableImageAdapter.deleteImage(position) + selectedImages.removeAt(position) + } + }) + + binding.dialogConfirmButton.setOnClickListener { + Log.d(kTag, imagePaths.toJson()) + } + + binding.dialogCancelButton.setOnClickListener { finish() } + } + + private fun selectPicture() { + PictureSelector.create(this).openGallery(SelectMimeType.ofImage()).isGif(false) + .isMaxSelectEnabledMask(true).setFilterMinFileSize(100).setMaxSelectNum(3) + .isDisplayCamera(true).setImageEngine(GlideLoadEngine.get) + .setSelectedData(selectedImages) + .forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + // 如果设置了setSelectedData,它会将之前的结果作为最新的返回值,要注意重复问题 + // 线程控制图片压缩上传过程,防止速度过快导致压缩失败 + val sum = (result.size * 500).toLong() + object : CountDownTimer(sum, 500) { + override fun onTick(millisUntilFinished: Long) { + val i = millisUntilFinished / 500 + val message = weakReferenceHandler.obtainMessage() + message.obj = result[i.toInt()] + message.what = 2024042201 + weakReferenceHandler.handleMessage(message) + } + + override fun onFinish() { + + } + }.start() + } + + override fun onCancel() {} + }) + } + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == 2024042201) { + val result = msg.obj as LocalMedia + selectedImages.add(result) + //压缩图片 + Luban.with(this).load(result.realPath).ignoreBy(100) + .setTargetDir(createCompressImageDir().toString()) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File) { + //上传图片 + imageFileViewModel.uploadImage(file) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + } + }).launch() + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 0d7828f..b0f7dbf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -28,6 +28,7 @@ import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.navigatePageTo import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView import java.io.ByteArrayOutputStream @@ -79,6 +80,7 @@ // 检查 CameraProvider 可用性 cameraProviderFuture.addListener({ try { + //TODO 暂时注掉 bindPreview(cameraProviderFuture.get()) } catch (e: ExecutionException) { e.printStackTrace() @@ -139,7 +141,7 @@ camera.cameraInfo.cameraState.observe(this) { //开始预览之后才人脸检测 if (it.type == CameraState.Type.OPEN) { - imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) +// imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) } } } catch (e: Exception) { @@ -211,20 +213,7 @@ override fun initEvent() { binding.stopButton.setOnClickListener { - CheckResultDialog.Builder() - .setContext(this) - .setPositiveButton("确认,检查结束") - .setNegativeButton("返回") - .setOnDialogButtonClickListener(object : - CheckResultDialog.OnDialogButtonClickListener { - override fun onConfirmClick() { - - } - - override fun onCancelClick() { - - } - }).build().show() + CheckResultDialog(this).show() } binding.tipsButton.setOnClickListener { @@ -232,7 +221,7 @@ } binding.addButton.setOnClickListener { - + navigatePageTo() } binding.listButton.setOnClickListener { diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt new file mode 100644 index 0000000..08be23c --- /dev/null +++ b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt @@ -0,0 +1,163 @@ +package com.casic.br.app.view + +import android.app.ActionBar.LayoutParams +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.casic.br.app.adapter.EditableImageAdapter +import com.casic.br.app.databinding.ActivityAddHiddenTroubleBinding +import com.casic.br.app.extensions.combineFilePath +import com.casic.br.app.utils.GlideLoadEngine +import com.casic.br.app.vm.ImageFileViewModel +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets +import com.pengxh.kt.lite.extensions.createCompressImageDir +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.extensions.toJson +import com.pengxh.kt.lite.utils.LoadState +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.File + +class AddHiddenTroubleActivity : KotlinBaseActivity(), + Handler.Callback { + + private val kTag = "AddHiddenTroubleActivity" + private val selectedImages = ArrayList() + private val imagePaths = ArrayList() + private val recyclerViewImages = ArrayList() + private val marginOffset by lazy { 1.dp2px(this) } + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private lateinit var editableImageAdapter: EditableImageAdapter + private lateinit var imageFileViewModel: ImageFileViewModel + + override fun initViewBinding(): ActivityAddHiddenTroubleBinding { + return ActivityAddHiddenTroubleBinding.inflate(layoutInflater) + } + + override fun setupTopBarLayout() { + + } + + override fun observeRequestState() { + imageFileViewModel.loadState.observe(this) { + if (it == LoadState.Loading) { + "图片上传中,请稍后...".show(this) + } + } + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + val params = window.attributes + params.height = LayoutParams.WRAP_CONTENT + window.attributes = params + + imageFileViewModel = ViewModelProvider(this)[ImageFileViewModel::class.java] + imageFileViewModel.resultModel.observe(this) { + if (it.code == 200) { + val url = it.data.toString() + imagePaths.add(url) + recyclerViewImages.add(url.combineFilePath()) + editableImageAdapter.notifyDataSetChanged() + } + } + + //左外边距,左内边距,TextView宽度,内间距,右内边距,右外边距 + val viewWidth = getScreenWidth() - (15 + 15 + 65 + 10 + 15 + 15).dp2px(this) + editableImageAdapter = EditableImageAdapter(this, viewWidth, 3, 3) + editableImageAdapter.setupImage(recyclerViewImages) + binding.recyclerView.addItemDecoration( + RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) + ) + binding.recyclerView.adapter = editableImageAdapter + } + + override fun initEvent() { + editableImageAdapter.setOnItemClickListener(object : + EditableImageAdapter.OnItemClickListener { + override fun onAddImageClick() { + selectPicture() + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(view: View?, position: Int) { + editableImageAdapter.deleteImage(position) + selectedImages.removeAt(position) + } + }) + + binding.dialogConfirmButton.setOnClickListener { + Log.d(kTag, imagePaths.toJson()) + } + + binding.dialogCancelButton.setOnClickListener { finish() } + } + + private fun selectPicture() { + PictureSelector.create(this).openGallery(SelectMimeType.ofImage()).isGif(false) + .isMaxSelectEnabledMask(true).setFilterMinFileSize(100).setMaxSelectNum(3) + .isDisplayCamera(true).setImageEngine(GlideLoadEngine.get) + .setSelectedData(selectedImages) + .forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + // 如果设置了setSelectedData,它会将之前的结果作为最新的返回值,要注意重复问题 + // 线程控制图片压缩上传过程,防止速度过快导致压缩失败 + val sum = (result.size * 500).toLong() + object : CountDownTimer(sum, 500) { + override fun onTick(millisUntilFinished: Long) { + val i = millisUntilFinished / 500 + val message = weakReferenceHandler.obtainMessage() + message.obj = result[i.toInt()] + message.what = 2024042201 + weakReferenceHandler.handleMessage(message) + } + + override fun onFinish() { + + } + }.start() + } + + override fun onCancel() {} + }) + } + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == 2024042201) { + val result = msg.obj as LocalMedia + selectedImages.add(result) + //压缩图片 + Luban.with(this).load(result.realPath).ignoreBy(100) + .setTargetDir(createCompressImageDir().toString()) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File) { + //上传图片 + imageFileViewModel.uploadImage(file) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + } + }).launch() + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 0d7828f..b0f7dbf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -28,6 +28,7 @@ import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.navigatePageTo import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView import java.io.ByteArrayOutputStream @@ -79,6 +80,7 @@ // 检查 CameraProvider 可用性 cameraProviderFuture.addListener({ try { + //TODO 暂时注掉 bindPreview(cameraProviderFuture.get()) } catch (e: ExecutionException) { e.printStackTrace() @@ -139,7 +141,7 @@ camera.cameraInfo.cameraState.observe(this) { //开始预览之后才人脸检测 if (it.type == CameraState.Type.OPEN) { - imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) +// imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) } } } catch (e: Exception) { @@ -211,20 +213,7 @@ override fun initEvent() { binding.stopButton.setOnClickListener { - CheckResultDialog.Builder() - .setContext(this) - .setPositiveButton("确认,检查结束") - .setNegativeButton("返回") - .setOnDialogButtonClickListener(object : - CheckResultDialog.OnDialogButtonClickListener { - override fun onConfirmClick() { - - } - - override fun onCancelClick() { - - } - }).build().show() + CheckResultDialog(this).show() } binding.tipsButton.setOnClickListener { @@ -232,7 +221,7 @@ } binding.addButton.setOnClickListener { - + navigatePageTo() } binding.listButton.setOnClickListener { diff --git a/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt new file mode 100644 index 0000000..c76af42 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt @@ -0,0 +1,35 @@ +package com.casic.br.app.vm + +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.ActionResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.utils.LoadState +import java.io.File + +class ImageFileViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val resultModel = MutableLiveData() + + fun uploadImage(image: File) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.uploadImage(image) + val responseCode = response.getResponseCode() + if (responseCode == 200) { + loadState.value = LoadState.Success + resultModel.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } else { + loadState.value = LoadState.Fail + } + }, { + loadState.value = LoadState.Fail + it.printStackTrace() + }) +} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt new file mode 100644 index 0000000..08be23c --- /dev/null +++ b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt @@ -0,0 +1,163 @@ +package com.casic.br.app.view + +import android.app.ActionBar.LayoutParams +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.casic.br.app.adapter.EditableImageAdapter +import com.casic.br.app.databinding.ActivityAddHiddenTroubleBinding +import com.casic.br.app.extensions.combineFilePath +import com.casic.br.app.utils.GlideLoadEngine +import com.casic.br.app.vm.ImageFileViewModel +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets +import com.pengxh.kt.lite.extensions.createCompressImageDir +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.extensions.toJson +import com.pengxh.kt.lite.utils.LoadState +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.File + +class AddHiddenTroubleActivity : KotlinBaseActivity(), + Handler.Callback { + + private val kTag = "AddHiddenTroubleActivity" + private val selectedImages = ArrayList() + private val imagePaths = ArrayList() + private val recyclerViewImages = ArrayList() + private val marginOffset by lazy { 1.dp2px(this) } + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private lateinit var editableImageAdapter: EditableImageAdapter + private lateinit var imageFileViewModel: ImageFileViewModel + + override fun initViewBinding(): ActivityAddHiddenTroubleBinding { + return ActivityAddHiddenTroubleBinding.inflate(layoutInflater) + } + + override fun setupTopBarLayout() { + + } + + override fun observeRequestState() { + imageFileViewModel.loadState.observe(this) { + if (it == LoadState.Loading) { + "图片上传中,请稍后...".show(this) + } + } + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + val params = window.attributes + params.height = LayoutParams.WRAP_CONTENT + window.attributes = params + + imageFileViewModel = ViewModelProvider(this)[ImageFileViewModel::class.java] + imageFileViewModel.resultModel.observe(this) { + if (it.code == 200) { + val url = it.data.toString() + imagePaths.add(url) + recyclerViewImages.add(url.combineFilePath()) + editableImageAdapter.notifyDataSetChanged() + } + } + + //左外边距,左内边距,TextView宽度,内间距,右内边距,右外边距 + val viewWidth = getScreenWidth() - (15 + 15 + 65 + 10 + 15 + 15).dp2px(this) + editableImageAdapter = EditableImageAdapter(this, viewWidth, 3, 3) + editableImageAdapter.setupImage(recyclerViewImages) + binding.recyclerView.addItemDecoration( + RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) + ) + binding.recyclerView.adapter = editableImageAdapter + } + + override fun initEvent() { + editableImageAdapter.setOnItemClickListener(object : + EditableImageAdapter.OnItemClickListener { + override fun onAddImageClick() { + selectPicture() + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(view: View?, position: Int) { + editableImageAdapter.deleteImage(position) + selectedImages.removeAt(position) + } + }) + + binding.dialogConfirmButton.setOnClickListener { + Log.d(kTag, imagePaths.toJson()) + } + + binding.dialogCancelButton.setOnClickListener { finish() } + } + + private fun selectPicture() { + PictureSelector.create(this).openGallery(SelectMimeType.ofImage()).isGif(false) + .isMaxSelectEnabledMask(true).setFilterMinFileSize(100).setMaxSelectNum(3) + .isDisplayCamera(true).setImageEngine(GlideLoadEngine.get) + .setSelectedData(selectedImages) + .forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + // 如果设置了setSelectedData,它会将之前的结果作为最新的返回值,要注意重复问题 + // 线程控制图片压缩上传过程,防止速度过快导致压缩失败 + val sum = (result.size * 500).toLong() + object : CountDownTimer(sum, 500) { + override fun onTick(millisUntilFinished: Long) { + val i = millisUntilFinished / 500 + val message = weakReferenceHandler.obtainMessage() + message.obj = result[i.toInt()] + message.what = 2024042201 + weakReferenceHandler.handleMessage(message) + } + + override fun onFinish() { + + } + }.start() + } + + override fun onCancel() {} + }) + } + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == 2024042201) { + val result = msg.obj as LocalMedia + selectedImages.add(result) + //压缩图片 + Luban.with(this).load(result.realPath).ignoreBy(100) + .setTargetDir(createCompressImageDir().toString()) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File) { + //上传图片 + imageFileViewModel.uploadImage(file) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + } + }).launch() + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 0d7828f..b0f7dbf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -28,6 +28,7 @@ import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.navigatePageTo import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView import java.io.ByteArrayOutputStream @@ -79,6 +80,7 @@ // 检查 CameraProvider 可用性 cameraProviderFuture.addListener({ try { + //TODO 暂时注掉 bindPreview(cameraProviderFuture.get()) } catch (e: ExecutionException) { e.printStackTrace() @@ -139,7 +141,7 @@ camera.cameraInfo.cameraState.observe(this) { //开始预览之后才人脸检测 if (it.type == CameraState.Type.OPEN) { - imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) +// imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) } } } catch (e: Exception) { @@ -211,20 +213,7 @@ override fun initEvent() { binding.stopButton.setOnClickListener { - CheckResultDialog.Builder() - .setContext(this) - .setPositiveButton("确认,检查结束") - .setNegativeButton("返回") - .setOnDialogButtonClickListener(object : - CheckResultDialog.OnDialogButtonClickListener { - override fun onConfirmClick() { - - } - - override fun onCancelClick() { - - } - }).build().show() + CheckResultDialog(this).show() } binding.tipsButton.setOnClickListener { @@ -232,7 +221,7 @@ } binding.addButton.setOnClickListener { - + navigatePageTo() } binding.listButton.setOnClickListener { diff --git a/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt new file mode 100644 index 0000000..c76af42 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt @@ -0,0 +1,35 @@ +package com.casic.br.app.vm + +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.ActionResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.utils.LoadState +import java.io.File + +class ImageFileViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val resultModel = MutableLiveData() + + fun uploadImage(image: File) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.uploadImage(image) + val responseCode = response.getResponseCode() + if (responseCode == 200) { + loadState.value = LoadState.Success + resultModel.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } else { + loadState.value = LoadState.Fail + } + }, { + loadState.value = LoadState.Fail + it.printStackTrace() + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt b/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt index 5721f81..43e5881 100644 --- a/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt +++ b/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt @@ -3,49 +3,11 @@ import android.app.Dialog import android.content.Context import android.os.Bundle -import com.casic.br.app.R import com.casic.br.app.databinding.DialogCheckResultBinding import com.pengxh.kt.lite.extensions.binding import com.pengxh.kt.lite.extensions.initDialogLayoutParams -class CheckResultDialog private constructor(builder: Builder) : Dialog( - builder.context, R.style.UserDefinedDialogStyle -) { - private val context = builder.context - private val positiveBtn = builder.positiveBtn - private val negativeBtn = builder.negativeBtn - private val listener = builder.listener - - class Builder { - lateinit var context: Context - lateinit var positiveBtn: String - lateinit var negativeBtn: String - lateinit var listener: OnDialogButtonClickListener - - fun setContext(context: Context): Builder { - this.context = context - return this - } - - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - - fun setNegativeButton(name: String): Builder { - this.negativeBtn = name - return this - } - - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { - this.listener = listener - return this - } - - fun build(): CheckResultDialog { - return CheckResultDialog(this) - } - } +class CheckResultDialog constructor(context: Context) : Dialog(context) { private val binding: DialogCheckResultBinding by binding() @@ -54,24 +16,10 @@ this.initDialogLayoutParams(1f) setCanceledOnTouchOutside(false) - binding.dialogConfirmButton.setOnClickListener { - - } - - - if (negativeBtn.isNotBlank()) { - binding.dialogCancelButton.text = negativeBtn - } - binding.dialogCancelButton.setOnClickListener { - listener.onCancelClick() dismiss() } - } - interface OnDialogButtonClickListener { - fun onConfirmClick() - - fun onCancelClick() + binding.dialogCancelButton.setOnClickListener { dismiss() } } } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index af9b29a..c95e04f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -102,4 +102,6 @@ implementation 'com.github.bumptech.glide:glide:4.9.0' //图片选择框架 implementation 'io.github.lucksiege:pictureselector:v3.11.1' + //图片压缩 + implementation 'top.zibin:Luban:1.1.8' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1e93201..607ca75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,9 @@ + diff --git a/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt new file mode 100644 index 0000000..41e3603 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/adapter/EditableImageAdapter.kt @@ -0,0 +1,95 @@ +package com.casic.br.app.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.casic.br.app.R +import com.pengxh.kt.lite.adapter.ViewHolder + +/** + * 数量可编辑图片适配器 + * + * @param context 使用适配的上下文 + * @param viewWidth RecyclerView实际宽度,一般情况下就是屏幕宽度,但是如果有其他控件和它在同一行,需要计算实际宽度,不然无法正确显示RecyclerView item的布局 + * @param imageCountLimit 最多显示的图片数目 + * @param spanCount 每行显示的图片数目 + * */ +class EditableImageAdapter( + private val context: Context, + private val viewWidth: Int, + private val imageCountLimit: Int, + private val spanCount: Int +) : RecyclerView.Adapter() { + + private val kTag = "EditableImageAdapter" + private var images: MutableList = ArrayList() + + fun setupImage(images: MutableList) { + this.images = images + } + + fun deleteImage(position: Int) { + if (images.isNotEmpty()) { + images.removeAt(position) + /** + * 发生变化的item数目 + * */ + notifyItemRangeRemoved(position, 1) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(context).inflate(R.layout.item_editable_rv_g, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val imageView = holder.getView(R.id.imageView) + val imageSize = viewWidth / spanCount + val params = LinearLayout.LayoutParams(imageSize, imageSize) + imageView.layoutParams = params + + if (position == itemCount - 1 && images.size < imageCountLimit) { + imageView.setImageResource(R.drawable.ic_add_pic) + imageView.setOnClickListener { //添加图片 + itemClickListener?.onAddImageClick() + } + } else { + Glide.with(context).load(images[position]).into(imageView) + imageView.setOnClickListener { // 点击操作,查看大图 + itemClickListener?.onItemClick(holder.bindingAdapterPosition) + } + // 长按监听 + imageView.setOnLongClickListener { v -> //长按删除 + itemClickListener?.onItemLongClick(v, holder.bindingAdapterPosition) + true + } + } + } + + override fun getItemCount(): Int = if (images.size >= imageCountLimit) { + imageCountLimit + } else { + images.size + 1 + } + + private var itemClickListener: OnItemClickListener? = null + + fun setOnItemClickListener(itemClickListener: OnItemClickListener?) { + this.itemClickListener = itemClickListener + } + + interface OnItemClickListener { + fun onAddImageClick() + + fun onItemClick(position: Int) + + fun onItemLongClick(view: View?, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt index 41f8f85..8c431ef 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitService.kt @@ -1,11 +1,13 @@ package com.casic.br.app.retrofit +import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.PartMap import retrofit2.http.Query import retrofit2.http.QueryMap @@ -116,4 +118,14 @@ */ @GET("/knowledge-class/list") suspend fun getLibraryList(@Header("token") token: String): String + + /** + * 上传文件 + */ + @Multipart + @POST("/file/upload") + suspend fun uploadImage( + @Header("token") token: String, + @Part file: MultipartBody.Part + ): String } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt index 94d44e9..8712d19 100644 --- a/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt +++ b/app/src/main/java/com/casic/br/app/retrofit/RetrofitServiceManager.kt @@ -6,8 +6,12 @@ import com.pengxh.kt.lite.utils.RetrofitFactory import com.pengxh.kt.lite.utils.SaveKeyValues import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File object RetrofitServiceManager { @@ -142,4 +146,13 @@ suspend fun getLibraryList(): String { return api.getLibraryList(AuthenticationHelper.token) } + + /** + * 上传图片 + */ + suspend fun uploadImage(image: File): String { + val requestBody = image.asRequestBody("image/png".toMediaTypeOrNull()) + val imagePart = MultipartBody.Part.createFormData("file", image.name, requestBody) + return api.uploadImage(AuthenticationHelper.token, imagePart) + } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt new file mode 100644 index 0000000..8187b83 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/GlideLoadEngine.kt @@ -0,0 +1,71 @@ +package com.casic.br.app.utils + +import android.content.Context +import android.widget.ImageView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.RequestOptions +import com.casic.br.app.R +import com.luck.picture.lib.engine.ImageEngine +import com.luck.picture.lib.utils.ActivityCompatHelper + +class GlideLoadEngine private constructor() : ImageEngine { + companion object { + val get: GlideLoadEngine by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { + GlideLoadEngine() + } + } + + override fun loadImage(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context).load(url).into(imageView) + } + + override fun loadImage( + context: Context, + imageView: ImageView, + url: String, + maxWidth: Int, + maxHeight: Int + ) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .load(url) + .override(maxWidth, maxHeight) + .into(imageView) + } + + override fun loadAlbumCover(context: Context, url: String, imageView: ImageView) { + if (!ActivityCompatHelper.assertValidRequest(context)) { + return + } + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .sizeMultiplier(0.5f) + .transform(CenterCrop(), RoundedCorners(8)) + .placeholder(R.mipmap.load_image_error) + .into(imageView) + } + + override fun pauseRequests(context: Context?) { + context?.let { Glide.with(it).pauseRequests() } + } + + override fun resumeRequests(context: Context?) { + context?.let { Glide.with(it).resumeRequests() } + } + + override fun loadGridImage(context: Context, url: String, imageView: ImageView) { + Glide.with(context) + .load(url) + .apply(RequestOptions().placeholder(R.mipmap.load_image_error)) + .into(imageView) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt new file mode 100644 index 0000000..08be23c --- /dev/null +++ b/app/src/main/java/com/casic/br/app/view/AddHiddenTroubleActivity.kt @@ -0,0 +1,163 @@ +package com.casic.br.app.view + +import android.app.ActionBar.LayoutParams +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.os.Message +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModelProvider +import com.casic.br.app.adapter.EditableImageAdapter +import com.casic.br.app.databinding.ActivityAddHiddenTroubleBinding +import com.casic.br.app.extensions.combineFilePath +import com.casic.br.app.utils.GlideLoadEngine +import com.casic.br.app.vm.ImageFileViewModel +import com.luck.picture.lib.basic.PictureSelector +import com.luck.picture.lib.config.SelectMimeType +import com.luck.picture.lib.entity.LocalMedia +import com.luck.picture.lib.interfaces.OnResultCallbackListener +import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.divider.RecyclerViewItemOffsets +import com.pengxh.kt.lite.extensions.createCompressImageDir +import com.pengxh.kt.lite.extensions.dp2px +import com.pengxh.kt.lite.extensions.getScreenWidth +import com.pengxh.kt.lite.extensions.show +import com.pengxh.kt.lite.extensions.toJson +import com.pengxh.kt.lite.utils.LoadState +import com.pengxh.kt.lite.utils.WeakReferenceHandler +import top.zibin.luban.Luban +import top.zibin.luban.OnCompressListener +import java.io.File + +class AddHiddenTroubleActivity : KotlinBaseActivity(), + Handler.Callback { + + private val kTag = "AddHiddenTroubleActivity" + private val selectedImages = ArrayList() + private val imagePaths = ArrayList() + private val recyclerViewImages = ArrayList() + private val marginOffset by lazy { 1.dp2px(this) } + private val weakReferenceHandler by lazy { WeakReferenceHandler(this) } + private lateinit var editableImageAdapter: EditableImageAdapter + private lateinit var imageFileViewModel: ImageFileViewModel + + override fun initViewBinding(): ActivityAddHiddenTroubleBinding { + return ActivityAddHiddenTroubleBinding.inflate(layoutInflater) + } + + override fun setupTopBarLayout() { + + } + + override fun observeRequestState() { + imageFileViewModel.loadState.observe(this) { + if (it == LoadState.Loading) { + "图片上传中,请稍后...".show(this) + } + } + } + + override fun initOnCreate(savedInstanceState: Bundle?) { + val params = window.attributes + params.height = LayoutParams.WRAP_CONTENT + window.attributes = params + + imageFileViewModel = ViewModelProvider(this)[ImageFileViewModel::class.java] + imageFileViewModel.resultModel.observe(this) { + if (it.code == 200) { + val url = it.data.toString() + imagePaths.add(url) + recyclerViewImages.add(url.combineFilePath()) + editableImageAdapter.notifyDataSetChanged() + } + } + + //左外边距,左内边距,TextView宽度,内间距,右内边距,右外边距 + val viewWidth = getScreenWidth() - (15 + 15 + 65 + 10 + 15 + 15).dp2px(this) + editableImageAdapter = EditableImageAdapter(this, viewWidth, 3, 3) + editableImageAdapter.setupImage(recyclerViewImages) + binding.recyclerView.addItemDecoration( + RecyclerViewItemOffsets(marginOffset, marginOffset, marginOffset, marginOffset) + ) + binding.recyclerView.adapter = editableImageAdapter + } + + override fun initEvent() { + editableImageAdapter.setOnItemClickListener(object : + EditableImageAdapter.OnItemClickListener { + override fun onAddImageClick() { + selectPicture() + } + + override fun onItemClick(position: Int) { + + } + + override fun onItemLongClick(view: View?, position: Int) { + editableImageAdapter.deleteImage(position) + selectedImages.removeAt(position) + } + }) + + binding.dialogConfirmButton.setOnClickListener { + Log.d(kTag, imagePaths.toJson()) + } + + binding.dialogCancelButton.setOnClickListener { finish() } + } + + private fun selectPicture() { + PictureSelector.create(this).openGallery(SelectMimeType.ofImage()).isGif(false) + .isMaxSelectEnabledMask(true).setFilterMinFileSize(100).setMaxSelectNum(3) + .isDisplayCamera(true).setImageEngine(GlideLoadEngine.get) + .setSelectedData(selectedImages) + .forResult(object : OnResultCallbackListener { + override fun onResult(result: ArrayList) { + // 如果设置了setSelectedData,它会将之前的结果作为最新的返回值,要注意重复问题 + // 线程控制图片压缩上传过程,防止速度过快导致压缩失败 + val sum = (result.size * 500).toLong() + object : CountDownTimer(sum, 500) { + override fun onTick(millisUntilFinished: Long) { + val i = millisUntilFinished / 500 + val message = weakReferenceHandler.obtainMessage() + message.obj = result[i.toInt()] + message.what = 2024042201 + weakReferenceHandler.handleMessage(message) + } + + override fun onFinish() { + + } + }.start() + } + + override fun onCancel() {} + }) + } + + override fun handleMessage(msg: Message): Boolean { + if (msg.what == 2024042201) { + val result = msg.obj as LocalMedia + selectedImages.add(result) + //压缩图片 + Luban.with(this).load(result.realPath).ignoreBy(100) + .setTargetDir(createCompressImageDir().toString()) + .setCompressListener(object : OnCompressListener { + override fun onStart() { + + } + + override fun onSuccess(file: File) { + //上传图片 + imageFileViewModel.uploadImage(file) + } + + override fun onError(e: Throwable) { + e.printStackTrace() + } + }).launch() + } + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index 0d7828f..b0f7dbf 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -28,6 +28,7 @@ import com.casic.br.app.widgets.SelectSceneDialog import com.google.common.util.concurrent.ListenableFuture import com.pengxh.kt.lite.base.KotlinBaseActivity +import com.pengxh.kt.lite.extensions.navigatePageTo import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.widget.TitleBarView import java.io.ByteArrayOutputStream @@ -79,6 +80,7 @@ // 检查 CameraProvider 可用性 cameraProviderFuture.addListener({ try { + //TODO 暂时注掉 bindPreview(cameraProviderFuture.get()) } catch (e: ExecutionException) { e.printStackTrace() @@ -139,7 +141,7 @@ camera.cameraInfo.cameraState.observe(this) { //开始预览之后才人脸检测 if (it.type == CameraState.Type.OPEN) { - imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) +// imageAnalysis.setAnalyzer(cameraExecutor, imageAnalyzer) } } } catch (e: Exception) { @@ -211,20 +213,7 @@ override fun initEvent() { binding.stopButton.setOnClickListener { - CheckResultDialog.Builder() - .setContext(this) - .setPositiveButton("确认,检查结束") - .setNegativeButton("返回") - .setOnDialogButtonClickListener(object : - CheckResultDialog.OnDialogButtonClickListener { - override fun onConfirmClick() { - - } - - override fun onCancelClick() { - - } - }).build().show() + CheckResultDialog(this).show() } binding.tipsButton.setOnClickListener { @@ -232,7 +221,7 @@ } binding.addButton.setOnClickListener { - + navigatePageTo() } binding.listButton.setOnClickListener { diff --git a/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt new file mode 100644 index 0000000..c76af42 --- /dev/null +++ b/app/src/main/java/com/casic/br/app/vm/ImageFileViewModel.kt @@ -0,0 +1,35 @@ +package com.casic.br.app.vm + +import androidx.lifecycle.MutableLiveData +import com.casic.br.app.extensions.getResponseCode +import com.casic.br.app.model.ActionResultModel +import com.casic.br.app.retrofit.RetrofitServiceManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.pengxh.kt.lite.base.BaseViewModel +import com.pengxh.kt.lite.extensions.launch +import com.pengxh.kt.lite.utils.LoadState +import java.io.File + +class ImageFileViewModel : BaseViewModel() { + + private val gson by lazy { Gson() } + val resultModel = MutableLiveData() + + fun uploadImage(image: File) = launch({ + loadState.value = LoadState.Loading + val response = RetrofitServiceManager.uploadImage(image) + val responseCode = response.getResponseCode() + if (responseCode == 200) { + loadState.value = LoadState.Success + resultModel.value = gson.fromJson( + response, object : TypeToken() {}.type + ) + } else { + loadState.value = LoadState.Fail + } + }, { + loadState.value = LoadState.Fail + it.printStackTrace() + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt b/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt index 5721f81..43e5881 100644 --- a/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt +++ b/app/src/main/java/com/casic/br/app/widgets/CheckResultDialog.kt @@ -3,49 +3,11 @@ import android.app.Dialog import android.content.Context import android.os.Bundle -import com.casic.br.app.R import com.casic.br.app.databinding.DialogCheckResultBinding import com.pengxh.kt.lite.extensions.binding import com.pengxh.kt.lite.extensions.initDialogLayoutParams -class CheckResultDialog private constructor(builder: Builder) : Dialog( - builder.context, R.style.UserDefinedDialogStyle -) { - private val context = builder.context - private val positiveBtn = builder.positiveBtn - private val negativeBtn = builder.negativeBtn - private val listener = builder.listener - - class Builder { - lateinit var context: Context - lateinit var positiveBtn: String - lateinit var negativeBtn: String - lateinit var listener: OnDialogButtonClickListener - - fun setContext(context: Context): Builder { - this.context = context - return this - } - - fun setPositiveButton(name: String): Builder { - this.positiveBtn = name - return this - } - - fun setNegativeButton(name: String): Builder { - this.negativeBtn = name - return this - } - - fun setOnDialogButtonClickListener(listener: OnDialogButtonClickListener): Builder { - this.listener = listener - return this - } - - fun build(): CheckResultDialog { - return CheckResultDialog(this) - } - } +class CheckResultDialog constructor(context: Context) : Dialog(context) { private val binding: DialogCheckResultBinding by binding() @@ -54,24 +16,10 @@ this.initDialogLayoutParams(1f) setCanceledOnTouchOutside(false) - binding.dialogConfirmButton.setOnClickListener { - - } - - - if (negativeBtn.isNotBlank()) { - binding.dialogCancelButton.text = negativeBtn - } - binding.dialogCancelButton.setOnClickListener { - listener.onCancelClick() dismiss() } - } - interface OnDialogButtonClickListener { - fun onConfirmClick() - - fun onCancelClick() + binding.dialogCancelButton.setOnClickListener { dismiss() } } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_add_hidden_trouble.xml b/app/src/main/res/layout/activity_add_hidden_trouble.xml new file mode 100644 index 0000000..bd2fb8d --- /dev/null +++ b/app/src/main/res/layout/activity_add_hidden_trouble.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +