diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FullScreen.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FullScreen.kt index 6bb26c5d..079e6961 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FullScreen.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/FullScreen.kt @@ -3,6 +3,7 @@ package com.huanchengfly.tieba.post.ui.widgets.compose import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat +import android.os.Build import android.view.KeyEvent import android.view.View import android.view.WindowManager @@ -101,6 +102,10 @@ private class FullScreenLayout( format = PixelFormat.TRANSLUCENT flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + flags = flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND + blurBehindRadius = 64 + } } fun show() { @@ -120,6 +125,8 @@ private class FullScreenLayout( fun dismiss() { disposeComposition() setViewTreeLifecycleOwner(null) + setViewTreeViewModelStoreOwner(null) + setViewTreeSavedStateRegistryOwner(null) windowManager.removeViewImmediate(this) } } diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt index cd119ab3..4770212e 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Images.kt @@ -2,32 +2,55 @@ package com.huanchengfly.tieba.post.ui.widgets.compose import android.content.Context import android.os.Parcelable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.IntrinsicSize +import android.util.Log +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.Text +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.lerp +import androidx.compose.ui.util.lerp import com.github.panpf.sketch.compose.AsyncImage import com.github.panpf.sketch.request.Depth import com.github.panpf.sketch.request.DisplayRequest +import com.github.panpf.sketch.stateimage.ThumbnailMemoryCacheStateImage import com.github.panpf.sketch.transform.MaskTransformation -import com.huanchengfly.tieba.post.R +import com.huanchengfly.tieba.post.App +import com.huanchengfly.tieba.post.arch.BaseComposeActivity.Companion.LocalWindowSizeClass import com.huanchengfly.tieba.post.arch.ImmutableHolder import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.models.protos.PhotoViewData import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme +import com.huanchengfly.tieba.post.ui.common.windowsizeclass.WindowWidthSizeClass import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity.Companion.EXTRA_PHOTO_VIEW_DATA import com.huanchengfly.tieba.post.utils.ImageUtil @@ -45,6 +68,154 @@ fun shouldLoadImage(context: Context, skipNetworkCheck: Boolean): Boolean { )) } +@Composable +private fun PreviewImage( + imageUri: String, + show: Boolean, + layoutSizeProvider: () -> IntSize, + layoutOffsetProvider: () -> Offset, + imageAspectRatioProvider: () -> Float, + originImageUri: String? = null, +) { + val context = LocalContext.current + val density = LocalDensity.current + + var showFullScreenLayout by remember { mutableStateOf(false) } + var showPreview by remember { mutableStateOf(false) } + + LaunchedEffect(show) { + if (show) { + showFullScreenLayout = true + showPreview = true + } else { + showPreview = false + } + } + + val windowWidthSizeClass = LocalWindowSizeClass.current.widthSizeClass + val widthFraction = remember(windowWidthSizeClass) { + when (windowWidthSizeClass) { + WindowWidthSizeClass.Compact -> 0.8f + WindowWidthSizeClass.Medium -> 0.6f + else -> 0.4f + } + } + val imageAspectRatio = imageAspectRatioProvider() + val previewImageWidthPx = remember(widthFraction) { + App.ScreenInfo.EXACT_SCREEN_WIDTH * widthFraction + } + val previewImageHeightPx = remember(imageAspectRatio, previewImageWidthPx) { + previewImageWidthPx * imageAspectRatio + } + val previewImageWidthDp = remember(previewImageWidthPx, density) { + with(density) { previewImageWidthPx.toDp() } + } + val previewImageHeightDp = remember(previewImageHeightPx, density) { + with(density) { previewImageHeightPx.toDp() } + } + + val screenWidth = App.ScreenInfo.EXACT_SCREEN_WIDTH + val screenCenterX = screenWidth / 2 + val screenHeight = App.ScreenInfo.EXACT_SCREEN_HEIGHT + val screenCenterY = screenHeight / 2 + val statusBarHeight = WindowInsets.statusBars.getTop(density) + + if (showFullScreenLayout) { + val animProgress = remember { Animatable(0f) } + + LaunchedEffect(showPreview) { + animProgress.animateTo( + if (showPreview) 1f else 0f, + animationSpec = spring() + ) + if (!showPreview) { + showFullScreenLayout = false + } + } + + FullScreen { + val (layoutWidthPx, layoutHeightPx) = layoutSizeProvider() + val layoutWidthDp = remember(layoutWidthPx, density) { + with(density) { layoutWidthPx.toDp() } + } + val layoutHeightDp = remember(layoutHeightPx, density) { + with(density) { layoutHeightPx.toDp() } + } + + val request = remember(imageUri) { + DisplayRequest(context, imageUri) + } + + val originRequest = remember(imageUri, originImageUri) { + DisplayRequest(context, originImageUri ?: imageUri) { + placeholder(ThumbnailMemoryCacheStateImage(imageUri)) + crossfade(fadeStart = false) + } + } + + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .width( + lerp(layoutWidthDp, previewImageWidthDp, animProgress.value) + ) + .height( + lerp(layoutHeightDp, previewImageHeightDp, animProgress.value) + ) + .absoluteOffset { + val layoutOffset = layoutOffsetProvider() + val currentLayoutWidthPx = lerp( + layoutWidthPx.toFloat(), + previewImageWidthPx, + animProgress.value + ) + val currentLayoutHeightPx = lerp( + layoutHeightPx.toFloat(), + previewImageHeightPx, + animProgress.value + ) + IntOffset( + lerp( + layoutOffset.x - (screenCenterX - currentLayoutWidthPx / 2), + 0f, + animProgress.value + ).toInt(), + lerp( + layoutOffset.y - (screenCenterY - currentLayoutHeightPx / 2 + statusBarHeight / 2), + 0f, + animProgress.value + ).toInt() + ) + } + .clip(RoundedCornerShape(6.dp)) + ) { + AsyncImage( + request = request, + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + + if (originImageUri != null && animProgress.value >= 1f) { + AsyncImage( + request = originRequest, + contentDescription = null, + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop + ) + } + } + } + } + } +} + @Composable fun NetworkImage( imageUri: String, @@ -55,6 +226,7 @@ fun NetworkImage( skipNetworkCheck: Boolean = false, ) { val context = LocalContext.current + var shouldLoad by remember { mutableStateOf(shouldLoadImage(context, skipNetworkCheck)) } val enableClick = remember(photoViewData, shouldLoad) { photoViewData != null || !shouldLoad } @@ -63,6 +235,11 @@ fun NetworkImage( MaskTransformation(0x35000000) } else null + var imageAspectRatio by remember(imageUri) { mutableFloatStateOf(0f) } + var layoutSize by remember { mutableStateOf(IntSize.Zero) } + + var layoutOffset by remember { mutableStateOf(Offset.Zero) } + val request = remember(imageUri, shouldLoad, colorMask) { DisplayRequest(context, imageUri) { placeholder(ImageUtil.getPlaceHolder(context, 0)) @@ -73,40 +250,72 @@ fun NetworkImage( if (colorMask != null) { transformations(colorMask) } + listener( + onSuccess = { _, result -> + imageAspectRatio = result.imageInfo.height.toFloat() / result.imageInfo.width + } + ) } } - LongClickMenu( - enabled = enableClick, - indication = null, - interactionSource = remember { MutableInteractionSource() }, - onClick = { - if (!shouldLoad) { - shouldLoad = true - } else if (photoViewData != null) { - context.goToActivity { - putExtra(EXTRA_PHOTO_VIEW_DATA, photoViewData.get() as Parcelable) + var isLongPressing by remember { mutableStateOf(false) } + + PreviewImage( + imageUri = imageUri, + show = isLongPressing, + layoutSizeProvider = { layoutSize }, + layoutOffsetProvider = { layoutOffset }, + imageAspectRatioProvider = { imageAspectRatio }, + originImageUri = photoViewData?.get { data_?.originUrl } + ) + + Box( + modifier = Modifier + .pointerInput(Unit) { + if (enableClick) { + detectTapGestures( + onLongPress = { + isLongPressing = true + }, + onPress = { + awaitRelease() + isLongPressing = false + }, + onTap = { + if (isLongPressing) { + return@detectTapGestures + } + if (!shouldLoad) { + shouldLoad = true + } else if (photoViewData != null) { + context.goToActivity { + putExtra( + EXTRA_PHOTO_VIEW_DATA, + photoViewData.get() as Parcelable + ) + } + } + } + ) } } - }, - menuContent = { - DropdownMenuItem( - onClick = { - ImageUtil.download( - context, - photoViewData?.get { this.data_?.originUrl } ?: imageUri) + .pointerInput(Unit) { + detectDragGesturesAfterLongPress { change, dragAmount -> + Log.i("NetworkImage", "dragAmount: $dragAmount") } - ) { - Text(text = stringResource(id = R.string.title_save_image)) } - }, - modifier = modifier - .width(IntrinsicSize.Min) - .height(IntrinsicSize.Min), + .then(modifier) ) { AsyncImage( request = request, - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .onSizeChanged { + layoutSize = it + } + .onGloballyPositioned { + layoutOffset = it.positionInWindow() + }, contentDescription = contentDescription, contentScale = contentScale, )