diff --git a/.gitignore b/.gitignore
index 5fd2736..26449cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,5 @@
local.properties
/SecretRingKey.gpg
/secring.gpg
+/zoomables_testing
+/buildSrc/src/main/kotlin/myPublishData.kt
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index e2dd078..a47b1ae 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -7,10 +7,13 @@
+
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2a4d5b5..95ee4c6 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 559435e..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,18 +0,0 @@
-buildscript {
- ext {
- compose_version = '1.1.1'
- }
-}// Top-level build file where you can add configuration options common to all sub-projects/modules.
-plugins {
- id 'com.android.application' version '7.1.2' apply false
- id 'com.android.library' version '7.1.2' apply false
- id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
- id "io.github.gradle-nexus.publish-plugin" version "1.1.0"
- id "org.jetbrains.dokka" version "1.6.10"
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
-}
-
-apply from: "${rootDir}/scripts/publish-root.gradle"
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..dc9d047
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.dokka) apply false
+ kotlin("android") version libs.versions.kotlin.get() apply false
+ id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
+}
+
+tasks.create("clean") {
+ delete(rootProject.buildDir)
+}
+
+nexusPublishing {
+ this.repositories {
+ sonatype {
+ stagingProfileId.set(publishData.sonatypeStagingProfileId)
+ username.set(publishData.ossrh.username)
+ password.set(publishData.ossrh.password)
+ nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
+ snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+ }
+ }
+}
\ No newline at end of file
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..b22ed73
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/PublishData.kt b/buildSrc/src/main/kotlin/PublishData.kt
new file mode 100644
index 0000000..865f292
--- /dev/null
+++ b/buildSrc/src/main/kotlin/PublishData.kt
@@ -0,0 +1,7 @@
+data class PublishData(val signing: Signing, val artifact: Artifact, val ossrh: OSSRH, val sonatypeStagingProfileId: String)
+
+data class Signing(val keyname: String, val passphrase: String, val executable: String)
+
+data class Artifact(val group: String, val version: String, val id: String)
+
+data class OSSRH(val username: String, val password: String)
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/artifact.kt b/buildSrc/src/main/kotlin/artifact.kt
new file mode 100644
index 0000000..a87449d
--- /dev/null
+++ b/buildSrc/src/main/kotlin/artifact.kt
@@ -0,0 +1,5 @@
+val artifact = Artifact(
+ group = "de.mr-pine.utils",
+ id = "zoomables",
+ version = "1.2.0"
+)
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..fd227eb
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,18 @@
+[versions]
+composeCompiler = "1.4.4"
+compose = "1.4.0"
+ktx = "1.9.0"
+library = "7.4.2"
+application = "7.4.2"
+kotlin = "1.8.10"
+
+[libraries]
+androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }
+compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
+compose-ui-util = { group = "androidx.compose.ui", name = "ui-util", version.ref = "compose" }
+compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose" }
+
+[plugins]
+android-library = { id = "com.android.library", version.ref = "library" }
+android-application = { id = "com.android.application", version.ref = "application" }
+dokka = { id = "org.jetbrains.dokka", version.ref = "kotlin" }
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 3eed18a..444e3c4 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Wed Mar 02 00:37:49 CET 2022
+#Mon Mar 27 16:36:42 CEST 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-rc-1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle
deleted file mode 100644
index 8e1581c..0000000
--- a/scripts/publish-module.gradle
+++ /dev/null
@@ -1,92 +0,0 @@
-apply plugin: "maven-publish"
-apply plugin: "signing"
-apply plugin: 'org.jetbrains.dokka'
-
-task androidSourcesJar(type: Jar) {
- archiveClassifier.set('sources')
- if (project.plugins.findPlugin("com.android.library")) {
- // For Android libraries
- from android.sourceSets.main.java.srcDirs
- from android.sourceSets.main.kotlin.srcDirs
- } else {
- // For pure Kotlin libraries, in case you have them
- from sourceSets.main.java.srcDirs
- from sourceSets.main.kotlin.srcDirs
- }
-}
-
-task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
- archiveClassifier.set('javadoc')
- from dokkaJavadoc.outputDirectory
-}
-
-tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
- pluginsMapConfiguration.set(
- ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
- )
-}
-
-artifacts {
- archives androidSourcesJar
- archives javadocJar
-}
-
-group = PUBLISH_GROUP_ID
-version = PUBLISH_VERSION
-
-afterEvaluate {
- publishing{
- publications {
- release(MavenPublication) {
- groupId PUBLISH_GROUP_ID
- artifactId PUBLISH_ARTIFACT_ID
- version PUBLISH_VERSION
-
- if (project.plugins.findPlugin("com.android.library")) {
- from components.release
- } else {
- from components.java
- }
-
- artifact androidSourcesJar
- artifact javadocJar
-
- pom {
- name = PUBLISH_ARTIFACT_ID
- description = 'A library provides Composables that handle nice and smooth zooming behaviour for you'
- url = 'https://github.com/Mr-Pine/Zoomables'
- licenses {
- license {
- name = 'Apache License 2.0'
- url = 'https://github.com/Mr-Pine/Zoomables/blob/master/LICENSE'
- }
- }
- developers {
- developer {
- id = 'Mr-Pine'
- }
- // Add all other devs here...
- }
-
- // Version control info - if you're using GitHub, follow the
- // format as seen here
- scm {
- connection = 'scm:git:github.com/Mr-Pine/Zoomables.git'
- developerConnection = 'scm:git:ssh://github.com/Mr-Pine/Zoomables.git'
- url = 'https://github.com/Mr-Pine/Zoomables'
- }
- }
- }
- }
- }
-}
-
-signing {
- useGpgCmd()
- /*useInMemoryPgpKeys(
- rootProject.ext["signing.keyId"],
- rootProject.ext["signing.secretKeyRingFile"],
- rootProject.ext["signing.password"],
- )*/
- sign publishing.publications
-}
\ No newline at end of file
diff --git a/scripts/publish-root.gradle b/scripts/publish-root.gradle
deleted file mode 100644
index 4cd5d25..0000000
--- a/scripts/publish-root.gradle
+++ /dev/null
@@ -1,34 +0,0 @@
-ext["signing.gnupg.keyName"] = ''
-ext["signing.gnupg.passphrase"] = ''
-ext["signing.gnupg.executable"] = ''
-ext["ossrhUsername"] = ''
-ext["ossrhPassword"] = ''
-ext["sonatypeStagingProfileId"] = ''
-
-File secretPropsFile = project.rootProject.file('scripts/local.properties')
-if (secretPropsFile.exists()) {
- // Read local.properties file first if it exists
- Properties p = new Properties()
- new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
- p.each { name, value -> ext[name] = value }
-} else {
- // Use system environment variables
- ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
- ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
- ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
- ext["signing.gnupg.keyName"] = System.getenv('SIGNING_KEY_NAME')
- ext["signing.gnupg.passphrase"] = System.getenv('SIGNING_PASSPHRASE')
- ext["signing.gnupg.executable"] = System.getenv('SIGNING_GNUPG_CMD')
-}
-
-nexusPublishing {
- repositories {
- sonatype {
- stagingProfileId = sonatypeStagingProfileId
- username = ossrhUsername
- password = ossrhPassword
- nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
- snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
- }
- }
-}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle.kts
similarity index 86%
rename from settings.gradle
rename to settings.gradle.kts
index f9192b9..3118717 100644
--- a/settings.gradle
+++ b/settings.gradle.kts
@@ -14,4 +14,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "Zoomables Library"
-include ':zoomables'
+include(":zoomables")
+include(":zoomables_testing")
diff --git a/zoomables/build.gradle b/zoomables/build.gradle
deleted file mode 100644
index 09148a3..0000000
--- a/zoomables/build.gradle
+++ /dev/null
@@ -1,82 +0,0 @@
-plugins {
- id 'com.android.library'
- id 'org.jetbrains.kotlin.android'
- id 'maven-publish'
- id 'kotlin-android'
-}
-
-android {
- compileSdk 31
-
- defaultConfig {
- minSdk 22
- targetSdk 31
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- consumerProguardFiles "consumer-rules.pro"
- }
-
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- composeOptions {
- kotlinCompilerExtensionVersion compose_version
- }
-
- kotlinOptions {
- jvmTarget = '1.8'
- }
-
- buildFeatures {
- compose true
- }
-}
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
- kotlinOptions.freeCompilerArgs += ["-Xexplicit-api=strict"]
-}
-
-dependencies {
-
- implementation 'androidx.core:core-ktx:1.7.0'
- implementation "androidx.compose.ui:ui:$compose_version"
- implementation "androidx.compose.ui:ui-util:$compose_version"
- implementation "androidx.compose.material:material:$compose_version"
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
-}
-
-ext{
- PUBLISH_GROUP_ID = 'de.mr-pine.utils'
- PUBLISH_VERSION = '1.1.2'
- PUBLISH_ARTIFACT_ID = 'zoomables'
-}
-
-apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"
-
-// Because the components are created only during the afterEvaluate phase, you must
-// configure your publications using the afterEvaluate() lifecycle method.
-afterEvaluate {
- publishing {
- publications {
- mavenLocal(MavenPublication) {
- // Applies the component for the release build variant.
- from components.release
-
- // You can then customize attributes of the publication as shown below.
- groupId = PUBLISH_GROUP_ID
- artifactId = PUBLISH_ARTIFACT_ID
- version = PUBLISH_VERSION
- }
- }
- }
-}
\ No newline at end of file
diff --git a/zoomables/build.gradle.kts b/zoomables/build.gradle.kts
new file mode 100644
index 0000000..0b90c6e
--- /dev/null
+++ b/zoomables/build.gradle.kts
@@ -0,0 +1,111 @@
+import org.jetbrains.kotlin.gradle.tasks.*
+
+plugins {
+ alias(libs.plugins.android.library)
+ kotlin("android")
+ `maven-publish`
+ alias(libs.plugins.dokka)
+ signing
+}
+
+android {
+ compileSdk = 33
+
+ defaultConfig {
+ minSdk = 22
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get()
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ namespace = "de.mr_pine.zoomables"
+
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ withJavadocJar()
+ }
+ }
+}
+
+tasks.withType {
+ kotlinOptions.freeCompilerArgs += listOf("-Xexplicit-api=strict")
+}
+
+dependencies {
+ implementation(libs.androidx.ktx)
+ implementation(libs.compose.ui)
+ implementation(libs.compose.ui.util)
+ implementation(libs.compose.material)
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.5")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+}
+
+publishing {
+ publications {
+ register("zoomables") {
+ groupId = publishData.artifact.group
+ artifactId = publishData.artifact.id
+ version = publishData.artifact.version
+
+ afterEvaluate {
+ from(components["release"])
+ }
+
+ pom {
+ description.set("A library provides Composables that handle nice and smooth zooming behaviour for you")
+ name.set(publishData.artifact.id)
+ url.set("https://github.com/Mr-Pine/Zoomables")
+
+ licenses {
+ license {
+ name.set("Apache License 2.0")
+ url.set("https://github.com/Mr-Pine/Zoomables/blob/master/LICENSE")
+ }
+ }
+
+ developers {
+ developer {
+ id.set("Mr-Pine")
+ }
+ }
+
+ scm {
+ connection.set("scm:git:github.com/Mr-Pine/Zoomables.git")
+ developerConnection.set("scm:git:ssh://github.com/Mr-Pine/Zoomables.git")
+ url.set("https://github.com/Mr-Pine/Zoomables")
+ }
+ }
+ }
+
+ }
+}
+
+signing {
+ useGpgCmd()
+ sign(publishing.publications)
+}
\ No newline at end of file
diff --git a/zoomables/proguard-rules.pro b/zoomables/proguard-rules.pro
index 481bb43..ff59496 100644
--- a/zoomables/proguard-rules.pro
+++ b/zoomables/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/zoomables/src/main/AndroidManifest.xml b/zoomables/src/main/AndroidManifest.xml
index b784098..44008a4 100644
--- a/zoomables/src/main/AndroidManifest.xml
+++ b/zoomables/src/main/AndroidManifest.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/DragGestureMode.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/DragGestureMode.kt
new file mode 100644
index 0000000..5cd454b
--- /dev/null
+++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/DragGestureMode.kt
@@ -0,0 +1,7 @@
+package de.mr_pine.zoomables
+
+public enum class DragGestureMode {
+ DISABLED,
+ PAN,
+ SWIPE_GESTURES;
+}
\ No newline at end of file
diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt
index ef31f84..07b9e74 100644
--- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt
+++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableImage.kt
@@ -23,7 +23,7 @@ import kotlinx.coroutines.CoroutineScope
* @param contentDescription text for accessibility see [Image] for further info
* @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default
* @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default
- * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case
+ * @param dragGestureMode A function with a [ZoomableState] scope that returns a [DragGestureMode] value that signals which drag gesture should currently be active. By default panning is enabled when zoomed, else swipe gestures are enabled.
* @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default)
*/
@Composable
@@ -35,7 +35,7 @@ public fun ZoomableImage(
contentDescription: String? = null,
onSwipeLeft: () -> Unit = {},
onSwipeRight: () -> Unit = {},
- dragGesturesEnabled: ZoomableState.() -> Boolean = { true },
+ dragGestureMode: ZoomableState.() -> DragGestureMode = { if (zoomed) DragGestureMode.SWIPE_GESTURES else DragGestureMode.PAN },
onDoubleTap: ((Offset) -> Unit)? = null
) {
Zoomable(
@@ -43,7 +43,7 @@ public fun ZoomableImage(
zoomableState = zoomableState,
onSwipeLeft = onSwipeLeft,
onSwipeRight = onSwipeRight,
- dragGesturesEnabled = dragGesturesEnabled,
+ dragGestureMode = dragGestureMode,
onDoubleTap = onDoubleTap
) {
Image(bitmap = bitmap, contentDescription = contentDescription, modifier = modifier)
@@ -60,7 +60,7 @@ public fun ZoomableImage(
* @param contentDescription text for accessibility see [Image] for further info
* @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default
* @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default
- * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case
+ * @param dragGestureMode A function with a [ZoomableState] scope that returns a [DragGestureMode] value that signals which drag gesture should currently be active. By default panning is enabled when zoomed, else swipe gestures are enabled.
* @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default)
*/
@Composable
@@ -72,7 +72,7 @@ public fun ZoomableImage(
contentDescription: String? = null,
onSwipeLeft: () -> Unit = {},
onSwipeRight: () -> Unit = {},
- dragGesturesEnabled: ZoomableState.() -> Boolean = { true },
+ dragGestureMode: ZoomableState.() -> DragGestureMode = { if (zoomed) DragGestureMode.SWIPE_GESTURES else DragGestureMode.PAN },
onDoubleTap: ((Offset) -> Unit)? = null
) {
Zoomable(
@@ -80,7 +80,7 @@ public fun ZoomableImage(
zoomableState = zoomableState,
onSwipeLeft = onSwipeLeft,
onSwipeRight = onSwipeRight,
- dragGesturesEnabled = dragGesturesEnabled,
+ dragGestureMode = dragGestureMode,
onDoubleTap = onDoubleTap
) {
Image(
@@ -101,7 +101,7 @@ public fun ZoomableImage(
* @param contentDescription text for accessibility see [Image] for further info
* @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default
* @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default
- * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case
+ * @param dragGestureMode A function with a [ZoomableState] scope that returns a [DragGestureMode] value that signals which drag gesture should currently be active. By default panning is enabled when zoomed, else swipe gestures are enabled.
* @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x when scale is currently 1 and zooms out to scale = 1 when zoomed in when null (default)
*/
@Composable
@@ -113,7 +113,7 @@ public fun ZoomableImage(
contentDescription: String? = null,
onSwipeLeft: () -> Unit = {},
onSwipeRight: () -> Unit = {},
- dragGesturesEnabled: ZoomableState.() -> Boolean = { true },
+ dragGestureMode: ZoomableState.() -> DragGestureMode = { if (zoomed) DragGestureMode.SWIPE_GESTURES else DragGestureMode.PAN },
onDoubleTap: ((Offset) -> Unit)? = null
) {
Zoomable(
@@ -121,7 +121,7 @@ public fun ZoomableImage(
zoomableState = zoomableState,
onSwipeLeft = onSwipeLeft,
onSwipeRight = onSwipeRight,
- dragGesturesEnabled = dragGesturesEnabled,
+ dragGestureMode = dragGestureMode,
onDoubleTap = onDoubleTap
) {
Image(painter = painter, contentDescription = contentDescription, modifier = modifier)
@@ -147,7 +147,12 @@ public fun EasyZoomableImage(
) {
val coroutineScope = rememberCoroutineScope()
val zoomableState = rememberZoomableState()
- Zoomable(coroutineScope = coroutineScope, zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight) {
+ Zoomable(
+ coroutineScope = coroutineScope,
+ zoomableState = zoomableState,
+ onSwipeLeft = onSwipeLeft,
+ onSwipeRight = onSwipeRight
+ ) {
Image(bitmap = bitmap, contentDescription = contentDescription, modifier = modifier)
}
}
@@ -172,7 +177,12 @@ public fun EasyZoomableImage(
val coroutineScope = rememberCoroutineScope()
val zoomableState = rememberZoomableState()
- Zoomable(coroutineScope = coroutineScope, zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight) {
+ Zoomable(
+ coroutineScope = coroutineScope,
+ zoomableState = zoomableState,
+ onSwipeLeft = onSwipeLeft,
+ onSwipeRight = onSwipeRight
+ ) {
Image(
imageVector = imageVector,
contentDescription = contentDescription,
@@ -200,7 +210,12 @@ public fun EasyZoomableImage(
) {
val coroutineScope = rememberCoroutineScope()
val zoomableState = rememberZoomableState()
- Zoomable(coroutineScope = coroutineScope, zoomableState = zoomableState, onSwipeLeft = onSwipeLeft, onSwipeRight = onSwipeRight) {
+ Zoomable(
+ coroutineScope = coroutineScope,
+ zoomableState = zoomableState,
+ onSwipeLeft = onSwipeLeft,
+ onSwipeRight = onSwipeRight
+ ) {
Image(painter = painter, contentDescription = contentDescription, modifier = modifier)
}
}
diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt
index 54a49d5..dd83b0d 100644
--- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt
+++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/ZoomableState.kt
@@ -17,11 +17,14 @@ import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
+private const val zoomedThreshold = 1.0E-3f
/**
* An implementation of [TransformableState] containing the values for the current [scale], [offset] and [rotation]. It's normally obtained using [rememberTransformableState]
* Other than [TransformableState] obtained by [rememberTransformableState], [ZoomableState] exposes [scale], [offset] and [rotation]
*
+ * As
+ *
* @param scale [MutableState]<[Float]> of the scale this state is initialized with
* @param offset [MutableState]<[Offset]> of the offset this state is initialized with
* @param rotation [MutableState]<[Float]> in degrees of the rotation this state is initialized with
@@ -33,7 +36,8 @@ import kotlin.math.sqrt
* @property scale The current scale as [MutableState]<[Float]>
* @property offset The current offset as [MutableState]<[Offset]>
* @property rotation The current rotation in degrees as [MutableState]<[Float]>
- * @property notTransformed `true` if [scale] is `1`, [offset] is [Offset.Zero] and [rotation] is `0`
+ * @property transformed `false` if [scale] is `1`, [offset] is [Offset.Zero] and [rotation] is `0`
+ * @property zoomed Whether the content is zoomed (in or out)
*/
public class ZoomableState(
public var scale: MutableState,
@@ -43,10 +47,11 @@ public class ZoomableState(
onTransformation: ZoomableState.(zoomChange: Float, panChange: Offset, rotationChange: Float) -> Unit
) : TransformableState {
- public val notTransformed: Boolean
- get() {
- return scale.value in (1 - 1.0E-3f)..(1 + 1.0E-3f) && offset.value.getDistanceSquared() in -1.0E-6f..1.0E-6f && rotation.value in -1.0E-3f..1.0E-3f
- }
+ public val zoomed: Boolean
+ get() = scale.value in (1 - zoomedThreshold)..(1 + zoomedThreshold)
+
+ public val transformed: Boolean
+ get() = zoomed || offset.value.getDistanceSquared() !in -1.0E-6f..1.0E-6f || rotation.value !in -1.0E-3f..1.0E-3f
private val transformScope: TransformScope = object : TransformScope {
override fun transformBy(zoomChange: Float, panChange: Offset, rotationChange: Float) =
diff --git a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt
index e9edc98..302f52a 100644
--- a/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt
+++ b/zoomables/src/main/kotlin/de/mr_pine/zoomables/Zoomables.kt
@@ -12,7 +12,10 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.input.pointer.*
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
+import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.util.fastAny
@@ -34,18 +37,17 @@ import kotlin.math.*
*
* @param coroutineScope used for smooth asynchronous zoom/pan/rotation animations
* @param zoomableState Contains the current transform states - obtained via [rememberZoomableState]
- * @param dragGesturesEnabled A function with a [ZoomableState] scope that returns a boolean value to enable/disable dragging gestures (swiping and panning). Returns `true` by default. *Note*: For some use cases it may be required that only panning is possible. Use `{!notTransformed}` in that case
+ * @param dragGestureMode A function with a [ZoomableState] scope that returns a [DragGestureMode] value that signals which drag gesture should currently be active. By default panning is enabled when zoomed, else swipe gestures are enabled.
* @param onSwipeLeft Optional function to run when user swipes from right to left - does nothing by default
* @param onSwipeRight Optional function to run when user swipes from left to right - does nothing by default
* @param minimumSwipeDistance Minimum distance the user has to travel on the screen for it to count as swiping
* @param onDoubleTap Optional function to run when user double taps. Zooms in by 2x to the touch point when scale is currently 1 and zooms out to scale = 1 when zoomed in when `null` (default)
*/
-
@Composable
public fun Zoomable(
coroutineScope: CoroutineScope,
zoomableState: ZoomableState,
- dragGesturesEnabled: ZoomableState.() -> Boolean = { true },
+ dragGestureMode: ZoomableState.() -> DragGestureMode = { if (zoomed) DragGestureMode.SWIPE_GESTURES else DragGestureMode.PAN },
onSwipeLeft: () -> Unit = {},
onSwipeRight: () -> Unit = {},
minimumSwipeDistance: Int = 0,
@@ -57,7 +59,7 @@ public fun Zoomable(
var composableCenter by remember { mutableStateOf(Offset.Zero) }
var transformOffset by remember { mutableStateOf(Offset.Zero) }
- val doubleTapFunction = onDoubleTap?: {
+ val doubleTapFunction = onDoubleTap ?: {
if (zoomableState.scale.value != 1f) {
coroutineScope.launch {
zoomableState.animateBy(
@@ -70,19 +72,16 @@ public fun Zoomable(
} else {
coroutineScope.launch {
zoomableState.animateZoomToPosition(2f, position = it, composableCenter)
- //zoomableState.animateZoomBy(2f)
}
Unit
}
}
fun onTransformGesture(
- centroid: Offset,
- pan: Offset,
- zoom: Float,
- transformRotation: Float
+ centroid: Offset, pan: Offset, zoom: Float, transformRotation: Float
) {
- val rotationChange = if(zoomableState.rotationBehavior == ZoomableState.Rotation.DISABLED) 0f else transformRotation
+ val rotationChange =
+ if (zoomableState.rotationBehavior == ZoomableState.Rotation.DISABLED) 0f else transformRotation
val tempOffset = zoomableState.offset.value + pan
@@ -126,124 +125,111 @@ public fun Zoomable(
)
}
.pointerInput(Unit) {
- forEachGesture {
- awaitPointerEventScope {
- var transformRotation = 0f
- var zoom = 1f
- var pan = Offset.Zero
- var pastTouchSlop = false
- val touchSlop = viewConfiguration.touchSlop
- var lockedToPanZoom = false
- var drag: PointerInputChange?
- var overSlop = Offset.Zero
-
- val down = awaitFirstDown(requireUnconsumed = false)
-
+ awaitEachGesture {
+ var transformRotation = 0f
+ var zoom = 1f
+ var pan = Offset.Zero
+ var pastTouchSlop = false
+ val touchSlop = viewConfiguration.touchSlop
+ var lockedToPanZoom = false
+ var drag: PointerInputChange?
+ var overSlop = Offset.Zero
- var transformEventCounter = 0
- do {
- val event = awaitPointerEvent()
- val canceled = event.changes.fastAny { it.positionChangeConsumed() }
- var relevant = true
- if (event.changes.size > 1) {
- if (!canceled) {
- val zoomChange = event.calculateZoom()
- val rotationChange = event.calculateRotation()
- val panChange = event.calculatePan()
+ var transformEventCounter = 0
+ do {
+ val event = awaitPointerEvent()
+ val canceled = event.changes.fastAny { it.isConsumed }
+ var relevant = true
+ if (event.changes.size > 1) {
+ if (!canceled) {
+ val zoomChange = event.calculateZoom()
+ val rotationChange = event.calculateRotation()
+ val panChange = event.calculatePan()
- if (!pastTouchSlop) {
- zoom *= zoomChange
- transformRotation += rotationChange
- pan += panChange
+ if (!pastTouchSlop) {
+ zoom *= zoomChange
+ transformRotation += rotationChange
+ pan += panChange
- val centroidSize =
- event.calculateCentroidSize(useCurrent = false)
- val zoomMotion = abs(1 - zoom) * centroidSize
- val rotationMotion =
- abs(transformRotation * PI.toFloat() * centroidSize / 180f)
- val panMotion = pan.getDistance()
+ val centroidSize =
+ event.calculateCentroidSize(useCurrent = false)
+ val zoomMotion = abs(1 - zoom) * centroidSize
+ val rotationMotion =
+ abs(transformRotation * PI.toFloat() * centroidSize / 180f)
+ val panMotion = pan.getDistance()
- if (zoomMotion > touchSlop ||
- rotationMotion > touchSlop ||
- panMotion > touchSlop
- ) {
- pastTouchSlop = true
- lockedToPanZoom =
- zoomableState.rotationBehavior == ZoomableState.Rotation.LOCK_ROTATION_ON_ZOOM_PAN && rotationMotion < touchSlop
- }
+ if (zoomMotion > touchSlop || rotationMotion > touchSlop || panMotion > touchSlop) {
+ pastTouchSlop = true
+ lockedToPanZoom =
+ zoomableState.rotationBehavior == ZoomableState.Rotation.LOCK_ROTATION_ON_ZOOM_PAN && rotationMotion < touchSlop
}
+ }
- if (pastTouchSlop) {
- val eventCentroid =
- event.calculateCentroid(useCurrent = false)
- val effectiveRotation =
- if (lockedToPanZoom) 0f else rotationChange
- if (effectiveRotation != 0f ||
- zoomChange != 1f ||
- panChange != Offset.Zero
- ) {
- onTransformGesture(
- eventCentroid,
- panChange,
- zoomChange,
- effectiveRotation
- )
- }
- event.changes.fastForEach {
- if (it.positionChanged()) {
- it.consumeAllChanges()
- }
+ if (pastTouchSlop) {
+ val eventCentroid = event.calculateCentroid(useCurrent = false)
+ val effectiveRotation =
+ if (lockedToPanZoom) 0f else rotationChange
+ if (effectiveRotation != 0f || zoomChange != 1f || panChange != Offset.Zero) {
+ onTransformGesture(
+ eventCentroid, panChange, zoomChange, effectiveRotation
+ )
+ }
+ event.changes.fastForEach {
+ if (it.positionChanged()) {
+ it.consume()
}
}
}
- } else if (transformEventCounter > 3) relevant = false
- transformEventCounter++
- } while (!canceled && event.changes.fastAny { it.pressed } && relevant)
+ }
+ } else if (transformEventCounter > 3) relevant = false
+ transformEventCounter++
+ } while (!canceled && event.changes.fastAny { it.pressed } && relevant)
- if (zoomableState.dragGesturesEnabled()) {
- do {
- awaitPointerEvent()
- drag = awaitTouchSlopOrCancellation(down.id) { change, over ->
- change.consumePositionChange()
+ if (zoomableState.dragGestureMode() != DragGestureMode.DISABLED) {
+ do {
+ val event = awaitPointerEvent()
+ drag = event.changes.firstOrNull()?.id?.let { pointerId ->
+ awaitTouchSlopOrCancellation(pointerId) { change, over ->
+ if (change.positionChange() != Offset.Zero) change.consume()
overSlop = over
}
- } while (drag != null && !drag.positionChangeConsumed())
- if (drag != null) {
- dragOffset = Offset.Zero
- if (zoomableState.scale.value !in 0.92f..1.08f) {
- coroutineScope.launch {
- zoomableState.transform {
- transformBy(1f, overSlop, 0f)
- }
+ }
+ } while (drag != null && !drag.isConsumed)
+ if (drag != null) {
+ dragOffset = Offset.Zero
+ when (zoomableState.dragGestureMode()) {
+ DragGestureMode.PAN -> coroutineScope.launch {
+ zoomableState.transform {
+ transformBy(1f, overSlop, 0f)
}
- } else {
- dragOffset += overSlop
}
- if (drag(drag.id) {
- if (zoomableState.scale.value !in 0.92f..1.08f) {
+ DragGestureMode.SWIPE_GESTURES -> dragOffset += overSlop
+ else -> {}
+ }
+ if (drag(drag.id) {
+ when (zoomableState.dragGestureMode()) {
+ DragGestureMode.PAN -> {
zoomableState.offset.value += it.positionChange()
- } else {
- dragOffset += it.positionChange()
}
- it.consumePositionChange()
+ DragGestureMode.SWIPE_GESTURES -> dragOffset += it.positionChange()
+ else -> {}
}
- ) {
- if (zoomableState.scale.value in 0.92f..1.08f) {
- val offsetX = dragOffset.x
- if (offsetX > minimumSwipeDistance) {
- onSwipeRight()
+ if (it.positionChange() != Offset.Zero) it.consume()
+ }) {
+ if (zoomableState.dragGestureMode() == DragGestureMode.SWIPE_GESTURES) {
+ val offsetX = dragOffset.x
+ if (offsetX > minimumSwipeDistance) {
+ onSwipeRight()
- } else if (offsetX < -minimumSwipeDistance) {
- onSwipeLeft()
- }
+ } else if (offsetX < -minimumSwipeDistance) {
+ onSwipeLeft()
}
}
}
}
}
}
- }
- ) {
+ }) {
Box(
modifier = Modifier
.clip(RectangleShape)
@@ -259,11 +245,9 @@ public fun Zoomable(
rotationZ = zoomableState.rotation.value
)
.onGloballyPositioned { coordinates ->
- val localOffset =
- Offset(
- coordinates.size.width.toFloat() / 2,
- coordinates.size.height.toFloat() / 2
- )
+ val localOffset = Offset(
+ coordinates.size.width.toFloat() / 2, coordinates.size.height.toFloat() / 2
+ )
val windowOffset = coordinates.localToWindow(localOffset)
composableCenter =
coordinates.parentLayoutCoordinates?.windowToLocal(windowOffset)