Browse Source

Updated imageviewer example to use CoroutineScope (loading and processing images).

pull/994/head v1.0.0-alpha1-rc5
Roman Sedaikin 3 years ago
parent
commit
8b16eea289
  1. 4
      examples/imageviewer/android/src/main/java/example/imageviewer/MainActivity.kt
  2. 2
      examples/imageviewer/build.gradle.kts
  3. 40
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/AndroidContentState.kt
  4. 77
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  5. 8
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt
  6. 114
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  7. 73
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
  8. 4
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt
  9. 2
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt
  10. 43
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/LoadingScreen.kt
  11. 4
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt
  12. 6
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt
  13. 156
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
  14. 78
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  15. 8
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt
  16. 249
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullscreenImage.kt
  17. 116
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
  18. 35
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Tooltip.kt
  19. 20
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
  20. 4
      examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt

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

2
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"))
}

40
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt → 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
}

77
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()
)
}

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

114
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt → 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()
)
}

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

4
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<ScreenType>
init {
screen = mutableStateOf(ScreenType.Main)
screen = mutableStateOf(ScreenType.MainScreen)
}
fun screenState() : ScreenType {

2
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()
}
}

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

4
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
}

6
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"
}
}
}

156
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
}

78
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()

8
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt

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

249
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt → 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)
}

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

35
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()
}
}

20
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()
}
}

4
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 {

Loading…
Cancel
Save