mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Updated imageviewer example to use CoroutineScope (loading and processing images).
This commit is contained in:
@@ -3,7 +3,7 @@ package example.imageviewer
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import example.imageviewer.view.BuildAppUI
|
||||
import example.imageviewer.view.AppUI
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.ImageRepository
|
||||
|
||||
@@ -17,7 +17,7 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
setContent {
|
||||
BuildAppUI(content)
|
||||
AppUI(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ buildscript {
|
||||
|
||||
dependencies {
|
||||
// __LATEST_COMPOSE_RELEASE_VERSION__
|
||||
classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1-rc1")
|
||||
classpath("org.jetbrains.compose:compose-gradle-plugin:1.0.0-alpha1-rc3")
|
||||
classpath("com.android.tools.build:gradle:7.0.0")
|
||||
classpath(kotlin("gradle-plugin", version = "1.5.21"))
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ object ContentState {
|
||||
this.uriRepository = uriRepository
|
||||
repository = ImageRepository(uriRepository)
|
||||
appliedFilters = FiltersManager(context)
|
||||
isAppUIReady.value = false
|
||||
isContentReady.value = false
|
||||
|
||||
initData()
|
||||
|
||||
@@ -50,9 +50,14 @@ object ContentState {
|
||||
return context.resources.configuration.orientation
|
||||
}
|
||||
|
||||
private val isAppUIReady = mutableStateOf(false)
|
||||
private val isAppReady = mutableStateOf(false)
|
||||
fun isAppReady(): Boolean {
|
||||
return isAppReady.value
|
||||
}
|
||||
|
||||
private val isContentReady = mutableStateOf(false)
|
||||
fun isContentReady(): Boolean {
|
||||
return isAppUIReady.value
|
||||
return isContentReady.value
|
||||
}
|
||||
|
||||
fun getString(id: Int): String {
|
||||
@@ -142,7 +147,7 @@ object ContentState {
|
||||
|
||||
// application content initialization
|
||||
private fun initData() {
|
||||
if (isAppUIReady.value)
|
||||
if (isContentReady.value)
|
||||
return
|
||||
|
||||
val directory = context.cacheDir.absolutePath
|
||||
@@ -158,7 +163,7 @@ object ContentState {
|
||||
getString(R.string.repo_invalid),
|
||||
context
|
||||
)
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
@@ -171,7 +176,7 @@ object ContentState {
|
||||
getString(R.string.repo_empty),
|
||||
context
|
||||
)
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
val picture = loadFullImage(imageList[0])
|
||||
@@ -186,7 +191,7 @@ object ContentState {
|
||||
mainImage.value = MainImageWrapper.getImage()
|
||||
currentImageIndex.value = MainImageWrapper.getId()
|
||||
}
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -195,7 +200,7 @@ object ContentState {
|
||||
getString(R.string.no_internet),
|
||||
context
|
||||
)
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -210,7 +215,7 @@ object ContentState {
|
||||
}
|
||||
|
||||
fun fullscreen(picture: Picture) {
|
||||
isAppUIReady.value = false
|
||||
isContentReady.value = false
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
setMainImage(picture)
|
||||
}
|
||||
@@ -218,9 +223,10 @@ object ContentState {
|
||||
fun setMainImage(picture: Picture) {
|
||||
if (MainImageWrapper.getId() == picture.id) {
|
||||
if (!isContentReady())
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
return
|
||||
}
|
||||
isContentReady.value = false
|
||||
|
||||
executor.execute {
|
||||
if (isInternetAvailable()) {
|
||||
@@ -230,7 +236,7 @@ object ContentState {
|
||||
|
||||
handler.post {
|
||||
wrapPictureIntoMainImage(fullSizePicture)
|
||||
isAppUIReady.value = true
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
handler.post {
|
||||
@@ -244,6 +250,11 @@ object ContentState {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentReady() {
|
||||
isContentReady.value = true
|
||||
isAppReady.value = true
|
||||
}
|
||||
|
||||
private fun wrapPictureIntoMainImage(picture: Picture) {
|
||||
MainImageWrapper.wrapPicture(picture)
|
||||
MainImageWrapper.saveOrigin()
|
||||
@@ -282,8 +293,9 @@ object ContentState {
|
||||
if (isInternetAvailable()) {
|
||||
handler.post {
|
||||
clearCache(context)
|
||||
MainImageWrapper.clear()
|
||||
miniatures.clear()
|
||||
isAppUIReady.value = false
|
||||
isContentReady.value = false
|
||||
initData()
|
||||
}
|
||||
} else {
|
||||
@@ -334,6 +346,10 @@ private object MainImageWrapper {
|
||||
return (picture.value.name == "")
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
picture.value = Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return picture.value.name
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicBlur
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import example.imageviewer.view.DragHandler
|
||||
|
||||
fun scaleBitmapAspectRatio(
|
||||
bitmap: Bitmap,
|
||||
@@ -116,3 +119,77 @@ fun displayWidth(): Int {
|
||||
fun displayHeight(): Int {
|
||||
return Resources.getSystem().displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap),
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return Bitmap.createBitmap(
|
||||
bitmap,
|
||||
crop.left,
|
||||
crop.top,
|
||||
crop.right - crop.left,
|
||||
crop.bottom - crop.top
|
||||
)
|
||||
}
|
||||
|
||||
fun cropBitmapByBounds(
|
||||
bitmap: Bitmap,
|
||||
bounds: Rect,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rect {
|
||||
if (scaleFactor <= 1f)
|
||||
return Rect(0, 0, bitmap.width, bitmap.height)
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width() / scale).roundToInt()
|
||||
var boundH = (bounds.height() / scale).roundToInt()
|
||||
|
||||
scale *= displayWidth() / bounds.width().toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW.toFloat()
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0f
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH.toFloat()
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0f
|
||||
}
|
||||
|
||||
return Rect(
|
||||
leftOffset.toInt(),
|
||||
topOffset.toInt(),
|
||||
(leftOffset + boundW).toInt(),
|
||||
(topOffset + boundH).toInt()
|
||||
)
|
||||
}
|
||||
@@ -14,18 +14,18 @@ import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.style.Gray
|
||||
|
||||
@Composable
|
||||
fun BuildAppUI(content: ContentState) {
|
||||
fun AppUI(content: ContentState) {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Gray
|
||||
) {
|
||||
when (AppState.screenState()) {
|
||||
ScreenType.Main -> {
|
||||
setMainScreen(content)
|
||||
ScreenType.MainScreen -> {
|
||||
MainScreen(content)
|
||||
}
|
||||
ScreenType.FullscreenImage -> {
|
||||
setImageFullScreen(content)
|
||||
FullscreenImage(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import example.imageviewer.style.icFilterGrayscaleOn
|
||||
import example.imageviewer.style.icFilterPixelOff
|
||||
import example.imageviewer.style.icFilterPixelOn
|
||||
import example.imageviewer.utils.adjustImageScale
|
||||
import example.imageviewer.utils.cropBitmapByScale
|
||||
import example.imageviewer.utils.displayWidth
|
||||
import example.imageviewer.utils.getDisplayBounds
|
||||
import kotlin.math.abs
|
||||
@@ -54,37 +55,20 @@ import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun setImageFullScreen(
|
||||
fun FullscreenImage(
|
||||
content: ContentState
|
||||
) {
|
||||
if (content.isContentReady()) {
|
||||
Column {
|
||||
setToolBar(content.getSelectedImageName(), content)
|
||||
setImage(content)
|
||||
}
|
||||
} else {
|
||||
setLoadingScreen()
|
||||
Column {
|
||||
ToolBar(content.getSelectedImageName(), content)
|
||||
Image(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun setLoadingScreen() {
|
||||
|
||||
Box {
|
||||
Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {}
|
||||
Box {
|
||||
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
|
||||
color = DarkGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setToolBar(
|
||||
fun ToolBar(
|
||||
text: String,
|
||||
content: ContentState
|
||||
) {
|
||||
@@ -100,7 +84,7 @@ fun setToolBar(
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.restoreMainImage()
|
||||
AppState.screenState(ScreenType.Main)
|
||||
AppState.screenState(ScreenType.MainScreen)
|
||||
}
|
||||
}) {
|
||||
Image(
|
||||
@@ -160,7 +144,6 @@ fun FilterButton(
|
||||
|
||||
@Composable
|
||||
fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
|
||||
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
|
||||
@@ -169,8 +152,7 @@ fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setImage(content: ContentState) {
|
||||
|
||||
fun Image(content: ContentState) {
|
||||
val drag = remember { DragHandler() }
|
||||
val scale = remember { ScaleHandler() }
|
||||
|
||||
@@ -213,79 +195,3 @@ fun imageByGesture(
|
||||
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun cropBitmapByScale(bitmap: Bitmap, scale: Float, drag: DragHandler): Bitmap {
|
||||
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap),
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return Bitmap.createBitmap(
|
||||
bitmap,
|
||||
crop.left,
|
||||
crop.top,
|
||||
crop.right - crop.left,
|
||||
crop.bottom - crop.top
|
||||
)
|
||||
}
|
||||
|
||||
private fun cropBitmapByBounds(
|
||||
bitmap: Bitmap,
|
||||
bounds: Rect,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rect {
|
||||
|
||||
if (scaleFactor <= 1f)
|
||||
return Rect(0, 0, bitmap.width, bitmap.height)
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width() / scale).roundToInt()
|
||||
var boundH = (bounds.height() / scale).roundToInt()
|
||||
|
||||
scale *= displayWidth() / bounds.width().toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt().toFloat()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW.toFloat()
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0f
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt().toFloat()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH.toFloat()
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0f
|
||||
}
|
||||
|
||||
return Rect(
|
||||
leftOffset.toInt(),
|
||||
topOffset.toInt(),
|
||||
(leftOffset + boundW).toInt(),
|
||||
(topOffset + boundH).toInt()
|
||||
)
|
||||
}
|
||||
@@ -47,58 +47,30 @@ import example.imageviewer.style.icDots
|
||||
import example.imageviewer.style.icEmpty
|
||||
import example.imageviewer.style.icRefresh
|
||||
|
||||
|
||||
@Composable
|
||||
fun setMainScreen(content: ContentState) {
|
||||
|
||||
if (content.isContentReady()) {
|
||||
Column {
|
||||
setTopContent(content)
|
||||
setScrollableArea(content)
|
||||
}
|
||||
} else {
|
||||
setLoadingScreen(content)
|
||||
fun MainScreen(content: ContentState) {
|
||||
Column {
|
||||
TopContent(content)
|
||||
ScrollableArea(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen(content.getString(R.string.loading))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setLoadingScreen(content: ContentState) {
|
||||
|
||||
Box {
|
||||
Column {
|
||||
setTopContent(content)
|
||||
}
|
||||
Box(modifier = Modifier.align(Alignment.Center)) {
|
||||
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp).padding(4.dp),
|
||||
color = DarkGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = content.getString(R.string.loading),
|
||||
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = Foreground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setTopContent(content: ContentState) {
|
||||
setTitleBar(text = content.getString(R.string.app_name), content = content)
|
||||
fun TopContent(content: ContentState) {
|
||||
TitleBar(text = content.getString(R.string.app_name), content = content)
|
||||
if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
|
||||
setPreviewImageUI(content)
|
||||
setSpacer(h = 10)
|
||||
setDivider()
|
||||
PreviewImage(content)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Divider()
|
||||
}
|
||||
setSpacer(h = 5)
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setTitleBar(text: String, content: ContentState) {
|
||||
|
||||
fun TitleBar(text: String, content: ContentState) {
|
||||
TopAppBar(
|
||||
backgroundColor = DarkGreen,
|
||||
title = {
|
||||
@@ -132,8 +104,7 @@ fun setTitleBar(text: String, content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setPreviewImageUI(content: ContentState) {
|
||||
|
||||
fun PreviewImage(content: ContentState) {
|
||||
Clickable(onClick = {
|
||||
AppState.screenState(ScreenType.FullscreenImage)
|
||||
}) {
|
||||
@@ -159,11 +130,10 @@ fun setPreviewImageUI(content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setMiniatureUI(
|
||||
fun Miniature(
|
||||
picture: Picture,
|
||||
content: ContentState
|
||||
) {
|
||||
|
||||
Card(
|
||||
backgroundColor = MiniatureColor,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp).height(70.dp)
|
||||
@@ -224,12 +194,12 @@ fun setMiniatureUI(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setScrollableArea(content: ContentState) {
|
||||
fun ScrollableArea(content: ContentState) {
|
||||
var index = 1
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.verticalScroll(scrollState)) {
|
||||
for (picture in content.getMiniatures()) {
|
||||
setMiniatureUI(
|
||||
Miniature(
|
||||
picture = picture,
|
||||
content = content
|
||||
)
|
||||
@@ -240,16 +210,9 @@ fun setScrollableArea(content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setDivider() {
|
||||
|
||||
fun Divider() {
|
||||
Divider(
|
||||
color = LightGray,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setSpacer(h: Int) {
|
||||
|
||||
Spacer(modifier = Modifier.height(h.dp))
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
enum class ScreenType {
|
||||
Main, FullscreenImage
|
||||
MainScreen, FullscreenImage
|
||||
}
|
||||
|
||||
object AppState {
|
||||
private var screen: MutableState<ScreenType>
|
||||
init {
|
||||
screen = mutableStateOf(ScreenType.Main)
|
||||
screen = mutableStateOf(ScreenType.MainScreen)
|
||||
}
|
||||
|
||||
fun screenState() : ScreenType {
|
||||
|
||||
@@ -15,6 +15,7 @@ import example.imageviewer.style.Transparent
|
||||
fun Draggable(
|
||||
dragHandler: DragHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
onUpdate: (() -> Unit)? = null,
|
||||
children: @Composable() () -> Unit
|
||||
) {
|
||||
Surface(
|
||||
@@ -26,6 +27,7 @@ fun Draggable(
|
||||
onDragCancel = { dragHandler.cancel() },
|
||||
) { change, dragAmount ->
|
||||
dragHandler.drag(dragAmount)
|
||||
onUpdate?.invoke()
|
||||
change.consumePositionChange()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.DarkGreen
|
||||
import example.imageviewer.style.Foreground
|
||||
import example.imageviewer.style.TranslucentBlack
|
||||
|
||||
@Composable
|
||||
fun LoadingScreen(text: String = "") {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(color = TranslucentBlack)
|
||||
) {
|
||||
Box(modifier = Modifier.align(Alignment.Center)) {
|
||||
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
|
||||
color = DarkGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = Foreground
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ fun Scalable(
|
||||
Surface(
|
||||
color = Transparent,
|
||||
modifier = modifier.pointerInput(Unit) {
|
||||
detectTapGestures(onDoubleTap = { onScale.resetFactor() })
|
||||
detectTapGestures(onDoubleTap = { onScale.reset() })
|
||||
detectTransformGestures { _, _, zoom, _ ->
|
||||
onScale.onScale(zoom)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fun Scalable(
|
||||
class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) {
|
||||
val factor = mutableStateOf(1f)
|
||||
|
||||
fun resetFactor() {
|
||||
fun reset() {
|
||||
if (factor.value > minFactor)
|
||||
factor.value = minFactor
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ object ResString {
|
||||
val picture: String
|
||||
val size: String
|
||||
val pixels: String
|
||||
val back: String
|
||||
val refresh: String
|
||||
|
||||
init {
|
||||
if (System.getProperty("user.language").equals("ru")) {
|
||||
@@ -29,6 +31,8 @@ object ResString {
|
||||
picture = "Изображение:"
|
||||
size = "Размеры:"
|
||||
pixels = "пикселей."
|
||||
back = "Назад"
|
||||
refresh = "Обновить"
|
||||
} else {
|
||||
appName = "ImageViewer"
|
||||
loading = "Loading images..."
|
||||
@@ -42,6 +46,8 @@ object ResString {
|
||||
picture = "Picture:"
|
||||
size = "Size:"
|
||||
pixels = "pixels."
|
||||
back = "Back"
|
||||
refresh = "Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package example.imageviewer.model
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.RememberObserver
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.window.WindowState
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import example.imageviewer.ResString
|
||||
import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.filtration.FiltersManager
|
||||
@@ -11,18 +12,28 @@ import example.imageviewer.utils.cacheImagePath
|
||||
import example.imageviewer.utils.clearCache
|
||||
import example.imageviewer.utils.isInternetAvailable
|
||||
import example.imageviewer.view.showPopUpMessage
|
||||
import example.imageviewer.view.DragHandler
|
||||
import example.imageviewer.view.ScaleHandler
|
||||
import example.imageviewer.utils.cropBitmapByScale
|
||||
import example.imageviewer.utils.toByteArray
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import javax.swing.SwingUtilities.invokeLater
|
||||
|
||||
|
||||
object ContentState : RememberObserver {
|
||||
import org.jetbrains.skija.Image.makeFromEncoded
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
object ContentState {
|
||||
val drag = DragHandler()
|
||||
val scale = ScaleHandler()
|
||||
lateinit var windowState: WindowState
|
||||
private lateinit var repository: ImageRepository
|
||||
private lateinit var uriRepository: String
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
fun applyContent(state: WindowState, uriRepository: String): ContentState {
|
||||
windowState = state
|
||||
@@ -38,8 +49,6 @@ object ContentState : RememberObserver {
|
||||
return this
|
||||
}
|
||||
|
||||
private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
|
||||
|
||||
private val isAppReady = mutableStateOf(false)
|
||||
fun isAppReady(): Boolean {
|
||||
return isAppReady.value
|
||||
@@ -51,7 +60,6 @@ object ContentState : RememberObserver {
|
||||
}
|
||||
|
||||
// drawable content
|
||||
private val mainImageWrapper = MainImageWrapper
|
||||
private val mainImage = mutableStateOf(BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
private val currentImageIndex = mutableStateOf(0)
|
||||
private val miniatures = Miniatures()
|
||||
@@ -60,12 +68,12 @@ object ContentState : RememberObserver {
|
||||
return miniatures.getMiniatures()
|
||||
}
|
||||
|
||||
fun getSelectedImage(): BufferedImage {
|
||||
return mainImage.value
|
||||
fun getSelectedImage(): ImageBitmap {
|
||||
return MainImageWrapper.mainImageAsImageBitmap.value
|
||||
}
|
||||
|
||||
fun getSelectedImageName(): String {
|
||||
return mainImageWrapper.getName()
|
||||
return MainImageWrapper.getName()
|
||||
}
|
||||
|
||||
// filters managing
|
||||
@@ -82,7 +90,6 @@ object ContentState : RememberObserver {
|
||||
}
|
||||
|
||||
fun toggleFilter(filter: FilterType) {
|
||||
|
||||
if (containsFilter(filter)) {
|
||||
removeFilter(filter)
|
||||
} else {
|
||||
@@ -91,23 +98,24 @@ object ContentState : RememberObserver {
|
||||
|
||||
toggleFilterState(filter)
|
||||
|
||||
var bitmap = mainImageWrapper.origin
|
||||
var bitmap = MainImageWrapper.origin
|
||||
|
||||
if (bitmap != null) {
|
||||
bitmap = appliedFilters.applyFilters(bitmap)
|
||||
mainImageWrapper.setImage(bitmap)
|
||||
MainImageWrapper.setImage(bitmap)
|
||||
mainImage.value = bitmap
|
||||
updateMainImage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFilter(filter: FilterType) {
|
||||
appliedFilters.add(filter)
|
||||
mainImageWrapper.addFilter(filter)
|
||||
MainImageWrapper.addFilter(filter)
|
||||
}
|
||||
|
||||
private fun removeFilter(filter: FilterType) {
|
||||
appliedFilters.remove(filter)
|
||||
mainImageWrapper.removeFilter(filter)
|
||||
MainImageWrapper.removeFilter(filter)
|
||||
}
|
||||
|
||||
private fun containsFilter(type: FilterType): Boolean {
|
||||
@@ -124,7 +132,7 @@ object ContentState : RememberObserver {
|
||||
private fun restoreFilters(): BufferedImage {
|
||||
filterUIState.clear()
|
||||
appliedFilters.clear()
|
||||
return mainImageWrapper.restore()
|
||||
return MainImageWrapper.restore()
|
||||
}
|
||||
|
||||
fun restoreMainImage() {
|
||||
@@ -141,52 +149,41 @@ object ContentState : RememberObserver {
|
||||
directory.mkdir()
|
||||
}
|
||||
|
||||
executor.execute {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
if (isInternetAvailable()) {
|
||||
val imageList = repository.get()
|
||||
|
||||
if (imageList.isEmpty()) {
|
||||
invokeLater {
|
||||
showPopUpMessage(
|
||||
ResString.repoInvalid
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
showPopUpMessage(
|
||||
ResString.repoInvalid
|
||||
)
|
||||
onContentReady()
|
||||
} else {
|
||||
val pictureList = loadImages(cacheImagePath, imageList)
|
||||
|
||||
val pictureList = loadImages(cacheImagePath, imageList)
|
||||
|
||||
if (pictureList.isEmpty()) {
|
||||
invokeLater {
|
||||
if (pictureList.isEmpty()) {
|
||||
showPopUpMessage(
|
||||
ResString.repoEmpty
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
val picture = loadFullImage(imageList[0])
|
||||
|
||||
invokeLater {
|
||||
} else {
|
||||
val picture = loadFullImage(imageList[0])
|
||||
miniatures.setMiniatures(pictureList)
|
||||
|
||||
if (isMainImageEmpty()) {
|
||||
wrapPictureIntoMainImage(picture)
|
||||
} else {
|
||||
appliedFilters.add(mainImageWrapper.getFilters())
|
||||
currentImageIndex.value = mainImageWrapper.getId()
|
||||
appliedFilters.add(MainImageWrapper.getFilters())
|
||||
currentImageIndex.value = MainImageWrapper.getId()
|
||||
}
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
invokeLater {
|
||||
showPopUpMessage(
|
||||
ResString.noInternet
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
showPopUpMessage(
|
||||
ResString.noInternet
|
||||
)
|
||||
onContentReady()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -196,7 +193,7 @@ object ContentState : RememberObserver {
|
||||
|
||||
// preview/fullscreen image managing
|
||||
fun isMainImageEmpty(): Boolean {
|
||||
return mainImageWrapper.isEmpty()
|
||||
return MainImageWrapper.isEmpty()
|
||||
}
|
||||
|
||||
fun fullscreen(picture: Picture) {
|
||||
@@ -206,31 +203,27 @@ object ContentState : RememberObserver {
|
||||
}
|
||||
|
||||
fun setMainImage(picture: Picture) {
|
||||
if (mainImageWrapper.getId() == picture.id) {
|
||||
if (MainImageWrapper.getId() == picture.id) {
|
||||
if (!isContentReady()) {
|
||||
onContentReady()
|
||||
}
|
||||
return
|
||||
}
|
||||
isContentReady.value = false
|
||||
|
||||
executor.execute {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
scale.reset()
|
||||
if (isInternetAvailable()) {
|
||||
|
||||
invokeLater {
|
||||
val fullSizePicture = loadFullImage(picture.source)
|
||||
fullSizePicture.id = picture.id
|
||||
wrapPictureIntoMainImage(fullSizePicture)
|
||||
onContentReady()
|
||||
}
|
||||
} else {
|
||||
invokeLater {
|
||||
showPopUpMessage(
|
||||
"${ResString.noInternet}\n${ResString.loadImageUnavailable}"
|
||||
)
|
||||
wrapPictureIntoMainImage(picture)
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
onContentReady()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,10 +233,24 @@ object ContentState : RememberObserver {
|
||||
}
|
||||
|
||||
private fun wrapPictureIntoMainImage(picture: Picture) {
|
||||
mainImageWrapper.wrapPicture(picture)
|
||||
mainImageWrapper.saveOrigin()
|
||||
MainImageWrapper.wrapPicture(picture)
|
||||
MainImageWrapper.saveOrigin()
|
||||
mainImage.value = picture.image
|
||||
currentImageIndex.value = picture.id
|
||||
updateMainImage()
|
||||
}
|
||||
|
||||
fun updateMainImage() {
|
||||
MainImageWrapper.mainImageAsImageBitmap.value = makeFromEncoded(
|
||||
toByteArray(
|
||||
cropBitmapByScale(
|
||||
mainImage.value,
|
||||
windowState.size,
|
||||
scale.factor.value,
|
||||
drag
|
||||
)
|
||||
)
|
||||
).asImageBitmap()
|
||||
}
|
||||
|
||||
fun swipeNext() {
|
||||
@@ -267,29 +274,20 @@ object ContentState : RememberObserver {
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
executor.execute {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (isInternetAvailable()) {
|
||||
invokeLater {
|
||||
clearCache()
|
||||
miniatures.clear()
|
||||
isContentReady.value = false
|
||||
initData()
|
||||
}
|
||||
clearCache()
|
||||
MainImageWrapper.clear()
|
||||
miniatures.clear()
|
||||
isContentReady.value = false
|
||||
initData()
|
||||
} else {
|
||||
invokeLater {
|
||||
showPopUpMessage(
|
||||
"${ResString.noInternet}\n${ResString.refreshUnavailable}"
|
||||
)
|
||||
}
|
||||
showPopUpMessage(
|
||||
"${ResString.noInternet}\n${ResString.refreshUnavailable}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemembered() { }
|
||||
override fun onAbandoned() { }
|
||||
override fun onForgotten() {
|
||||
executor.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
private object MainImageWrapper {
|
||||
@@ -302,15 +300,15 @@ private object MainImageWrapper {
|
||||
}
|
||||
|
||||
fun restore(): BufferedImage {
|
||||
|
||||
if (origin != null) {
|
||||
picture.value.image = copy(origin!!)
|
||||
filtersSet.clear()
|
||||
}
|
||||
|
||||
return copy(picture.value.image)
|
||||
}
|
||||
|
||||
var mainImageAsImageBitmap = mutableStateOf(ImageBitmap(1, 1))
|
||||
|
||||
// picture adapter
|
||||
private var picture = mutableStateOf(
|
||||
Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
@@ -328,6 +326,10 @@ private object MainImageWrapper {
|
||||
return (picture.value.name == "")
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
picture.value = Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
|
||||
}
|
||||
|
||||
fun getName(): String {
|
||||
return picture.value.name
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import javax.imageio.ImageIO
|
||||
import java.awt.image.BufferedImageOp
|
||||
import java.awt.image.ConvolveOp
|
||||
import java.awt.image.Kernel
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import example.imageviewer.view.DragHandler
|
||||
|
||||
fun scaleBitmapAspectRatio(
|
||||
bitmap: BufferedImage,
|
||||
@@ -118,6 +121,81 @@ fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
|
||||
return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height)
|
||||
}
|
||||
|
||||
fun cropBitmapByScale(
|
||||
bitmap: BufferedImage,
|
||||
size: WindowSize,
|
||||
scale: Float,
|
||||
drag: DragHandler
|
||||
): BufferedImage {
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap, size),
|
||||
size,
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return cropImage(
|
||||
bitmap,
|
||||
Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y)
|
||||
)
|
||||
}
|
||||
|
||||
fun cropBitmapByBounds(
|
||||
bitmap: BufferedImage,
|
||||
bounds: Rectangle,
|
||||
size: WindowSize,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rectangle {
|
||||
|
||||
if (scaleFactor <= 1f) {
|
||||
return Rectangle(0, 0, bitmap.width, bitmap.height)
|
||||
}
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width / scale).roundToInt()
|
||||
var boundH = (bounds.height / scale).roundToInt()
|
||||
|
||||
scale *= size.width.value / bounds.width.toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0
|
||||
}
|
||||
|
||||
return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH)
|
||||
}
|
||||
|
||||
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): WindowSize {
|
||||
val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
|
||||
val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
|
||||
|
||||
@@ -15,18 +15,18 @@ private val message: MutableState<String> = mutableStateOf("")
|
||||
private val state: MutableState<Boolean> = mutableStateOf(false)
|
||||
|
||||
@Composable
|
||||
fun BuildAppUI(content: ContentState) {
|
||||
fun AppUI(content: ContentState) {
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = Gray
|
||||
) {
|
||||
when (AppState.screenState()) {
|
||||
ScreenType.Main -> {
|
||||
setMainScreen(content)
|
||||
ScreenType.MainScreen -> {
|
||||
MainScreen(content)
|
||||
}
|
||||
ScreenType.FullscreenImage -> {
|
||||
setImageFullScreen(content)
|
||||
FullscreenImage(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@@ -42,6 +41,7 @@ import example.imageviewer.core.FilterType
|
||||
import example.imageviewer.model.AppState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.model.ScreenType
|
||||
import example.imageviewer.ResString
|
||||
import example.imageviewer.style.DarkGray
|
||||
import example.imageviewer.style.DarkGreen
|
||||
import example.imageviewer.style.Foreground
|
||||
@@ -55,46 +55,22 @@ import example.imageviewer.style.icFilterGrayscaleOff
|
||||
import example.imageviewer.style.icFilterGrayscaleOn
|
||||
import example.imageviewer.style.icFilterPixelOff
|
||||
import example.imageviewer.style.icFilterPixelOn
|
||||
import example.imageviewer.utils.cropImage
|
||||
import example.imageviewer.utils.getDisplayBounds
|
||||
import example.imageviewer.utils.toByteArray
|
||||
import java.awt.Rectangle
|
||||
import java.awt.image.BufferedImage
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun setImageFullScreen(
|
||||
fun FullscreenImage(
|
||||
content: ContentState
|
||||
) {
|
||||
if (content.isContentReady()) {
|
||||
Column {
|
||||
setToolBar(content.getSelectedImageName(), content)
|
||||
setImage(content)
|
||||
}
|
||||
} else {
|
||||
setLoadingScreen()
|
||||
Column {
|
||||
ToolBar(content.getSelectedImageName(), content)
|
||||
Image(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun setLoadingScreen() {
|
||||
|
||||
Box {
|
||||
Surface(color = MiniatureColor, modifier = Modifier.height(44.dp)) {}
|
||||
Box(modifier = Modifier.align(Alignment.Center)) {
|
||||
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
|
||||
color = DarkGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setToolBar(
|
||||
fun ToolBar(
|
||||
text: String,
|
||||
content: ContentState
|
||||
) {
|
||||
@@ -109,28 +85,30 @@ fun setToolBar(
|
||||
modifier = Modifier.padding(start = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
backButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
backButtonHover.value = false
|
||||
false
|
||||
})
|
||||
.background(color = if (backButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.restoreMainImage()
|
||||
AppState.screenState(ScreenType.Main)
|
||||
}
|
||||
}) {
|
||||
Image(
|
||||
icBack(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
)
|
||||
Tooltip(ResString.back) {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
backButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
backButtonHover.value = false
|
||||
false
|
||||
})
|
||||
.background(color = if (backButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.restoreMainImage()
|
||||
AppState.screenState(ScreenType.MainScreen)
|
||||
}
|
||||
}) {
|
||||
Image(
|
||||
icBack(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(38.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
@@ -167,37 +145,37 @@ fun FilterButton(
|
||||
type: FilterType,
|
||||
modifier: Modifier = Modifier.size(38.dp)
|
||||
) {
|
||||
val filterButtonHover = remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier.background(color = Transparent).clip(CircleShape)
|
||||
) {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
filterButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
filterButtonHover.value = false
|
||||
false
|
||||
})
|
||||
.background(color = if (filterButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = { content.toggleFilter(type)}
|
||||
val filterButtonHover = remember { mutableStateOf(false) }
|
||||
Box(
|
||||
modifier = Modifier.background(color = Transparent).clip(CircleShape)
|
||||
) {
|
||||
Image(
|
||||
getFilterImage(type = type, content = content),
|
||||
contentDescription = null,
|
||||
modifier
|
||||
)
|
||||
Tooltip("$type") {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
filterButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
filterButtonHover.value = false
|
||||
false
|
||||
})
|
||||
.background(color = if (filterButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = { content.toggleFilter(type)}
|
||||
) {
|
||||
Image(
|
||||
getFilterImage(type = type, content = content),
|
||||
contentDescription = null,
|
||||
modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Spacer(Modifier.width(20.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
|
||||
return when (type) {
|
||||
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
|
||||
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
|
||||
@@ -207,120 +185,41 @@ fun getFilterImage(type: FilterType, content: ContentState): Painter {
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun setImage(content: ContentState) {
|
||||
val drag = remember { DragHandler() }
|
||||
val scale = remember { ScaleHandler() }
|
||||
|
||||
fun Image(content: ContentState) {
|
||||
val onUpdate = remember { { content.updateMainImage() } }
|
||||
Surface(
|
||||
color = DarkGray,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Draggable(dragHandler = drag, modifier = Modifier.fillMaxSize()) {
|
||||
Draggable(
|
||||
onUpdate = onUpdate,
|
||||
dragHandler = content.drag,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Zoomable(
|
||||
onScale = scale,
|
||||
onUpdate = onUpdate,
|
||||
scaleHandler = content.scale,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.onPreviewKeyEvent {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
when (it.key) {
|
||||
Key.DirectionLeft -> content.swipePrevious()
|
||||
Key.DirectionRight -> content.swipeNext()
|
||||
Key.DirectionLeft -> {
|
||||
content.swipePrevious()
|
||||
}
|
||||
Key.DirectionRight -> {
|
||||
content.swipeNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
) {
|
||||
val bitmap = imageByGesture(content, scale, drag)
|
||||
Image(
|
||||
bitmap = bitmap,
|
||||
bitmap = content.getSelectedImage(),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun imageByGesture(
|
||||
content: ContentState,
|
||||
scale: ScaleHandler,
|
||||
drag: DragHandler
|
||||
): ImageBitmap {
|
||||
val bitmap = cropBitmapByScale(content.getSelectedImage(), content.windowState.size, scale.factor.value, drag)
|
||||
return org.jetbrains.skija.Image.makeFromEncoded(toByteArray(bitmap)).asImageBitmap()
|
||||
}
|
||||
|
||||
private fun cropBitmapByScale(
|
||||
bitmap: BufferedImage,
|
||||
size: WindowSize,
|
||||
scale: Float,
|
||||
drag: DragHandler
|
||||
): BufferedImage {
|
||||
val crop = cropBitmapByBounds(
|
||||
bitmap,
|
||||
getDisplayBounds(bitmap, size),
|
||||
size,
|
||||
scale,
|
||||
drag
|
||||
)
|
||||
return cropImage(
|
||||
bitmap,
|
||||
Rectangle(crop.x, crop.y, crop.width - crop.x, crop.height - crop.y)
|
||||
)
|
||||
}
|
||||
|
||||
private fun cropBitmapByBounds(
|
||||
bitmap: BufferedImage,
|
||||
bounds: Rectangle,
|
||||
size: WindowSize,
|
||||
scaleFactor: Float,
|
||||
drag: DragHandler
|
||||
): Rectangle {
|
||||
|
||||
if (scaleFactor <= 1f) {
|
||||
return Rectangle(0, 0, bitmap.width, bitmap.height)
|
||||
}
|
||||
|
||||
var scale = scaleFactor.toDouble().pow(1.4)
|
||||
|
||||
var boundW = (bounds.width / scale).roundToInt()
|
||||
var boundH = (bounds.height / scale).roundToInt()
|
||||
|
||||
scale *= size.width.value / bounds.width.toDouble()
|
||||
|
||||
val offsetX = drag.getAmount().x / scale
|
||||
val offsetY = drag.getAmount().y / scale
|
||||
|
||||
if (boundW > bitmap.width) {
|
||||
boundW = bitmap.width
|
||||
}
|
||||
if (boundH > bitmap.height) {
|
||||
boundH = bitmap.height
|
||||
}
|
||||
|
||||
val invisibleW = bitmap.width - boundW
|
||||
var leftOffset = (invisibleW / 2.0 - offsetX).roundToInt()
|
||||
|
||||
if (leftOffset > invisibleW) {
|
||||
leftOffset = invisibleW
|
||||
drag.getAmount().x = -((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (leftOffset < 0) {
|
||||
drag.getAmount().x = ((invisibleW / 2.0) * scale).roundToInt().toFloat()
|
||||
leftOffset = 0
|
||||
}
|
||||
|
||||
val invisibleH = bitmap.height - boundH
|
||||
var topOffset = (invisibleH / 2 - offsetY).roundToInt()
|
||||
|
||||
if (topOffset > invisibleH) {
|
||||
topOffset = invisibleH
|
||||
drag.getAmount().y = -((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
}
|
||||
if (topOffset < 0) {
|
||||
drag.getAmount().y = ((invisibleH / 2.0) * scale).roundToInt().toFloat()
|
||||
topOffset = 0
|
||||
}
|
||||
|
||||
return Rectangle(leftOffset, topOffset, leftOffset + boundW, topOffset + boundH)
|
||||
}
|
||||
@@ -60,51 +60,27 @@ import example.imageviewer.style.icRefresh
|
||||
import example.imageviewer.utils.toByteArray
|
||||
|
||||
@Composable
|
||||
fun setMainScreen(content: ContentState) {
|
||||
if (content.isContentReady()) {
|
||||
Column {
|
||||
setTopContent(content)
|
||||
setScrollableArea(content)
|
||||
}
|
||||
} else {
|
||||
setLoadingScreen(content)
|
||||
fun MainScreen(content: ContentState) {
|
||||
Column {
|
||||
TopContent(content)
|
||||
ScrollableArea(content)
|
||||
}
|
||||
if (!content.isContentReady()) {
|
||||
LoadingScreen(ResString.loading)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun setLoadingScreen(content: ContentState) {
|
||||
Box {
|
||||
Column {
|
||||
setTopContent(content)
|
||||
}
|
||||
Box(modifier = Modifier.align(Alignment.Center)) {
|
||||
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(50.dp).padding(3.dp, 3.dp, 4.dp, 4.dp),
|
||||
color = DarkGreen
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = ResString.loading,
|
||||
modifier = Modifier.align(Alignment.Center).offset(0.dp, 70.dp),
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = Foreground
|
||||
)
|
||||
}
|
||||
fun TopContent(content: ContentState) {
|
||||
TitleBar(text = ResString.appName, content = content)
|
||||
PreviewImage(content)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setTopContent(content: ContentState) {
|
||||
setTitleBar(text = ResString.appName, content = content)
|
||||
setPreviewImageUI(content)
|
||||
setSpacer(h = 10)
|
||||
setDivider()
|
||||
setSpacer(h = 5)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setTitleBar(text: String, content: ContentState) {
|
||||
fun TitleBar(text: String, content: ContentState) {
|
||||
val refreshButtonHover = remember { mutableStateOf(false) }
|
||||
TopAppBar(
|
||||
backgroundColor = DarkGreen,
|
||||
@@ -120,29 +96,31 @@ fun setTitleBar(text: String, content: ContentState) {
|
||||
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
|
||||
shape = CircleShape
|
||||
) {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
refreshButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
refreshButtonHover.value = false
|
||||
false
|
||||
}
|
||||
)
|
||||
.background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.refresh()
|
||||
Tooltip(ResString.refresh) {
|
||||
Clickable(
|
||||
modifier = Modifier.hover(
|
||||
onEnter = {
|
||||
refreshButtonHover.value = true
|
||||
false
|
||||
},
|
||||
onExit = {
|
||||
refreshButtonHover.value = false
|
||||
false
|
||||
}
|
||||
)
|
||||
.background(color = if (refreshButtonHover.value) TranslucentBlack else Transparent),
|
||||
onClick = {
|
||||
if (content.isContentReady()) {
|
||||
content.refresh()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icRefresh(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp)
|
||||
)
|
||||
}
|
||||
) {
|
||||
Image(
|
||||
icRefresh(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(35.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +128,7 @@ fun setTitleBar(text: String, content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setPreviewImageUI(content: ContentState) {
|
||||
fun PreviewImage(content: ContentState) {
|
||||
Clickable(
|
||||
modifier = Modifier.background(color = DarkGray),
|
||||
onClick = {
|
||||
@@ -166,9 +144,8 @@ fun setPreviewImageUI(content: ContentState) {
|
||||
Image(
|
||||
if (content.isMainImageEmpty())
|
||||
icEmpty()
|
||||
else BitmapPainter(org.jetbrains.skija.Image.makeFromEncoded(
|
||||
toByteArray(content.getSelectedImage())
|
||||
).asImageBitmap()),
|
||||
else
|
||||
BitmapPainter(content.getSelectedImage()),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
|
||||
@@ -179,7 +156,7 @@ fun setPreviewImageUI(content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setMiniatureUI(
|
||||
fun Miniature(
|
||||
picture: Picture,
|
||||
content: ContentState
|
||||
) {
|
||||
@@ -266,7 +243,7 @@ fun setMiniatureUI(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setScrollableArea(content: ContentState) {
|
||||
fun ScrollableArea(content: ContentState) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
.padding(end = 8.dp)
|
||||
@@ -276,7 +253,7 @@ fun setScrollableArea(content: ContentState) {
|
||||
var index = 1
|
||||
Column {
|
||||
for (picture in content.getMiniatures()) {
|
||||
setMiniatureUI(
|
||||
Miniature(
|
||||
picture = picture,
|
||||
content = content
|
||||
)
|
||||
@@ -294,16 +271,9 @@ fun setScrollableArea(content: ContentState) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setDivider() {
|
||||
|
||||
fun Divider() {
|
||||
Divider(
|
||||
color = LightGray,
|
||||
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun setSpacer(h: Int) {
|
||||
|
||||
Spacer(modifier = Modifier.height(h.dp))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package example.imageviewer.view
|
||||
|
||||
import androidx.compose.foundation.BoxWithTooltip
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun Tooltip(
|
||||
text: String = "Tooltip",
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
BoxWithTooltip(
|
||||
tooltip = {
|
||||
Surface(
|
||||
color = Color(210, 210, 210),
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(10.dp),
|
||||
style = MaterialTheme.typography.caption
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
@@ -21,8 +21,9 @@ import example.imageviewer.style.Transparent
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun Zoomable(
|
||||
onScale: ScaleHandler,
|
||||
scaleHandler: ScaleHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
onUpdate: (() -> Unit)? = null,
|
||||
children: @Composable() () -> Unit
|
||||
) {
|
||||
val focusRequester = FocusRequester()
|
||||
@@ -32,9 +33,18 @@ fun Zoomable(
|
||||
modifier = modifier.onPreviewKeyEvent {
|
||||
if (it.type == KeyEventType.KeyUp) {
|
||||
when (it.key) {
|
||||
Key.I -> onScale.onScale(1.2f)
|
||||
Key.O -> onScale.onScale(0.8f)
|
||||
Key.R -> onScale.resetFactor()
|
||||
Key.I -> {
|
||||
scaleHandler.onScale(1.2f)
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
Key.O -> {
|
||||
scaleHandler.onScale(0.8f)
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
Key.R -> {
|
||||
scaleHandler.reset()
|
||||
onUpdate?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -42,7 +52,7 @@ fun Zoomable(
|
||||
.focusRequester(focusRequester)
|
||||
.focusable()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(onDoubleTap = { onScale.resetFactor() }) {
|
||||
detectTapGestures(onDoubleTap = { scaleHandler.reset() }) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import androidx.compose.ui.window.rememberWindowState
|
||||
import example.imageviewer.model.ContentState
|
||||
import example.imageviewer.style.icAppRounded
|
||||
import example.imageviewer.utils.getPreferredWindowSize
|
||||
import example.imageviewer.view.BuildAppUI
|
||||
import example.imageviewer.view.AppUI
|
||||
import example.imageviewer.view.SplashUI
|
||||
|
||||
fun main() = application {
|
||||
@@ -37,7 +37,7 @@ fun main() = application {
|
||||
icon = icon
|
||||
) {
|
||||
MaterialTheme {
|
||||
BuildAppUI(content)
|
||||
AppUI(content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user