Dev:
1、加入两套抠图方案:本地和服务器 2、抠图规则添加先服务器后本地(服务器失败会使用本地,同事可以选择只使用本地抠图)
This commit is contained in:
parent
1f1cb1ba55
commit
5f527ca9d7
|
|
@ -4,10 +4,10 @@
|
||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
<DropdownSelection timestamp="2026-03-12T07:23:49.466247900Z">
|
<DropdownSelection timestamp="2026-03-12T08:19:46.116780300Z">
|
||||||
<Target type="DEFAULT_BOOT">
|
<Target type="DEFAULT_BOOT">
|
||||||
<handle>
|
<handle>
|
||||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=JRBI89BIE6AI5TG6" />
|
<DeviceId pluginId="PhysicalDevice" identifier="serial=Y5DELZR46DZTCI9D" />
|
||||||
</handle>
|
</handle>
|
||||||
</Target>
|
</Target>
|
||||||
</DropdownSelection>
|
</DropdownSelection>
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,12 @@ import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|
@ -37,6 +43,7 @@ import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -53,6 +60,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.layer.drawLayer
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
|
@ -63,6 +71,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.unit.toSize
|
import androidx.compose.ui.unit.toSize
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
|
@ -92,6 +101,7 @@ import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import com.img.rabbit.utils.PhotoCutter
|
import com.img.rabbit.utils.PhotoCutter
|
||||||
|
import com.img.rabbit.viewmodel.CutoutViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -126,7 +136,7 @@ object ResourceManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CutoutScreen(navController: NavController) {
|
fun CutoutScreen(navController: NavController, viewModel: CutoutViewModel = viewModel()) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
// 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap)
|
// 图片显示区域 - 支持头像编辑(用于最终数据的导出保存Bitmap)
|
||||||
val graphicsLayer = rememberGraphicsLayer()
|
val graphicsLayer = rememberGraphicsLayer()
|
||||||
|
|
@ -185,6 +195,8 @@ fun CutoutScreen(navController: NavController) {
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE // 旧版本使用常规存储权限
|
Manifest.permission.WRITE_EXTERNAL_STORAGE // 旧版本使用常规存储权限
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
// 抠图方式是否从服务器抠图
|
||||||
|
val isLoadingCutoutMethodFromService = true
|
||||||
|
|
||||||
// 图片选择启动器
|
// 图片选择启动器
|
||||||
val imagePickerLauncher = rememberLauncherForActivityResult(
|
val imagePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -193,37 +205,45 @@ fun CutoutScreen(navController: NavController) {
|
||||||
uri?.let {
|
uri?.let {
|
||||||
selectedImageUri.value = it
|
selectedImageUri.value = it
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
// 执行抠图操作
|
// 执行抠图操作
|
||||||
Thread {
|
if(isLoadingCutoutMethodFromService){
|
||||||
try {
|
// 服务器完成抠图后,再执行本地加载
|
||||||
val originalBitmap = ImageUtils.getBitmapFromUri(context, it)
|
viewModel.cutoutImageFromService(context, it){isSuccess, croppedBitmap ->
|
||||||
originalBitmap?.let { bitmap ->
|
if (isSuccess) {
|
||||||
PhotoCutter.cutPureHead(bitmap) { croppedBitmap ->
|
cutoutResultBitmap.value = croppedBitmap
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
// 重置头像变换
|
||||||
context.mainExecutor.execute {
|
headTransform.value = TransformState()
|
||||||
cutoutResultBitmap.value = croppedBitmap
|
isLoading.value = false
|
||||||
isLoading.value = false
|
} else {
|
||||||
|
viewModel.cutoutImageFromLocal(context, it) { isSuccess, croppedBitmap ->
|
||||||
// 重置头像变换
|
if (isSuccess) {
|
||||||
headTransform.value = TransformState()
|
cutoutResultBitmap.value = croppedBitmap
|
||||||
}
|
// 重置头像变换
|
||||||
|
headTransform.value = TransformState()
|
||||||
|
} else {
|
||||||
|
CenterToast.show("抠图失败,请重试")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("CutoutScreen", "抠图失败: ${e.message}")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
context.mainExecutor.execute {
|
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
CenterToast.show("抠图失败,请重试")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start()
|
}else{
|
||||||
|
// 本地完成抠图后,再执行加载
|
||||||
|
viewModel.cutoutImageFromLocal(context, it) { isSuccess, croppedBitmap ->
|
||||||
|
if (isSuccess) {
|
||||||
|
cutoutResultBitmap.value = croppedBitmap
|
||||||
|
// 重置头像变换
|
||||||
|
headTransform.value = TransformState()
|
||||||
|
} else {
|
||||||
|
CenterToast.show("抠图失败,请重试")
|
||||||
|
}
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 加载服装资源
|
// 加载服装资源
|
||||||
fun loadClothingResource(clothing: ClothingBean?) {
|
fun loadClothingResource(clothing: ClothingBean?) {
|
||||||
if(clothing?.clothing == null){
|
if(clothing?.clothing == null){
|
||||||
|
|
@ -261,18 +281,23 @@ fun CutoutScreen(navController: NavController) {
|
||||||
imagePickerLauncher.launch("image/*")
|
imagePickerLauncher.launch("image/*")
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().background(Color(0xFFF4F4F4)).navigationBarsPadding()
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color(0xFFF4F4F4))
|
||||||
|
.navigationBarsPadding()
|
||||||
) {
|
) {
|
||||||
TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){
|
TitleBar(navController = navController, paddingValues = it, title = "", showSave = true){
|
||||||
// 保存图片
|
// 保存图片
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
// 从 Layer 捕获 Bitmap
|
// 从 Layer 捕获 Bitmap
|
||||||
val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap()
|
val bitmap = graphicsLayer.toImageBitmap().asAndroidBitmap()
|
||||||
// // 保存图片到系统相册(图片已按比例裁剪)
|
// 保存图片到系统相册(图片已按比例裁剪)
|
||||||
// saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess ->
|
/*
|
||||||
// val tips = if(isSuccess){ "已保存为: $fileName" }else{ "保存失败" }
|
saveCanvasToGallery(context, bitmap, ExportFormat.JPG){fileName, isSuccess ->
|
||||||
// CenterToast.show(tips)
|
val tips = if(isSuccess){ "已保存为: $fileName" }else{ "保存失败" }
|
||||||
// }
|
CenterToast.show(tips)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊)
|
// 保存图片到系统相册(指定尺寸,如果targetWidth与targetHeight比原始值小太多会导致图片模糊)
|
||||||
|
|
@ -321,21 +346,37 @@ fun CutoutScreen(navController: NavController) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 38.dp, end = 38.dp
|
.padding(
|
||||||
|
start = 38.dp, end = 38.dp
|
||||||
)
|
)
|
||||||
.aspectRatio(selectedSize.value.width/selectedSize.value.height)
|
.aspectRatio(selectedSize.value.width / selectedSize.value.height)
|
||||||
.background(Color(0xFFFFFFFF))
|
.background(Color(0xFFFFFFFF))
|
||||||
.border(1.dp, Color(0xFFD8D8D8))
|
.border(1.dp, Color(0xFFD8D8D8))
|
||||||
) {
|
) {
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
|
// 1. 定义动画
|
||||||
|
val infiniteTransition =
|
||||||
|
rememberInfiniteTransition(label = "loading")
|
||||||
|
val rotation by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 360f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1000, easing = LinearEasing),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
),
|
||||||
|
label = "rotation"
|
||||||
|
)
|
||||||
// 加载中状态
|
// 加载中状态
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
) {
|
) {
|
||||||
|
// 2. 应用旋转
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.ic_loading),
|
painter = painterResource(id = R.mipmap.ic_loading),
|
||||||
contentDescription = "加载效果图片",
|
contentDescription = "加载效果图片",
|
||||||
modifier = Modifier.size(48.dp)
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.graphicsLayer { rotationZ = rotation } // 让图片转起来
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = "正在抠图中...",
|
text = "正在抠图中...",
|
||||||
|
|
@ -366,12 +407,28 @@ fun CutoutScreen(navController: NavController) {
|
||||||
// 1. 修改判定逻辑:只有当服装可见时,才进行它的 hitTest
|
// 1. 修改判定逻辑:只有当服装可见时,才进行它的 hitTest
|
||||||
selectedTarget = when {
|
selectedTarget = when {
|
||||||
// 处理发型
|
// 处理发型
|
||||||
viewHairstyle.value == ViewType.VISIBLE && hitTest(down.position, hairTransform.value, selectedHairstyleBitmap.value, center) -> "HAIR"
|
viewHairstyle.value == ViewType.VISIBLE && hitTest(
|
||||||
|
down.position,
|
||||||
|
hairTransform.value,
|
||||||
|
selectedHairstyleBitmap.value,
|
||||||
|
center
|
||||||
|
) -> "HAIR"
|
||||||
|
|
||||||
// 处理服装
|
// 处理服装
|
||||||
viewClothing.value == ViewType.VISIBLE && hitTest(down.position, clothesTransform.value, selectedClothingBitmap.value, center) -> "CLOTHES"
|
viewClothing.value == ViewType.VISIBLE && hitTest(
|
||||||
|
down.position,
|
||||||
|
clothesTransform.value,
|
||||||
|
selectedClothingBitmap.value,
|
||||||
|
center
|
||||||
|
) -> "CLOTHES"
|
||||||
|
|
||||||
|
hitTest(
|
||||||
|
down.position,
|
||||||
|
headTransform.value,
|
||||||
|
cutoutResultBitmap.value,
|
||||||
|
center
|
||||||
|
) -> "HEAD"
|
||||||
|
|
||||||
hitTest(down.position, headTransform.value, cutoutResultBitmap.value, center) -> "HEAD"
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -381,12 +438,12 @@ fun CutoutScreen(navController: NavController) {
|
||||||
detectTransformGestures { _, pan, zoom, rotation ->
|
detectTransformGestures { _, pan, zoom, rotation ->
|
||||||
selectedTarget?.let { target ->
|
selectedTarget?.let { target ->
|
||||||
// 更新逻辑保持不变...
|
// 更新逻辑保持不变...
|
||||||
val stateRef = when(target) {
|
val stateRef = when (target) {
|
||||||
"HEAD" -> headTransform
|
"HEAD" -> headTransform
|
||||||
"CLOTHES" -> clothesTransform
|
"CLOTHES" -> clothesTransform
|
||||||
else -> hairTransform
|
else -> hairTransform
|
||||||
}
|
}
|
||||||
val bmp = when(target) {
|
val bmp = when (target) {
|
||||||
"HEAD" -> cutoutResultBitmap.value
|
"HEAD" -> cutoutResultBitmap.value
|
||||||
"CLOTHES" -> selectedClothingBitmap.value
|
"CLOTHES" -> selectedClothingBitmap.value
|
||||||
else -> selectedHairstyleBitmap.value
|
else -> selectedHairstyleBitmap.value
|
||||||
|
|
@ -394,7 +451,8 @@ fun CutoutScreen(navController: NavController) {
|
||||||
|
|
||||||
bmp?.let { b ->
|
bmp?.let { b ->
|
||||||
val current = stateRef.value
|
val current = stateRef.value
|
||||||
val newScale = (current.scale * zoom).coerceIn(0.1f, 5f)
|
val newScale =
|
||||||
|
(current.scale * zoom).coerceIn(0.1f, 5f)
|
||||||
stateRef.value = current.copy(
|
stateRef.value = current.copy(
|
||||||
offset = current.offset + pan,
|
offset = current.offset + pan,
|
||||||
scale = newScale,
|
scale = newScale,
|
||||||
|
|
@ -458,12 +516,14 @@ fun CutoutScreen(navController: NavController) {
|
||||||
} else {
|
} else {
|
||||||
// 空状态
|
// 空状态
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.align(Alignment.Center).clickable(
|
modifier = Modifier
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
.align(Alignment.Center)
|
||||||
indication = null
|
.clickable(
|
||||||
){
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
imagePickerLauncher.launch("image/*")
|
indication = null
|
||||||
}
|
) {
|
||||||
|
imagePickerLauncher.launch("image/*")
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.mipmap.ic_image_empty_pld),
|
painter = painterResource(id = R.mipmap.ic_image_empty_pld),
|
||||||
|
|
@ -491,7 +551,10 @@ fun CutoutScreen(navController: NavController) {
|
||||||
if (selectedAppearance.value == AppearanceType.CLOTHING || selectedAppearance.value == AppearanceType.HAIRSTYLE) {
|
if (selectedAppearance.value == AppearanceType.CLOTHING || selectedAppearance.value == AppearanceType.HAIRSTYLE) {
|
||||||
// 服装和发型查看
|
// 服装和发型查看
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp)
|
||||||
) {
|
) {
|
||||||
ViewOption(
|
ViewOption(
|
||||||
viewType = viewClothing.value,
|
viewType = viewClothing.value,
|
||||||
|
|
@ -500,7 +563,9 @@ fun CutoutScreen(navController: NavController) {
|
||||||
viewClothing.value = if (viewClothing.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE
|
viewClothing.value = if (viewClothing.value == ViewType.HIDE) ViewType.VISIBLE else ViewType.HIDE
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
Box(modifier = Modifier.wrapContentSize().padding(end = 8.dp))
|
Box(modifier = Modifier
|
||||||
|
.wrapContentSize()
|
||||||
|
.padding(end = 8.dp))
|
||||||
ViewOption(
|
ViewOption(
|
||||||
viewType = viewHairstyle.value,
|
viewType = viewHairstyle.value,
|
||||||
title = "发型",
|
title = "发型",
|
||||||
|
|
@ -511,7 +576,10 @@ fun CutoutScreen(navController: NavController) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().weight(1f).padding(start = 16.dp)
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp)
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -615,7 +683,7 @@ private fun ViewOption(
|
||||||
)
|
)
|
||||||
.border(
|
.border(
|
||||||
width = 1.dp,
|
width = 1.dp,
|
||||||
color = if(viewType == ViewType.VISIBLE) Color(0xFF000000) else Color(0xFFEDEDED),
|
color = if (viewType == ViewType.VISIBLE) Color(0xFF000000) else Color(0xFFEDEDED),
|
||||||
shape = RoundedCornerShape(359.dp)
|
shape = RoundedCornerShape(359.dp)
|
||||||
)
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
|
|
@ -653,7 +721,6 @@ private fun ViewOption(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
enum class ViewType {
|
enum class ViewType {
|
||||||
HIDE,
|
HIDE,
|
||||||
VISIBLE
|
VISIBLE
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
package com.img.rabbit.viewmodel
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import com.img.rabbit.bean.local.ErrorBean
|
||||||
|
import com.img.rabbit.provider.api.ApiManager
|
||||||
|
import com.img.rabbit.utils.DownLoadUtils
|
||||||
|
import com.img.rabbit.utils.FileUtils
|
||||||
|
import com.img.rabbit.utils.ImageUtils
|
||||||
|
import com.img.rabbit.utils.PhotoCutter
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import kotlin.text.ifEmpty
|
||||||
|
|
||||||
|
|
||||||
|
class CutoutViewModel : BaseViewModel() {
|
||||||
|
private val targetPath = FileUtils.instance?.cacheImageDir?.absolutePath?:""
|
||||||
|
private val targetFileName = "cutoutCache.png"
|
||||||
|
// 错误状态
|
||||||
|
val errorState = mutableStateOf<ErrorBean?>(null)
|
||||||
|
|
||||||
|
// 本地抠图
|
||||||
|
fun cutoutImageFromLocal(context: Context, uri: Uri, onResult: (isSuccess: Boolean, croppedBitmap: Bitmap?) -> Unit) {
|
||||||
|
// 执行抠图操作
|
||||||
|
Thread {
|
||||||
|
try {
|
||||||
|
val originalBitmap = ImageUtils.getBitmapFromUri(context, uri)
|
||||||
|
originalBitmap?.let { bitmap ->
|
||||||
|
PhotoCutter.cutPureHead(bitmap) { croppedBitmap ->
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
onResult(true, croppedBitmap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("CutoutScreen", "抠图失败: ${e.message}")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
context.mainExecutor.execute {
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
//服务器抠图
|
||||||
|
fun cutoutImageFromService(context: Context, uri: Uri, onResult: (isSuccess: Boolean, croppedBitmap: Bitmap?) -> Unit) {
|
||||||
|
mLaunch {
|
||||||
|
try {
|
||||||
|
// 1. 获取并压缩图片为 PNG (后端示例中使用了 .png,可能只支持 PNG)
|
||||||
|
val bitmap = ImageUtils.getBitmapFromUri(context, uri) ?: throw IOException("无法获取图片")
|
||||||
|
val outputStream = java.io.ByteArrayOutputStream()
|
||||||
|
// PNG 是无损的,质量参数 100 即可
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||||
|
val compressedBytes = outputStream.toByteArray()
|
||||||
|
bitmap.recycle()
|
||||||
|
|
||||||
|
// 2. MediaType 修改为 image/png
|
||||||
|
val requestFile = RequestBody.create("image/png".toMediaTypeOrNull(), compressedBytes)
|
||||||
|
|
||||||
|
// 3. 构造 MultipartBody.Part,关键:强制给文件名加上 .png 后缀
|
||||||
|
// 不要直接用 uri.lastPathSegment,除非你能确定它带后缀
|
||||||
|
val fileName = "cutout_${System.currentTimeMillis()}.png"
|
||||||
|
val filePart = MultipartBody.Part.createFormData("file", fileName, requestFile)
|
||||||
|
|
||||||
|
// 4. 发起请求
|
||||||
|
val response = ApiManager.serviceVo.cutoutImage(filePart)
|
||||||
|
|
||||||
|
if (response.status) {
|
||||||
|
Log.i("CutoutViewModel", "抠图成功: ${response.data}")
|
||||||
|
|
||||||
|
val resultUrl = response.data.toString()
|
||||||
|
// 将 http 替换为 https
|
||||||
|
//val resultUrl = response.data.toString().replace("http://", "https://")
|
||||||
|
saveUrlToCache(resultUrl, onResult)
|
||||||
|
} else {
|
||||||
|
errorState.value = ErrorBean(response.code.toString(), response.message.ifEmpty { "抠图失败" })
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) { // 捕获更广泛的异常
|
||||||
|
e.printStackTrace()
|
||||||
|
Log.e("CutoutViewModel", "服务请求异常: ${e.message}")
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUrlToCache(url: String, onResult: (isSuccess: Boolean, croppedBitmap: Bitmap?) -> Unit) {
|
||||||
|
try {
|
||||||
|
val targetFile = File(targetPath,targetFileName)
|
||||||
|
if(targetFile.exists()){
|
||||||
|
targetFile.delete()
|
||||||
|
}
|
||||||
|
DownLoadUtils.getInstance()
|
||||||
|
.initUrl(url, null)
|
||||||
|
.setFilePath(targetPath)
|
||||||
|
.setFileName(targetFileName)
|
||||||
|
.setDownCallBack(object : DownLoadUtils.DownCallBack {
|
||||||
|
override fun success(file: File) {
|
||||||
|
// 3. 下载成功后,将文件解析为 Bitmap 并回调给 UI
|
||||||
|
val bitmap = android.graphics.BitmapFactory.decodeFile(file.absolutePath)
|
||||||
|
onResult(true, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fail(str: String) {
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun progress(position: Long) {
|
||||||
|
// 如果需要进度可以在这里处理
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun total(total: Long) {
|
||||||
|
// 获取总大小
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun cancel() {
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.down()
|
||||||
|
}catch (e: Exception){
|
||||||
|
e.printStackTrace()
|
||||||
|
onResult(false, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -115,4 +115,12 @@ interface ServiceVo {
|
||||||
//事件上报
|
//事件上报
|
||||||
@POST("/api/user/event")
|
@POST("/api/user/event")
|
||||||
suspend fun report(@Body requestBody: RequestBody): ResultVo<Any>
|
suspend fun report(@Body requestBody: RequestBody): ResultVo<Any>
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片扣取
|
||||||
|
*/
|
||||||
|
@Multipart
|
||||||
|
@POST("/api/image/segment")
|
||||||
|
suspend fun cutoutImage(@Part part: MultipartBody.Part): ResultVo<Any>
|
||||||
}
|
}
|
||||||
|
|
@ -2,5 +2,6 @@
|
||||||
<network-security-config>
|
<network-security-config>
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
<domain-config cleartextTrafficPermitted="true">
|
||||||
<domain includeSubdomains="true">enrichgw.10010.com</domain>
|
<domain includeSubdomains="true">enrichgw.10010.com</domain>
|
||||||
|
<domain includeSubdomains="true">aliyuncs.com</domain>
|
||||||
</domain-config>
|
</domain-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
Loading…
Reference in New Issue