feat: support compose multiplatform

This commit is contained in:
2023-10-31 03:07:47 +08:00
parent 0eb979c257
commit 2feee4c266
115 changed files with 1362 additions and 1729 deletions

View File

@@ -1,32 +1,65 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin)
alias(libs.plugins.mavenpublish)
id("convention.jvm.toolchain")
autowire(libs.plugins.kotlin.multiplatform)
autowire(libs.plugins.android.library)
autowire(libs.plugins.jetbrains.compose)
}
group = property.project.groupName
kotlin {
androidTarget()
jvm("desktop")
iosX64()
iosArm64()
iosSimulatorArm64()
jvmToolchain(17)
sourceSets {
all {
languageSettings {
optIn("kotlinx.cinterop.ExperimentalForeignApi")
}
}
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
}
}
val androidMain by getting
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
}
}
android {
namespace = "eu.wewox.pagecurl"
namespace = property.project.groupName
compileSdk = property.project.android.compileSdk
compileSdk = libs.versions.sdk.compile.get().toInt()
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = libs.versions.sdk.min.get().toInt()
minSdk = property.project.android.minSdk
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
}
kotlinOptions {
freeCompilerArgs = freeCompilerArgs +
"-Xexplicit-api=strict"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.foundation)
implementation(libs.compose.ui)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

View File

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

View File

@@ -0,0 +1,9 @@
package eu.wewox.pagecurl.page
import android.graphics.BlurMaskFilter
import androidx.compose.ui.graphics.Paint
actual fun Paint.setBlurred(value: Float) {
if (value == 0f) return
asFrameworkPaint().maskFilter = BlurMaskFilter(value, BlurMaskFilter.Blur.NORMAL)
}

View File

@@ -0,0 +1,3 @@
package eu.wewox.pagecurl.page
internal actual fun systemCurrentTimeMillis() = System.currentTimeMillis()

View File

@@ -48,7 +48,7 @@ public fun rememberPageCurlConfig(
backPageColor: Color = Color.White,
backPageContentAlpha: Float = 0.1f,
shadowColor: Color = Color.Black,
shadowAlpha: Float = 0.2f,
shadowAlpha: Float = 0.5f,
shadowRadius: Dp = 15.dp,
shadowOffset: DpOffset = DpOffset((-5).dp, 0.dp),
dragForwardEnabled: Boolean = true,

View File

@@ -1,23 +1,22 @@
package eu.wewox.pagecurl.page
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.Build
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.draw.DrawResult
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.toRect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmapConfig
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.drawscope.rotateRad
import androidx.compose.ui.graphics.drawscope.withTransform
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import eu.wewox.pagecurl.ExperimentalPageCurlApi
@@ -25,8 +24,9 @@ import eu.wewox.pagecurl.config.PageCurlConfig
import eu.wewox.pagecurl.utils.Polygon
import eu.wewox.pagecurl.utils.lineLineIntersection
import eu.wewox.pagecurl.utils.rotate
import java.lang.Float.max
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.max
@ExperimentalPageCurlApi
internal fun Modifier.drawCurl(
@@ -157,7 +157,7 @@ private fun CacheDrawScope.prepareCurl(
// Calculate the angle in radians between X axis and the curl line, this is used to rotate mirrored content to the
// right position of the curled back-page
val lineVector = topCurlOffset - bottomCurlOffset
val angle = Math.PI.toFloat() - atan2(lineVector.y, lineVector.x) * 2
val angle = PI.toFloat() - atan2(lineVector.y, lineVector.x) * 2
// Prepare a lambda to draw the shadow of the back-page
val drawShadow = prepareShadow(config, polygon, angle)
@@ -197,29 +197,27 @@ private fun CacheDrawScope.prepareShadow(
// Prepare shadow parameters
val radius = config.shadowRadius.toPx()
val shadowColor = config.shadowColor.copy(alpha = config.shadowAlpha).toArgb()
val transparent = config.shadowColor.copy(alpha = 0f).toArgb()
// TODO shadowOffset to be set here
val shadowOffset = Offset(-config.shadowOffset.x.toPx(), config.shadowOffset.y.toPx())
.rotate(2 * Math.PI.toFloat() - angle)
.rotate(2 * PI.toFloat() - angle)
// Prepare shadow paint with a shadow layer
val paint = Paint().apply {
val frameworkPaint = asFrameworkPaint()
frameworkPaint.color = transparent
frameworkPaint.setShadowLayer(
config.shadowRadius.toPx(),
shadowOffset.x,
shadowOffset.y,
shadowColor
)
color = Color(shadowColor)
setBlurred(radius)
// val frameworkPaint = asFrameworkPaint()
// frameworkPaint.color = transparent
// frameworkPaint.setShadowLayer(
// config.shadowRadius.toPx(),
// shadowOffset.x,
// shadowOffset.y,
// shadowColor
// )
}
// Hardware acceleration supports setShadowLayer() only on API 28 and above, thus to support previous API versions
// draw a shadow to the bitmap instead
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
prepareShadowApi28(radius, paint, polygon)
} else {
prepareShadowImage(radius, paint, polygon)
}
return prepareShadowImage(radius, paint, polygon)
}
private fun prepareShadowApi28(
@@ -228,12 +226,7 @@ private fun prepareShadowApi28(
polygon: Polygon,
): ContentDrawScope.() -> Unit = {
drawIntoCanvas {
it.nativeCanvas.drawPath(
polygon
.offset(radius).toPath()
.asAndroidPath(),
paint.asFrameworkPaint()
)
it.drawPath(polygon.offset(radius).toPath(), paint)
}
}
@@ -243,26 +236,26 @@ private fun CacheDrawScope.prepareShadowImage(
polygon: Polygon,
): ContentDrawScope.() -> Unit {
// Increase the size a little bit so that shadow is not clipped
val bitmap = Bitmap.createBitmap(
val bitmap = ImageBitmap(
(size.width + radius * 4).toInt(),
(size.height + radius * 4).toInt(),
Bitmap.Config.ARGB_8888
ImageBitmapConfig.Argb8888
)
Canvas(bitmap).apply {
drawPath(
polygon
// As bitmap size is increased we should translate the polygon so that shadow remains in center
.translate(Offset(2 * radius, 2 * radius))
.offset(radius).toPath()
.asAndroidPath(),
paint.asFrameworkPaint()
.offset(radius).toPath(), paint
)
}
return {
drawIntoCanvas {
// As bitmap size is increased we should shift the drawing so that shadow remains in center
it.nativeCanvas.drawBitmap(bitmap, -2 * radius, -2 * radius, null)
it.drawImage(bitmap, Offset(-2 * radius, -2 * radius), paint)
}
}
}
internal expect fun Paint.setBlurred(value: Float)

View File

@@ -5,9 +5,9 @@ import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.calculateTargetValue
import androidx.compose.animation.splineBasedDecay
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -84,50 +84,50 @@ internal fun Modifier.curlGesture(
val velocityTracker = VelocityTracker()
val startRect by lazy { targetStart.multiply(size) }
val endRect by lazy { targetEnd.multiply(size) }
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown(requireUnconsumed = false)
if (!startRect.contains(down.position)) {
return@awaitPointerEventScope
}
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
if (!startRect.contains(down.position)) {
return@awaitEachGesture
}
// Change X position to be always on the right side for more convenient gesture tracking
val dragStart = down.position.copy(x = size.width.toFloat())
// Change X position to be always on the right side for more convenient gesture tracking
val dragStart = down.position.copy(x = size.width.toFloat())
onStart()
onStart()
var dragCurrent = dragStart
drag(down.id) { change ->
dragCurrent = change.position
velocityTracker.addPosition(System.currentTimeMillis(), dragCurrent)
change.consume()
val vector = (dragStart - dragCurrent).rotate(PI.toFloat() / 2)
onCurl(dragCurrent - vector, dragCurrent + vector)
}
var dragCurrent = dragStart
drag(down.id) { change ->
dragCurrent = change.position
velocityTracker.addPosition(systemCurrentTimeMillis(), dragCurrent)
change.consume()
val vector = (dragStart - dragCurrent).rotate(PI.toFloat() / 2)
onCurl(dragCurrent - vector, dragCurrent + vector)
}
if (dragCurrent == dragStart) {
onCancel()
return@awaitPointerEventScope
}
if (dragCurrent == dragStart) {
onCancel()
return@awaitEachGesture
}
val velocity = velocityTracker.calculateVelocity()
val decay = splineBasedDecay<Offset>(this)
val target = decay.calculateTargetValue(
Offset.VectorConverter,
dragCurrent,
Offset(velocity.x, velocity.y)
).let {
Offset(
it.x.coerceIn(0f, size.width.toFloat() - 1),
it.y.coerceIn(0f, size.height.toFloat() - 1)
)
}
val velocity = velocityTracker.calculateVelocity()
val decay = splineBasedDecay<Offset>(this)
val target = decay.calculateTargetValue(
Offset.VectorConverter,
dragCurrent,
Offset(velocity.x, velocity.y)
).let {
Offset(
it.x.coerceIn(0f, size.width.toFloat() - 1),
it.y.coerceIn(0f, size.height.toFloat() - 1)
)
}
if (endRect.contains(target)) {
onEnd()
} else {
onCancel()
}
if (endRect.contains(target)) {
onEnd()
} else {
onCancel()
}
}
}
internal expect fun systemCurrentTimeMillis(): Long

View File

@@ -0,0 +1,51 @@
package eu.wewox.pagecurl.page
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.config.PageCurlConfig
import eu.wewox.pagecurl.utils.multiply
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ExperimentalPageCurlApi
internal fun Modifier.tapGesture(
config: PageCurlConfig,
scope: CoroutineScope,
onTapForward: suspend () -> Unit,
onTapBackward: suspend () -> Unit,
): Modifier = pointerInput(config) {
awaitEachGesture {
val down = awaitFirstDown().also { it.consume() }
val up = waitForUpOrCancellation() ?: return@awaitEachGesture
if ((down.position - up.position).getDistance() > viewConfiguration.touchSlop) {
return@awaitEachGesture
}
if (config.tapCustomEnabled && config.onCustomTap(this, size, up.position)) {
return@awaitEachGesture
}
if (config.tapForwardEnabled && config.tapForwardInteraction.target.multiply(size).contains(up.position)) {
scope.launch {
onTapForward()
}
return@awaitEachGesture
}
if (config.tapBackwardEnabled &&
config.tapBackwardInteraction.target
.multiply(size)
.contains(up.position)
) {
scope.launch {
onTapBackward()
}
return@awaitEachGesture
}
}
}

View File

@@ -0,0 +1,10 @@
package eu.wewox.pagecurl.page
import androidx.compose.ui.graphics.Paint
import org.jetbrains.skia.FilterBlurMode
import org.jetbrains.skia.MaskFilter
actual fun Paint.setBlurred(value: Float) {
if (value == 0f) return
asFrameworkPaint().maskFilter = MaskFilter.makeBlur(FilterBlurMode.NORMAL, value / 2f)
}

View File

@@ -0,0 +1,3 @@
package eu.wewox.pagecurl.page
internal actual fun systemCurrentTimeMillis() = System.currentTimeMillis()

View File

@@ -0,0 +1,10 @@
package eu.wewox.pagecurl.page
import androidx.compose.ui.graphics.Paint
import org.jetbrains.skia.FilterBlurMode
import org.jetbrains.skia.MaskFilter
actual fun Paint.setBlurred(value: Float) {
if (value == 0f) return
asFrameworkPaint().maskFilter = MaskFilter.makeBlur(FilterBlurMode.NORMAL, value / 2f)
}

View File

@@ -0,0 +1,13 @@
package eu.wewox.pagecurl.page
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import platform.posix.gettimeofday
import platform.posix.timeval
internal actual fun systemCurrentTimeMillis() = memScoped {
val timeVal = alloc<timeval>()
gettimeofday(timeVal.ptr, null)
(timeVal.tv_sec * 1000) + (timeVal.tv_usec / 1000)
}

View File

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

View File

@@ -1,53 +0,0 @@
package eu.wewox.pagecurl.page
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import eu.wewox.pagecurl.ExperimentalPageCurlApi
import eu.wewox.pagecurl.config.PageCurlConfig
import eu.wewox.pagecurl.utils.multiply
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ExperimentalPageCurlApi
internal fun Modifier.tapGesture(
config: PageCurlConfig,
scope: CoroutineScope,
onTapForward: suspend () -> Unit,
onTapBackward: suspend () -> Unit,
): Modifier = pointerInput(config) {
forEachGesture {
awaitPointerEventScope {
val down = awaitFirstDown().also { it.consume() }
val up = waitForUpOrCancellation() ?: return@awaitPointerEventScope
if ((down.position - up.position).getDistance() > viewConfiguration.touchSlop) {
return@awaitPointerEventScope
}
if (config.tapCustomEnabled && config.onCustomTap(this, size, up.position)) {
return@awaitPointerEventScope
}
if (config.tapForwardEnabled && config.tapForwardInteraction.target.multiply(size).contains(up.position)) {
scope.launch {
onTapForward()
}
return@awaitPointerEventScope
}
if (config.tapBackwardEnabled &&
config.tapBackwardInteraction.target
.multiply(size)
.contains(up.position)
) {
scope.launch {
onTapBackward()
}
return@awaitPointerEventScope
}
}
}
}