Browse Source

ImageVIewer. Splash screen, composable window API

pull/328/head
Igor Demin 4 years ago
parent
commit
bde57fd891
  1. 2
      examples/imageviewer/build.gradle.kts
  2. 91
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
  3. 27
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/SplashUI.kt
  4. 12
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt
  5. 153
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Application.kt
  6. 95
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
  7. 16
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Zoomable.kt
  8. 49
      examples/imageviewer/desktop/src/jvmMain/kotlin/imageviewer/Main.kt

2
examples/imageviewer/build.gradle.kts

@ -11,7 +11,7 @@ buildscript {
dependencies {
// __LATEST_COMPOSE_RELEASE_VERSION__
classpath("org.jetbrains.compose:compose-gradle-plugin:0.3.0-build135")
classpath("org.jetbrains.compose:compose-gradle-plugin:0.3.0-build141")
classpath("com.android.tools.build:gradle:4.0.1")
// __KOTLIN_COMPOSE_VERSION__
classpath(kotlin("gradle-plugin", version = "1.4.21"))

91
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt

@ -1,50 +1,49 @@
package example.imageviewer.view
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Surface
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.common.R
import example.imageviewer.model.AppState
import example.imageviewer.model.ContentState
import example.imageviewer.model.Picture
import example.imageviewer.model.ScreenType
import example.imageviewer.model.ContentState
import example.imageviewer.style.DarkGray
import example.imageviewer.style.DarkGreen
import example.imageviewer.style.Foreground
import example.imageviewer.style.Transparent
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.LightGray
import example.imageviewer.style.icRefresh
import example.imageviewer.style.icEmpty
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.Transparent
import example.imageviewer.style.icDots
import example.imageviewer.style.icEmpty
import example.imageviewer.style.icRefresh
@Composable
@ -86,8 +85,7 @@ fun setLoadingScreen(content: ContentState) {
@Composable
fun setTopContent(content: ContentState) {
setTitleBar(text = "ImageViewer", content = content)
setTitleBar(text = content.getString(R.string.app_name), content = content)
if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
setPreviewImageUI(content)
setSpacer(h = 10)
@ -102,32 +100,32 @@ fun setTitleBar(text: String, content: ContentState) {
TopAppBar(
backgroundColor = DarkGreen,
title = {
Row(Modifier.preferredHeight(50.dp)) {
Text(
text,
color = Foreground,
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
)
Surface(
color = Transparent,
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady()) {
content.refresh()
Row(Modifier.preferredHeight(50.dp)) {
Text(
text,
color = Foreground,
modifier = Modifier.weight(1f).align(Alignment.CenterVertically)
)
Surface(
color = Transparent,
modifier = Modifier.padding(end = 20.dp).align(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady()) {
content.refresh()
}
}
) {
Image(
icRefresh(),
modifier = Modifier.preferredSize(35.dp)
)
}
) {
Image(
icRefresh(),
modifier = Modifier.preferredSize(35.dp)
)
}
}
}
})
})
}
@Composable
@ -145,8 +143,7 @@ fun setPreviewImageUI(content: ContentState) {
Image(
if (content.isMainImageEmpty()) {
icEmpty()
}
else {
} else {
content.getSelectedImage().asImageBitmap()
},
modifier = Modifier
@ -196,14 +193,14 @@ fun setMiniatureUI(
Clickable(
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(30.dp),
.preferredWidth(30.dp),
onClick = {
showPopUpMessage(
"${content.getString(R.string.picture)} " +
"${picture.name} \n" +
"${content.getString(R.string.size)} " +
"${picture.width}x${picture.height} " +
"${content.getString(R.string.pixels)}",
"${picture.name} \n" +
"${content.getString(R.string.size)} " +
"${picture.width}x${picture.height} " +
"${content.getString(R.string.pixels)}",
content.getContext()
)
}

27
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/SplashUI.kt

@ -0,0 +1,27 @@
package example.imageviewer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import example.imageviewer.style.DarkGray
@Composable
fun SplashUI() {
Box(Modifier.fillMaxSize().background(DarkGray)) {
Text(
// TODO implement common resources
"Image Viewer",
Modifier.align(Alignment.Center),
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 100.sp
)
}
}

12
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt → examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/DesktopContentState.kt

@ -1,20 +1,19 @@
package example.imageviewer.model
import java.awt.image.BufferedImage
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import example.imageviewer.ResString
import example.imageviewer.core.FilterType
import example.imageviewer.model.filtration.FiltersManager
import example.imageviewer.utils.clearCache
import example.imageviewer.utils.cacheImagePath
import example.imageviewer.utils.clearCache
import example.imageviewer.utils.isInternetAvailable
import example.imageviewer.view.showPopUpMessage
import example.imageviewer.ResString
import java.awt.image.BufferedImage
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import javax.swing.SwingUtilities.invokeLater
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
object ContentState {
@ -22,7 +21,6 @@ object ContentState {
private lateinit var repository: ImageRepository
private lateinit var uriRepository: String
@Composable
fun applyContent(uriRepository: String): ContentState {
if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
return this

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

@ -0,0 +1,153 @@
package example.imageviewer.utils
import androidx.compose.desktop.AppManager
import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.WindowEvents
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Recomposer
import androidx.compose.runtime.compositionFor
import androidx.compose.runtime.dispatch.MonotonicFrameClock
import androidx.compose.runtime.emptyContent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.window.MenuBar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.awt.image.BufferedImage
import javax.swing.SwingUtilities
import kotlin.system.exitProcess
fun Application(
content: @Composable ApplicationScope.() -> Unit
) = SwingUtilities.invokeLater {
AppManager.setEvents(onWindowsEmpty = null)
val scope = ApplicationScope(content)
scope.start()
}
@OptIn(ExperimentalComposeApi::class, ExperimentalCoroutinesApi::class)
class ApplicationScope(
private val content: @Composable ApplicationScope.() -> Unit
) {
private val frameClock = ImmediateFrameClock()
private val context = Dispatchers.Main + frameClock
private val scope = CoroutineScope(context)
private val recomposer = Recomposer(context)
private val composition = compositionFor(Unit, EmptyApplier(), recomposer)
private val windows = mutableSetOf<AppWindow>()
private var windowsVersion by mutableStateOf(Any())
fun start() {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
composition.setContent {
content()
WindowsMonitor()
}
}
@Composable
private fun WindowsMonitor() {
LaunchedEffect(windowsVersion) {
if (windows.isEmpty()) {
dispose()
exitProcess(0)
}
}
}
private fun dispose() {
composition.dispose()
scope.cancel()
}
// TODO make parameters observable (now if any parameter is changed we don't change the window)
@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 = emptyContent()
) {
var isOpened by remember { mutableStateOf(true) }
if (isOpened) {
DisposableEffect(Unit) {
lateinit var window: AppWindow
fun onClose() {
if (isOpened) {
windows.remove(window)
onDismissRequest?.invoke()
windowsVersion = Any()
isOpened = false
}
}
window = AppWindow(
title = title,
size = size,
location = location,
centered = centered,
icon = icon,
menuBar = menuBar,
undecorated = undecorated,
resizable = resizable,
events = events,
onDismissRequest = {
onClose()
}
)
windows.add(window)
window.show(recomposer, content)
onDispose {
if (!window.isClosed) {
window.close()
}
onClose()
}
}
}
}
}
private class ImmediateFrameClock : MonotonicFrameClock {
override suspend fun <R> withFrameNanos(
onFrame: (frameTimeNanos: Long) -> R
) = onFrame(System.nanoTime())
}
@OptIn(ExperimentalComposeApi::class)
private class EmptyApplier : Applier<Unit> {
override val current: Unit = Unit
override fun down(node: Unit) = Unit
override fun up() = Unit
override fun insertTopDown(index: Int, instance: Unit) = Unit
override fun insertBottomUp(index: Int, instance: Unit) = Unit
override fun remove(index: Int, count: Int) = Unit
override fun move(from: Int, to: Int, count: Int) = Unit
override fun clear() = Unit
}

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

@ -1,106 +1,71 @@
package example.imageviewer.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.runtime.Composable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.preferredWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.material.Surface
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.TextUnit
import example.imageviewer.ResString
import example.imageviewer.model.AppState
import example.imageviewer.model.ContentState
import example.imageviewer.model.Picture
import example.imageviewer.model.ScreenType
import example.imageviewer.model.ContentState
import example.imageviewer.style.DarkGray
import example.imageviewer.style.DarkGreen
import example.imageviewer.style.Foreground
import example.imageviewer.style.Transparent
import example.imageviewer.style.LightGray
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.MiniatureHoverColor
import example.imageviewer.style.TranslucentBlack
import example.imageviewer.style.TranslucentWhite
import example.imageviewer.style.PreviewImageAreaHoverColor
import example.imageviewer.style.MiniatureHoverColor
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.LightGray
import example.imageviewer.style.icRefresh
import example.imageviewer.style.icEmpty
import example.imageviewer.style.Transparent
import example.imageviewer.style.icDots
import example.imageviewer.style.icEmpty
import example.imageviewer.style.icRefresh
import example.imageviewer.utils.toByteArray
import example.imageviewer.ResString
import org.jetbrains.skija.Image
import org.jetbrains.skija.IRect
@Composable
fun setMainScreen(content: ContentState) {
if (content.isContentReady()) {
Column {
setTopContent(content)
setScrollableArea(content)
}
} else {
setLoadingScreen(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.preferredSize(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
)
check(content.isContentReady())
Column {
setTopContent(content)
setScrollableArea(content)
}
}
@Composable
fun setTopContent(content: ContentState) {
setTitleBar(text = "ImageViewer", content = content)
setTitleBar(text = ResString.appName, content = content)
setPreviewImageUI(content)
setSpacer(h = 10)
setDivider()
@ -229,7 +194,7 @@ fun setMiniatureUI(
.padding(start = 16.dp),
style = MaterialTheme.typography.body1
)
Clickable(
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(30.dp)

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

@ -1,14 +1,16 @@
package example.imageviewer.view
import androidx.compose.foundation.InteractionState
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.focus.FocusReference
import androidx.compose.ui.input.key.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.focus.focusReference
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.shortcuts
import example.imageviewer.style.Transparent
@Composable
@ -17,7 +19,7 @@ fun Zoomable(
modifier: Modifier = Modifier,
children: @Composable() () -> Unit
) {
val focusRequester = remember { FocusReference() }
val focusRequester = remember { FocusRequester() }
Surface(
color = Transparent,
@ -32,9 +34,9 @@ fun Zoomable(
onScale.resetFactor()
}
}
.focusReference(focusRequester)
.focusRequester(focusRequester)
.focusModifier()
.clickable(indication = null) { focusRequester.requestFocus() }
.clickable(interactionState = InteractionState(), indication = null) { focusRequester.requestFocus() }
) {
children()
}

49
examples/imageviewer/desktop/src/jvmMain/kotlin/imageviewer/Main.kt

@ -1,28 +1,47 @@
package example.imageviewer
import androidx.compose.desktop.DesktopTheme
import androidx.compose.desktop.Window
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.remember
import example.imageviewer.utils.getPreferredWindowSize
import example.imageviewer.view.BuildAppUI
import example.imageviewer.model.ContentState
import example.imageviewer.model.ImageRepository
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() {
val content = ContentState.applyContent(
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
)
Application {
val icon = remember(::icAppRounded)
Window(
title = "ImageViewer",
size = getPreferredWindowSize(800, 1000),
icon = icAppRounded()
) {
val content = ContentState.applyContent(
"https://raw.githubusercontent.com/JetBrains/compose-jb/master/artwork/imageviewerrepo/fetching.list"
)
MaterialTheme {
DesktopTheme {
BuildAppUI(content)
if (content.isContentReady()) {
ComposableWindow(
title = "Image Viewer",
size = getPreferredWindowSize(800, 1000),
icon = icon
) {
MaterialTheme {
DesktopTheme {
BuildAppUI(content)
}
}
}
} else {
ComposableWindow(
title = "Image Viewer",
size = getPreferredWindowSize(800, 300),
undecorated = true,
icon = icon,
) {
MaterialTheme {
DesktopTheme {
SplashUI()
}
}
}
}
}

Loading…
Cancel
Save