@ -0,0 +1,3 @@
|
||||
.DS_Store |
||||
.gradle |
||||
build |
@ -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> |
@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<state> |
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" /> |
||||
</state> |
||||
</component> |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project version="4"> |
||||
<component name="VcsDirectoryMappings"> |
||||
<mapping directory="" vcs="Git" /> |
||||
</component> |
||||
</project> |
@ -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> |
@ -0,0 +1 @@
|
||||
An example of image gallery for remote server image viewing, based on Jetpack Compose UI library (desktop). |
@ -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")) |
||||
} |
@ -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> |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,7 @@
|
||||
allprojects { |
||||
repositories { |
||||
google() |
||||
jcenter() |
||||
maven("https://packages.jetbrains.team/maven/p/ui/dev") |
||||
} |
||||
} |
@ -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") |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<manifest package="example.imageviewer"/> |
@ -0,0 +1,7 @@
|
||||
package example.imageviewer.core |
||||
|
||||
import android.graphics.Bitmap |
||||
|
||||
interface BitmapFilter { |
||||
fun apply(bitmap: Bitmap) : Bitmap |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
) |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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() |
||||
} |
@ -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() |
||||
) |
||||
} |
@ -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)) |
||||
} |
@ -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> |
@ -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> |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 9.6 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 8.7 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 107 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 8.0 KiB |
After Width: | Height: | Size: 8.2 KiB |
@ -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> |
@ -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> |
@ -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 |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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 |
@ -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 |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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." |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
) |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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") |
@ -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() |
||||
} |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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)) |
||||
} |
@ -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 |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 8.0 KiB |