Browse Source

Updated ImageViewer to 0.5.0-build270

pull/952/head
Roman Sedaikin 3 years ago
parent
commit
2e571c8734
  1. 12
      examples/imageviewer/build.gradle.kts
  2. 5
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
  3. 44
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt
  4. 158
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt
  5. 30
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  6. 42
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt
  7. 7
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
  8. 28
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
  9. 35
      examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt

12
examples/imageviewer/build.gradle.kts

@ -1,25 +1,21 @@
buildscript {
val composeVersion = System.getenv("COMPOSE_TEMPLATE_COMPOSE_VERSION") ?: "0.5.0-build270"
repositories {
mavenLocal().mavenContent {
includeModule("org.jetbrains.compose", "compose-gradle-plugin")
}
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
// __LATEST_COMPOSE_RELEASE_VERSION__
classpath("org.jetbrains.compose:compose-gradle-plugin:0.4.0")
classpath("org.jetbrains.compose:compose-gradle-plugin:$composeVersion")
classpath("com.android.tools.build:gradle:4.0.1")
// __KOTLIN_COMPOSE_VERSION__
classpath(kotlin("gradle-plugin", version = "1.5.10"))
classpath(kotlin("gradle-plugin", version = "1.5.21"))
}
}
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")

5
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt

@ -3,6 +3,7 @@ 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 example.imageviewer.ResString
import example.imageviewer.core.FilterType
import example.imageviewer.model.filtration.FiltersManager
@ -19,10 +20,12 @@ import javax.swing.SwingUtilities.invokeLater
object ContentState : RememberObserver {
lateinit var windowState: WindowState
private lateinit var repository: ImageRepository
private lateinit var uriRepository: String
fun applyContent(uriRepository: String): ContentState {
fun applyContent(state: WindowState, uriRepository: String): ContentState {
windowState = state
if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
return this
}

44
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt

@ -1,58 +1,42 @@
package example.imageviewer.style
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import java.awt.image.BufferedImage
import javax.imageio.ImageIO
@Composable
fun icEmpty() = imageResource("images/empty.png")
fun icEmpty() = painterResource("images/empty.png")
@Composable
fun icBack() = imageResource("images/back.png")
fun icBack() = painterResource("images/back.png")
@Composable
fun icRefresh() = imageResource("images/refresh.png")
fun icRefresh() = painterResource("images/refresh.png")
@Composable
fun icDots() = imageResource("images/dots.png")
fun icDots() = painterResource("images/dots.png")
@Composable
fun icFilterGrayscaleOn() = imageResource("images/grayscale_on.png")
fun icFilterGrayscaleOn() = painterResource("images/grayscale_on.png")
@Composable
fun icFilterGrayscaleOff() = imageResource("images/grayscale_off.png")
fun icFilterGrayscaleOff() = painterResource("images/grayscale_off.png")
@Composable
fun icFilterPixelOn() = imageResource("images/pixel_on.png")
fun icFilterPixelOn() = painterResource("images/pixel_on.png")
@Composable
fun icFilterPixelOff() = imageResource("images/pixel_off.png")
fun icFilterPixelOff() = painterResource("images/pixel_off.png")
@Composable
fun icFilterBlurOn() = imageResource("images/blur_on.png")
fun icFilterBlurOn() = painterResource("images/blur_on.png")
@Composable
fun icFilterBlurOff() = imageResource("images/blur_off.png")
fun icFilterBlurOff() = painterResource("images/blur_off.png")
@Composable
fun icFilterUnknown() = imageResource("images/filter_unknown.png")
fun icFilterUnknown() = painterResource("images/filter_unknown.png")
private var icon: BufferedImage? = null
fun icAppRounded(): BufferedImage {
if (icon != null) {
return icon!!
}
try {
val imageRes = "images/ic_imageviewer_round.png"
val img = Thread.currentThread().contextClassLoader.getResource(imageRes)
val bitmap: BufferedImage? = ImageIO.read(img)
if (bitmap != null) {
icon = bitmap
return bitmap
}
} catch (e: Exception) {
e.printStackTrace()
}
return BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB)
}
@Composable
fun icAppRounded() = painterResource("images/ic_imageviewer_round.png")

158
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt

@ -1,158 +0,0 @@
package example.imageviewer.utils
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.WindowEvents
import androidx.compose.runtime.*
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.v1.MenuBar
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import java.awt.image.BufferedImage
fun Application(
content: @Composable ApplicationScope.() -> Unit
) {
GlobalScope.launch(Dispatchers.Swing + ImmediateFrameClock()) {
AppManager.setEvents(onWindowsEmpty = null)
withRunningRecomposer { recomposer ->
val latch = CompletableDeferred<Unit>()
val applier = ApplicationApplier { latch.complete(Unit) }
val composition = Composition(applier, recomposer)
try {
val scope = ApplicationScope(recomposer)
composition.setContent { scope.content() }
latch.join()
} finally {
composition.dispose()
}
}
}
}
class ApplicationScope internal constructor(private val recomposer: Recomposer) {
@Composable
fun ComposableWindow(
title: String = "JetpackDesktopWindow",
size: IntSize = IntSize(800, 600),
location: IntOffset = IntOffset.Zero,
centered: Boolean = true,
icon: BufferedImage? = null,
menuBar: MenuBar? = null,
undecorated: Boolean = false,
resizable: Boolean = true,
events: WindowEvents = WindowEvents(),
onDismissRequest: (() -> Unit)? = null,
content: @Composable () -> Unit = {}
) {
var isOpened by remember { mutableStateOf(true) }
if (!isOpened) return
ComposeNode<AppWindow, ApplicationApplier>(
factory = {
val window = AppWindow(
title = title,
size = size,
location = location,
centered = centered,
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
resizable = resizable,
events = events,
onDismissRequest = {
onDismissRequest?.invoke()
isOpened = false
}
)
window.show(recomposer, content)
window
},
update = {
set(title) { setTitle(it) }
set(size) { setSize(it.width, it.height) }
// set(location) { setLocation(it.x, it.y) }
set(icon) { setIcon(it) }
// set(menuBar) { if (it != null) setMenuBar(it) else removeMenuBar() }
// set(resizable) { setResizable(it) }
// set(events) { setEvents(it) }
// set(onDismissRequest) { setDismiss(it) }
}
)
}
}
private class ImmediateFrameClock : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(
onFrame: (frameTimeNanos: Long) -> R
) = onFrame(System.nanoTime())
}
private class ApplicationApplier(
private val onWindowsEmpty: () -> Unit
) : Applier<AppWindow?> {
private val windows = mutableListOf<AppWindow>()
override var current: AppWindow? = null
override fun insertBottomUp(index: Int, instance: AppWindow?) {
requireNotNull(instance)
check(current == null) { "Windows cannot be nested!" }
windows.add(index, instance)
}
override fun remove(index: Int, count: Int) {
repeat(count) {
val window = windows.removeAt(index)
if (!window.isClosed) {
window.close()
}
}
}
override fun move(from: Int, to: Int, count: Int) {
if (from > to) {
var current = to
repeat(count) {
val node = windows.removeAt(from)
windows.add(current, node)
current++
}
} else {
repeat(count) {
val node = windows.removeAt(from)
windows.add(to - 1, node)
}
}
}
override fun clear() {
windows.forEach { if (!it.isClosed) it.close() }
windows.clear()
}
override fun onEndChanges() {
if (windows.isEmpty()) {
onWindowsEmpty()
}
}
override fun down(node: AppWindow?) {
requireNotNull(node)
check(current == null) { "Windows cannot be nested!" }
current = node
}
override fun up() {
check(current != null) { "Windows cannot be nested!" }
current = null
}
override fun insertTopDown(index: Int, instance: AppWindow?) {
// ignored. Building tree bottom-up
}
}

30
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt

@ -1,7 +1,7 @@
package example.imageviewer.utils
import androidx.compose.desktop.AppManager
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.WindowSize
import androidx.compose.ui.unit.dp
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Rectangle
@ -38,10 +38,10 @@ fun scaleBitmapAspectRatio(
return result
}
fun getDisplayBounds(bitmap: BufferedImage): Rectangle {
fun getDisplayBounds(bitmap: BufferedImage, windowSize: WindowSize): Rectangle {
val boundW: Float = displayWidth().toFloat()
val boundH: Float = displayHeight().toFloat()
val boundW: Float = windowSize.width.value.toFloat()
val boundH: Float = windowSize.height.value.toFloat()
val ratioX: Float = bitmap.width / boundW
val ratioY: Float = bitmap.height / boundH
@ -108,22 +108,6 @@ fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
)
}
fun displayWidth(): Int {
val window = AppManager.focusedWindow
if (window != null) {
return window.width
}
return 0
}
fun displayHeight(): Int {
val window = AppManager.focusedWindow
if (window != null) {
return window.height
}
return 0
}
fun toByteArray(bitmap: BufferedImage) : ByteArray {
val baos = ByteArrayOutputStream()
ImageIO.write(bitmap, "png", baos)
@ -134,11 +118,11 @@ fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
return bitmap.getSubimage(crop.x, crop.y, crop.width, crop.height)
}
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize {
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): WindowSize {
val screenSize: Dimension = Toolkit.getDefaultToolkit().screenSize
val preferredWidth: Int = (screenSize.width * 0.8f).toInt()
val preferredHeight: Int = (screenSize.height * 0.8f).toInt()
val width: Int = if (desiredWidth < preferredWidth) desiredWidth else preferredWidth
val height: Int = if (desiredHeight < preferredHeight) desiredHeight else preferredHeight
return IntSize(width, height)
return WindowSize(width.dp, height.dp)
}

42
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt

@ -25,13 +25,19 @@ import androidx.compose.runtime.remember
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.asImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.shortcuts
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowSize
import example.imageviewer.core.FilterType
import example.imageviewer.model.AppState
import example.imageviewer.model.ContentState
@ -50,11 +56,9 @@ import example.imageviewer.style.icFilterGrayscaleOn
import example.imageviewer.style.icFilterPixelOff
import example.imageviewer.style.icFilterPixelOn
import example.imageviewer.utils.cropImage
import example.imageviewer.utils.displayWidth
import example.imageviewer.utils.getDisplayBounds
import example.imageviewer.utils.toByteArray
import java.awt.Rectangle
import java.awt.event.KeyEvent
import java.awt.image.BufferedImage
import kotlin.math.pow
import kotlin.math.roundToInt
@ -192,7 +196,7 @@ fun FilterButton(
}
@Composable
fun getFilterImage(type: FilterType, content: ContentState): ImageBitmap {
fun getFilterImage(type: FilterType, content: ContentState): Painter {
return when (type) {
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
@ -201,6 +205,7 @@ fun getFilterImage(type: FilterType, content: ContentState): ImageBitmap {
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun setImage(content: ContentState) {
val drag = remember { DragHandler() }
@ -214,13 +219,14 @@ fun setImage(content: ContentState) {
Zoomable(
onScale = scale,
modifier = Modifier.fillMaxSize()
.shortcuts {
on(Key(KeyEvent.VK_LEFT)) {
content.swipePrevious()
.onPreviewKeyEvent {
if (it.type == KeyEventType.KeyUp) {
when (it.key) {
Key.DirectionLeft -> content.swipePrevious()
Key.DirectionRight -> content.swipeNext()
}
on(Key(KeyEvent.VK_RIGHT)) {
content.swipeNext()
}
false
}
) {
val bitmap = imageByGesture(content, scale, drag)
@ -240,15 +246,20 @@ fun imageByGesture(
scale: ScaleHandler,
drag: DragHandler
): ImageBitmap {
val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag)
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, scale: Float, drag: DragHandler): BufferedImage {
private fun cropBitmapByScale(
bitmap: BufferedImage,
size: WindowSize,
scale: Float,
drag: DragHandler
): BufferedImage {
val crop = cropBitmapByBounds(
bitmap,
getDisplayBounds(bitmap),
getDisplayBounds(bitmap, size),
size,
scale,
drag
)
@ -261,6 +272,7 @@ private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHan
private fun cropBitmapByBounds(
bitmap: BufferedImage,
bounds: Rectangle,
size: WindowSize,
scaleFactor: Float,
drag: DragHandler
): Rectangle {
@ -274,7 +286,7 @@ private fun cropBitmapByBounds(
var boundW = (bounds.width / scale).roundToInt()
var boundH = (bounds.height / scale).roundToInt()
scale *= displayWidth() / bounds.width.toDouble()
scale *= size.width.value / bounds.width.toDouble()
val offsetX = drag.getAmount().x / scale
val offsetY = drag.getAmount().y / scale

7
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt

@ -35,8 +35,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.ResString
@ -165,9 +166,9 @@ fun setPreviewImageUI(content: ContentState) {
Image(
if (content.isMainImageEmpty())
icEmpty()
else org.jetbrains.skija.Image.makeFromEncoded(
else BitmapPainter(org.jetbrains.skija.Image.makeFromEncoded(
toByteArray(content.getSelectedImage())
).asImageBitmap(),
).asImageBitmap()),
contentDescription = null,
modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),

28
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt

@ -1,19 +1,24 @@
package example.imageviewer.view
import androidx.compose.foundation.focusable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.shortcuts
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.ExperimentalComposeUiApi
import example.imageviewer.style.Transparent
import androidx.compose.runtime.DisposableEffect
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun Zoomable(
onScale: ScaleHandler,
@ -24,19 +29,18 @@ fun Zoomable(
Surface(
color = Transparent,
modifier = modifier.shortcuts {
on(Key.I) {
onScale.onScale(1.2f)
}
on(Key.O) {
onScale.onScale(0.8f)
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()
}
on(Key.R) {
onScale.resetFactor()
}
false
}
.focusRequester(focusRequester)
.focusModifier()
.focusable()
.pointerInput(Unit) {
detectTapGestures(onDoubleTap = { onScale.resetFactor() }) {
focusRequester.requestFocus()

35
examples/imageviewer/desktop/src/jvmMain/kotlin/example/imageviewer/Main.kt

@ -1,48 +1,59 @@
package example.imageviewer
import androidx.compose.desktop.DesktopTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import example.imageviewer.model.ContentState
import example.imageviewer.style.icAppRounded
import example.imageviewer.utils.Application
import example.imageviewer.utils.getPreferredWindowSize
import example.imageviewer.view.BuildAppUI
import example.imageviewer.view.SplashUI
fun main() = Application {
fun main() = application {
val state = rememberWindowState()
val content = remember {
ContentState.applyContent(
state,
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
)
}
val icon = remember(::icAppRounded)
val icon = icAppRounded()
if (content.isAppReady()) {
ComposableWindow(
Window(
onCloseRequest = ::exitApplication,
title = "Image Viewer",
size = getPreferredWindowSize(800, 1000),
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(800, 1000)
),
icon = icon
) {
MaterialTheme {
DesktopTheme {
BuildAppUI(content)
}
}
}
} else {
ComposableWindow(
Window(
onCloseRequest = ::exitApplication,
title = "Image Viewer",
size = getPreferredWindowSize(800, 300),
state = WindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = getPreferredWindowSize(800, 300)
),
undecorated = true,
icon = icon,
) {
MaterialTheme {
DesktopTheme {
SplashUI()
}
}
}
}
}
Loading…
Cancel
Save