feat: QMUIPhotoPicker Preview

This commit is contained in:
cgspine
2022-02-24 21:01:41 +08:00
parent 516601fd64
commit 90cfc71b2b
6 changed files with 381 additions and 103 deletions
@@ -1,12 +1,23 @@
package com.qmuiteam.compose.core.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.qmuiteam.compose.core.R
@Composable
@@ -22,28 +33,73 @@ enum class CheckStatus {
none, partial, checked
}
@Composable
fun QMUICheckBox(
size: Dp,
status: CheckStatus = CheckStatus.none,
isEnabled: Boolean = true,
tint: Color?
tint: Color?,
background: Color = Color.Transparent
) {
val icon = when (status) {
CheckStatus.checked -> R.drawable.ic_qmui_checkbox_checked
CheckStatus.partial -> R.drawable.ic_qmui_checkbox_partial
else -> R.drawable.ic_qmui_checkbox_normal
Box(
modifier = Modifier
.size(size)
.clip(CircleShape)
) {
AnimatedVisibility(
visible = status == CheckStatus.none,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_normal, isEnabled, tint, background)
}
AnimatedVisibility(
visible = status == CheckStatus.checked,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_checked, isEnabled, tint, background)
}
AnimatedVisibility(
visible = status == CheckStatus.partial,
enter = fadeIn(),
exit = fadeOut()
) {
QMUICheckBoxImage(R.drawable.ic_qmui_checkbox_partial, isEnabled, tint, background)
}
}
}
@Composable
private fun QMUICheckBoxImage(
resourceId: Int,
isEnabled: Boolean = true,
tint: Color?,
background: Color = Color.Transparent
){
Image(
painter = painterResource(id = icon),
painter = painterResource(id = resourceId),
contentScale = ContentScale.Fit,
contentDescription = "",
colorFilter = tint?.let { ColorFilter.tint(it) },
modifier = Modifier.let {
if (isEnabled) {
it
} else {
it.alpha(0.5f)
modifier = Modifier
.fillMaxSize()
.let {
if (isEnabled) {
it
} else {
it.alpha(0.5f)
}
}.let {
if (background != Color.Transparent) {
it.background(background)
} else {
it
}
}
}
)
}
@@ -44,7 +44,10 @@ import com.qmuiteam.photo.compose.*
import com.qmuiteam.photo.compose.picker.*
import com.qmuiteam.photo.data.*
import com.qmuiteam.photo.vm.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
const val QMUI_PHOTO_DEFAULT_PICK_LIMIT_COUNT = 9
internal const val QMUI_PHOTO_RESULT_URI_LIST = "qmui_photo_result_uri_list"
@@ -161,8 +164,7 @@ open class QMUIPhotoPickerActivity : AppCompatActivity() {
onStartCheckPermission()
onBackPressedDispatcher.addCallback(object: OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val currentScene = viewModel.photoPickerSceneFlow.value
when (currentScene) {
when (val currentScene = viewModel.photoPickerSceneFlow.value) {
is QMUIPhotoPickerEditScene -> {
viewModel.updateScene(currentScene.prevScene)
}
@@ -233,30 +235,22 @@ open class QMUIPhotoPickerActivity : AppCompatActivity() {
enter = fadeIn(),
exit = fadeOut()
) {
PhotoPickerGridContent(viewModel, data, pickedItems)
PhotoPickerGridScene(viewModel, data, pickedItems)
}
AnimatedVisibility(
visible = scene is QMUIPhotoPickerPreviewScene,
enter = fadeIn() + scaleIn(initialScale = 0.8f),
exit = fadeOut() + scaleOut(targetScale = 0.8f)
) {
val previewScheme = scene as? QMUIPhotoPickerPreviewScene
if (previewScheme != null) {
val list = remember(previewScheme) {
if (previewScheme.onlySelected) {
viewModel.getPickedVOList()
} else {
data.find { it.id == previewScheme.buckedId }?.list ?: emptyList<QMUIMediaPhotoVO>()
}
}
PhotoPickerPreviewContent(viewModel, list, pickedItems, previewScheme.currentId)
val previewScene = scene as? QMUIPhotoPickerPreviewScene
if (previewScene != null) {
PhotoPickerPreviewScene(viewModel, previewScene, data, pickedItems)
}
}
}
@Composable
protected open fun BoxScope.PhotoPickerGridContent(
protected open fun BoxScope.PhotoPickerGridScene(
viewModel: QMUIPhotoPickerViewModel,
data: List<QMUIMediaPhotoBucketVO>,
pickedItems: List<Long>,
@@ -370,6 +364,22 @@ open class QMUIPhotoPickerActivity : AppCompatActivity() {
}
}
@Composable
protected open fun BoxScope.PhotoPickerPreviewScene(
viewModel: QMUIPhotoPickerViewModel,
scene: QMUIPhotoPickerPreviewScene,
data: List<QMUIMediaPhotoBucketVO>,
pickedItems: List<Long>
){
val list = remember(scene) {
if (scene.onlySelected) {
viewModel.getPickedVOList()
} else {
data.find { it.id == scene.buckedId }?.list ?: emptyList<QMUIMediaPhotoVO>()
}
}
PhotoPickerPreviewContent(viewModel, list, pickedItems, scene.currentId)
}
@OptIn(ExperimentalPagerApi::class)
@Composable
@@ -401,22 +411,68 @@ open class QMUIPhotoPickerActivity : AppCompatActivity() {
})
}
QMUIPhotoPickerPreview(
pagerState,
data,
loading = { Loading() },
loadingFailed = {},
) {
isFullPageState = !isFullPageState
}
val scope = rememberCoroutineScope()
QMUITopBar(
title = "${pagerState.currentPage + 1}/${data.size}",
separatorHeight = 0.dp,
backgroundColor = QMUILocalPickerConfig.current.topBarBgColor,
leftItems = topBarLeftItems,
rightItems = topBarRightItems
)
Box(modifier = Modifier.fillMaxSize()){
QMUIPhotoPickerPreview(
pagerState,
data,
loading = { Loading() },
loadingFailed = {},
) {
isFullPageState = !isFullPageState
}
AnimatedVisibility(
visible = !isFullPageState,
enter = slideInVertically(initialOffsetY = {-it}),
exit = slideOutVertically(targetOffsetY = {-it})
) {
QMUITopBar(
title = "${pagerState.currentPage + 1}/${data.size}",
separatorHeight = 0.dp,
backgroundColor = QMUILocalPickerConfig.current.topBarBgColor,
leftItems = topBarLeftItems,
rightItems = topBarRightItems
)
}
AnimatedVisibility(
visible = !isFullPageState,
modifier = Modifier.align(Alignment.BottomCenter),
enter = slideInVertically(initialOffsetY = {it}),
exit = slideOutVertically(targetOffsetY = {it})
) {
Column(modifier = Modifier.fillMaxWidth()) {
QMUIPhotoPickerPreviewPickedItems(data, pickedItems, data[pagerState.currentPage].model.id){
scope.launch {
pagerState.scrollToPage(data.indexOf(it))
}
}
val isCurrentPicked = remember(data, pickedItems, pagerState.currentPage) {
pickedItems.indexOf(data[pagerState.currentPage].model.id) >= 0
}
QMUIPhotoPickerPreviewToolBar(
modifier = Modifier.fillMaxWidth(),
isCurrentPicked = isCurrentPicked,
enableOrigin = viewModel.enableOrigin,
isOriginOpenFlow = viewModel.isOriginOpenFlow,
onToggleOrigin = {
viewModel.toggleOrigin(it)
},
onEdit = {
},
onToggleSelect = {
viewModel.togglePick(data[pagerState.currentPage])
}
)
}
}
}
}
@@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
class QMUIPhotoPickerConfig(
val commonTextButtonTextColor: Color = Color.White,
val commonSeparatorColor: Color = Color.White.copy(alpha = 0.3f),
val commonCheckIconNormalTintColor: Color = Color.White,
val commonCheckIconNormalTintColor: Color = Color.White.copy(0.8f),
val commonCheckIconCheckedTintColor: Color = qmuiPrimaryColor,
val commonCheckIconCheckedTextColor: Color = Color.White.copy(alpha = 0.6f),
@@ -1,17 +1,21 @@
package com.qmuiteam.photo.compose.picker
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
import com.qmuiteam.compose.core.ui.QMUITopBar
import com.qmuiteam.compose.core.ui.QMUITopBarBackIconItem
import com.qmuiteam.photo.compose.QMUIGesturePhoto
import com.qmuiteam.photo.data.PhotoLoadStatus
import com.qmuiteam.photo.data.QMUIMediaPhotoVO
@@ -88,4 +92,64 @@ private fun QMUIPhotoPickerPreviewItemContent(
loadingFailed()
}
}
}
@Composable
fun QMUIPhotoPickerPreviewPickedItems(
data: List<QMUIMediaPhotoVO>,
pickedItems: List<Long>,
currentId: Long,
onClick:(QMUIMediaPhotoVO) -> Unit
) {
if (pickedItems.isNotEmpty()) {
val list = remember(data, pickedItems) {
data.filter { pickedItems.contains(it.model.id) }
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
.height(60.dp)
.background(QMUILocalPickerConfig.current.toolBarBgColor),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = spacedBy(5.dp),
contentPadding = PaddingValues(horizontal = 5.dp)
) {
items(list, { it.model.id }){
QMUIPhotoPickerPreviewPickedItem(it, it.model.id == currentId, onClick)
}
}
}
}
@Composable
private fun QMUIPhotoPickerPreviewPickedItem(
item: QMUIMediaPhotoVO,
isCurrent: Boolean,
onClick:(QMUIMediaPhotoVO) -> Unit
){
val thumb = remember(item){
item.photoProvider.thumbnail()
}
Box(modifier = Modifier
.size(40.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
onClick(item)
}
.let {
if(isCurrent){
it.border(2.dp, QMUILocalPickerConfig.current.commonCheckIconCheckedTintColor)
} else {
it
}
}
){
thumb?.Compose(
contentScale = ContentScale.Crop,
isContainerDimenExactly = true,
onSuccess = null,
onError = null)
}
}
@@ -12,68 +12,18 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowInsetsCompat
import com.qmuiteam.compose.core.ex.drawTopSeparator
import com.qmuiteam.compose.core.provider.QMUILocalWindowInsets
import com.qmuiteam.compose.core.provider.dp
import com.qmuiteam.compose.core.ui.CheckStatus
import com.qmuiteam.compose.core.ui.PressWithAlphaBox
import com.qmuiteam.compose.core.ui.QMUICheckBox
import kotlinx.coroutines.flow.StateFlow
@Composable
fun QMUIPhotoPickerGridPageToolBar(
modifier: Modifier = Modifier,
enableOrigin: Boolean,
pickedItems: List<Long>,
isOriginOpenFlow: StateFlow<Boolean>,
onToggleOrigin: (toOpen: Boolean) -> Unit,
onPreview: () -> Unit
) {
val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility(
WindowInsetsCompat.Type.navigationBars()
).dp()
val config = QMUILocalPickerConfig.current
Box(modifier = modifier
.background(config.toolBarBgColor)
.padding(bottom = insets.bottom)
.height(44.dp)
.drawBehind {
drawTopSeparator(config.commonSeparatorColor)
}
) {
PressWithAlphaBox(
enable = pickedItems.isNotEmpty(),
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.align(Alignment.CenterStart),
onClick = {
onPreview()
}
) {
Text(
"预览",
fontSize = 17.sp,
color = config.commonTextButtonTextColor,
modifier = Modifier.align(Alignment.Center)
)
}
if(enableOrigin){
OriginOpenButton(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.align(Alignment.Center),
isOriginOpenFlow = isOriginOpenFlow,
onToggleOrigin = onToggleOrigin
)
}
}
}
@Composable
fun OriginOpenButton(
modifier: Modifier = Modifier,
@@ -100,4 +50,156 @@ fun OriginOpenButton(
color = QMUILocalPickerConfig.current.commonTextButtonTextColor
)
}
}
@Composable
fun PickCurrentCheckButton(
modifier: Modifier = Modifier,
isPicked: Boolean,
onPicked: (toPick: Boolean) -> Unit,
) {
val config = QMUILocalPickerConfig.current
Row(
modifier = modifier.clickable(
interactionSource = remember {
MutableInteractionSource()
},
indication = null
) {
onPicked.invoke(!isPicked)
},
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Absolute.spacedBy(5.dp)
) {
QMUICheckBox(
size = 18.dp,
status = if(isPicked) CheckStatus.checked else CheckStatus.none,
tint = if(isPicked) config.commonCheckIconCheckedTintColor else config.commonCheckIconNormalTintColor,
background = if(isPicked) config.commonCheckIconNormalTintColor else Color.Transparent,
)
Text(
"选择",
fontSize = 17.sp,
color = QMUILocalPickerConfig.current.commonTextButtonTextColor
)
}
}
@Composable
fun CommonTextButton(
modifier: Modifier,
enable: Boolean,
text: String,
onClick: () -> Unit
){
PressWithAlphaBox(
enable = enable,
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.then(modifier),
onClick = {
onClick()
}
) {
Text(
text,
fontSize = 17.sp,
color = QMUILocalPickerConfig.current.commonTextButtonTextColor,
modifier = Modifier.align(Alignment.Center)
)
}
}
@Composable
fun QMUIPhotoPickerGridPageToolBar(
modifier: Modifier = Modifier,
enableOrigin: Boolean,
pickedItems: List<Long>,
isOriginOpenFlow: StateFlow<Boolean>,
onToggleOrigin: (toOpen: Boolean) -> Unit,
onPreview: () -> Unit
) {
val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility(
WindowInsetsCompat.Type.navigationBars()
).dp()
val config = QMUILocalPickerConfig.current
Box(modifier = modifier
.background(config.toolBarBgColor)
.padding(bottom = insets.bottom)
.height(44.dp)
.drawBehind {
drawTopSeparator(config.commonSeparatorColor)
}
) {
CommonTextButton(
modifier = Modifier.align(Alignment.CenterStart),
enable = pickedItems.isNotEmpty(),
text = "预览",
onClick = onPreview
)
if(enableOrigin){
OriginOpenButton(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.align(Alignment.Center),
isOriginOpenFlow = isOriginOpenFlow,
onToggleOrigin = onToggleOrigin
)
}
}
}
@Composable
fun QMUIPhotoPickerPreviewToolBar(
modifier: Modifier = Modifier,
isCurrentPicked: Boolean,
enableOrigin: Boolean,
isOriginOpenFlow: StateFlow<Boolean>,
onToggleOrigin: (toOpen: Boolean) -> Unit,
onEdit: () -> Unit,
onToggleSelect: (toSelect: Boolean) -> Unit
){
val insets = QMUILocalWindowInsets.current.getInsetsIgnoringVisibility(
WindowInsetsCompat.Type.navigationBars()
).dp()
val config = QMUILocalPickerConfig.current
Box(modifier = modifier
.background(config.toolBarBgColor)
.padding(bottom = insets.bottom)
.height(44.dp)
.drawBehind {
drawTopSeparator(config.commonSeparatorColor)
}
) {
CommonTextButton(
modifier = Modifier.align(Alignment.CenterStart),
enable = true,
text = "预览",
onClick = onEdit
)
if(enableOrigin){
OriginOpenButton(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.align(Alignment.Center),
isOriginOpenFlow = isOriginOpenFlow,
onToggleOrigin = onToggleOrigin
)
}
PickCurrentCheckButton(
modifier = Modifier
.fillMaxHeight()
.padding(horizontal = 16.dp)
.align(Alignment.CenterEnd),
isPicked = isCurrentPicked,
onPicked = onToggleSelect
)
}
}
@@ -14,7 +14,7 @@ object Dep {
object QMUI {
const val group = "com.qmuiteam"
const val qmuiVer = "2.0.1"
const val archVer = "2.0.1"
const val archVer = "2.0.1_f_16"
const val typeVer = "0.0.14"
const val composeCoreVer = "0.0.1"
const val composeVer = "0.0.1"