mirror of
https://github.com/Tencent/QMUI_Android.git
synced 2026-10-29 03:26:44 +08:00
feat: photo thumb
This commit is contained in:
@@ -15,26 +15,36 @@
|
||||
*/
|
||||
package com.qmuiteam.photo.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
@@ -57,34 +67,37 @@ class PhotoViewerViewModel(val state: SavedStateHandle) : ViewModel() {
|
||||
val enterIndex = state.get<Int>(PHOTO_CURRENT_INDEX) ?: 0
|
||||
val data: PhotoViewerData?
|
||||
|
||||
var isEnterTransitionFinished: Boolean = false
|
||||
|
||||
private val transitionDeliverKey = state.get<Long>(PHOTO_TRANSITION_DELIVERY_KEY) ?: -1
|
||||
|
||||
init {
|
||||
val transitionDeliverData = PhotoTransitionDelivery.getAndRemove(transitionDeliverKey)
|
||||
data = if(transitionDeliverData != null){
|
||||
data = if (transitionDeliverData != null) {
|
||||
transitionDeliverData
|
||||
}else{
|
||||
} else {
|
||||
val count = state.get<Int>(PHOTO_COUNT) ?: 0
|
||||
if(count > 0){
|
||||
val list = arrayListOf<PhotoProvider>()
|
||||
for(i in 0 until count){
|
||||
try{
|
||||
if (count > 0) {
|
||||
val list = arrayListOf<QMUIPhotoTransition>()
|
||||
for (i in 0 until count) {
|
||||
try {
|
||||
val meta = state.get<Bundle>("${PHOTO_META_KEY_PREFIX}${i}")
|
||||
val clsName = state.get<String>("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}")
|
||||
if(meta == null || clsName.isNullOrBlank()){
|
||||
list.add(LossPhotoProvider.instance)
|
||||
}else{
|
||||
val clsName =
|
||||
state.get<String>("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}")
|
||||
if (meta == null || clsName.isNullOrBlank()) {
|
||||
list.add(lossPhotoTransition)
|
||||
} else {
|
||||
val cls = Class.forName(clsName)
|
||||
val recover = cls.newInstance() as PhotoProviderRecover
|
||||
list.add(recover.recover(meta) ?: LossPhotoProvider.instance)
|
||||
val recover = cls.newInstance() as PhotoTransitionProviderRecover
|
||||
list.add(recover.recover(meta) ?: lossPhotoTransition)
|
||||
}
|
||||
|
||||
}catch (e: Throwable){
|
||||
list.add(LossPhotoProvider.instance)
|
||||
} catch (e: Throwable) {
|
||||
list.add(lossPhotoTransition)
|
||||
}
|
||||
}
|
||||
PhotoViewerData(list, enterIndex, null)
|
||||
}else{
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -101,9 +114,9 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
|
||||
fun intentOf(
|
||||
activity: AppCompatActivity,
|
||||
activity: ComponentActivity,
|
||||
cls: Class<in QMUIPhotoViewerActivity>,
|
||||
list: List<PhotoProvider>,
|
||||
list: List<QMUIPhotoTransition>,
|
||||
index: Int
|
||||
): Intent {
|
||||
val data = PhotoViewerData(list, index, activity.window.decorView.asBitmap())
|
||||
@@ -111,12 +124,15 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
intent.putExtra(PHOTO_TRANSITION_DELIVERY_KEY, PhotoTransitionDelivery.put(data))
|
||||
intent.putExtra(PHOTO_CURRENT_INDEX, index)
|
||||
intent.putExtra(PHOTO_COUNT, list.size)
|
||||
list.forEachIndexed { i, provider ->
|
||||
val meta = provider.meta()
|
||||
val recoverCls = provider.recoverCls()
|
||||
list.forEachIndexed { i, transition ->
|
||||
val meta = transition.photoProvider.meta()
|
||||
val recoverCls = transition.photoProvider.recoverCls()
|
||||
if (meta != null && recoverCls != null) {
|
||||
intent.putExtra("${PHOTO_META_KEY_PREFIX}${i}", meta)
|
||||
intent.putExtra("${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}", recoverCls.name)
|
||||
intent.putExtra(
|
||||
"${PHOTO_PROVIDER_RECOVER_CLASS_KEY_PREFIX}${i}",
|
||||
recoverCls.name
|
||||
)
|
||||
}
|
||||
}
|
||||
return intent
|
||||
@@ -133,9 +149,9 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
it.isAppearanceLightNavigationBars = false
|
||||
}
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = android.graphics.Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||
window.navigationBarDividerColor = android.graphics.Color.TRANSPARENT
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,16 +181,173 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
protected open fun PhotoViewer(list: List<PhotoProvider>, index: Int) {
|
||||
val currentIndex = rememberSaveable {
|
||||
mutableStateOf(index)
|
||||
}
|
||||
protected open fun PhotoViewer(list: List<QMUIPhotoTransition>, index: Int) {
|
||||
val pagerState = rememberPagerState(index)
|
||||
HorizontalPager(
|
||||
count = list.size,
|
||||
state = pagerState
|
||||
) { page ->
|
||||
PhotoPager(list[page], page == index && !viewModel.isEnterTransitionFinished)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
protected open fun PhotoPager(
|
||||
photoTransition: QMUIPhotoTransition,
|
||||
animateEnterTarget: Boolean,
|
||||
) {
|
||||
var transitionStartOffset = photoTransition.offsetInWindow
|
||||
var transitionStartSize = photoTransition.size
|
||||
val transitionStartPhoto = photoTransition.photo
|
||||
val radio = photoTransition.photoProvider.ratio()
|
||||
val transitionDurationMs = 1000
|
||||
PhotoBackgroundWithTransition(animateEnterTarget, transitionDurationMs)
|
||||
if(transitionStartOffset == null ||
|
||||
transitionStartSize == null ||
|
||||
transitionStartPhoto == null ||
|
||||
radio <= 0f
|
||||
){
|
||||
PhotoContentWithAlphaTransition(photoTransition, animateEnterTarget, transitionDurationMs)
|
||||
}else{
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
val targetViewRatio = constraints.maxWidth * 1f / constraints.maxHeight
|
||||
val targetWidth = if(radio >= targetViewRatio){
|
||||
constraints.maxWidth.toFloat()
|
||||
} else {
|
||||
constraints.maxHeight * radio
|
||||
}
|
||||
val targetHeight = if(radio >= targetViewRatio){
|
||||
constraints.maxWidth / radio
|
||||
}else{
|
||||
constraints.maxHeight.toFloat()
|
||||
}
|
||||
val targetLeft = (constraints.maxWidth - targetWidth) / 2f
|
||||
val targetTop = (constraints.maxHeight - targetHeight) / 2f
|
||||
PhotoContentWithRectTransition(
|
||||
photoTransition = photoTransition,
|
||||
initRect = Rect(transitionStartOffset, transitionStartSize.toSize()),
|
||||
targetRect = Rect(
|
||||
targetLeft,
|
||||
targetTop,
|
||||
targetLeft + targetWidth,
|
||||
targetTop + targetHeight
|
||||
),
|
||||
transitionDurationMs = transitionDurationMs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
protected open fun PhotoBackgroundWithTransition(
|
||||
animateEnterTarget: Boolean,
|
||||
transitionDurationMs: Int
|
||||
){
|
||||
val alphaTransitionState = remember {
|
||||
MutableTransitionState(if(animateEnterTarget) 0f else 1f)
|
||||
}
|
||||
alphaTransitionState.targetState = 1f
|
||||
val transition = updateTransition(transitionState = alphaTransitionState, label = "")
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = ""
|
||||
) {
|
||||
it
|
||||
}
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.alpha(alpha.value)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
protected open fun PhotoContentWithRectTransition(
|
||||
photoTransition: QMUIPhotoTransition,
|
||||
initRect: Rect,
|
||||
targetRect: Rect,
|
||||
transitionDurationMs: Int
|
||||
){
|
||||
val alphaTransitionState = remember {
|
||||
MutableTransitionState(initRect)
|
||||
}
|
||||
alphaTransitionState.targetState = targetRect
|
||||
val transition = updateTransition(transitionState = alphaTransitionState, label = "")
|
||||
val rect = transition.animateRect(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = ""
|
||||
) {
|
||||
it
|
||||
}
|
||||
val left = with(LocalDensity.current){
|
||||
rect.value.left.toDp()
|
||||
}
|
||||
val top = with(LocalDensity.current){
|
||||
rect.value.top.toDp()
|
||||
}
|
||||
val width = with(LocalDensity.current){
|
||||
rect.value.width.toDp()
|
||||
}
|
||||
val height = with(LocalDensity.current){
|
||||
rect.value.height.toDp()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier.offset(left, top).width(width).height(height)
|
||||
){
|
||||
PhotoContent(photoTransition)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
protected open fun PhotoContentWithAlphaTransition(
|
||||
photoTransition: QMUIPhotoTransition,
|
||||
animateEnterTarget: Boolean,
|
||||
transitionDurationMs: Int
|
||||
){
|
||||
val alphaTransitionState = remember {
|
||||
MutableTransitionState(if(animateEnterTarget) 0f else 1f)
|
||||
}
|
||||
alphaTransitionState.targetState = 1f
|
||||
val transition = updateTransition(transitionState = alphaTransitionState, label = "")
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = ""
|
||||
) {
|
||||
it
|
||||
}
|
||||
Box(modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(alpha.value)
|
||||
){
|
||||
PhotoContent(photoTransition)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
protected open fun PhotoContent(
|
||||
photoTransition: QMUIPhotoTransition
|
||||
){
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
var isPhotoLoadSuccess by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
photoTransition.photoProvider.photo()?.Compose(
|
||||
contentScale = ContentScale.Fit,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onSuccess = { isPhotoLoadSuccess = true },
|
||||
onError = null
|
||||
)
|
||||
if(!isPhotoLoadSuccess){
|
||||
photoTransition.photoProvider.thumbnail()?.Compose(
|
||||
contentScale = ContentScale.Fit,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onSuccess = null,
|
||||
onError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package com.qmuiteam.photo.compose
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.positionInWindow
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.qmuiteam.photo.activity.QMUIPhotoViewerActivity
|
||||
import com.qmuiteam.photo.data.QMUIPhoto
|
||||
import com.qmuiteam.photo.data.QMUIPhotoProvider
|
||||
import com.qmuiteam.photo.data.QMUIPhotoTransition
|
||||
|
||||
class QMUIPhotoThumbnailConfig(
|
||||
val singleSquireImageWidthRatio: Float = 0.5f,
|
||||
val singleWideImageMaxWidthRatio: Float = 0.667f,
|
||||
val singleHighImageDefaultWidthRatio: Float = 0.5f,
|
||||
val singleHighImageMaxHeight: Dp = 320.dp,
|
||||
val singleLongImageWidthRatio: Float = 0.5f,
|
||||
val isLongImageIfRatioLessThan: Float = 0.2f,
|
||||
val longImageShowTopRatio: Float = 0.25f,
|
||||
val averageIfTwoImage: Boolean = true,
|
||||
val horGap: Dp = 5.dp,
|
||||
val verGap: Dp = 5.dp,
|
||||
val alphaWhenPressed: Float = 0.5f
|
||||
)
|
||||
|
||||
val qmuiDefaultPhotoThumbnailConfig = QMUIPhotoThumbnailConfig()
|
||||
|
||||
@Composable
|
||||
private fun QMUIPhotoThumbnailItem(
|
||||
thumb: QMUIPhoto?,
|
||||
width: Dp,
|
||||
height: Dp,
|
||||
alphaWhenPressed: Float,
|
||||
isContainerFixed: Boolean,
|
||||
isLongImage: Boolean,
|
||||
onLayout: (offset: Offset, size: IntSize) -> Unit,
|
||||
onPhotoLoaded: (Drawable) -> Unit,
|
||||
click: (() -> Unit)?,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed = interactionSource.collectIsPressedAsState()
|
||||
Box(modifier = Modifier
|
||||
.width(width)
|
||||
.height(height)
|
||||
.let {
|
||||
if (click != null) {
|
||||
it
|
||||
.clickable(interactionSource, null) {
|
||||
click.invoke()
|
||||
}
|
||||
.alpha(if (isPressed.value) alphaWhenPressed else 1f)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.onGloballyPositioned {
|
||||
onLayout(it.positionInWindow(), it.size)
|
||||
}
|
||||
) {
|
||||
thumb?.Compose(
|
||||
contentScale = ContentScale.Crop,
|
||||
isContainerFixed = isContainerFixed,
|
||||
isLongImage = isLongImage,
|
||||
onSuccess = {
|
||||
onPhotoLoaded(it)
|
||||
},
|
||||
onError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun QMUIPhotoThumbnailWithViewer(
|
||||
activity: ComponentActivity,
|
||||
images: List<QMUIPhotoProvider>,
|
||||
config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig }
|
||||
){
|
||||
QMUIPhotoThumbnail(images, config){ list, index ->
|
||||
val intent = QMUIPhotoViewerActivity.intentOf(activity, QMUIPhotoViewerActivity::class.java, list, index)
|
||||
activity.startActivity(intent)
|
||||
activity.overridePendingTransition(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QMUIPhotoThumbnail(
|
||||
images: List<QMUIPhotoProvider>,
|
||||
config: QMUIPhotoThumbnailConfig = remember { qmuiDefaultPhotoThumbnailConfig },
|
||||
onClick: ((images: List<QMUIPhotoTransition>, index: Int) -> Unit)? = null
|
||||
) {
|
||||
if (images.size < 0) {
|
||||
return
|
||||
}
|
||||
val renderInfo = remember {
|
||||
Array(images.size) {
|
||||
QMUIPhotoTransition(images[it], null, null, null)
|
||||
}
|
||||
}
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||
if (images.size == 1) {
|
||||
val image = images[0]
|
||||
val thumb = image.thumbnail()
|
||||
if (thumb != null) {
|
||||
val ratio = image.ratio()
|
||||
when {
|
||||
ratio <= 0 -> {
|
||||
QMUIPhotoThumbnailItem(
|
||||
thumb,
|
||||
Dp.Unspecified,
|
||||
Dp.Unspecified,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = false,
|
||||
isLongImage = false,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[0].offsetInWindow = offset
|
||||
renderInfo[0].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[0].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), 0)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
ratio == 1f -> {
|
||||
val wh = maxWidth * config.singleSquireImageWidthRatio
|
||||
QMUIPhotoThumbnailItem(
|
||||
thumb,
|
||||
wh,
|
||||
wh,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[0].offsetInWindow = offset
|
||||
renderInfo[0].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[0].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), 0)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
ratio > 1f -> {
|
||||
val width = maxWidth * config.singleWideImageMaxWidthRatio
|
||||
val height = width / ratio
|
||||
QMUIPhotoThumbnailItem(
|
||||
thumb,
|
||||
width,
|
||||
height,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[0].offsetInWindow = offset
|
||||
renderInfo[0].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[0].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), 0)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
ratio <= config.isLongImageIfRatioLessThan -> {
|
||||
val width = maxWidth * config.singleLongImageWidthRatio
|
||||
val height = width / config.longImageShowTopRatio
|
||||
QMUIPhotoThumbnailItem(
|
||||
thumb,
|
||||
width,
|
||||
height,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = true,
|
||||
isLongImage = true,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[0].offsetInWindow = offset
|
||||
renderInfo[0].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[0].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), 0)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
var width = maxWidth * config.singleHighImageDefaultWidthRatio
|
||||
var height = width / ratio
|
||||
if (height > config.singleHighImageMaxHeight) {
|
||||
height = config.singleHighImageMaxHeight
|
||||
width = height * ratio
|
||||
}
|
||||
QMUIPhotoThumbnailItem(
|
||||
thumb,
|
||||
width,
|
||||
height,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[0].offsetInWindow = offset
|
||||
renderInfo[0].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[0].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), 0)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (images.size == 2 && config.averageIfTwoImage) {
|
||||
RowImages(images, renderInfo, config, maxWidth, 2, 0, onClick)
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
for (i in 0 until (images.size / 3 + if (images.size % 3 > 0) 1 else 0).coerceAtMost(
|
||||
3
|
||||
)) {
|
||||
if (i > 0) {
|
||||
Spacer(modifier = Modifier.height(config.verGap))
|
||||
}
|
||||
RowImages(
|
||||
images,
|
||||
renderInfo,
|
||||
config,
|
||||
this@BoxWithConstraints.maxWidth,
|
||||
3,
|
||||
i * 3,
|
||||
onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowImages(
|
||||
images: List<QMUIPhotoProvider>,
|
||||
renderInfo: Array<QMUIPhotoTransition>,
|
||||
config: QMUIPhotoThumbnailConfig,
|
||||
containerWidth: Dp,
|
||||
rowCount: Int,
|
||||
startIndex: Int,
|
||||
onClick: ((images: List<QMUIPhotoTransition>, index: Int) -> Unit)?
|
||||
) {
|
||||
val wh = (containerWidth - config.horGap * (rowCount - 1)) / rowCount
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(wh)
|
||||
) {
|
||||
for (i in startIndex until (startIndex + rowCount).coerceAtMost(images.size)) {
|
||||
if (i != startIndex) {
|
||||
Spacer(modifier = Modifier.width(config.horGap))
|
||||
}
|
||||
val image = images[i]
|
||||
val ratio = image.ratio()
|
||||
val isLongImage = ratio > 0f && ratio < config.isLongImageIfRatioLessThan
|
||||
QMUIPhotoThumbnailItem(
|
||||
image.thumbnail(),
|
||||
wh,
|
||||
wh,
|
||||
config.alphaWhenPressed,
|
||||
isContainerFixed = true,
|
||||
isLongImage = isLongImage,
|
||||
onLayout = { offset, size ->
|
||||
renderInfo[i].offsetInWindow = offset
|
||||
renderInfo[i].size = size
|
||||
},
|
||||
onPhotoLoaded = {
|
||||
renderInfo[i].photo = it
|
||||
},
|
||||
click = if (onClick != null) {
|
||||
{
|
||||
onClick.invoke(renderInfo.toList(), i)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
-32
@@ -16,54 +16,53 @@
|
||||
package com.qmuiteam.photo.data
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
|
||||
class PhotoViewerData(
|
||||
val list: List<PhotoProvider>,
|
||||
val list: List<QMUIPhotoTransition>,
|
||||
val index: Int,
|
||||
val background: Bitmap?
|
||||
)
|
||||
|
||||
interface Photo {
|
||||
interface QMUIPhoto {
|
||||
|
||||
@Composable
|
||||
fun Compose()
|
||||
fun Compose(
|
||||
contentScale: ContentScale,
|
||||
isContainerFixed: Boolean,
|
||||
isLongImage: Boolean,
|
||||
onSuccess: ((Drawable) -> Unit)?,
|
||||
onError: ((Throwable) -> Unit)?
|
||||
)
|
||||
}
|
||||
|
||||
interface PhotoProvider {
|
||||
suspend fun thumbnail(): Photo?
|
||||
suspend fun photo(): Photo?
|
||||
fun transitionStart(): Rect?
|
||||
fun transitionEnd(): Rect?
|
||||
interface QMUIPhotoProvider {
|
||||
fun thumbnail(): QMUIPhoto?
|
||||
fun photo(): QMUIPhoto?
|
||||
fun ratio(): Float = -1f
|
||||
|
||||
// used to recover PhotoProvider from arguments if activity recreate.
|
||||
fun meta(): Bundle?
|
||||
fun recoverCls(): Class<in PhotoProviderRecover>?
|
||||
fun recoverCls(): Class<in PhotoTransitionProviderRecover>?
|
||||
}
|
||||
|
||||
class LossPhotoProvider private constructor(): PhotoProvider {
|
||||
class QMUIPhotoTransition(
|
||||
val photoProvider: QMUIPhotoProvider,
|
||||
var offsetInWindow: Offset?,
|
||||
var size: IntSize?,
|
||||
var photo: Drawable?
|
||||
)
|
||||
|
||||
companion object {
|
||||
val instance by lazy {
|
||||
LossPhotoProvider()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun thumbnail(): Photo? {
|
||||
val lossPhotoProvider = object: QMUIPhotoProvider {
|
||||
override fun thumbnail(): QMUIPhoto? {
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun photo(): Photo? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun transitionStart(): Rect? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun transitionEnd(): Rect? {
|
||||
override fun photo(): QMUIPhoto? {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -71,14 +70,16 @@ class LossPhotoProvider private constructor(): PhotoProvider {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun recoverCls(): Class<in PhotoProviderRecover>? {
|
||||
override fun recoverCls(): Class<in PhotoTransitionProviderRecover>? {
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface PhotoProviderRecover {
|
||||
fun recover(bundle: Bundle): PhotoProvider?
|
||||
val lossPhotoTransition = QMUIPhotoTransition(lossPhotoProvider, null, null, null)
|
||||
|
||||
|
||||
interface PhotoTransitionProviderRecover {
|
||||
fun recover(bundle: Bundle): QMUIPhotoTransition?
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ fun View.asBitmap(): Bitmap? {
|
||||
canvas.setBitmap(null)
|
||||
bm
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -61,4 +61,8 @@ object Dep {
|
||||
const val butterknife = "com.jakewharton:butterknife:$ver"
|
||||
const val compiler = "com.jakewharton:butterknife-compiler:$ver"
|
||||
}
|
||||
|
||||
object Coil {
|
||||
const val compose = "io.coil-kt:coil-compose:2.0.0-alpha06"
|
||||
}
|
||||
}
|
||||
@@ -79,8 +79,11 @@ dependencies {
|
||||
implementation(project(":arch"))
|
||||
implementation(project(":type"))
|
||||
implementation(project(":compose"))
|
||||
implementation(project(":photo"))
|
||||
implementation(Dep.Flipper.soLoader)
|
||||
implementation(Dep.Flipper.flipper)
|
||||
kapt(project(":compiler"))
|
||||
kapt(project(":arch-compiler"))
|
||||
|
||||
implementation(Dep.Coil.compose)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,13 @@
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize">
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.qmuiteam.photo.activity.QMUIPhotoViewerActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:ignore="LockedOrientationActivity">
|
||||
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package com.qmuiteam.qmuidemo.fragment.lab
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.qmuiteam.compose.ui.QMUITopBarBackIconItem
|
||||
import com.qmuiteam.compose.ui.QMUITopBarWithLazyScrollState
|
||||
import com.qmuiteam.photo.compose.QMUIPhotoThumbnail
|
||||
import com.qmuiteam.photo.compose.QMUIPhotoThumbnailWithViewer
|
||||
import com.qmuiteam.photo.data.PhotoTransitionProviderRecover
|
||||
import com.qmuiteam.photo.data.QMUIPhoto
|
||||
import com.qmuiteam.photo.data.QMUIPhotoProvider
|
||||
import com.qmuiteam.qmui.arch.annotation.LatestVisitRecord
|
||||
import com.qmuiteam.qmuidemo.R
|
||||
import com.qmuiteam.qmuidemo.base.ComposeBaseFragment
|
||||
import com.qmuiteam.qmuidemo.lib.annotation.Widget
|
||||
|
||||
@Widget(name = "QMUI Photo", iconRes = R.mipmap.icon_grid_in_progress)
|
||||
@LatestVisitRecord
|
||||
class QDPhotoFragment : ComposeBaseFragment() {
|
||||
|
||||
@Composable
|
||||
override fun PageContent() {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
val scrollState = rememberLazyListState()
|
||||
QMUITopBarWithLazyScrollState(
|
||||
scrollState = scrollState,
|
||||
title = "QMUIPhoto",
|
||||
leftItems = arrayListOf(
|
||||
QMUITopBarBackIconItem {
|
||||
popBackStack()
|
||||
}
|
||||
),
|
||||
)
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.background(Color.White),
|
||||
contentPadding = PaddingValues(start = 44.dp)
|
||||
) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg",
|
||||
1f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg",
|
||||
1f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg",
|
||||
1f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp, vertical = 20.dp)
|
||||
) {
|
||||
QMUIPhotoThumbnailWithViewer(
|
||||
requireActivity(),
|
||||
images = listOf(
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg",
|
||||
1f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9136/1yn0KLFwy6Vb0nE6Sg.png",
|
||||
1.379f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/8779/6WY7guGLeGfp0KK6Sb.jpeg",
|
||||
0.749f
|
||||
),
|
||||
CoilPhotoProvider(
|
||||
"https://weread-picture-1258476243.file.myqcloud.com/9979/31y68oGufDGL3zQ6TT.jpg",
|
||||
1f
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CoilPhoto(val url: String) : QMUIPhoto {
|
||||
|
||||
@Composable
|
||||
override fun Compose(
|
||||
contentScale: ContentScale,
|
||||
isContainerFixed: Boolean,
|
||||
isLongImage: Boolean,
|
||||
onSuccess: ((Drawable) -> Unit)?,
|
||||
onError: ((Throwable) -> Unit)?
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.allowHardware(false)
|
||||
.listener(onError = { _, result ->
|
||||
onError?.invoke(result.throwable)
|
||||
}) { _, result ->
|
||||
onSuccess?.invoke(result.drawable)
|
||||
}.build(),
|
||||
contentDescription = "",
|
||||
contentScale = contentScale,
|
||||
alignment = if (isLongImage) Alignment.TopCenter else Alignment.Center,
|
||||
modifier = Modifier.let {
|
||||
if(isContainerFixed){
|
||||
it.fillMaxSize()
|
||||
}else{
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CoilPhotoProvider(val url: String, val ratio: Float) : QMUIPhotoProvider {
|
||||
|
||||
override fun thumbnail(): QMUIPhoto? {
|
||||
return photo()
|
||||
}
|
||||
|
||||
override fun photo(): QMUIPhoto? {
|
||||
return CoilPhoto(url)
|
||||
}
|
||||
|
||||
override fun ratio(): Float {
|
||||
return ratio
|
||||
}
|
||||
|
||||
override fun meta(): Bundle? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun recoverCls(): Class<in PhotoTransitionProviderRecover>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import com.qmuiteam.qmuidemo.fragment.components.viewpager.QDViewPagerFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDAnimationListViewFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDArchTestFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDContinuousNestedScrollFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDPhotoFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDSchemeFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDSnapHelperFragment;
|
||||
import com.qmuiteam.qmuidemo.fragment.lab.QDWebViewFragment;
|
||||
@@ -145,6 +146,7 @@ public class QDDataManager {
|
||||
mLabNames.add(QDArchTestFragment.class);
|
||||
mLabNames.add(QDWebViewFragment.class);
|
||||
mLabNames.add(QDSchemeFragment.class);
|
||||
mLabNames.add(QDPhotoFragment.class);
|
||||
}
|
||||
|
||||
public QDItemDescription getDescription(Class<? extends BaseFragment> cls) {
|
||||
|
||||
Reference in New Issue
Block a user