Browse Source

Example Image Viewer.

pull/2/head
Nikolay Igotti 4 years ago
parent
commit
f201961e31
  1. 3
      examples/imageviewer/.gitignore
  2. 138
      examples/imageviewer/.idea/codeStyles/Project.xml
  3. 5
      examples/imageviewer/.idea/codeStyles/codeStyleConfig.xml
  4. 6
      examples/imageviewer/.idea/vcs.xml
  5. 21
      examples/imageviewer/.run/desktop.run.xml
  6. 1
      examples/imageviewer/README.md
  7. 25
      examples/imageviewer/android/build.gradle.kts
  8. 27
      examples/imageviewer/android/src/main/AndroidManifest.xml
  9. 38
      examples/imageviewer/android/src/main/java/imageviewer/MainActivity.kt
  10. 7
      examples/imageviewer/build.gradle.kts
  11. 56
      examples/imageviewer/common/build.gradle.kts
  12. 2
      examples/imageviewer/common/src/androidMain/AndroidManifest.xml
  13. 7
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt
  14. 375
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt
  15. 146
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt
  16. 27
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt
  17. 13
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
  18. 12
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
  19. 57
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
  20. 12
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
  21. 12
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
  22. 54
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt
  23. 67
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt
  24. 133
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  25. 59
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt
  26. 288
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt
  27. 264
      examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/MainScreen.kt
  28. 5
      examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml
  29. 5
      examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml
  30. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png
  31. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png
  32. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png
  33. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png
  34. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png
  35. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png
  36. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png
  37. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png
  38. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png
  39. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png
  40. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png
  41. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png
  42. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png
  43. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png
  44. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png
  45. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png
  46. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png
  47. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png
  48. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png
  49. BIN
      examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png
  50. BIN
      examples/imageviewer/common/src/androidMain/res/raw/back.png
  51. BIN
      examples/imageviewer/common/src/androidMain/res/raw/blur_off.png
  52. BIN
      examples/imageviewer/common/src/androidMain/res/raw/blur_on.png
  53. BIN
      examples/imageviewer/common/src/androidMain/res/raw/dots.png
  54. BIN
      examples/imageviewer/common/src/androidMain/res/raw/empty.png
  55. BIN
      examples/imageviewer/common/src/androidMain/res/raw/filter_unknown.png
  56. BIN
      examples/imageviewer/common/src/androidMain/res/raw/grayscale_off.png
  57. BIN
      examples/imageviewer/common/src/androidMain/res/raw/grayscale_on.png
  58. BIN
      examples/imageviewer/common/src/androidMain/res/raw/pixel_off.png
  59. BIN
      examples/imageviewer/common/src/androidMain/res/raw/pixel_on.png
  60. BIN
      examples/imageviewer/common/src/androidMain/res/raw/refresh.png
  61. 15
      examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml
  62. 14
      examples/imageviewer/common/src/androidMain/res/values/strings.xml
  63. 33
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt
  64. 20
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt
  65. 20
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt
  66. 51
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt
  67. 41
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt
  68. 38
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt
  69. 27
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt
  70. 27
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt
  71. 37
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt
  72. 107
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt
  73. 64
      examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt
  74. 62
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt
  75. 22
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt
  76. 335
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt
  77. 145
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt
  78. 27
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt
  79. 27
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt
  80. 27
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt
  81. 71
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt
  82. 27
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt
  83. 27
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt
  84. 53
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/style/Decoration.kt
  85. 68
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt
  86. 162
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/GraphicsMath.kt
  87. 61
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/AppUI.kt
  88. 282
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/FullImageScreen.kt
  89. 264
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/MainScreen.kt
  90. 79
      examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt
  91. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/back.png
  92. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png
  93. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png
  94. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/dots.png
  95. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/empty.png
  96. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png
  97. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png
  98. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png
  99. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png
  100. BIN
      examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png
  101. Some files were not shown because too many files have changed in this diff Show More

3
examples/imageviewer/.gitignore vendored

@ -0,0 +1,3 @@
.DS_Store
.gradle
build

138
examples/imageviewer/.idea/codeStyles/Project.xml

@ -0,0 +1,138 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
examples/imageviewer/.idea/codeStyles/codeStyleConfig.xml

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
examples/imageviewer/.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

21
examples/imageviewer/.run/desktop.run.xml

@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="desktop" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$/desktop" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="run" />
</list>
</option>
<option name="vmOptions" value="" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
</component>

1
examples/imageviewer/README.md

@ -0,0 +1 @@
An example of image gallery for remote server image viewing, based on Jetpack Compose UI library (desktop).

25
examples/imageviewer/android/build.gradle.kts

@ -0,0 +1,25 @@
plugins {
id("com.android.application")
kotlin("android")
id("org.jetbrains.compose")
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation(project(":common"))
}

27
examples/imageviewer/android/src/main/AndroidManifest.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="example.imageviewer">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_imageviewer"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_imageviewer_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name="example.imageviewer.MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

38
examples/imageviewer/android/src/main/java/imageviewer/MainActivity.kt

@ -0,0 +1,38 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.setContent
import example.imageviewer.view.BuildAppUI
import example.imageviewer.model.ContentState
import example.imageviewer.model.ImageRepository
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val content = ContentState.applyContent(
this@MainActivity,
"https://spvessel.com/iv/images/fetching.list"
)
setContent {
BuildAppUI(content)
}
}
}

7
examples/imageviewer/build.gradle.kts

@ -0,0 +1,7 @@
allprojects {
repositories {
google()
jcenter()
maven("https://packages.jetbrains.team/maven/p/ui/dev")
}
}

56
examples/imageviewer/common/build.gradle.kts

@ -0,0 +1,56 @@
import org.jetbrains.compose.compose
plugins {
id("com.android.library")
kotlin("multiplatform")
id("org.jetbrains.compose")
}
kotlin {
android()
jvm("desktop")
sourceSets {
named("commonMain") {
dependencies {
api(compose.runtime)
api(compose.foundation)
api(compose.material)
}
}
named("androidMain") {
dependencies {
api("androidx.appcompat:appcompat:1.1.0")
api("androidx.core:core-ktx:1.3.1")
}
}
named("desktopMain") {
dependencies {
api(compose.desktop.common)
}
}
}
}
android {
compileSdkVersion(30)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
sourceSets {
named("main") {
manifest.srcFile("src/androidMain/AndroidManifest.xml")
res.srcDirs("src/androidMain/res")
}
}
}

2
examples/imageviewer/common/src/androidMain/AndroidManifest.xml

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="example.imageviewer"/>

7
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/core/BitmapFilter.kt

@ -0,0 +1,7 @@
package example.imageviewer.core
import android.graphics.Bitmap
interface BitmapFilter {
fun apply(bitmap: Bitmap) : Bitmap
}

375
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ContentState.kt

@ -0,0 +1,375 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import android.content.Context
import android.graphics.*
import android.os.Handler
import android.os.Looper
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import example.imageviewer.core.FilterType
import example.imageviewer.model.filtration.FiltersManager
import example.imageviewer.utils.clearCache
import example.imageviewer.utils.isInternetAvailable
import example.imageviewer.view.showPopUpMessage
import example.imageviewer.R
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
object ContentState {
private lateinit var context: Context
private lateinit var repository: ImageRepository
private lateinit var uriRepository: String
fun applyContent(context: Context, uriRepository: String): ContentState {
if (this::uriRepository.isInitialized && this.uriRepository == uriRepository) {
return this
}
this.context = context
this.uriRepository = uriRepository
repository = ImageRepository(uriRepository)
appliedFilters = FiltersManager(context)
isAppUIReady.value = false
initData()
return this
}
private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
private val handler: Handler by lazy { Handler(Looper.getMainLooper()) }
fun getContext(): Context {
return context
}
fun getOrientation(): Int {
return context.resources.configuration.orientation
}
private val isAppUIReady = mutableStateOf(false)
fun isContentReady(): Boolean {
return isAppUIReady.value
}
fun getString(id: Int): String {
return context.getString(id)
}
// drawable content
private val mainImage = mutableStateOf(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
private val currentImageIndex = mutableStateOf(0)
private val miniatures = Miniatures()
fun getMiniatures(): List<Picture> {
return miniatures.getMiniatures()
}
fun getSelectedImage(): Bitmap {
return mainImage.value
}
fun getSelectedImageName(): String {
return MainImageWrapper.getName()
}
// filters managing
private lateinit var appliedFilters: FiltersManager
private val filterUIState: MutableMap<FilterType, MutableState<Boolean>> = LinkedHashMap()
private fun toggleFilterState(filter: FilterType) {
if (!filterUIState.containsKey(filter)) {
filterUIState[filter] = mutableStateOf(true)
} else {
val value = filterUIState[filter]!!.value
filterUIState[filter]!!.value = !value
}
}
fun toggleFilter(filter: FilterType) {
if (containsFilter(filter)) {
removeFilter(filter)
} else {
addFilter(filter)
}
toggleFilterState(filter)
var bitmap = MainImageWrapper.origin
if (bitmap != null) {
bitmap = appliedFilters.applyFilters(bitmap)
MainImageWrapper.setImage(bitmap)
mainImage.value = bitmap
}
}
private fun addFilter(filter: FilterType) {
appliedFilters.add(filter)
MainImageWrapper.addFilter(filter)
}
private fun removeFilter(filter: FilterType) {
appliedFilters.remove(filter)
MainImageWrapper.removeFilter(filter)
}
private fun containsFilter(type: FilterType): Boolean {
return appliedFilters.contains(type)
}
fun isFilterEnabled(type: FilterType): Boolean {
if (!filterUIState.containsKey(type)) {
filterUIState[type] = mutableStateOf(false)
}
return filterUIState[type]!!.value
}
private fun restoreFilters(): Bitmap {
filterUIState.clear()
appliedFilters.clear()
return MainImageWrapper.restore()
}
fun restoreMainImage() {
mainImage.value = restoreFilters()
}
// application content initialization
// @Composable
fun initData() {
if (isAppUIReady.value)
return
val directory = context.cacheDir.absolutePath
executor.execute {
try {
if (isInternetAvailable()) {
val imageList = repository.get()
if (imageList.isEmpty()) {
handler.post {
showPopUpMessage(
getString(R.string.repo_invalid),
context
)
isAppUIReady.value = true
}
return@execute
}
val pictureList = loadImages(directory, imageList)
if (pictureList.isEmpty()) {
handler.post {
showPopUpMessage(
getString(R.string.repo_empty),
context
)
isAppUIReady.value = true
}
} else {
val picture = loadFullImage(imageList[0])
handler.post {
miniatures.setMiniatures(pictureList)
if (isMainImageEmpty()) {
wrapPictureIntoMainImage(picture)
} else {
appliedFilters.add(MainImageWrapper.getFilters())
mainImage.value = MainImageWrapper.getImage()
currentImageIndex.value = MainImageWrapper.getId()
}
isAppUIReady.value = true
}
}
} else {
handler.post {
showPopUpMessage(
getString(R.string.no_internet),
context
)
isAppUIReady.value = true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// preview/fullscreen image managing
fun isMainImageEmpty(): Boolean {
return MainImageWrapper.isEmpty()
}
fun setMainImage(picture: Picture) {
if (MainImageWrapper.getId() == picture.id)
return
executor.execute {
if (isInternetAvailable()) {
val fullSizePicture = loadFullImage(picture.source)
fullSizePicture.id = picture.id
handler.post {
wrapPictureIntoMainImage(fullSizePicture)
}
} else {
handler.post {
showPopUpMessage(
"${getString(R.string.no_internet)}\n${getString(R.string.load_image_unavailable)}",
context
)
wrapPictureIntoMainImage(picture)
}
}
}
}
private fun wrapPictureIntoMainImage(picture: Picture) {
MainImageWrapper.wrapPicture(picture)
MainImageWrapper.saveOrigin()
mainImage.value = picture.image
currentImageIndex.value = picture.id
}
fun swipeNext() {
if (currentImageIndex.value == miniatures.size() - 1) {
showPopUpMessage(
getString(R.string.last_image),
context
)
return
}
restoreFilters()
setMainImage(miniatures.get(++currentImageIndex.value))
}
fun swipePrevious() {
if (currentImageIndex.value == 0) {
showPopUpMessage(
getString(R.string.first_image),
context
)
return
}
restoreFilters()
setMainImage(miniatures.get(--currentImageIndex.value))
}
fun refresh() {
executor.execute {
if (isInternetAvailable()) {
handler.post {
clearCache(context)
miniatures.clear()
isAppUIReady.value = false
}
} else {
handler.post {
showPopUpMessage(
"${getString(R.string.no_internet)}\n${getString(R.string.refresh_unavailable)}",
context
)
}
}
}
}
}
private object MainImageWrapper {
// origin image
var origin: Bitmap? = null
private set
fun saveOrigin() {
origin = copy(picture.value.image)
}
fun restore(): Bitmap {
if (origin != null) {
filtersSet.clear()
picture.value.image = copy(origin!!)
}
return copy(picture.value.image)
}
// picture adapter
private var picture = mutableStateOf(
Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
)
fun wrapPicture(picture: Picture) {
this.picture.value = picture
}
fun setImage(bitmap: Bitmap) {
picture.value.image = bitmap
}
fun isEmpty(): Boolean {
return (picture.value.name == "")
}
fun getName(): String {
return picture.value.name
}
fun getImage(): Bitmap {
return picture.value.image
}
fun getId(): Int {
return picture.value.id
}
// applied filters
private var filtersSet: MutableSet<FilterType> = LinkedHashSet()
fun addFilter(filter: FilterType) {
filtersSet.add(filter)
}
fun removeFilter(filter: FilterType) {
filtersSet.remove(filter)
}
fun getFilters(): Set<FilterType> {
return filtersSet
}
private fun copy(bitmap: Bitmap) : Bitmap {
return bitmap.copy(bitmap.config, false)
}
}

146
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/ImageHandler.kt

@ -0,0 +1,146 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import example.imageviewer.utils.cacheImage
import example.imageviewer.utils.cacheImagePostfix
import example.imageviewer.utils.scaleBitmapAspectRatio
import example.imageviewer.utils.toPx
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.io.BufferedReader
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
fun loadFullImage(source: String): Picture {
try {
val url = URL(source)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val bitmap: Bitmap? = BitmapFactory.decodeStream(input)
if (bitmap != null) {
return Picture(
source = source,
image = bitmap,
name = getNameURL(source),
width = bitmap.width,
height = bitmap.height
)
}
} catch (e: Exception) {
e.printStackTrace()
}
return Picture(image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
}
fun loadImages(cachePath: String, list: List<String>): MutableList<Picture> {
val result: MutableList<Picture> = ArrayList()
for (source in list) {
val name = getNameURL(source)
val path = cachePath + File.separator + name
if (File(path + "info").exists()) {
addCachedMiniature(filePath = path, outList = result)
} else {
addFreshMiniature(source = source, outList = result, path = cachePath)
}
result.last().id = result.size - 1
}
return result
}
private fun addFreshMiniature(
source: String,
outList: MutableList<Picture>,
path: String
) {
try {
val url = URL(source)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val result: Bitmap? = BitmapFactory.decodeStream(input)
if (result != null) {
val picture = Picture(
source,
getNameURL(source),
scaleBitmapAspectRatio(result, 200, 164),
result.width,
result.height
)
outList.add(picture)
cacheImage(path + getNameURL(source), picture)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun addCachedMiniature(
filePath: String,
outList: MutableList<Picture>
) {
try {
val read = BufferedReader(
InputStreamReader(
FileInputStream(filePath + cacheImagePostfix),
StandardCharsets.UTF_8
)
)
val source = read.readLine()
val width = read.readLine().toInt()
val height = read.readLine().toInt()
read.close()
val result: Bitmap? = BitmapFactory.decodeFile(filePath)
if (result != null) {
val picture = Picture(
source,
getNameURL(source),
result,
width,
height
)
outList.add(picture)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getNameURL(url: String): String {
return url.substring(url.lastIndexOf('/') + 1, url.length)
}

27
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/Picture.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import android.graphics.Bitmap
data class Picture(
var source: String = "",
var name: String = "",
var image: Bitmap,
var width: Int = 0,
var height: Int = 0,
var id: Int = 0
)

13
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt

@ -0,0 +1,13 @@
package example.imageviewer.model.filtration
import android.content.Context
import android.graphics.Bitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyBlurFilter
class BlurFilter(private val context: Context) : BitmapFilter {
override fun apply(bitmap: Bitmap): Bitmap {
return applyBlurFilter(bitmap, context)
}
}

12
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt

@ -0,0 +1,12 @@
package example.imageviewer.model.filtration
import android.graphics.Bitmap
import example.imageviewer.core.BitmapFilter
class EmptyFilter : BitmapFilter {
override fun apply(bitmap: Bitmap): Bitmap {
return bitmap
}
}

57
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt

@ -0,0 +1,57 @@
package example.imageviewer.model.filtration
import android.content.Context
import android.graphics.Bitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
class FiltersManager(private val context: Context) {
private var filtersMap: MutableMap<FilterType, BitmapFilter> = LinkedHashMap()
fun clear() {
filtersMap = LinkedHashMap()
}
fun add(filters: Collection<FilterType>) {
for (filter in filters)
add(filter)
}
fun add(filter: FilterType) {
if (!filtersMap.containsKey(filter))
filtersMap[filter] = getFilter(filter, context)
}
fun remove(filter: FilterType) {
filtersMap.remove(filter)
}
fun contains(filter: FilterType): Boolean {
return filtersMap.contains(filter)
}
fun applyFilters(bitmap: Bitmap): Bitmap {
var result: Bitmap = bitmap
for (filter in filtersMap) {
result = filter.value.apply(result)
}
return result
}
}
private fun getFilter(type: FilterType, context: Context): BitmapFilter {
return when (type) {
FilterType.GrayScale -> GrayScaleFilter()
FilterType.Pixel -> PixelFilter()
FilterType.Blur -> BlurFilter(context)
else -> {
EmptyFilter()
}
}
}

12
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt

@ -0,0 +1,12 @@
package example.imageviewer.model.filtration
import android.graphics.Bitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyGrayScaleFilter
class GrayScaleFilter : BitmapFilter {
override fun apply(bitmap: Bitmap) : Bitmap {
return applyGrayScaleFilter(bitmap)
}
}

12
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt

@ -0,0 +1,12 @@
package example.imageviewer.model.filtration
import android.graphics.Bitmap
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyPixelFilter
class PixelFilter : BitmapFilter {
override fun apply(bitmap: Bitmap): Bitmap {
return applyPixelFilter(bitmap)
}
}

54
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/style/Decoration.kt

@ -0,0 +1,54 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.style
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.res.imageResource
import example.imageviewer.R
@Composable
fun icEmpty(): ImageAsset = imageResource(R.raw.empty)
@Composable
fun icBack(): ImageAsset = imageResource(R.raw.back)
@Composable
fun icRefresh(): ImageAsset = imageResource(R.raw.refresh)
@Composable
fun icDots(): ImageAsset = imageResource(R.raw.dots)
@Composable
fun icFilterGrayscaleOn(): ImageAsset = imageResource(R.raw.grayscale_on)
@Composable
fun icFilterGrayscaleOff(): ImageAsset = imageResource(R.raw.grayscale_off)
@Composable
fun icFilterPixelOn(): ImageAsset = imageResource(R.raw.pixel_on)
@Composable
fun icFilterPixelOff(): ImageAsset = imageResource(R.raw.pixel_off)
@Composable
fun icFilterBlurOn(): ImageAsset = imageResource(R.raw.blur_on)
@Composable
fun icFilterBlurOff(): ImageAsset = imageResource(R.raw.blur_off)
@Composable
fun icFilterUnknown(): ImageAsset = imageResource(R.raw.filter_unknown)

67
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/Caching.kt

@ -0,0 +1,67 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.utils
import android.content.Context
import android.graphics.*
import example.imageviewer.model.Picture
import java.io.File
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
val cacheImagePostfix = "info"
fun cacheImage(path: String, picture: Picture) {
try {
FileOutputStream(path).use { out ->
picture.image.compress(Bitmap.CompressFormat.PNG, 100, out)
}
val bw =
BufferedWriter(
OutputStreamWriter(
FileOutputStream(path + cacheImagePostfix), StandardCharsets.UTF_8
)
)
bw.write(picture.source)
bw.write("\r\n${picture.width}")
bw.write("\r\n${picture.height}")
bw.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
fun clearCache(context: Context) {
val directory = File(context.cacheDir.absolutePath)
val files: Array<File>? = directory.listFiles()
if (files != null) {
for (file in files) {
if (file.isDirectory)
continue
file.delete()
}
}
}

133
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/utils/GraphicsMath.kt

@ -0,0 +1,133 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.utils
import android.content.Context
import android.content.res.Resources
import android.graphics.*
import android.renderscript.Allocation
import android.renderscript.Element
import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import androidx.compose.ui.layout.ContentScale
fun scaleBitmapAspectRatio(
bitmap: Bitmap,
width: Int,
height: Int,
filter: Boolean = false
): Bitmap {
val boundW: Float = width.toFloat()
val boundH: Float = height.toFloat()
val ratioX: Float = boundW / bitmap.width
val ratioY: Float = boundH / bitmap.height
val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
val resultH = (bitmap.height * ratio).toInt()
val resultW = (bitmap.width * ratio).toInt()
return Bitmap.createScaledBitmap(bitmap, resultW, resultH, filter)
}
fun getDisplayBounds(bitmap: Bitmap): Rect {
val boundW: Float = displayWidth().toFloat()
val boundH: Float = displayHeight().toFloat()
val ratioX: Float = bitmap.width / boundW
val ratioY: Float = bitmap.height / boundH
val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
val resultW = (boundW * ratio)
val resultH = (boundH * ratio)
return Rect(0, 0, resultW.toInt(), resultH.toInt())
}
fun applyGrayScaleFilter(bitmap: Bitmap): Bitmap {
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(result)
val colorMatrix = ColorMatrix()
colorMatrix.setSaturation(0f)
val paint = Paint()
paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
canvas.drawBitmap(result, 0f, 0f, paint)
return result
}
fun applyPixelFilter(bitmap: Bitmap): Bitmap {
var result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val w: Int = bitmap.width
val h: Int = bitmap.height
result = scaleBitmapAspectRatio(result, w / 20, h / 20)
result = scaleBitmapAspectRatio(result, w, h)
return result
}
fun applyBlurFilter(bitmap: Bitmap, context: Context): Bitmap {
val result: Bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val renderScript: RenderScript = RenderScript.create(context)
val tmpIn: Allocation = Allocation.createFromBitmap(renderScript, bitmap)
val tmpOut: Allocation = Allocation.createFromBitmap(renderScript, result)
val theIntrinsic: ScriptIntrinsicBlur =
ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
theIntrinsic.setRadius(25f)
theIntrinsic.setInput(tmpIn)
theIntrinsic.forEach(tmpOut)
tmpOut.copyTo(result)
return result
}
fun adjustImageScale(bitmap: Bitmap): ContentScale {
val bitmapRatio = (10 * bitmap.width.toFloat() / bitmap.height).toInt()
val displayRatio = (10 * displayWidth().toFloat() / displayHeight()).toInt()
if (displayRatio > bitmapRatio) {
return ContentScale.FillHeight
}
return ContentScale.FillWidth
}
fun toPx(dp: Int): Int {
return (dp * Resources.getSystem().displayMetrics.density).toInt()
}
fun toDp(px: Int): Int {
return (px / Resources.getSystem().displayMetrics.density).toInt()
}
fun displayWidth(): Int {
return Resources.getSystem().displayMetrics.widthPixels
}
fun displayHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels
}

59
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/AppUI.kt

@ -0,0 +1,59 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import android.content.Context
import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import example.imageviewer.model.AppState
import example.imageviewer.model.ScreenType
import example.imageviewer.model.ContentState
import example.imageviewer.style.Gray
@Composable
fun BuildAppUI(content: ContentState) {
Surface(
modifier = Modifier.fillMaxSize(),
color = Gray
) {
when (AppState.screenState()) {
ScreenType.Main -> {
if (content.isContentReady()) {
setMainScreen(content)
} else {
setLoadingScreen(content)
}
}
ScreenType.FullscreenImage -> {
setImageFullScreen(content)
}
}
}
}
fun showPopUpMessage(text: String, context: Context) {
Toast.makeText(
context,
text,
Toast.LENGTH_SHORT
).show()
}

288
examples/imageviewer/common/src/androidMain/kotlin/example/imageviewer/view/FullImageScreen.kt

@ -0,0 +1,288 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import android.content.Context
import android.content.res.Configuration
import android.graphics.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ScrollableRow
import androidx.compose.foundation.Image
import androidx.compose.foundation.Text
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.material.MaterialTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.RowScope.gravity
import androidx.compose.material.Surface
import androidx.compose.ui.unit.dp
import example.imageviewer.core.FilterType
import example.imageviewer.model.AppState
import example.imageviewer.model.ContentState
import example.imageviewer.model.ScreenType
import example.imageviewer.style.Foreground
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.Transparent
import example.imageviewer.style.icBack
import example.imageviewer.style.icFilterGrayscaleOn
import example.imageviewer.style.icFilterGrayscaleOff
import example.imageviewer.style.icFilterPixelOn
import example.imageviewer.style.icFilterPixelOff
import example.imageviewer.style.icFilterBlurOn
import example.imageviewer.style.icFilterBlurOff
import example.imageviewer.style.icFilterUnknown
import example.imageviewer.style.DarkGray
import example.imageviewer.utils.displayHeight
import example.imageviewer.utils.displayWidth
import example.imageviewer.utils.getDisplayBounds
import example.imageviewer.utils.adjustImageScale
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
@Composable
fun setImageFullScreen(
content: ContentState
) {
Column {
setToolBar(content.getSelectedImageName(), content)
setImage(content)
}
}
@Composable
fun setToolBar(
text: String,
content: ContentState
) {
Surface(color = MiniatureColor, modifier = Modifier.preferredHeight(44.dp)) {
Row(modifier = Modifier.padding(end = 30.dp)) {
Surface(
color = Transparent,
modifier = Modifier.padding(start = 20.dp).gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady()) {
content.restoreMainImage()
AppState.screenState(ScreenType.Main)
}
}) {
Image(
icBack(),
modifier = Modifier.preferredSize(38.dp)
)
}
}
Text(
text,
color = Foreground,
maxLines = 1,
modifier = Modifier.padding(start = 30.dp).weight(1f)
.gravity(Alignment.CenterVertically),
style = MaterialTheme.typography.body1
)
Surface(
color = Color(255, 255, 255, 40),
modifier = Modifier.preferredSize(154.dp, 38.dp)
.gravity(Alignment.CenterVertically),
shape = CircleShape
) {
ScrollableRow {
Row {
for (type in FilterType.values()) {
FilterButton(content, type)
}
}
}
}
}
}
}
@Composable
fun FilterButton(
content: ContentState,
type: FilterType,
modifier: Modifier = Modifier.gravity(Alignment.CenterVertically).preferredSize(38.dp)
) {
Surface(
color = Transparent,
modifier = Modifier.gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = { content.toggleFilter(type) }
) {
Image(
getFilterImage(type = type, content = content),
modifier
)
}
}
Spacer(Modifier.width(20.dp))
}
@Composable
fun getFilterImage(type: FilterType, content: ContentState): ImageAsset {
return when (type) {
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
else -> {
icFilterUnknown()
}
}
}
@Composable
fun setImage(content: ContentState) {
val drag = DragHandler()
val scale = ScaleHandler()
Surface(
color = DarkGray,
modifier = Modifier.fillMaxSize()
) {
Draggable(onDrag = drag, modifier = Modifier.fillMaxSize()) {
Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) {
val bitmap = imageByGesture(content, scale, drag)
Image(
asset = bitmap.asImageAsset(),
contentScale = adjustImageScale(bitmap)
)
}
}
}
}
@Composable
fun imageByGesture(
content: ContentState,
scale: ScaleHandler,
drag: DragHandler
): Bitmap {
val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag)
if (scale.factor.value > 1f)
return bitmap
if (abs(drag.getDistance().x) > displayWidth() / 10) {
if (drag.getDistance().x < 0) {
content.swipeNext()
} else {
content.swipePrevious()
}
drag.onCancel()
}
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()
)
}

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

@ -0,0 +1,264 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import android.content.Context
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.Text
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Stack
import androidx.compose.foundation.Box
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.offset
import androidx.compose.material.Surface
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TopAppBar
import androidx.compose.material.Card
import androidx.compose.material.Divider
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.layout.ContentScale
import androidx.compose.ui.unit.dp
import example.imageviewer.model.AppState
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.icDots
import example.imageviewer.R
@Composable
fun setMainScreen(content: ContentState) {
Column {
setTopContent(content)
setScrollableArea(content)
}
}
@Composable
fun setLoadingScreen(content: ContentState) {
Stack {
Column {
setTopContent(content)
}
Box(modifier = Modifier.gravity(Alignment.Center)) {
Surface(color = DarkGray, elevation = 4.dp, shape = CircleShape) {
CircularProgressIndicator(
modifier = Modifier.preferredSize(50.dp).padding(4.dp),
color = DarkGreen
)
}
}
Text(
text = content.getString(R.string.loading),
modifier = Modifier.gravity(Alignment.Center).offset(0.dp, 70.dp),
style = MaterialTheme.typography.body1,
color = Foreground
)
}
}
@Composable
fun setTopContent(content: ContentState) {
setTitleBar(text = "ImageViewer", content = content)
if (content.getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
setPreviewImageUI(content)
setSpacer(h = 10)
setDivider()
}
setSpacer(h = 5)
}
@Composable
fun setTitleBar(text: String, content: ContentState) {
TopAppBar(
backgroundColor = DarkGreen,
title = {
Row(Modifier.preferredHeight(50.dp)) {
Text(
text,
color = Foreground,
modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically)
)
Surface(
color = Transparent,
modifier = Modifier.padding(end = 20.dp).gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady())
content.refresh()
}) {
Image(
icRefresh(),
modifier = Modifier.preferredSize(35.dp)
)
}
}
}
})
}
@Composable
fun setPreviewImageUI(content: ContentState) {
Clickable(onClick = {
AppState.screenState(ScreenType.FullscreenImage)
}) {
Card(
backgroundColor = DarkGray,
modifier = Modifier.preferredHeight(250.dp),
shape = RectangleShape,
elevation = 1.dp
) {
Image(
if (content.isMainImageEmpty()) {
icEmpty()
}
else {
content.getSelectedImage().asImageAsset()
},
modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
contentScale = ContentScale.Fit
)
}
}
}
@Composable
fun setMiniatureUI(
picture: Picture,
content: ContentState
) {
Card(
backgroundColor = MiniatureColor,
modifier = Modifier.padding(start = 10.dp, end = 10.dp).preferredHeight(70.dp)
.fillMaxWidth()
.clickable {
content.setMainImage(picture)
},
shape = RectangleShape,
elevation = 2.dp
) {
Row(modifier = Modifier.padding(end = 30.dp)) {
Clickable(
onClick = {
content.setMainImage(picture)
AppState.screenState(ScreenType.FullscreenImage)
}
) {
Image(
picture.image.asImageAsset(),
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(90.dp)
.padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
contentScale = ContentScale.Crop
)
}
Text(
text = picture.name,
color = Foreground,
modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically).padding(start = 16.dp),
style = MaterialTheme.typography.body1
)
Clickable(
modifier = Modifier.preferredHeight(70.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)}",
content.getContext()
)
}
) {
Image(
icDots(),
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(30.dp)
.padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
contentScale = ContentScale.FillHeight
)
}
}
}
}
@Composable
fun setScrollableArea(content: ContentState) {
ScrollableColumn {
var index = 1
Column {
for (picture in content.getMiniatures()) {
setMiniatureUI(
picture = picture,
content = content
)
Spacer(modifier = Modifier.height(5.dp))
index++
}
}
}
}
@Composable
fun setDivider() {
Divider(
color = LightGray,
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
)
}
@Composable
fun setSpacer(h: Int) {
Spacer(modifier = Modifier.height(h.dp))
}

5
examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_imageviewer_background"/>
<foreground android:drawable="@mipmap/ic_imageviewer_foreground"/>
</adaptive-icon>

5
examples/imageviewer/common/src/androidMain/res/mipmap-anydpi-v26/ic_imageviewer_round.xml

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_imageviewer_background"/>
<foreground android:drawable="@mipmap/ic_imageviewer_foreground"/>
</adaptive-icon>

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-hdpi/ic_imageviewer_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-mdpi/ic_imageviewer_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xhdpi/ic_imageviewer_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxhdpi/ic_imageviewer_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_background.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_foreground.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
examples/imageviewer/common/src/androidMain/res/mipmap-xxxhdpi/ic_imageviewer_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/blur_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/blur_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/dots.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/empty.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/filter_unknown.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/grayscale_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/grayscale_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/pixel_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/pixel_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
examples/imageviewer/common/src/androidMain/res/raw/refresh.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

15
examples/imageviewer/common/src/androidMain/res/values-ru/strings.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">ImageViewer</string>
<string name="loading">Загружаем изображения...</string>
<string name="repo_empty">Репозиторий пуст.</string>
<string name="no_internet">Нет доступа в интернет.</string>
<string name="repo_invalid">Список изображений в репозитории пуст или имеет неверный формат.</string>
<string name="refresh_unavailable">Невозможно обновить изображения.</string>
<string name="load_image_unavailable">Невозможно загузить полное изображение.</string>
<string name="last_image">Это последнее изображение.</string>
<string name="first_image">Это первое изображение.</string>
<string name="picture">Изображение:</string>
<string name="size">Размеры:</string>
<string name="pixels">пикселей.</string>
</resources>

14
examples/imageviewer/common/src/androidMain/res/values/strings.xml

@ -0,0 +1,14 @@
<resources>
<string name="app_name">ImageViewer</string>
<string name="loading">Loading images...</string>
<string name="repo_empty">Repository is empty.</string>
<string name="no_internet">No internet access.</string>
<string name="repo_invalid">List of images in current repository is invalid or empty.</string>
<string name="refresh_unavailable">Cannot refresh images.</string>
<string name="load_image_unavailable">Cannot load full size image.</string>
<string name="last_image">This is last image.</string>
<string name="first_image">This is first image.</string>
<string name="picture">Picture:</string>
<string name="size">Size:</string>
<string name="pixels">pixels.</string>
</resources>

33
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/EventLocker.kt

@ -0,0 +1,33 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.core
class EventLocker {
private var value: Boolean = false
fun lock() {
value = false
}
fun unlock() {
value = true
}
fun isLocked(): Boolean {
return value
}
}

20
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/FilterType.kt

@ -0,0 +1,20 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.core
enum class FilterType {
GrayScale, Pixel, Blur
}

20
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/core/Repository.kt

@ -0,0 +1,20 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.core
interface Repository<T> {
fun get() : T
}

51
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ImageRepository.kt

@ -0,0 +1,51 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import example.imageviewer.core.Repository
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
class ImageRepository(
private val httpsURL: String
) : Repository<MutableList<String>> {
override fun get(): MutableList<String> {
val list: MutableList<String> = ArrayList()
try {
val url = URL(httpsURL)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val read = BufferedReader(InputStreamReader(connection.inputStream))
var line: String? = read.readLine()
while (line != null) {
list.add(line)
line = read.readLine()
}
read.close()
return list
} catch (e: Exception) {
e.printStackTrace()
}
return list
}
}

41
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/Miniatures.kt

@ -0,0 +1,41 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
class Miniatures(
private var list: MutableList<Picture> = ArrayList()
) {
fun get(index: Int): Picture {
return list[index]
}
fun getMiniatures(): List<Picture> {
return ArrayList(list)
}
fun setMiniatures(list: List<Picture>) {
this.list = ArrayList(list)
}
fun size(): Int {
return list.size
}
fun clear() {
list = ArrayList()
}
}

38
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/model/ScreenType.kt

@ -0,0 +1,38 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
enum class ScreenType {
Main, FullscreenImage
}
object AppState {
private var screen: MutableState<ScreenType>
init {
screen = mutableStateOf(ScreenType.Main)
}
fun screenState() : ScreenType {
return screen.value
}
fun screenState(state: ScreenType) {
screen.value = state
}
}

27
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/style/Palette.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.style
import androidx.compose.ui.graphics.Color
val DarkGreen = Color(16, 139, 102)
val Gray = Color.DarkGray
val LightGray = Color(100, 100, 100)
val DarkGray = Color(32, 32, 32)
val ToastBackground = Color(23, 23, 23)
val MiniatureColor = Color(50,50,50)
val Foreground = Color(210, 210, 210)
val Transparent = Color.Transparent

27
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/utils/Network.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.utils
import java.net.InetAddress
fun isInternetAvailable(): Boolean {
return try {
val ipAddress: InetAddress = InetAddress.getByName("google.com")
!ipAddress.equals("")
} catch (e: Exception) {
false
}
}

37
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Clickable.kt

@ -0,0 +1,37 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.runtime.Composable
import androidx.compose.runtime.emptyContent
import androidx.compose.foundation.Box
import androidx.compose.foundation.clickable
import androidx.compose.ui.Modifier
@Composable
fun Clickable(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
children: @Composable () -> Unit = emptyContent()
) {
Box(
modifier = modifier.clickable {
onClick?.invoke()
}
) {
children()
}
}

107
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Draggable.kt

@ -0,0 +1,107 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onDispose
import androidx.compose.ui.Modifier
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.gesture.DragObserver
import androidx.compose.ui.gesture.dragGestureFilter
import androidx.compose.runtime.mutableStateOf
import example.imageviewer.core.EventLocker
import example.imageviewer.style.Transparent
@Composable
fun Draggable(
onDrag: DragHandler,
modifier: Modifier = Modifier,
children: @Composable() () -> Unit
) {
Surface(
color = Transparent,
modifier = modifier.dragGestureFilter(
dragObserver = onDrag
)
) {
children()
}
}
class DragHandler : DragObserver {
private val amount = mutableStateOf(Point(0f, 0f))
private val distance = mutableStateOf(Point(0f, 0f))
private val locker: EventLocker = EventLocker()
fun getAmount(): Point {
return amount.value
}
fun getDistance(): Point {
return distance.value
}
override fun onStart(downPosition: Offset) {
distance.value = Point(Offset.Zero)
locker.unlock()
}
override fun onStop(velocity: Offset) {
distance.value = Point(Offset.Zero)
locker.unlock()
}
override fun onCancel() {
distance.value = Point(Offset.Zero)
locker.lock()
}
override fun onDrag(dragDistance: Offset): Offset {
if (locker.isLocked()) {
val dx = dragDistance.x
val dy = dragDistance.y
distance.value = Point(distance.value.x + dx, distance.value.y + dy)
amount.value = Point(amount.value.x + dx, amount.value.y + dy)
return dragDistance
}
return Offset.Zero
}
}
class Point {
var x: Float = 0f
var y: Float = 0f
constructor(x: Float, y: Float) {
this.x = x
this.y = y
}
constructor(point: Offset) {
this.x = point.x
this.y = point.y
}
fun setAttr(x: Float, y: Float) {
this.x = x
this.y = y
}
}

64
examples/imageviewer/common/src/commonMain/kotlin/example/imageviewer/view/Scalable.kt

@ -0,0 +1,64 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onDispose
import androidx.compose.ui.gesture.RawScaleObserver
import androidx.compose.ui.gesture.doubleTapGestureFilter
import androidx.compose.ui.gesture.rawScaleGestureFilter
import androidx.compose.ui.Modifier
import androidx.compose.foundation.ContentGravity
import androidx.compose.foundation.Interaction
import androidx.compose.foundation.InteractionState
import androidx.compose.material.Surface
import example.imageviewer.style.Transparent
import androidx.compose.runtime.mutableStateOf
@Composable
fun Scalable(
onScale: ScaleHandler,
modifier: Modifier = Modifier,
children: @Composable() () -> Unit
) {
Surface(
color = Transparent,
modifier = modifier.rawScaleGestureFilter(
scaleObserver = onScale
).doubleTapGestureFilter(onDoubleTap = { onScale.resetFactor() }),
) {
children()
}
}
class ScaleHandler(private val maxFactor: Float = 5f, private val minFactor: Float = 1f) :
RawScaleObserver {
val factor = mutableStateOf(1f)
fun resetFactor() {
if (factor.value > minFactor)
factor.value = minFactor
}
override fun onScale(scaleFactor: Float): Float {
factor.value += scaleFactor - 1f
if (maxFactor < factor.value) factor.value = maxFactor
if (minFactor > factor.value) factor.value = minFactor
return scaleFactor
}
}

62
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/R.kt

@ -0,0 +1,62 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer
object ResString {
val appName: String
val loading: String
val repoEmpty: String
val noInternet: String
val repoInvalid: String
val refreshUnavailable: String
val loadImageUnavailable: String
val lastImage: String
val firstImage: String
val picture: String
val size: String
val pixels: String
init {
if (System.getProperty("user.language").equals("ru")) {
appName = "ImageViewer"
loading = "Загружаем изображения..."
repoEmpty = "Репозиторий пуст."
noInternet = "Нет доступа в интернет."
repoInvalid = "Список изображений в репозитории пуст или имеет неверный формат."
refreshUnavailable = "Невозможно обновить изображения."
loadImageUnavailable = "Невозможно загузить полное изображение."
lastImage = "Это последнее изображение."
firstImage = "Это первое изображение."
picture = "Изображение:"
size = "Размеры:"
pixels = "пикселей."
} else {
appName = "ImageViewer"
loading = "Loading images..."
repoEmpty = "Repository is empty."
noInternet = "No internet access."
repoInvalid = "List of images in current repository is invalid or empty."
refreshUnavailable = "Cannot refresh images."
loadImageUnavailable = "Cannot load full size image."
lastImage = "This is last image."
firstImage = "This is first image."
picture = "Picture:"
size = "Size:"
pixels = "pixels."
}
}
}

22
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/core/BitmapFilter.kt

@ -0,0 +1,22 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.core
import java.awt.image.BufferedImage
interface BitmapFilter {
fun apply(bitmap: BufferedImage) : BufferedImage
}

335
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ContentState.kt

@ -0,0 +1,335 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import java.awt.image.BufferedImage
import androidx.compose.runtime.Composable
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.isInternetAvailable
import example.imageviewer.view.showPopUpMessage
import example.imageviewer.ResString
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
class ContentState(
private val repository: ImageRepository
) {
private val executor: ExecutorService by lazy { Executors.newFixedThreadPool(2) }
private val isAppUIReady = mutableStateOf(false)
fun isContentReady(): Boolean {
return isAppUIReady.value
}
// 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()
fun getMiniatures(): List<Picture> {
return miniatures.getMiniatures()
}
fun getSelectedImage(): BufferedImage {
return mainImage.value
}
fun getSelectedImageName(): String {
return mainImageWrapper.getName()
}
// filters managing
private val appliedFilters = FiltersManager()
private val filterUIState: MutableMap<FilterType, MutableState<Boolean>> = LinkedHashMap()
private fun toggleFilterState(filter: FilterType) {
if (!filterUIState.containsKey(filter)) {
filterUIState[filter] = mutableStateOf(true)
} else {
val value = filterUIState[filter]!!.value
filterUIState[filter]!!.value = !value
}
}
fun toggleFilter(filter: FilterType) {
if (containsFilter(filter)) {
removeFilter(filter)
} else {
addFilter(filter)
}
toggleFilterState(filter)
var bitmap = mainImageWrapper.origin
if (bitmap != null) {
bitmap = appliedFilters.applyFilters(bitmap)
mainImageWrapper.setImage(bitmap)
mainImage.value = bitmap
}
}
private fun addFilter(filter: FilterType) {
appliedFilters.add(filter)
mainImageWrapper.addFilter(filter)
}
private fun removeFilter(filter: FilterType) {
appliedFilters.remove(filter)
mainImageWrapper.removeFilter(filter)
}
private fun containsFilter(type: FilterType): Boolean {
return appliedFilters.contains(type)
}
fun isFilterEnabled(type: FilterType): Boolean {
if (!filterUIState.containsKey(type)) {
filterUIState[type] = mutableStateOf(false)
}
return filterUIState[type]!!.value
}
private fun restoreFilters(): BufferedImage {
filterUIState.clear()
appliedFilters.clear()
return mainImageWrapper.restore()
}
fun restoreMainImage() {
mainImage.value = restoreFilters()
}
// application content initialization
@Composable
fun initData() {
if (isAppUIReady.value)
return
val directory = File(cacheImagePath)
if (!directory.exists()) {
directory.mkdir()
}
executor.execute {
try {
if (isInternetAvailable()) {
val imageList = repository.get()
if (imageList.isEmpty()) {
invokeLater {
showPopUpMessage(
ResString.repoInvalid
)
isAppUIReady.value = true
}
return@execute
}
val pictureList = loadImages(cacheImagePath, imageList)
if (pictureList.isEmpty()) {
invokeLater {
showPopUpMessage(
ResString.repoEmpty
)
isAppUIReady.value = true
}
} else {
val picture = loadFullImage(imageList[0])
invokeLater {
miniatures.setMiniatures(pictureList)
if (isMainImageEmpty()) {
wrapPictureIntoMainImage(picture)
} else {
appliedFilters.add(mainImageWrapper.getFilters())
currentImageIndex.value = mainImageWrapper.getId()
}
isAppUIReady.value = true
}
}
} else {
invokeLater {
showPopUpMessage(
ResString.noInternet
)
isAppUIReady.value = true
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
// preview/fullscreen image managing
fun isMainImageEmpty(): Boolean {
return mainImageWrapper.isEmpty()
}
fun setMainImage(picture: Picture) {
if (mainImageWrapper.getId() == picture.id)
return
executor.execute {
if (isInternetAvailable()) {
val fullSizePicture = loadFullImage(picture.source)
fullSizePicture.id = picture.id
invokeLater {
wrapPictureIntoMainImage(fullSizePicture)
}
} else {
invokeLater {
showPopUpMessage(
"${ResString.noInternet}\n${ResString.loadImageUnavailable}"
)
wrapPictureIntoMainImage(picture)
}
}
}
}
private fun wrapPictureIntoMainImage(picture: Picture) {
mainImageWrapper.wrapPicture(picture)
mainImageWrapper.saveOrigin()
mainImage.value = picture.image
currentImageIndex.value = picture.id
}
fun swipeNext() {
if (currentImageIndex.value == miniatures.size() - 1) {
showPopUpMessage(ResString.lastImage)
return
}
restoreFilters()
setMainImage(miniatures.get(++currentImageIndex.value))
}
fun swipePrevious() {
if (currentImageIndex.value == 0) {
showPopUpMessage(ResString.firstImage)
return
}
restoreFilters()
setMainImage(miniatures.get(--currentImageIndex.value))
}
fun refresh() {
executor.execute {
if (isInternetAvailable()) {
invokeLater {
clearCache()
miniatures.clear()
isAppUIReady.value = false
}
} else {
invokeLater {
showPopUpMessage(
"${ResString.noInternet}\n${ResString.refreshUnavailable}"
)
}
}
}
}
}
private object MainImageWrapper {
// origin image
var origin: BufferedImage? = null
private set
fun saveOrigin() {
origin = copy(picture.value.image)
}
fun restore(): BufferedImage {
if (origin != null) {
picture.value.image = copy(origin!!)
filtersSet.clear()
}
return copy(picture.value.image)
}
// picture adapter
private var picture = mutableStateOf(
Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
)
fun wrapPicture(picture: Picture) {
this.picture.value = picture
}
fun setImage(bitmap: BufferedImage) {
picture.value.image = bitmap
}
fun isEmpty(): Boolean {
return (picture.value.name == "")
}
fun getName(): String {
return picture.value.name
}
fun getImage(): BufferedImage {
return picture.value.image
}
fun getId(): Int {
return picture.value.id
}
// applied filters
private var filtersSet: MutableSet<FilterType> = LinkedHashSet()
fun addFilter(filter: FilterType) {
filtersSet.add(filter)
}
fun removeFilter(filter: FilterType) {
filtersSet.remove(filter)
}
fun getFilters(): Set<FilterType> {
return filtersSet
}
private fun copy(bitmap: BufferedImage) : BufferedImage {
var result = BufferedImage(bitmap.width, bitmap.height, bitmap.type)
val graphics = result.createGraphics()
graphics.drawImage(bitmap, 0, 0, result.width, result.height, null)
return result
}
}

145
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/ImageHandler.kt

@ -0,0 +1,145 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import java.awt.image.BufferedImage
import example.imageviewer.utils.cacheImage
import example.imageviewer.utils.cacheImagePostfix
import example.imageviewer.utils.scaleBitmapAspectRatio
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.io.InputStreamReader
import java.io.BufferedReader
import javax.imageio.ImageIO
import java.lang.Exception
import java.net.HttpURLConnection
import java.net.URL
import java.nio.charset.StandardCharsets
fun loadFullImage(source: String): Picture {
try {
val url = URL(source)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val bitmap: BufferedImage? = ImageIO.read(input)
if (bitmap != null) {
return Picture(
source = source,
image = bitmap,
name = getNameURL(source),
width = bitmap.width,
height = bitmap.height
)
}
} catch (e: Exception) {
e.printStackTrace()
}
return Picture(image = BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB))
}
fun loadImages(cachePath: String, list: List<String>): MutableList<Picture> {
val result: MutableList<Picture> = ArrayList()
for (source in list) {
val name = getNameURL(source)
val path = cachePath + File.separator + name
if (File(path + "info").exists()) {
addCachedMiniature(filePath = path, outList = result)
} else {
addFreshMiniature(source = source, outList = result, path = cachePath)
}
result.last().id = result.size - 1
}
return result
}
private fun addFreshMiniature(
source: String,
outList: MutableList<Picture>,
path: String
) {
try {
val url = URL(source)
val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 5000
connection.connect()
val input: InputStream = connection.inputStream
val result: BufferedImage? = ImageIO.read(input)
if (result != null) {
val picture = Picture(
source,
getNameURL(source),
scaleBitmapAspectRatio(result, 200, 164),
result.width,
result.height
)
outList.add(picture)
cacheImage(path + getNameURL(source), picture)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun addCachedMiniature(
filePath: String,
outList: MutableList<Picture>
) {
try {
val read = BufferedReader(
InputStreamReader(
FileInputStream(filePath + cacheImagePostfix),
StandardCharsets.UTF_8
)
)
val source = read.readLine()
val width = read.readLine().toInt()
val height = read.readLine().toInt()
read.close()
val result: BufferedImage? = ImageIO.read(File(filePath))
if (result != null) {
val picture = Picture(
source,
getNameURL(source),
result,
width,
height
)
outList.add(picture)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getNameURL(url: String): String {
return url.substring(url.lastIndexOf('/') + 1, url.length)
}

27
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/Picture.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model
import java.awt.image.BufferedImage
data class Picture(
var source: String = "",
var name: String = "",
var image: BufferedImage,
var width: Int = 0,
var height: Int = 0,
var id: Int = 0
)

27
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/BlurFilter.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model.filtration
import java.awt.image.BufferedImage
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyBlurFilter
class BlurFilter : BitmapFilter {
override fun apply(bitmap: BufferedImage): BufferedImage {
return applyBlurFilter(bitmap)
}
}

27
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/EmptyFilter.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model.filtration
import java.awt.image.BufferedImage
import example.imageviewer.core.BitmapFilter
class EmptyFilter : BitmapFilter {
override fun apply(bitmap: BufferedImage): BufferedImage {
return bitmap
}
}

71
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/FiltersManager.kt

@ -0,0 +1,71 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model.filtration
import java.awt.image.BufferedImage
import example.imageviewer.core.BitmapFilter
import example.imageviewer.core.FilterType
class FiltersManager {
private var filtersMap: MutableMap<FilterType, BitmapFilter> = LinkedHashMap()
fun clear() {
filtersMap = LinkedHashMap()
}
fun add(filters: Collection<FilterType>) {
for (filter in filters)
add(filter)
}
fun add(filter: FilterType) {
if (!filtersMap.containsKey(filter))
filtersMap[filter] = getFilter(filter)
}
fun remove(filter: FilterType) {
filtersMap.remove(filter)
}
fun contains(filter: FilterType): Boolean {
return filtersMap.contains(filter)
}
fun applyFilters(bitmap: BufferedImage): BufferedImage {
var result: BufferedImage = bitmap
for (filter in filtersMap) {
result = filter.value.apply(result)
}
return result
}
}
private fun getFilter(type: FilterType): BitmapFilter {
return when (type) {
FilterType.GrayScale -> GrayScaleFilter()
FilterType.Pixel -> PixelFilter()
FilterType.Blur -> BlurFilter()
else -> {
EmptyFilter()
}
}
}

27
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/GrayScaleFilter.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model.filtration
import java.awt.image.BufferedImage
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyGrayScaleFilter
class GrayScaleFilter : BitmapFilter {
override fun apply(bitmap: BufferedImage) : BufferedImage {
return applyGrayScaleFilter(bitmap)
}
}

27
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/model/filtration/PixelFilter.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.model.filtration
import java.awt.image.BufferedImage
import example.imageviewer.core.BitmapFilter
import example.imageviewer.utils.applyPixelFilter
class PixelFilter : BitmapFilter {
override fun apply(bitmap: BufferedImage): BufferedImage {
return applyPixelFilter(bitmap)
}
}

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

@ -0,0 +1,53 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.style
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.res.imageResource
@Composable
fun icEmpty(): ImageAsset = imageResource("images/empty.png")
@Composable
fun icBack(): ImageAsset = imageResource("images/back.png")
@Composable
fun icRefresh(): ImageAsset = imageResource("images/refresh.png")
@Composable
fun icDots(): ImageAsset = imageResource("images/dots.png")
@Composable
fun icFilterGrayscaleOn(): ImageAsset = imageResource("images/grayscale_on.png")
@Composable
fun icFilterGrayscaleOff(): ImageAsset = imageResource("images/grayscale_off.png")
@Composable
fun icFilterPixelOn(): ImageAsset = imageResource("images/pixel_on.png")
@Composable
fun icFilterPixelOff(): ImageAsset = imageResource("images/pixel_off.png")
@Composable
fun icFilterBlurOn(): ImageAsset = imageResource("images/blur_on.png")
@Composable
fun icFilterBlurOff(): ImageAsset = imageResource("images/blur_off.png")
@Composable
fun icFilterUnknown(): ImageAsset = imageResource("images/filter_unknown.png")

68
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/utils/Caching.kt

@ -0,0 +1,68 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.utils
import java.awt.image.BufferedImage
import example.imageviewer.model.Picture
import javax.imageio.ImageIO
import java.io.File
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import java.io.FileOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
val cacheImagePostfix = "info"
val cacheImagePath = System.getProperty("user.home")!! +
File.separator + "Pictures/imageviewer" + File.separator
fun cacheImage(path: String, picture: Picture) {
try {
ImageIO.write(picture.image, "png", File(path))
val bw =
BufferedWriter(
OutputStreamWriter(
FileOutputStream(path + cacheImagePostfix),
StandardCharsets.UTF_8
)
)
bw.write(picture.source)
bw.write("\r\n${picture.width}")
bw.write("\r\n${picture.height}")
bw.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
fun clearCache() {
val directory = File(cacheImagePath)
val files: Array<File>? = directory.listFiles()
if (files != null) {
for (file in files) {
if (file.isDirectory)
continue
file.delete()
}
}
}

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

@ -0,0 +1,162 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.utils
import androidx.compose.desktop.AppManager
import androidx.compose.ui.unit.IntSize
import java.awt.Dimension
import java.awt.Graphics2D
import java.awt.Rectangle
import java.awt.Toolkit
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import javax.imageio.ImageIO
import java.awt.image.BufferedImageOp
import java.awt.image.ConvolveOp
import java.awt.image.Kernel
fun scaleBitmapAspectRatio(
bitmap: BufferedImage,
width: Int,
height: Int
): BufferedImage {
val boundW: Float = width.toFloat()
val boundH: Float = height.toFloat()
val ratioX: Float = boundW / bitmap.width
val ratioY: Float = boundH / bitmap.height
val ratio: Float = if (ratioX < ratioY) ratioX else ratioY
val resultH = (bitmap.height * ratio).toInt()
val resultW = (bitmap.width * ratio).toInt()
val result = BufferedImage(resultW, resultH, BufferedImage.TYPE_INT_ARGB)
val graphics = result.createGraphics()
graphics.drawImage(bitmap, 0, 0, resultW, resultH, null)
graphics.dispose()
return result
}
fun getDisplayBounds(bitmap: BufferedImage): Rectangle {
val boundW: Float = displayWidth().toFloat()
val boundH: Float = displayHeight().toFloat()
val ratioX: Float = bitmap.width / boundW
val ratioY: Float = bitmap.height / boundH
val ratio: Float = if (ratioX > ratioY) ratioX else ratioY
val resultW = (boundW * ratio)
val resultH = (boundH * ratio)
return Rectangle(0, 0, resultW.toInt(), resultH.toInt())
}
fun applyGrayScaleFilter(bitmap: BufferedImage): BufferedImage {
val result = BufferedImage(
bitmap.getWidth(),
bitmap.getHeight(),
BufferedImage.TYPE_BYTE_GRAY)
val graphics = result.getGraphics()
graphics.drawImage(bitmap, 0, 0, null)
graphics.dispose()
return result
}
fun applyPixelFilter(bitmap: BufferedImage): BufferedImage {
val w: Int = bitmap.width
val h: Int = bitmap.height
var result = scaleBitmapAspectRatio(bitmap, w / 20, h / 20)
result = scaleBitmapAspectRatio(result, w, h)
return result
}
fun applyBlurFilter(bitmap: BufferedImage): BufferedImage {
var result = BufferedImage(bitmap.getWidth(), bitmap.getHeight(), bitmap.type)
val graphics = result.getGraphics()
graphics.drawImage(bitmap, 0, 0, null)
graphics.dispose()
val radius = 11
val size = 11
val weight: Float = 1.0f / (size * size)
val matrix = FloatArray(size * size)
for (i in 0..matrix.size - 1) {
matrix[i] = weight
}
val kernel = Kernel(radius, size, matrix)
val op = ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null)
result = op.filter(result, null)
return result.getSubimage(
radius,
radius,
result.width - radius * 2,
result.height - radius * 2
)
}
fun displayWidth(): Int {
val window = AppManager.getCurrentFocusedWindow()
if (window != null) {
return window.width
}
return 0
}
fun displayHeight(): Int {
val window = AppManager.getCurrentFocusedWindow()
if (window != null) {
return window.height
}
return 0
}
fun toByteArray(bitmap: BufferedImage) : ByteArray {
val baos = ByteArrayOutputStream()
ImageIO.write(bitmap, "png", baos)
return baos.toByteArray()
}
fun cropImage(bitmap: BufferedImage, crop: Rectangle) : BufferedImage {
val result = BufferedImage(crop.width, crop.height, bitmap.type)
val graphics = result.createGraphics()
graphics.drawImage(bitmap, crop.x, crop.y, crop.width, crop.height, null)
return result
}
fun getPreferredWindowSize(desiredWidth: Int, desiredHeight: Int): IntSize {
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)
}

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

@ -0,0 +1,61 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import example.imageviewer.model.AppState
import example.imageviewer.model.ScreenType
import example.imageviewer.model.ContentState
import example.imageviewer.style.Gray
private val message: MutableState<String> = mutableStateOf("")
private val state: MutableState<Boolean> = mutableStateOf(false)
@Composable
fun BuildAppUI(content: ContentState) {
content.initData()
Surface(
modifier = Modifier.fillMaxSize(),
color = Gray
) {
when (AppState.screenState()) {
ScreenType.Main -> {
if (content.isContentReady()) {
setMainScreen(content)
} else {
setLoadingScreen(content)
}
}
ScreenType.FullscreenImage -> {
setImageFullScreen(content)
}
}
}
Toast(message.value, state)
}
fun showPopUpMessage(text: String) {
message.value = text
state.value = true
}

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

@ -0,0 +1,282 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import java.awt.image.BufferedImage
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.foundation.clickable
import androidx.compose.foundation.ScrollableRow
import androidx.compose.foundation.Image
import androidx.compose.foundation.Text
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageAsset
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.layout.ContentScale
import androidx.compose.material.MaterialTheme
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.preferredHeight
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.RowScope.gravity
import androidx.compose.material.Surface
import androidx.compose.ui.unit.dp
import example.imageviewer.core.FilterType
import example.imageviewer.model.AppState
import example.imageviewer.model.ContentState
import example.imageviewer.model.ScreenType
import example.imageviewer.style.Foreground
import example.imageviewer.style.MiniatureColor
import example.imageviewer.style.Transparent
import example.imageviewer.style.icBack
import example.imageviewer.style.icFilterGrayscaleOn
import example.imageviewer.style.icFilterGrayscaleOff
import example.imageviewer.style.icFilterPixelOn
import example.imageviewer.style.icFilterPixelOff
import example.imageviewer.style.icFilterBlurOn
import example.imageviewer.style.icFilterBlurOff
import example.imageviewer.style.icFilterUnknown
import example.imageviewer.style.DarkGray
import example.imageviewer.utils.displayHeight
import example.imageviewer.utils.displayWidth
import example.imageviewer.utils.getDisplayBounds
import example.imageviewer.utils.toByteArray
import example.imageviewer.utils.cropImage
import kotlin.math.abs
import kotlin.math.pow
import kotlin.math.roundToInt
import org.jetbrains.skija.Image
import org.jetbrains.skija.IRect
import java.awt.Rectangle
@Composable
fun setImageFullScreen(
content: ContentState
) {
Column {
setToolBar(content.getSelectedImageName(), content)
setImage(content)
}
}
@Composable
fun setToolBar(
text: String,
content: ContentState
) {
Surface(color = MiniatureColor, modifier = Modifier.preferredHeight(44.dp)) {
Row(modifier = Modifier.padding(end = 30.dp)) {
Surface(
color = Transparent,
modifier = Modifier.padding(start = 20.dp).gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady()) {
content.restoreMainImage()
AppState.screenState(ScreenType.Main)
}
}) {
Image(
icBack(),
modifier = Modifier.preferredSize(38.dp)
)
}
}
Text(
text,
color = Foreground,
maxLines = 1,
modifier = Modifier.padding(start = 30.dp).weight(1f)
.gravity(Alignment.CenterVertically),
style = MaterialTheme.typography.body1
)
Surface(
color = Color(255, 255, 255, 40),
modifier = Modifier.preferredSize(154.dp, 38.dp)
.gravity(Alignment.CenterVertically),
shape = CircleShape
) {
ScrollableRow {
Row {
for (type in FilterType.values()) {
FilterButton(content, type)
}
}
}
}
}
}
}
@Composable
fun FilterButton(
content: ContentState,
type: FilterType,
modifier: Modifier = Modifier.preferredSize(38.dp)
) {
Surface(
color = Transparent,
modifier = Modifier.gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = { content.toggleFilter(type)}
) {
Image(
getFilterImage(type = type, content = content),
modifier
)
}
}
Spacer(Modifier.width(20.dp))
}
@Composable
fun getFilterImage(type: FilterType, content: ContentState): ImageAsset {
return when (type) {
FilterType.GrayScale -> if (content.isFilterEnabled(type)) icFilterGrayscaleOn() else icFilterGrayscaleOff()
FilterType.Pixel -> if (content.isFilterEnabled(type)) icFilterPixelOn() else icFilterPixelOff()
FilterType.Blur -> if (content.isFilterEnabled(type)) icFilterBlurOn() else icFilterBlurOff()
else -> {
icFilterUnknown()
}
}
}
@Composable
fun setImage(content: ContentState) {
val drag = DragHandler()
val scale = ScaleHandler()
Surface(
color = DarkGray,
modifier = Modifier.fillMaxSize()
) {
Draggable(onDrag = drag, modifier = Modifier.fillMaxSize()) {
Scalable(onScale = scale, modifier = Modifier.fillMaxSize()) {
Image(
asset = imageByGesture(content, scale, drag).asImageAsset(),
contentScale = ContentScale.Fit
)
}
}
}
}
@Composable
fun imageByGesture(
content: ContentState,
scale: ScaleHandler,
drag: DragHandler
): Image {
val bitmap = cropBitmapByScale(content.getSelectedImage(), scale.factor.value, drag)
val image = Image.makeFromEncoded(toByteArray(bitmap), IRect(0, 0, bitmap.width, bitmap.height))
if (scale.factor.value > 1f)
return image
if (abs(drag.getDistance().x) > displayWidth() / 10) {
if (drag.getDistance().x < 0) {
content.swipeNext()
} else {
content.swipePrevious()
}
drag.onCancel()
}
return image
}
private fun cropBitmapByScale(bitmap: BufferedImage, scale: Float, drag: DragHandler): BufferedImage {
val crop = cropBitmapByBounds(
bitmap,
getDisplayBounds(bitmap),
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,
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 *= 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()
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)
}

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

@ -0,0 +1,264 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageAsset
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.foundation.clickable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.Text
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Stack
import androidx.compose.foundation.Box
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.offset
import androidx.compose.material.Surface
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TopAppBar
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.ui.unit.dp
import example.imageviewer.model.AppState
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.icDots
import example.imageviewer.utils.toByteArray
import example.imageviewer.ResString
import org.jetbrains.skija.Image
import org.jetbrains.skija.IRect
@Composable
fun setMainScreen(content: ContentState) {
Column {
setTopContent(content)
setScrollableArea(content)
}
}
@Composable
fun setLoadingScreen(content: ContentState) {
Stack {
Column {
setTopContent(content)
}
Box(modifier = Modifier.gravity(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.gravity(Alignment.Center).offset(0.dp, 70.dp),
style = MaterialTheme.typography.body1,
color = Foreground
)
}
}
@Composable
fun setTopContent(content: ContentState) {
setTitleBar(text = "ImageViewer", content = content)
setPreviewImageUI(content)
setSpacer(h = 10)
setDivider()
setSpacer(h = 5)
}
@Composable
fun setTitleBar(text: String, content: ContentState) {
TopAppBar(
backgroundColor = DarkGreen,
title = {
Row(Modifier.preferredHeight(50.dp)) {
Text(
text,
color = Foreground,
modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically)
)
Surface(
color = Transparent,
modifier = Modifier.padding(end = 20.dp).gravity(Alignment.CenterVertically),
shape = CircleShape
) {
Clickable(
onClick = {
if (content.isContentReady())
content.refresh()
}) {
Image(
icRefresh(),
modifier = Modifier.preferredSize(35.dp)
)
}
}
}
})
}
@Composable
fun setPreviewImageUI(content: ContentState) {
Clickable(onClick = {
AppState.screenState(ScreenType.FullscreenImage)
}) {
Card(
backgroundColor = DarkGray,
modifier = Modifier.preferredHeight(250.dp),
shape = RectangleShape,
elevation = 1.dp
) {
Image(
if (content.isMainImageEmpty())
icEmpty()
else Image.makeFromEncoded(
toByteArray(content.getSelectedImage()),
IRect(0, 0, content.getSelectedImage().width, content.getSelectedImage().height)
).asImageAsset(),
modifier = Modifier
.fillMaxWidth().padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 5.dp),
contentScale = ContentScale.Fit
)
}
}
}
@Composable
fun setMiniatureUI(
picture: Picture,
content: ContentState
) {
Card(
backgroundColor = MiniatureColor,
modifier = Modifier.padding(start = 10.dp, end = 10.dp).preferredHeight(70.dp)
.fillMaxWidth()
.clickable {
content.setMainImage(picture)
},
shape = RectangleShape,
elevation = 2.dp
) {
Row(modifier = Modifier.padding(end = 30.dp)) {
Clickable(
onClick = {
content.setMainImage(picture)
AppState.screenState(ScreenType.FullscreenImage)
}
) {
Image(
Image.makeFromEncoded(
toByteArray(picture.image),
IRect(0, 0, picture.image.width, picture.image.height)
).asImageAsset(),
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(90.dp)
.padding(start = 1.dp, top = 1.dp, end = 1.dp, bottom = 1.dp),
contentScale = ContentScale.Crop
)
}
Text(
text = picture.name,
color = Foreground,
modifier = Modifier.weight(1f).gravity(Alignment.CenterVertically).padding(start = 16.dp),
style = MaterialTheme.typography.body1
)
Clickable(
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(30.dp),
onClick = {
showPopUpMessage(
"${ResString.picture} " +
"${picture.name} \n" +
"${ResString.size} " +
"${picture.width}x${picture.height} " +
"${ResString.pixels}"
)
}
) {
Image(
icDots(),
modifier = Modifier.preferredHeight(70.dp)
.preferredWidth(30.dp)
.padding(start = 1.dp, top = 25.dp, end = 1.dp, bottom = 25.dp),
contentScale = ContentScale.FillHeight
)
}
}
}
}
@Composable
fun setScrollableArea(content: ContentState) {
ScrollableColumn {
var index = 1
Column {
for (picture in content.getMiniatures()) {
setMiniatureUI(
picture = picture,
content = content
)
Spacer(modifier = Modifier.height(5.dp))
index++
}
}
}
}
@Composable
fun setDivider() {
Divider(
color = LightGray,
modifier = Modifier.padding(start = 10.dp, end = 10.dp)
)
}
@Composable
fun setSpacer(h: Int) {
Spacer(modifier = Modifier.height(h.dp))
}

79
examples/imageviewer/common/src/desktopMain/kotlin/example/imageviewer/view/Toast.kt

@ -0,0 +1,79 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package example.imageviewer.view
import androidx.compose.foundation.Box
import androidx.compose.foundation.ContentGravity
import androidx.compose.foundation.layout.preferredSize
import androidx.compose.foundation.Text
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.onActive
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.window.Popup
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import kotlinx.coroutines.GlobalScope
import example.imageviewer.style.ToastBackground
import example.imageviewer.style.Foreground
enum class ToastDuration(val value: Int) {
Short(1000), Long(3000)
}
private var isShown: Boolean = false
@Composable
fun Toast(
text: String,
visibility: MutableState<Boolean> = mutableStateOf(false),
duration: ToastDuration = ToastDuration.Long
) {
if (isShown) {
return
}
if (visibility.value) {
isShown = true
Popup(
alignment = Alignment.BottomCenter
) {
Box(
Modifier.preferredSize(300.dp, 70.dp),
backgroundColor = ToastBackground,
shape = RoundedCornerShape(4.dp),
gravity = ContentGravity.Center
) {
Text(
text = text,
color = Foreground
)
}
onActive {
GlobalScope.launch {
delay(duration.value.toLong())
isShown = false
visibility.value = false
}
}
}
}
}

BIN
examples/imageviewer/common/src/desktopMain/resources/images/back.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/blur_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/blur_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/dots.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/empty.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/filter_unknown.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/grayscale_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/grayscale_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/pixel_off.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
examples/imageviewer/common/src/desktopMain/resources/images/pixel_on.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save