diff --git a/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt b/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt index 6b0f5911095..53bb8c61602 100755 --- a/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt +++ b/examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt @@ -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) } } } \ No newline at end of file diff --git a/examples/imageviewer/build.gradle.kts b/examples/imageviewer/build.gradle.kts index a9a0197e9c7..cb9474535de 100755 --- a/examples/imageviewer/build.gradle.kts +++ b/examples/imageviewer/build.gradle.kts @@ -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")) } diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt old mode 100755 new mode 100644 similarity index 91% rename from examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt rename to examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt index 95d38a2094c..00d4b026bc4 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt @@ -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 } diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt index 1878454c64e..32e234da085 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -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() + ) +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt index 89115c7e53d..dacce3b7c31 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt @@ -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) } } } diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt old mode 100755 new mode 100644 similarity index 69% rename from examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt rename to examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt index 97143b4c05e..1c0e7d73b4d --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt @@ -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) } -} - -@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 - ) - } - } + if (!content.isContentReady()) { + LoadingScreen() } } @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() - ) -} diff --git a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt index a8b88fb21b6..5509e280506 100755 --- a/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt +++ b/examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -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) } -} - -@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 - ) + if (!content.isContentReady()) { + LoadingScreen(content.getString(R.string.loading)) } } @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)) -} diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt index d347ed85ab6..8e38a79e522 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt @@ -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 init { - screen = mutableStateOf(ScreenType.Main) + screen = mutableStateOf(ScreenType.MainScreen) } fun screenState() : ScreenType { diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt index ed9ac2f2ec6..f9f3b30bcff 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt @@ -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() } } diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt new file mode 100644 index 00000000000..8a6a4191f61 --- /dev/null +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt index e380ff413f1..ef9887c4f6b 100755 --- a/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt +++ b/examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt @@ -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 } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt index 4b89b548a14..ef361ecc84e 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt @@ -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" } } } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt index 86009cff1ba..35a60c5dd63 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt @@ -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 - } - - val pictureList = loadImages(cacheImagePath, imageList) + showPopUpMessage( + ResString.repoInvalid + ) + onContentReady() + } else { + 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 } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt index 7b103af1feb..8082fdb53c6 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt @@ -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() diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt index cb7eee69d11..ef002711bf0 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt @@ -15,18 +15,18 @@ private val message: MutableState = mutableStateOf("") private val state: MutableState = 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) } } } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt old mode 100755 new mode 100644 similarity index 51% rename from examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt rename to examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt index d335e0f1be5..cd27a480853 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt @@ -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) } -} - -@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 - ) - } - } + if (!content.isContentReady()) { + LoadingScreen() } } @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) } diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt index 0ef28c04339..85b8d448e57 100755 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt @@ -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) } -} - -@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 - ) + if (!content.isContentReady()) { + LoadingScreen(ResString.loading) } } @Composable -fun setTopContent(content: ContentState) { - setTitleBar(text = ResString.appName, content = content) - setPreviewImageUI(content) - setSpacer(h = 10) - setDivider() - setSpacer(h = 5) +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 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)) -} diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt new file mode 100644 index 00000000000..9d6819058fc --- /dev/null +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt @@ -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() + } +} \ No newline at end of file diff --git a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt index 2a6f865e4c8..e9f6321be0b 100644 --- a/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt +++ b/examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt @@ -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() } } diff --git a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt index 509d84f3106..c17682c4051 100644 --- a/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt +++ b/examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt @@ -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 {