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.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.os.Build
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
@ -101,6 +102,10 @@ private class FullScreenLayout(
format = PixelFormat.TRANSLUCENT format = PixelFormat.TRANSLUCENT
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS 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() { fun show() {
@ -120,6 +125,8 @@ private class FullScreenLayout(
fun dismiss() { fun dismiss() {
disposeComposition() disposeComposition()
setViewTreeLifecycleOwner(null) setViewTreeLifecycleOwner(null)
setViewTreeViewModelStoreOwner(null)
setViewTreeSavedStateRegistryOwner(null)
windowManager.removeViewImmediate(this) windowManager.removeViewImmediate(this)
} }
} }

View File

@ -2,32 +2,55 @@ package com.huanchengfly.tieba.post.ui.widgets.compose
import android.content.Context import android.content.Context
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.interaction.MutableInteractionSource import android.util.Log
import androidx.compose.foundation.layout.IntrinsicSize 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.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.DropdownMenuItem import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.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.compose.AsyncImage
import com.github.panpf.sketch.request.Depth import com.github.panpf.sketch.request.Depth
import com.github.panpf.sketch.request.DisplayRequest import com.github.panpf.sketch.request.DisplayRequest
import com.github.panpf.sketch.stateimage.ThumbnailMemoryCacheStateImage
import com.github.panpf.sketch.transform.MaskTransformation 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.arch.ImmutableHolder
import com.huanchengfly.tieba.post.goToActivity import com.huanchengfly.tieba.post.goToActivity
import com.huanchengfly.tieba.post.models.protos.PhotoViewData 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.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
import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity.Companion.EXTRA_PHOTO_VIEW_DATA import com.huanchengfly.tieba.post.ui.page.photoview.PhotoViewActivity.Companion.EXTRA_PHOTO_VIEW_DATA
import com.huanchengfly.tieba.post.utils.ImageUtil 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 @Composable
fun NetworkImage( fun NetworkImage(
imageUri: String, imageUri: String,
@ -55,6 +226,7 @@ fun NetworkImage(
skipNetworkCheck: Boolean = false, skipNetworkCheck: Boolean = false,
) { ) {
val context = LocalContext.current val context = LocalContext.current
var shouldLoad by remember { mutableStateOf(shouldLoadImage(context, skipNetworkCheck)) } var shouldLoad by remember { mutableStateOf(shouldLoadImage(context, skipNetworkCheck)) }
val enableClick = remember(photoViewData, shouldLoad) { photoViewData != null || !shouldLoad } val enableClick = remember(photoViewData, shouldLoad) { photoViewData != null || !shouldLoad }
@ -63,6 +235,11 @@ fun NetworkImage(
MaskTransformation(0x35000000) MaskTransformation(0x35000000)
} else null } 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) { val request = remember(imageUri, shouldLoad, colorMask) {
DisplayRequest(context, imageUri) { DisplayRequest(context, imageUri) {
placeholder(ImageUtil.getPlaceHolder(context, 0)) placeholder(ImageUtil.getPlaceHolder(context, 0))
@ -73,40 +250,72 @@ fun NetworkImage(
if (colorMask != null) { if (colorMask != null) {
transformations(colorMask) transformations(colorMask)
} }
listener(
onSuccess = { _, result ->
imageAspectRatio = result.imageInfo.height.toFloat() / result.imageInfo.width
}
)
} }
} }
LongClickMenu( var isLongPressing by remember { mutableStateOf(false) }
enabled = enableClick,
indication = null, PreviewImage(
interactionSource = remember { MutableInteractionSource() }, imageUri = imageUri,
onClick = { 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) { if (!shouldLoad) {
shouldLoad = true shouldLoad = true
} else if (photoViewData != null) { } else if (photoViewData != null) {
context.goToActivity<PhotoViewActivity> { 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 .pointerInput(Unit) {
.width(IntrinsicSize.Min) detectDragGesturesAfterLongPress { change, dragAmount ->
.height(IntrinsicSize.Min), Log.i("NetworkImage", "dragAmount: $dragAmount")
}
}
.then(modifier)
) { ) {
AsyncImage( AsyncImage(
request = request, request = request,
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.onSizeChanged {
layoutSize = it
}
.onGloballyPositioned {
layoutOffset = it.positionInWindow()
},
contentDescription = contentDescription, contentDescription = contentDescription,
contentScale = contentScale, contentScale = contentScale,
) )