feat: 选图

This commit is contained in:
HuanCheng65 2023-07-20 22:58:41 +08:00
parent f552e48c77
commit d63d7a5a13
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
7 changed files with 98 additions and 38 deletions

View File

@ -54,6 +54,9 @@ import com.google.accompanist.navigation.material.ModalBottomSheetLayout
import com.google.accompanist.systemuicontroller.SystemUiController import com.google.accompanist.systemuicontroller.SystemUiController
import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage import com.huanchengfly.tieba.post.api.retrofit.exception.getErrorMessage
import com.huanchengfly.tieba.post.arch.BaseComposeActivity import com.huanchengfly.tieba.post.arch.BaseComposeActivity
import com.huanchengfly.tieba.post.arch.GlobalEvent
import com.huanchengfly.tieba.post.arch.emitGlobalEvent
import com.huanchengfly.tieba.post.arch.onGlobalEvent
import com.huanchengfly.tieba.post.services.NotifyJobService import com.huanchengfly.tieba.post.services.NotifyJobService
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.page.NavGraphs import com.huanchengfly.tieba.post.ui.page.NavGraphs
@ -69,9 +72,11 @@ import com.huanchengfly.tieba.post.utils.AccountUtil
import com.huanchengfly.tieba.post.utils.ClientUtils import com.huanchengfly.tieba.post.utils.ClientUtils
import com.huanchengfly.tieba.post.utils.JobServiceUtil import com.huanchengfly.tieba.post.utils.JobServiceUtil
import com.huanchengfly.tieba.post.utils.PermissionUtils import com.huanchengfly.tieba.post.utils.PermissionUtils
import com.huanchengfly.tieba.post.utils.PickMediasRequest
import com.huanchengfly.tieba.post.utils.TiebaUtil import com.huanchengfly.tieba.post.utils.TiebaUtil
import com.huanchengfly.tieba.post.utils.isIgnoringBatteryOptimizations import com.huanchengfly.tieba.post.utils.isIgnoringBatteryOptimizations
import com.huanchengfly.tieba.post.utils.newIntentFilter import com.huanchengfly.tieba.post.utils.newIntentFilter
import com.huanchengfly.tieba.post.utils.registerPickMediasLauncher
import com.huanchengfly.tieba.post.utils.requestIgnoreBatteryOptimizations import com.huanchengfly.tieba.post.utils.requestIgnoreBatteryOptimizations
import com.huanchengfly.tieba.post.utils.requestPermission import com.huanchengfly.tieba.post.utils.requestPermission
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
@ -124,6 +129,11 @@ class MainActivityV2 : BaseComposeActivity() {
private val notificationCountFlow: MutableSharedFlow<Int> = private val notificationCountFlow: MutableSharedFlow<Int> =
MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val pickMediasLauncher =
registerPickMediasLauncher {
emitGlobalEvent(GlobalEvent.SelectedImages(it.id, it.uris))
}
private val devicePostureFlow: StateFlow<DevicePosture> by lazy { private val devicePostureFlow: StateFlow<DevicePosture> by lazy {
WindowInfoTracker.getOrCreate(this) WindowInfoTracker.getOrCreate(this)
.windowLayoutInfo(this) .windowLayoutInfo(this)
@ -251,6 +261,11 @@ class MainActivityV2 : BaseComposeActivity() {
okSignAlertDialogState.show() okSignAlertDialogState.show()
} }
} }
onGlobalEvent<GlobalEvent.StartSelectImages> {
pickMediasLauncher.launch(
PickMediasRequest(it.id, it.maxCount, it.mediaType)
)
}
CompositionLocalProvider( CompositionLocalProvider(
LocalNotificationCountFlow provides notificationCountFlow, LocalNotificationCountFlow provides notificationCountFlow,
LocalDevicePosture provides devicePostureFlow.collectAsState(), LocalDevicePosture provides devicePostureFlow.collectAsState(),

View File

@ -99,9 +99,9 @@ class ReplyActivity : BaseActivity(), View.OnClickListener,
get() = false get() = false
@JvmField @JvmField
val pickMediasLauncher = registerPickMediasLauncher { val pickMediasLauncher = registerPickMediasLauncher { (_, uris) ->
val photoInfoBeans = insertPhotoAdapter.getFileList().toMutableList() val photoInfoBeans = insertPhotoAdapter.getFileList().toMutableList()
for (uri in it) { for (uri in uris) {
photoInfoBeans.add(PhotoInfoBean(this, uri)) photoInfoBeans.add(PhotoInfoBean(this, uri))
} }
insertPhotoAdapter.setFileList(photoInfoBeans) insertPhotoAdapter.setFileList(photoInfoBeans)

View File

@ -97,9 +97,9 @@ class TranslucentThemeActivity : BaseActivity(), View.OnClickListener, OnSeekBar
@BindView(R.id.color_theme) @BindView(R.id.color_theme)
lateinit var colorTheme: ViewGroup lateinit var colorTheme: ViewGroup
private val selectImageLauncher = registerPickMediasLauncher { private val selectImageLauncher = registerPickMediasLauncher { (_, uris) ->
if (it.isNotEmpty()) { if (uris.isNotEmpty()) {
val sourceUri = it[0] val sourceUri = uris[0]
launchUCrop(sourceUri) launchUCrop(sourceUri)
} }
} }

View File

@ -70,10 +70,14 @@ class InsertPhotoAdapter(private val mContext: Context) : RecyclerView.Adapter<M
holder.setItemOnClickListener { holder.setItemOnClickListener {
if (mContext is AppCompatActivity && mContext is PickMediasLauncherProvider) { if (mContext is AppCompatActivity && mContext is PickMediasLauncherProvider) {
askPermission { askPermission {
mContext.getPickMediasLauncher().launch(PickMediasRequest().apply { mContext.getPickMediasLauncher()
maxItems = 10 - fileList.size .launch(
PickMediasRequest(
"",
10 - fileList.size,
mediaType = PickMediasRequest.ImageOnly mediaType = PickMediasRequest.ImageOnly
}) )
)
} }
} }
} }

View File

@ -1,28 +1,49 @@
package com.huanchengfly.tieba.post.arch package com.huanchengfly.tieba.post.arch
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import com.huanchengfly.tieba.post.utils.PickMediasRequest
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
sealed interface GlobalEvent { sealed interface GlobalEvent {
object AccountSwitched : GlobalEvent object AccountSwitched : GlobalEvent
data class StartSelectImages(
val id: String,
val maxCount: Int,
val mediaType: PickMediasRequest.MediaType
) : GlobalEvent
data class SelectedImages(
val id: String,
val images: List<Uri>
) : GlobalEvent
} }
private val mutableGlobalEventFlow by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { MutableSharedFlow<GlobalEvent>() } private val globalEventChannel: Channel<GlobalEvent> = Channel()
val GlobalEventFlow by lazy { mutableGlobalEventFlow } val GlobalEventFlow: Flow<GlobalEvent>
get() = globalEventChannel.receiveAsFlow()
fun emitGlobalEvent(event: GlobalEvent) { fun emitGlobalEvent(event: GlobalEvent) {
mutableGlobalEventFlow.tryEmit(event) globalEventChannel.trySend(event)
} }
inline fun <reified Event : GlobalEvent> CoroutineScope.onGlobalEvent( @Composable
inline fun <reified Event : GlobalEvent> onGlobalEvent(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
noinline listener: suspend (Event) -> Unit noinline listener: suspend (Event) -> Unit
): Job { ) {
return launch { DisposableEffect(listener) {
val job = coroutineScope.launch {
GlobalEventFlow GlobalEventFlow
.filterIsInstance<Event>() .filterIsInstance<Event>()
.cancellable() .cancellable()
@ -32,4 +53,8 @@ inline fun <reified Event : GlobalEvent> CoroutineScope.onGlobalEvent(
} }
} }
} }
onDispose {
job.cancel()
}
}
} }

View File

@ -69,9 +69,9 @@ class EditProfileActivity : BaseActivity() {
} }
private val pickMediasLauncher = private val pickMediasLauncher =
registerPickMediasLauncher { registerPickMediasLauncher { (_, uris) ->
if (it.isNotEmpty()) { if (uris.isNotEmpty()) {
val sourceUri = it[0] val sourceUri = uris[0]
Glide.with(this) Glide.with(this)
.asFile() .asFile()
.load(sourceUri) .load(sourceUri)

View File

@ -1,5 +1,6 @@
package com.huanchengfly.tieba.post.utils package com.huanchengfly.tieba.post.utils
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -16,9 +17,9 @@ import com.zhihu.matisse.MimeType
import com.zhihu.matisse.engine.impl.GlideEngine import com.zhihu.matisse.engine.impl.GlideEngine
import com.zhihu.matisse.ui.MatisseActivity import com.zhihu.matisse.ui.MatisseActivity
fun AppCompatActivity.registerPickMediasLauncher(callback: (List<Uri>) -> Unit): ActivityResultLauncher<PickMediasRequest> { fun AppCompatActivity.registerPickMediasLauncher(callback: (PickMediasResult) -> Unit): ActivityResultLauncher<PickMediasRequest> {
return registerForActivityResult( return registerForActivityResult(
PickMediasContract() PickMediasContract
) { ) {
callback(it) callback(it)
} }
@ -55,15 +56,17 @@ private fun Intent.getClipDataUris(): List<Uri> {
return ArrayList(resultSet) return ArrayList(resultSet)
} }
@SuppressLint("NewApi")
private fun getMaxItems() = if (isPhotoPickerAvailable()) { private fun getMaxItems() = if (isPhotoPickerAvailable()) {
MediaStore.getPickImagesMaxLimit() MediaStore.getPickImagesMaxLimit()
} else { } else {
Integer.MAX_VALUE Integer.MAX_VALUE
} }
class PickMediasRequest( data class PickMediasRequest(
var maxItems: Int = 1, val id: String = "",
var mediaType: MediaType = ImageAndVideo val maxItems: Int = 1,
val mediaType: MediaType = ImageAndVideo
) { ) {
sealed interface MediaType sealed interface MediaType
@ -73,7 +76,7 @@ class PickMediasRequest(
object ImageAndVideo : MediaType object ImageAndVideo : MediaType
class SingleMimeType(val mimeType: String) : MediaType data class SingleMimeType(val mimeType: String) : MediaType
companion object { companion object {
internal fun getMimeType(input: MediaType): String? { internal fun getMimeType(input: MediaType): String? {
@ -87,8 +90,19 @@ class PickMediasRequest(
} }
} }
class PickMediasContract : ActivityResultContract<PickMediasRequest, List<Uri>>() { data class PickMediasResult(
val id: String,
val uris: List<Uri>
)
object PickMediasContract : ActivityResultContract<PickMediasRequest, PickMediasResult>() {
private var curRequestId: String? = null
val hasCurrentRequest: Boolean
get() = curRequestId != null
override fun createIntent(context: Context, input: PickMediasRequest): Intent { override fun createIntent(context: Context, input: PickMediasRequest): Intent {
curRequestId = input.id
if (isPhotoPickerAvailable()) { if (isPhotoPickerAvailable()) {
return Intent(MediaStore.ACTION_PICK_IMAGES).apply { return Intent(MediaStore.ACTION_PICK_IMAGES).apply {
type = PickMediasRequest.getMimeType(input.mediaType) type = PickMediasRequest.getMimeType(input.mediaType)
@ -112,13 +126,15 @@ class PickMediasContract : ActivityResultContract<PickMediasRequest, List<Uri>>(
return Intent(context, MatisseActivity::class.java) return Intent(context, MatisseActivity::class.java)
} }
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> { override fun parseResult(resultCode: Int, intent: Intent?): PickMediasResult {
val id = curRequestId + ""
curRequestId = null
if (resultCode != Activity.RESULT_OK || intent == null) { if (resultCode != Activity.RESULT_OK || intent == null) {
return emptyList() return PickMediasResult(id, emptyList())
} }
if (isPhotoPickerAvailable()) { if (isPhotoPickerAvailable()) {
return intent.getClipDataUris() return PickMediasResult(id, intent.getClipDataUris())
} }
return Matisse.obtainResult(intent) return PickMediasResult(id, Matisse.obtainResult(intent))
} }
} }