feat: 缩略图长按预览大图

This commit is contained in:
HuanCheng65 2023-10-02 01:08:26 +08:00
parent b7aeb2847b
commit 78e509b638
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
2 changed files with 246 additions and 30 deletions

View File

@ -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)
}
}

View File

@ -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 = {
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<PhotoViewActivity> {
putExtra(EXTRA_PHOTO_VIEW_DATA, photoViewData.get() as Parcelable)
putExtra(
EXTRA_PHOTO_VIEW_DATA,
photoViewData.get() as Parcelable
)
}
}
},
menuContent = {
DropdownMenuItem(
onClick = {
ImageUtil.download(
context,
photoViewData?.get { this.data_?.originUrl } ?: imageUri)
}
) {
Text(text = stringResource(id = R.string.title_save_image))
)
}
},
modifier = modifier
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min),
}
.pointerInput(Unit) {
detectDragGesturesAfterLongPress { change, dragAmount ->
Log.i("NetworkImage", "dragAmount: $dragAmount")
}
}
.then(modifier)
) {
AsyncImage(
request = request,
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.onSizeChanged {
layoutSize = it
}
.onGloballyPositioned {
layoutOffset = it.positionInWindow()
},
contentDescription = contentDescription,
contentScale = contentScale,
)