mirror of
https://github.com/Tencent/QMUI_Android.git
synced 2026-10-17 04:31:16 +08:00
feat: photo scale
This commit is contained in:
@@ -22,27 +22,18 @@ 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.background
|
||||
import androidx.compose.foundation.gestures.transformable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.Text
|
||||
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
|
||||
@@ -52,8 +43,8 @@ import androidx.lifecycle.ViewModel
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.qmuiteam.photo.compose.GesturePhoto
|
||||
import com.qmuiteam.photo.data.*
|
||||
import com.qmuiteam.photo.data.PhotoTransitionDelivery
|
||||
import com.qmuiteam.photo.util.asBitmap
|
||||
|
||||
private const val PHOTO_CURRENT_INDEX = "qmui_photo_current_index"
|
||||
@@ -67,8 +58,6 @@ 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 {
|
||||
@@ -187,147 +176,31 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
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{
|
||||
val item = list[page]
|
||||
val transitionStartOffset = item.offsetInWindow
|
||||
val transitionStartSize = item.size?.toSize()
|
||||
val initRect = if (transitionStartOffset != null && transitionStartSize != null) {
|
||||
Rect(transitionStartOffset, transitionStartSize)
|
||||
} else null
|
||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||
val targetViewRatio = constraints.maxWidth * 1f / constraints.maxHeight
|
||||
val targetWidth = if(radio >= targetViewRatio){
|
||||
constraints.maxWidth.toFloat()
|
||||
} else {
|
||||
constraints.maxHeight * radio
|
||||
GesturePhoto(
|
||||
containerWidth = maxWidth,
|
||||
containerHeight = maxHeight,
|
||||
imageRatio = item.photoProvider.ratio(),
|
||||
isLongImage = item.photoProvider.isLongImage(),
|
||||
initRect = initRect,
|
||||
transitionEnter = page == index
|
||||
) {
|
||||
PhotoContent(photoTransition = item)
|
||||
}
|
||||
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)
|
||||
@@ -339,14 +212,24 @@ open class QMUIPhotoViewerActivity : AppCompatActivity() {
|
||||
onSuccess = { isPhotoLoadSuccess = true },
|
||||
onError = null
|
||||
)
|
||||
if(!isPhotoLoadSuccess){
|
||||
photoTransition.photoProvider.thumbnail()?.Compose(
|
||||
contentScale = ContentScale.Fit,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onSuccess = null,
|
||||
onError = null
|
||||
)
|
||||
if (!isPhotoLoadSuccess) {
|
||||
val transitionPhoto = photoTransition.photo
|
||||
if (transitionPhoto != null) {
|
||||
Image(
|
||||
painter = BitmapPainter(transitionPhoto.toBitmap().asImageBitmap()),
|
||||
contentDescription = "",
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
photoTransition.photoProvider.thumbnail()?.Compose(
|
||||
contentScale = ContentScale.Fit,
|
||||
isContainerFixed = true,
|
||||
isLongImage = false,
|
||||
onSuccess = null,
|
||||
onError = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.qmuiteam.photo.compose
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@Composable
|
||||
fun GesturePhoto(
|
||||
containerWidth: Dp,
|
||||
containerHeight: Dp,
|
||||
imageRatio: Float,
|
||||
isLongImage: Boolean,
|
||||
initRect: Rect? = null,
|
||||
transitionEnter: Boolean = false,
|
||||
transitionDurationMs: Int = 1000,
|
||||
onPress: (() -> Unit)? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val layoutRatio = containerWidth / containerHeight
|
||||
val imageWidth: Dp
|
||||
val imageHeight: Dp
|
||||
when {
|
||||
imageRatio >= layoutRatio -> {
|
||||
imageWidth = containerWidth
|
||||
imageHeight = containerWidth / imageRatio
|
||||
}
|
||||
isLongImage -> {
|
||||
imageWidth = containerWidth
|
||||
imageHeight = containerHeight
|
||||
}
|
||||
else -> {
|
||||
imageWidth = containerHeight * imageRatio
|
||||
imageHeight = containerHeight
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundTargetAlpha by remember {
|
||||
mutableStateOf(1f)
|
||||
}
|
||||
|
||||
val photoTargetNormalTranslateX = with(LocalDensity.current) {
|
||||
((containerWidth - imageWidth) / 2f).toPx()
|
||||
}
|
||||
|
||||
val photoTargetNormalTranslateY = with(LocalDensity.current) {
|
||||
((containerHeight - imageHeight) / 2f).toPx()
|
||||
}
|
||||
|
||||
var photoTargetScale by remember(containerWidth, containerHeight) { mutableStateOf(1f) }
|
||||
var photoTargetTranslateX by remember(containerWidth, containerHeight) { mutableStateOf(photoTargetNormalTranslateX) }
|
||||
var photoTargetTranslateY by remember(containerWidth, containerHeight) { mutableStateOf(photoTargetNormalTranslateY) }
|
||||
|
||||
val containerWidthPx = with(LocalDensity.current) { containerWidth.toPx() }
|
||||
val containerHeightPx = with(LocalDensity.current) { containerHeight.toPx() }
|
||||
val imageWidthPx = with(LocalDensity.current) { imageWidth.toPx() }
|
||||
val imageHeightPx = with(LocalDensity.current) { imageHeight.toPx() }
|
||||
|
||||
val transitionState = remember(containerWidth, containerHeight) {
|
||||
val canTransitionEnter = initRect != null && transitionEnter
|
||||
MutableTransitionState(!canTransitionEnter)
|
||||
}
|
||||
|
||||
val scaleHandler: (Offset, Float) -> Unit = remember(containerWidth, containerHeight) {
|
||||
{ center, scale ->
|
||||
var targetLeft = center.x + ((photoTargetTranslateX - center.x) * scale)
|
||||
var targetTop = center.y + ((photoTargetTranslateY - center.y) * scale)
|
||||
val targetWidth = imageWidthPx * photoTargetScale * scale
|
||||
val targetHeight = imageHeightPx * photoTargetScale * scale
|
||||
when {
|
||||
containerWidthPx > targetWidth -> {
|
||||
targetLeft = (containerWidthPx - targetWidth) / 2
|
||||
}
|
||||
targetLeft > 0 -> {
|
||||
targetLeft = 0f
|
||||
}
|
||||
targetLeft + targetWidth < containerWidthPx -> {
|
||||
targetLeft = containerWidthPx - targetWidth
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
containerHeightPx > targetHeight -> {
|
||||
targetTop = (containerHeightPx - targetHeight) / 2
|
||||
}
|
||||
targetTop > 0 -> {
|
||||
targetTop = 0f
|
||||
}
|
||||
targetTop + targetHeight < containerHeightPx -> {
|
||||
targetTop = containerHeightPx - targetHeight
|
||||
}
|
||||
}
|
||||
photoTargetTranslateX = targetLeft
|
||||
photoTargetTranslateY = targetTop
|
||||
photoTargetScale *= scale
|
||||
}
|
||||
}
|
||||
|
||||
transitionState.targetState = true
|
||||
val transition = updateTransition(transitionState = transitionState, label = "PhotoPager")
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(containerWidth)
|
||||
.height(containerHeight)
|
||||
) {
|
||||
PhotoBackgroundWithTransition(backgroundTargetAlpha, transition, transitionDurationMs){
|
||||
PhotoBackground(alpha = it)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pointerInput("${containerWidth}_${containerHeight}") {
|
||||
coroutineScope {
|
||||
launch {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
onPress?.invoke()
|
||||
},
|
||||
onDoubleTap = {
|
||||
if (photoTargetScale == 1f) {
|
||||
var scale = 2f
|
||||
val alignScale = (containerWidth / imageWidth).coerceAtLeast((containerHeight / imageHeight))
|
||||
if (alignScale > 1.25 && alignScale < scale) {
|
||||
scale = alignScale
|
||||
}
|
||||
scaleHandler.invoke(it, scale)
|
||||
} else {
|
||||
photoTargetScale = 1f
|
||||
photoTargetTranslateX = photoTargetNormalTranslateX
|
||||
photoTargetTranslateY = photoTargetNormalTranslateY
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// launch {
|
||||
// detectTransformGestures(true) { centroid: Offset, pan: Offset, zoom: Float, _: Float ->
|
||||
// scaleHandler.invoke(centroid, zoom)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
if (initRect == null || initRect == Rect.Zero) {
|
||||
PhotoContentWithAlphaTransition(
|
||||
scale = photoTargetScale,
|
||||
translateX = photoTargetTranslateX,
|
||||
translateY = photoTargetTranslateY,
|
||||
transition = transition,
|
||||
transitionDurationMs = transitionDurationMs
|
||||
) { alpha, scale, translateX, translateY ->
|
||||
PhotoTransformContent(imageWidth, imageHeight, alpha, scale, translateX, translateY, content)
|
||||
}
|
||||
} else {
|
||||
PhotoContentWithRectTransition(
|
||||
imageWidth = imageWidthPx,
|
||||
imageHeight = imageHeightPx,
|
||||
initRect = initRect,
|
||||
scale = photoTargetScale,
|
||||
translateX = photoTargetTranslateX,
|
||||
translateY = photoTargetTranslateY,
|
||||
transition = transition,
|
||||
transitionDurationMs = transitionDurationMs
|
||||
) { scale, translateX, translateY ->
|
||||
PhotoTransformContent(imageWidth, imageHeight, 1f, scale, translateX, translateY, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoBackgroundWithTransition(
|
||||
backgroundTargetAlpha: Float,
|
||||
transition: Transition<Boolean>,
|
||||
transitionDurationMs: Int,
|
||||
content: @Composable (alpha: Float) -> Unit
|
||||
) {
|
||||
if (transition.currentState) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(backgroundTargetAlpha)
|
||||
.background(Color.Black)
|
||||
)
|
||||
} else {
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = "PhotoBackgroundWithTransition"
|
||||
) {
|
||||
if (it) backgroundTargetAlpha else 0f
|
||||
}
|
||||
content(alpha.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoContentWithAlphaTransition(
|
||||
scale: Float,
|
||||
translateX: Float,
|
||||
translateY: Float,
|
||||
transition: Transition<Boolean>,
|
||||
transitionDurationMs: Int,
|
||||
content: @Composable (alpha: Float, scale: Float, translateX: Float, translateY: Float) -> Unit
|
||||
) {
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = "PhotoContentWithAlphaTransition"
|
||||
) {
|
||||
if (it) 1f else 0f
|
||||
}
|
||||
content(alpha.value, scale, translateX, translateY)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoContentWithRectTransition(
|
||||
imageWidth: Float,
|
||||
imageHeight: Float,
|
||||
initRect: Rect,
|
||||
scale: Float,
|
||||
translateX: Float,
|
||||
translateY: Float,
|
||||
transition: Transition<Boolean>,
|
||||
transitionDurationMs: Int,
|
||||
content: @Composable (scale: Float, translateX: Float, translateY: Float) -> Unit
|
||||
) {
|
||||
val rect = transition.animateRect(
|
||||
transitionSpec = { tween(durationMillis = transitionDurationMs) },
|
||||
label = "PhotoContentWithRectTransition"
|
||||
) {
|
||||
if (it) Rect(translateX, translateY, translateX + imageWidth * scale, translateY + imageHeight * scale) else initRect
|
||||
}
|
||||
content(rect.value.width / imageWidth, rect.value.left, rect.value.top)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoBackground(
|
||||
alpha: Float
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(alpha)
|
||||
.background(Color.Black)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotoTransformContent(
|
||||
imageWidth: Dp,
|
||||
imageHeight: Dp,
|
||||
alpha: Float,
|
||||
scale: Float,
|
||||
translateX: Float,
|
||||
translateY: Float,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(imageWidth)
|
||||
.height(imageHeight)
|
||||
.graphicsLayer {
|
||||
this.transformOrigin = TransformOrigin(0f, 0f)
|
||||
this.scaleX = scale
|
||||
this.scaleY = scale
|
||||
this.translationX = translateX
|
||||
this.translationY = translateY
|
||||
this.alpha = alpha
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class QMUIPhotoThumbnailConfig(
|
||||
val averageIfTwoImage: Boolean = true,
|
||||
val horGap: Dp = 5.dp,
|
||||
val verGap: Dp = 5.dp,
|
||||
val alphaWhenPressed: Float = 0.5f
|
||||
val alphaWhenPressed: Float = 1f
|
||||
)
|
||||
|
||||
val qmuiDefaultPhotoThumbnailConfig = QMUIPhotoThumbnailConfig()
|
||||
|
||||
@@ -45,6 +45,7 @@ interface QMUIPhotoProvider {
|
||||
fun thumbnail(): QMUIPhoto?
|
||||
fun photo(): QMUIPhoto?
|
||||
fun ratio(): Float = -1f
|
||||
fun isLongImage(): Boolean = false
|
||||
|
||||
fun meta(): Bundle?
|
||||
fun recoverCls(): Class<in PhotoTransitionProviderRecover>?
|
||||
|
||||
Reference in New Issue
Block a user