Skip to content

Commit

Permalink
complete ImagePreview
Browse files Browse the repository at this point in the history
  • Loading branch information
SkyDynamic committed Mar 2, 2025
1 parent 457ef65 commit e19877c
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 148 deletions.
13 changes: 8 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
Expand Down Expand Up @@ -44,9 +52,4 @@
</provider>
</application>

<uses-permission android:name="android.permission.INTERNET"
tools:ignore="ManifestOrder" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

</manifest>
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
package io.github.skydynamic.maiproberplus.ui.component

import android.annotation.SuppressLint
import androidx.compose.foundation.Image
import android.graphics.Bitmap
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
Expand All @@ -27,145 +17,112 @@ import androidx.compose.runtime.remember
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.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil3.Bitmap
import io.github.skydynamic.maiproberplus.Application.Companion.application
import io.github.skydynamic.maiproberplus.R
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.unit.IntSize
import coil3.compose.AsyncImage
import kotlin.math.max
import kotlin.math.min

// TODO: 完善它,现在是能用就行
@Composable
@SuppressLint("UnusedBoxWithConstraintsScope")
@OptIn(ExperimentalMaterial3Api::class)
fun ImagePreview(
image: Bitmap,
onDismiss: () -> Unit
) {
BasicAlertDialog(
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.75f)),
onDismissRequest = onDismiss,
) {
Box(
modifier = Modifier
.fillMaxWidth()
) {
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Row(
modifier = Modifier
.align(Alignment.TopEnd)
) {
IconButton(
onClick = {
application.saveImageToGallery(
image,
"${System.currentTimeMillis()}.jpg"
)
}
) {
Icon(painterResource(R.drawable.save_24px), null)
}

IconButton(
onClick = onDismiss
) {
Icon(Icons.Default.Close, null)
}
}
}

var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }

BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
val maxWidth = constraints.maxWidth.toFloat()
val maxHeight = constraints.maxHeight.toFloat()
var imageInitialSize by remember { mutableStateOf(IntSize.Zero) }
var boxSize by remember { mutableStateOf(IntSize.Zero) }

Box(
modifier = Modifier
.fillMaxWidth()
.clip(RectangleShape)
) {
Image(
bitmap = image.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.Center)
.graphicsLayer(
scaleX = scale,
scaleY = scale,
translationX = offset.x,
translationY = offset.y
)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, _ ->
scale *= zoom
scale = scale.coerceIn(1f, 4f)
var maxScale by remember { mutableFloatStateOf(1f) }
var scale by remember { mutableFloatStateOf(1f) }
var offset by remember { mutableStateOf(Offset.Zero) }

val maxX = (maxWidth * (scale - 1)) / 2
val maxY = (maxHeight * (scale - 1)) / 2

val newOffsetX = min(max(offset.x + pan.x, -maxX), maxX)
val newOffsetY = min(max(offset.y + pan.y, -maxY), maxY)
Box(
modifier = Modifier
.background(Color.Black)
.fillMaxSize()
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offset.x
translationY = offset.y
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
do {
val event = awaitPointerEvent()
val zoomChange = event.calculateZoom()
scale *= zoomChange

offset = Offset(newOffsetX, newOffsetY)
}
detectTapGestures(
onTap = {
onDismiss()
},
onDoubleTap = {
scale = if (scale < 2f) {
2f
} else {
1f
}
val imageWidth = maxWidth * scale
val imageHeight = maxHeight * scale
val centerX = (maxWidth - imageWidth) / 2
val centerY = (maxHeight - imageHeight) / 2
offset = Offset(centerX, centerY)
}
)
detectDragGestures { change, dragAmount ->
change.consume()
var panChange = event.calculatePan()
panChange *= scale

val scaledDragAmount = Offset(dragAmount.x * scale, dragAmount.y * scale)
val tempScale = scale.coerceIn(1f, maxScale)
var x = offset.x
if (tempScale * imageInitialSize.width > boxSize.width) {
val delta =
tempScale * imageInitialSize.width - boxSize.width
x = (offset.x + panChange.x).coerceIn(-delta / 2, delta / 2)
}
var y = offset.y
if (tempScale * imageInitialSize.height > boxSize.height) {
val delta =
tempScale * imageInitialSize.height - boxSize.height
y = (offset.y + panChange.y).coerceIn(-delta / 2, delta / 2)
}

val maxX = (maxWidth * (scale - 1)) / 2
val maxY = (maxHeight * (scale - 1)) / 2
if (x != offset.x || y != offset.y) {
offset = Offset(x, y)
}

offset = Offset(
x = min(max(offset.x + scaledDragAmount.x, -maxX), maxX),
y = min(max(offset.y + scaledDragAmount.y, -maxY), maxY)
)
}
event.changes.forEach {
if (it.positionChanged()) {
it.consume()
}
)
}
} while (event.changes.any { it.pressed })

scale = scale.coerceIn(1f, maxScale)
if (scale == 1f) {
offset = Offset.Zero
}
}
}
}
}
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = {
offset = Offset.Zero
scale = if (scale != 1.0f) {
1.0f
} else {
min(2f, min(maxScale, 5f))
}
}, onTap = {
onDismiss()
}
)
}
.onSizeChanged {
boxSize = it
val xRatio = it.width.toFloat() / imageInitialSize.width
val yRatio = it.height.toFloat() / imageInitialSize.height
maxScale = min(5f, max(2f, max(xRatio, yRatio)))
},
contentAlignment = Alignment.Center
) {
AsyncImage(
model = image,
contentDescription = null,
modifier = Modifier
.onSizeChanged {
imageInitialSize = it
}
)
}
}

Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.github.skydynamic.maiproberplus.ui.compose.bests

import android.graphics.Bitmap
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
Expand All @@ -24,8 +28,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil3.compose.AsyncImage
Expand Down Expand Up @@ -64,15 +72,6 @@ fun BestsImageGenerateCompose() {
val config = application.configManager.config
var showImagePreview by remember { mutableStateOf(false) }

when {
showImagePreview -> {
ImagePreview(
BestsImageGenerateViewModel.imageBitmap!!,
onDismiss = { showImagePreview = false }
)
}
}

Column(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -166,6 +165,30 @@ fun BestsImageGenerateCompose() {
WindowInsetsSpacer.BottomPaddingSpacer()
}
}

if (showImagePreview) {
Popup(
onDismissRequest = { showImagePreview = false },
properties = PopupProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black)
.pointerInput(Unit) {
detectTapGestures(onTap = { showImagePreview = false }) // 点击外部关闭
}
) {
ImagePreview(
image = BestsImageGenerateViewModel.imageBitmap!!,
onDismiss = { showImagePreview = false }
)
}
}
}
}

@Composable
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
<cache-path name="cache" path="/" />
</paths>
2 changes: 0 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ kotlinxSerializationJson = "1.7.0"
ktorVersion = "3.0.1"
nanohttpdVersion = "2.2.0"
ksp = "2.0.21-1.0.27"
openimagecoillib = "2.3.9"
roomCompiler = "2.6.1"
roomKtx = "2.6.1"
roomRuntime = "2.6.1"
Expand Down Expand Up @@ -49,7 +48,6 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktorVersi
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorVersion" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorVersion" }
nanohttpd-nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpdVersion" }
openimagecoillib = { module = "io.github.FlyJingFish.OpenImage:OpenImageCoilLib", version.ref = "openimagecoillib" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Expand Down

0 comments on commit e19877c

Please sign in to comment.