feat: photo scale

This commit is contained in:
cgspine
2022-01-24 22:25:06 +08:00
parent cbe36c52c2
commit fe6088b324
4 changed files with 334 additions and 156 deletions
@@ -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>?