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)