Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiplatform: Port :zoomable module to Compose Multiplatform #10

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[*.{kt, kts}]
[*.{kt,kts}]
indent_size = 2
insert_final_newline = true
max_line_length = 120
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ allprojects {
repositories {
google()
mavenCentral()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}
}
Expand Down
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {

implementation(libs.plugin.agp)
implementation(libs.plugin.kotlin)
implementation(libs.plugin.jetbrains.compose)
implementation(libs.plugin.dokka)
implementation(libs.plugin.mavenPublish)
implementation(libs.plugin.dropshots)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ android {
}
lint.abortOnError = true
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
composeOptions.kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
@file:Suppress("UnstableApiUsage")

plugins {
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.compose")
id("com.android.library")
}

kotlin {
android {
publishLibraryVariants("release")
compilations.configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
}

jvm("desktop") {
compilations.configureEach {
kotlinOptions {
jvmTarget = "11"
}
}
}

// TODO what other targets?
DSteve595 marked this conversation as resolved.
Show resolved Hide resolved

targets.configureEach {
compilations.configureEach {
compilerOptions.configure {
freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-Xjvm-default=all", // TODO IDE complains this isn't set (transformableState.kt). is it working?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change, IDE still complains.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sigh okay. Let's get this PR merged for now. We can continue looking for a fix afterwards.

)
}
}
}

sourceSets {
named("commonMain") {
dependencies {
implementation(compose.runtime)
}
}

named("commonTest") {
dependencies {
implementation(libs.assertk)
}
}
}
}

compose {
val compilerDependencyDeclaration = libs.androidx.compose.compiler.get()
.run { "$module:$version" }
kotlinCompilerPlugin.set(compilerDependencyDeclaration)
}

android {
compileSdk = 33
defaultConfig {
minSdk = 24
resourcePrefix = "_telephoto"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
lint.abortOnError = true
}
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=false
android.useAndroidX=true

kotlin.mpp.androidSourceSetLayoutVersion=2

GROUP=me.saket.telephoto
VERSION_NAME=0.2.0

Expand Down
11 changes: 10 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
minSdk = "24"
compileSdk = "33"
kotlin = "1.8.10"
jetbrains-compose = "1.4.0"
agp = "7.4.2"
compose-runtime = "1.4.3"
compose-compiler = "1.4.4"
compose-ui = "1.4.3"
compose-ui-material3 = "1.1.0-rc01"
accompanist = "0.30.0"
androidx-annotation = "1.7.0-alpha02"
androidx-compose-compiler = "1.4.4"
androidx-appcompat = "1.6.1"
androidx-activity = "1.7.1"
androidx-ktx = "1.10.0"
Expand All @@ -30,16 +32,19 @@ emulatorWtf-cli = "0.9.8" # https://docs.emulator.wtf/changelog/
okio = "3.3.0"
okhttp-mockWebServer = "4.11.0"
dokka = "1.8.10"
assertk = "0.26"

[libraries]
plugin-paparazzi = { module = "app.cash.paparazzi:paparazzi-gradle-plugin", version.ref = "paparazzi" }
plugin-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
plugin-jetbrains-compose = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "jetbrains-compose" }
plugin-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
plugin-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" }
plugin-dropshots = { module = "com.dropbox.dropshots:dropshots-gradle-plugin", version.ref = "dropshots" }
plugin-emulatorWtf = { module = "wtf.emulator:gradle-plugin", version.ref = "emulatorWtf" }

androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-ktx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-ktx" }
androidx-ktx-palette = { module = "androidx.palette:palette-ktx", version.ref = "androidx-ktx-palette" }
Expand All @@ -48,6 +53,8 @@ androidx-activity = { module = "androidx.activity:activity-compose", version.ref
androidx-test-ktx = "androidx.test:core-ktx:1.5.0"
androidx-test-rules = "androidx.test:rules:1.5.0"
androidx-test-junit = "androidx.test.ext:junit-ktx:1.1.5"
androidx-compose-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "androidx-compose-compiler" }
# TODO remove compose libs in favor of declaring via jetbrains compose plugin
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you share more about this?

Copy link
Collaborator Author

@DSteve595 DSteve595 May 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jetbrains Compose has its own version (current latest 1.4.0), and that comes with a "blessed" set of versions for each of the Compose libraries. Those libraries are exposed via special dependency accessors. They look like:

dependencies {
  implementation(compose.ui)
  implementation(compose.material)
}

While it's totally possible to use arbitrary versions rather than the ones specified by the plugin, I went for the safe option of using the plugin's. Analogous to using the Compose BOM rather than specifying each library version individually.

If Telephoto totally adopts multiplatform, this TODO suggests to remove all the specific Compose library versions, in favor of fully using the plugin to add them as dependencies.

In the meantime, this split means that modules that use multiplatform could be using different versions of Compose libraries from those that use kotlin-android.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for explaining!

compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose-runtime" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-ui" }
compose-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose-ui" }
Expand Down Expand Up @@ -78,3 +85,5 @@ dropshots = { module = "com.dropbox.dropshots:dropshots", version.ref = "dropsho
dropboxDiffer = { module = "com.dropbox.differ:differ", version.ref = "dropboxDiffer" }

appyx = { module = "com.bumble.appyx:core", version.ref = "appyx" }

assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" }
2 changes: 1 addition & 1 deletion sample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ android {
lintConfig = file("lint.xml")
}
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
composeOptions.kotlinCompilerExtensionVersion = libs.versions.androidx.compose.compiler.get()
}

dependencies {
Expand Down
20 changes: 0 additions & 20 deletions zoomable/build.gradle

This file was deleted.

30 changes: 30 additions & 0 deletions zoomable/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import org.jetbrains.compose.compose

plugins {
id("kotlin-multiplatform-library-convention")
id("published-library-convention")
}

apply(plugin = "kotlin-parcelize")

kotlin {
sourceSets {
named("commonMain") {
dependencies {
implementation(compose("org.jetbrains.compose.ui:ui-util"))
api(compose.foundation)
api(libs.androidx.annotation)
}
}

named("commonTest") {
dependencies {
implementation(kotlin("test"))
}
}
}
}

android {
namespace = "me.saket.telephoto.zoomable"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package me.saket.telephoto.zoomable.internal

import android.os.Build
import android.view.HapticFeedbackConstants
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView

@Composable
internal actual fun rememberHapticFeedbackPerformer(): HapticFeedbackPerformer {
val view = LocalView.current

return remember(view) {
object : HapticFeedbackPerformer {
override fun performHapticFeedback() {
view.performHapticFeedback(HapticFeedbackConstantsCompat.GESTURE_END)
}
}
}
}

// Can be removed once https://issuetracker.google.com/u/1/issues/195043382 is fixed.
private object HapticFeedbackConstantsCompat {
val GESTURE_END: Int
get() {
return if (Build.VERSION.SDK_INT >= 30) {
HapticFeedbackConstants.GESTURE_END
} else {
// PhoneWindowManager#getVibrationEffect() maps
// GESTURE_END and CONTEXT_CLICK to the same effect.
HapticFeedbackConstants.CONTEXT_CLICK
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package me.saket.telephoto.zoomable.internal

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

actual typealias AndroidParcelize = Parcelize

actual typealias AndroidParcelable = Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

package me.saket.telephoto.zoomable

import android.os.Build
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
Expand All @@ -21,11 +19,11 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.launch
import me.saket.telephoto.zoomable.internal.MutatePriorities
import me.saket.telephoto.zoomable.internal.doubleTapZoomable
import me.saket.telephoto.zoomable.internal.rememberHapticFeedbackPerformer
import me.saket.telephoto.zoomable.internal.stopTransformation
import me.saket.telephoto.zoomable.internal.transformable

Expand Down Expand Up @@ -59,7 +57,7 @@ fun Modifier.zoomable(
val onClick by rememberUpdatedState(onClick)

val zoomableModifier = if (state.isReadyToInteract) {
val view = LocalView.current
val hapticFeedbackPerformer = rememberHapticFeedbackPerformer()
val density = LocalDensity.current
val scope = rememberCoroutineScope()
var isQuickZooming by remember { mutableStateOf(false) }
Expand All @@ -72,7 +70,7 @@ fun Modifier.zoomable(
onTransformStopped = { velocity ->
scope.launch {
if (state.isZoomOutsideRange()) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.GESTURE_END)
hapticFeedbackPerformer.performHapticFeedback()
state.smoothlySettleZoomOnGestureEnd()
} else {
state.fling(velocity = velocity, density = density)
Expand Down Expand Up @@ -110,7 +108,7 @@ fun Modifier.zoomable(
scope.launch {
isQuickZooming = false
if (state.isZoomOutsideRange()) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.GESTURE_END)
hapticFeedbackPerformer.performHapticFeedback()
state.smoothlySettleZoomOnGestureEnd()
}
}
Expand All @@ -137,17 +135,3 @@ fun Modifier.zoomable(
}
)
}

// Can be removed once https://issuetracker.google.com/u/1/issues/195043382 is fixed.
private object HapticFeedbackConstantsCompat {
val GESTURE_END: Int
get() {
return if (Build.VERSION.SDK_INT >= 30) {
HapticFeedbackConstants.GESTURE_END
} else {
// PhoneWindowManager#getVibrationEffect() maps
// GESTURE_END and CONTEXT_CLICK to the same effect.
HapticFeedbackConstants.CONTEXT_CLICK
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package me.saket.telephoto.zoomable.internal

import androidx.compose.runtime.Composable
import androidx.compose.ui.hapticfeedback.HapticFeedback

@Composable
internal expect fun rememberHapticFeedbackPerformer(): HapticFeedbackPerformer
DSteve595 marked this conversation as resolved.
Show resolved Hide resolved

/** Migrate to [HapticFeedback] once it supports the constant(s) we want */
internal interface HapticFeedbackPerformer {
fun performHapticFeedback()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package me.saket.telephoto.zoomable.internal

@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class AndroidParcelize()

expect interface AndroidParcelable

Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package me.saket.telephoto.zoomable.internal

import android.os.Parcelable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.ScaleFactor
import kotlinx.parcelize.Parcelize
import me.saket.telephoto.zoomable.ContentZoom
import me.saket.telephoto.zoomable.RawTransformation

@Parcelize
@AndroidParcelize
internal data class ZoomableSavedState(
private val offsetX: Float?,
private val offsetY: Float?,
private val userZoom: Float?,
) : Parcelable {
private val userZoom: Float?
) : AndroidParcelable {

fun gestureTransformation(): RawTransformation? {
return RawTransformation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.LayoutDirection
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import assertk.assertThat
import assertk.assertions.isEqualTo
import kotlin.test.Test

class CoerceInsideTest {
@Test fun `no zoom`() {
Expand Down
Loading