新增头像裁剪
This commit is contained in:
@@ -111,6 +111,7 @@ dependencies {
|
||||
implementation("com.google.firebase:firebase-messaging-ktx")
|
||||
implementation ("cn.jiguang.sdk:jpush-google:5.4.0")
|
||||
api ("com.tencent.imsdk:imsdk-plus:8.1.6116")
|
||||
implementation("io.github.rroohit:ImageCropView:3.0.1")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.aiosman.riderpro.ui.account.ResetPasswordScreen
|
||||
import com.aiosman.riderpro.ui.chat.ChatScreen
|
||||
import com.aiosman.riderpro.ui.comment.CommentsScreen
|
||||
import com.aiosman.riderpro.ui.comment.notice.CommentNoticeScreen
|
||||
import com.aiosman.riderpro.ui.crop.ImageCropScreen
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteListPage
|
||||
import com.aiosman.riderpro.ui.favourite.FavouriteNoticeScreen
|
||||
import com.aiosman.riderpro.ui.follower.FollowerListScreen
|
||||
@@ -88,6 +89,7 @@ sealed class NavigationRoute(
|
||||
data object FavouriteList : NavigationRoute("FavouriteList")
|
||||
data object Chat : NavigationRoute("Chat/{id}")
|
||||
data object CommentNoticeScreen : NavigationRoute("CommentNoticeScreen")
|
||||
data object ImageCrop : NavigationRoute("ImageCrop")
|
||||
}
|
||||
|
||||
|
||||
@@ -361,6 +363,13 @@ fun NavigationController(
|
||||
CommentNoticeScreen()
|
||||
}
|
||||
}
|
||||
composable(route = NavigationRoute.ImageCrop.route) {
|
||||
CompositionLocalProvider(
|
||||
LocalAnimatedContentScope provides this,
|
||||
) {
|
||||
ImageCropScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.aiosman.riderpro.ui.account
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
||||
import com.aiosman.riderpro.data.UploadImage
|
||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||
import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
|
||||
import java.io.File
|
||||
|
||||
object AccountEditViewModel : ViewModel() {
|
||||
var name by mutableStateOf("")
|
||||
var bio by mutableStateOf("")
|
||||
var imageUrl by mutableStateOf<Uri?>(null)
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
var profile by mutableStateOf<AccountProfileEntity?>(null)
|
||||
var croppedBitmap by mutableStateOf<Bitmap?>(null)
|
||||
suspend fun reloadProfile() {
|
||||
accountService.getMyAccountProfile().let {
|
||||
profile = it
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateUserProfile(context: Context) {
|
||||
|
||||
val newAvatar = croppedBitmap?.let {
|
||||
val file = File(context.cacheDir, "avatar.jpg")
|
||||
it.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
|
||||
UploadImage(file, "avatar.jpg", "", "jpg")
|
||||
}
|
||||
val newName = if (name == profile?.nickName) null else name
|
||||
accountService.updateProfile(
|
||||
avatar = newAvatar,
|
||||
banner = null,
|
||||
nickName = newName,
|
||||
bio = bio
|
||||
)
|
||||
// 刷新用户资料
|
||||
reloadProfile()
|
||||
// 刷新个人资料页面的用户资料
|
||||
MyProfileViewModel.loadUserProfile()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package com.aiosman.riderpro.ui.account
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -18,14 +14,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -35,47 +29,30 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.riderpro.AppColors
|
||||
import com.aiosman.riderpro.ConstVars
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.data.AccountService
|
||||
import com.aiosman.riderpro.data.AccountServiceImpl
|
||||
import com.aiosman.riderpro.data.UploadImage
|
||||
import com.aiosman.riderpro.entity.AccountProfileEntity
|
||||
import com.aiosman.riderpro.ui.NavigationRoute
|
||||
import com.aiosman.riderpro.ui.comment.NoticeScreenHeader
|
||||
import com.aiosman.riderpro.ui.composables.CustomAsyncImage
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||
import com.aiosman.riderpro.ui.composables.form.FormTextInput
|
||||
import com.aiosman.riderpro.ui.composables.pickupAndCompressLauncher
|
||||
import com.aiosman.riderpro.ui.index.tabs.profile.MyProfileViewModel
|
||||
import com.aiosman.riderpro.ui.modifiers.noRippleClickable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* 编辑用户资料界面
|
||||
*/
|
||||
@Composable
|
||||
fun AccountEditScreen2() {
|
||||
val accountService: AccountService = AccountServiceImpl()
|
||||
var name by remember { mutableStateOf("") }
|
||||
var bio by remember { mutableStateOf("") }
|
||||
var imageUrl by remember { mutableStateOf<Uri?>(null) }
|
||||
var imageFile by remember { mutableStateOf<File?>(null) }
|
||||
var profile by remember {
|
||||
mutableStateOf<AccountProfileEntity?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
val model = AccountEditViewModel
|
||||
val navController = LocalNavController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
var usernameError by remember { mutableStateOf<String?>(null) }
|
||||
var bioError by remember { mutableStateOf<String?>(null) }
|
||||
fun onNicknameChange(value: String) {
|
||||
name = value
|
||||
model.name = value
|
||||
usernameError = when {
|
||||
value.isEmpty() -> "昵称不能为空"
|
||||
value.length < 3 -> "昵称长度不能小于3"
|
||||
@@ -85,7 +62,7 @@ fun AccountEditScreen2() {
|
||||
}
|
||||
|
||||
fun onBioChange(value: String) {
|
||||
bio = value
|
||||
model.bio = value
|
||||
bioError = when {
|
||||
value.length > 100 -> "个人简介长度不能大于24"
|
||||
else -> null
|
||||
@@ -96,76 +73,11 @@ fun AccountEditScreen2() {
|
||||
return usernameError == null && bioError == null
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载用户资料
|
||||
*/
|
||||
suspend fun reloadProfile() {
|
||||
accountService.getMyAccountProfile().let {
|
||||
profile = it
|
||||
name = it.nickName
|
||||
bio = it.bio
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户资料
|
||||
*/
|
||||
fun updateUserProfile() {
|
||||
if (!validate()) {
|
||||
Toast.makeText(context, "请检查输入", Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
scope.launch {
|
||||
val newAvatar = imageUrl?.let {
|
||||
// 检查图片文件
|
||||
val avatarFile = imageFile ?: return@let null
|
||||
// 读取文件名
|
||||
val cursor = context.contentResolver.query(it, null, null, null, null)
|
||||
var newAvatar: UploadImage? = null
|
||||
cursor?.use { cur ->
|
||||
val columnIndex = cur.getColumnIndex("_display_name")
|
||||
if (columnIndex != -1 && cur.moveToFirst()) {
|
||||
val displayName = cur.getString(columnIndex)
|
||||
val extension = displayName.substringAfterLast(".")
|
||||
Log.d("Profile Edit", "File name: $displayName, extension: $extension")
|
||||
// read as file
|
||||
Log.d("Profile Edit", "File size: ${avatarFile.length()}")
|
||||
newAvatar = UploadImage(avatarFile, displayName, it.toString(), extension)
|
||||
}
|
||||
}
|
||||
newAvatar
|
||||
}
|
||||
val newName = if (name == profile?.nickName) null else name
|
||||
accountService.updateProfile(
|
||||
avatar = newAvatar,
|
||||
banner = null,
|
||||
nickName = newName,
|
||||
bio = bio
|
||||
)
|
||||
// 刷新用户资料
|
||||
reloadProfile()
|
||||
// 刷新个人资料页面的用户资料
|
||||
MyProfileViewModel.loadUserProfile()
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
val pickImageLauncher = pickupAndCompressLauncher(
|
||||
context = context,
|
||||
scope = scope,
|
||||
maxSize = ConstVars.AVATAR_IMAGE_MAX_SIZE
|
||||
) { uri, file ->
|
||||
if (file.length() <= ConstVars.AVATAR_FILE_SIZE_LIMIT) {
|
||||
imageUrl = uri
|
||||
imageFile = file
|
||||
} else {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
Toast.makeText(context, "图片过大", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
reloadProfile()
|
||||
if (model.profile == null){
|
||||
model.reloadProfile()
|
||||
}
|
||||
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -186,7 +98,10 @@ fun AccountEditScreen2() {
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.noRippleClickable {
|
||||
updateUserProfile()
|
||||
model.viewModelScope.launch {
|
||||
model.updateUserProfile(context)
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "保存",
|
||||
@@ -195,14 +110,14 @@ fun AccountEditScreen2() {
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
profile?.let {
|
||||
model.profile?.let {
|
||||
Box(
|
||||
modifier = Modifier.size(112.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CustomAsyncImage(
|
||||
context,
|
||||
imageUrl?.toString() ?: it.avatar,
|
||||
model.croppedBitmap ?: it.avatar,
|
||||
modifier = Modifier
|
||||
.size(112.dp)
|
||||
.clip(
|
||||
@@ -218,10 +133,7 @@ fun AccountEditScreen2() {
|
||||
.background(AppColors.mainColor)
|
||||
.align(Alignment.BottomEnd)
|
||||
.noRippleClickable {
|
||||
Intent(Intent.ACTION_PICK).apply {
|
||||
type = "image/*"
|
||||
pickImageLauncher.launch(this)
|
||||
}
|
||||
navController.navigate(NavigationRoute.ImageCrop.route)
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
@@ -240,7 +152,7 @@ fun AccountEditScreen2() {
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
FormTextInput(
|
||||
value = name,
|
||||
value = model.name,
|
||||
label = stringResource(R.string.nickname),
|
||||
hint = "Input nickname",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -250,7 +162,7 @@ fun AccountEditScreen2() {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
FormTextInput(
|
||||
value = bio,
|
||||
value = model.bio,
|
||||
label = stringResource(R.string.bio),
|
||||
hint = "Input bio",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -48,7 +48,7 @@ fun rememberImageBitmap(imageUrl: String, imageLoader: ImageLoader): Bitmap? {
|
||||
@Composable
|
||||
fun CustomAsyncImage(
|
||||
context: Context? = null,
|
||||
imageUrl: String?,
|
||||
imageUrl: Any?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
blurHash: String? = null,
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.aiosman.riderpro.ui.crop
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aiosman.riderpro.LocalNavController
|
||||
import com.aiosman.riderpro.R
|
||||
import com.aiosman.riderpro.ui.account.AccountEditViewModel
|
||||
import com.aiosman.riderpro.ui.composables.StatusBarSpacer
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.image.cropview.CropType
|
||||
import com.image.cropview.EdgeType
|
||||
import com.image.cropview.ImageCrop
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.InputStream
|
||||
|
||||
@Composable
|
||||
fun ImageCropScreen() {
|
||||
var imageCrop by remember { mutableStateOf<ImageCrop?>(null) }
|
||||
val context = LocalContext.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidth = configuration.screenWidthDp
|
||||
var imageWidthInDp by remember { mutableStateOf(0) }
|
||||
var imageHeightInDp by remember { mutableStateOf(0) }
|
||||
var density = LocalDensity.current
|
||||
var navController = LocalNavController.current
|
||||
var imagePickLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.GetContent()
|
||||
) { uri: Uri? ->
|
||||
uri?.let {
|
||||
val bitmap = uriToBitmap(context = context, uri = it)
|
||||
if (bitmap != null) {
|
||||
val aspectRatio = bitmap.height.toFloat() / bitmap.width.toFloat()
|
||||
imageHeightInDp = (imageWidthInDp.toFloat() * aspectRatio).toInt()
|
||||
imageCrop = ImageCrop(bitmap)
|
||||
}
|
||||
}
|
||||
if (uri == null) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
val systemUiController = rememberSystemUiController()
|
||||
LaunchedEffect(Unit) {
|
||||
systemUiController.setStatusBarColor(darkIcons = false, color = Color.Black)
|
||||
imagePickLauncher.launch("image/*")
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
imageCrop = null
|
||||
systemUiController.setStatusBarColor(darkIcons = true, color = Color.White)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.background(Color.Black).fillMaxSize()
|
||||
) {
|
||||
StatusBarSpacer()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.rider_pro_nav_back),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.clickable {
|
||||
navController.popBackStack()
|
||||
},
|
||||
colorFilter = ColorFilter.tint(Color.White)
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.clickable {
|
||||
imageCrop?.let {
|
||||
val bitmap = it.onCrop()
|
||||
AccountEditViewModel.croppedBitmap = bitmap
|
||||
AccountEditViewModel.viewModelScope.launch {
|
||||
AccountEditViewModel.updateUserProfile(context)
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
// Spacer(
|
||||
// modifier = Modifier.height(120.dp)
|
||||
// )
|
||||
// ActionButton(
|
||||
// modifier = Modifier.fillMaxWidth(),
|
||||
// text = "选择图片"
|
||||
// ) {
|
||||
// imagePickLauncher.launch("image/*")
|
||||
// }
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(24.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(imageHeightInDp.dp)
|
||||
.onGloballyPositioned {
|
||||
with(density) {
|
||||
imageWidthInDp = it.size.width.toDp().value.toInt()
|
||||
}
|
||||
}
|
||||
) {
|
||||
imageCrop?.ImageCropView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
guideLineColor = Color.White,
|
||||
guideLineWidth = 2.dp,
|
||||
edgeCircleSize = 5.dp,
|
||||
cropType = CropType.SQUARE,
|
||||
edgeType = EdgeType.CIRCULAR
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Configure ImageCropView.
|
||||
|
||||
|
||||
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
|
||||
return try {
|
||||
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user