diff --git a/.mapping.json b/.mapping.json index 3369e2d28..16b906476 100644 --- a/.mapping.json +++ b/.mapping.json @@ -596,6 +596,19 @@ "client/android/compose-interop/proguard-rules.pro":"divkit/public/client/android/compose-interop/proguard-rules.pro", "client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivContexts.kt":"divkit/public/client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivContexts.kt", "client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt":"divkit/public/client/android/compose-interop/src/main/java/com/yandex/div/compose/interop/DivViewInterop.kt", + "client/android/compose/build.gradle.kts":"divkit/public/client/android/compose/build.gradle.kts", + "client/android/compose/proguard-rules.pro":"divkit/public/client/android/compose/proguard-rules.pro", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt", + "client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt", "client/android/div-common.gradle":"divkit/public/client/android/div-common.gradle", "client/android/div-core/build.gradle":"divkit/public/client/android/div-core/build.gradle", "client/android/div-core/proguard-rules.pro":"divkit/public/client/android/div-core/proguard-rules.pro", diff --git a/client/android/build.gradle b/client/android/build.gradle index b9fdc2fb8..2a517fae1 100644 --- a/client/android/build.gradle +++ b/client/android/build.gradle @@ -30,6 +30,7 @@ buildscript { plugins { alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.multiplatform) apply false } @@ -124,6 +125,7 @@ buildTimeTracker { apiValidation { ignoredProjects += [ "api-generator-test", + "compose", "divkit-demo-app", "divkit-perftests", "divkit-regression-testing", diff --git a/client/android/compose-interop/build.gradle b/client/android/compose-interop/build.gradle index 1ef73a24e..283a9ded3 100644 --- a/client/android/compose-interop/build.gradle +++ b/client/android/compose-interop/build.gradle @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.compose) } apply from: "${project.projectDir}/../div-library.gradle" diff --git a/client/android/compose/build.gradle.kts b/client/android/compose/build.gradle.kts new file mode 100644 index 000000000..dac20abd7 --- /dev/null +++ b/client/android/compose/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.compose) +} + +apply(from = "../div-library.gradle") + +android { + namespace = "com.yandex.div.compose" +} + +dependencies { + implementation(project(":div-data")) + + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.coreKtx) + implementation(libs.coil.compose) + implementation(libs.coil.network) + + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/client/android/compose/proguard-rules.pro b/client/android/compose/proguard-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt new file mode 100644 index 000000000..bdf3c4d2d --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivContext.kt @@ -0,0 +1,11 @@ +package com.yandex.div.compose + +import android.content.Context +import android.content.ContextWrapper +import androidx.annotation.MainThread +import com.yandex.div.json.expressions.ExpressionResolver + +class DivContext @MainThread internal constructor( + baseContext: Context, + internal val expressionResolver: ExpressionResolver +) : ContextWrapper(baseContext) diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt new file mode 100644 index 000000000..9d8b51c48 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivView.kt @@ -0,0 +1,17 @@ +package com.yandex.div.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.yandex.div.compose.views.DivBlockView +import com.yandex.div2.DivData + +@Composable +fun DivView( + data: DivData, + modifier: Modifier = Modifier +) { + DivBlockView( + data = data.states.first().div, + modifier = modifier + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt new file mode 100644 index 000000000..dc0642889 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/DivViewPreview.kt @@ -0,0 +1,88 @@ +package com.yandex.div.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.core.net.toUri +import com.yandex.div.json.expressions.Expression +import com.yandex.div.json.expressions.ExpressionResolver +import com.yandex.div2.Div +import com.yandex.div2.DivBackground +import com.yandex.div2.DivContainer +import com.yandex.div2.DivData +import com.yandex.div2.DivEdgeInsets +import com.yandex.div2.DivFixedSize +import com.yandex.div2.DivImage +import com.yandex.div2.DivSize +import com.yandex.div2.DivSolidBackground +import com.yandex.div2.DivText + +@Preview +@Composable +private fun DivViewPreview() { + val divContext = DivContext( + baseContext = LocalContext.current, + expressionResolver = ExpressionResolver.EMPTY + ) + CompositionLocalProvider(LocalContext provides divContext) { + DivView(data = testData) + } +} + +private val testData = DivData( + logId = "preview", + states = listOf( + DivData.State( + stateId = 0, + div = Div.Container( + value = DivContainer( + orientation = constant(DivContainer.Orientation.VERTICAL), + paddings = DivEdgeInsets( + start = constant(10), + end = constant(10), + top = constant(20), + bottom = constant(20) + ), + margins = DivEdgeInsets( + top = constant(10), + bottom = constant(10) + ), + background = listOf( + DivBackground.Solid( + value = DivSolidBackground( + color = constant(0xFF909090.toInt()), + ) + ) + ), + items = listOf( + Div.Image( + value = DivImage( + width = fixedSize(48), + height = fixedSize(48), + imageUrl = constant("https://yastatic.net/s3/home/divkit/logo.png".toUri()) + ) + ), + Div.Text( + value = DivText( + fontSize = constant(36), + text = constant("Hello!"), + textColor = constant(0xFFBF0000.toInt()), + ) + ) + ) + ) + ) + ) + ) +) + +private fun constant(value: T): Expression { + return Expression.ConstantExpression(value) +} + +private fun fixedSize(value: Long): DivSize { + return DivSize.Fixed( + value = DivFixedSize(value = constant(value)) + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt new file mode 100644 index 000000000..013bf8685 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt @@ -0,0 +1,20 @@ +package com.yandex.div.compose.views + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.yandex.div.compose.views.modifiers.apply +import com.yandex.div2.Div + +@Composable +internal fun DivBlockView( + data: Div, + modifier: Modifier = Modifier +) { + val modifier = modifier.apply(data.value()) + when (data) { + is Div.Container -> DivContainerView(modifier, data.value) + is Div.Image -> DivImageView(modifier, data.value) + is Div.Text -> DivTextView(modifier, data.value) + else -> TODO() + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt new file mode 100644 index 000000000..0e5113b87 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivContainerView.kt @@ -0,0 +1,71 @@ +package com.yandex.div.compose.views + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.yandex.div2.Div +import com.yandex.div2.DivContainer + +@Composable +internal fun DivContainerView( + modifier: Modifier, + data: DivContainer +) { + when (data.orientation.evaluate()) { + DivContainer.Orientation.HORIZONTAL -> + HorizontalView( + modifier = modifier, + items = data.items.orEmpty() + ) + + DivContainer.Orientation.VERTICAL -> + VerticalView( + modifier = modifier, + items = data.items.orEmpty() + ) + + DivContainer.Orientation.OVERLAP -> + OverlapView( + modifier = modifier, + items = data.items.orEmpty() + ) + } +} + +@Composable +private fun HorizontalView( + modifier: Modifier, + items: List
+) { + Row(modifier = modifier) { + items.forEach { + DivBlockView(data = it) + } + } +} + +@Composable +private fun VerticalView( + modifier: Modifier, + items: List
+) { + Column(modifier = modifier) { + items.forEach { + DivBlockView(data = it) + } + } +} + +@Composable +private fun OverlapView( + modifier: Modifier, + items: List
+) { + Box(modifier = modifier) { + items.forEach { + DivBlockView(data = it) + } + } +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt new file mode 100644 index 000000000..fd07eafd0 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivImageView.kt @@ -0,0 +1,18 @@ +package com.yandex.div.compose.views + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import coil3.compose.AsyncImage +import com.yandex.div2.DivImage + +@Composable +internal fun DivImageView( + modifier: Modifier, + data: DivImage +) { + AsyncImage( + modifier = modifier, + model = data.imageUrl.evaluate(), + contentDescription = null + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt new file mode 100644 index 000000000..045bfc548 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt @@ -0,0 +1,20 @@ +package com.yandex.div.compose.views + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.sp +import com.yandex.div2.DivText + +@Composable +internal fun DivTextView( + modifier: Modifier, + data: DivText +) { + Text( + text = data.text.evaluate(expressionResolver), + modifier = modifier, + color = data.textColor.evaluate().toColor(), + fontSize = data.fontSize.evaluate().toFloat().sp, + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt new file mode 100644 index 000000000..809fd9a10 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/Utils.kt @@ -0,0 +1,31 @@ +package com.yandex.div.compose.views + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.yandex.div.compose.DivContext +import com.yandex.div.json.expressions.Expression +import com.yandex.div.json.expressions.ExpressionResolver + +internal fun Int.toColor(): Color { + return Color(this) +} + +internal fun Long.toDp(): Dp { + return toFloat().dp +} + +internal val divContext: DivContext + @Composable + get() = LocalContext.current as DivContext + +internal val expressionResolver: ExpressionResolver + @Composable + get() = divContext.expressionResolver + +@Composable +internal fun Expression.evaluate(): T { + return evaluate(expressionResolver) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt new file mode 100644 index 000000000..5edfc281c --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BackgroundModifiers.kt @@ -0,0 +1,33 @@ +package com.yandex.div.compose.views.modifiers + +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.yandex.div.compose.views.evaluate +import com.yandex.div.compose.views.toColor +import com.yandex.div2.DivBackground +import com.yandex.div2.DivSolidBackground + +@Composable +internal fun Modifier.backgrounds(value: List): Modifier { + var modifier = this + value.forEach { background -> + when (background) { + is DivBackground.Solid -> + modifier = modifier.solidBackground(background.value) + + is DivBackground.Image -> TODO() + is DivBackground.LinearGradient -> TODO() + is DivBackground.NinePatch -> TODO() + is DivBackground.RadialGradient -> TODO() + } + } + return modifier +} + +@Composable +private fun Modifier.solidBackground(value: DivSolidBackground): Modifier { + return background( + color = value.color.evaluate().toColor() + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt new file mode 100644 index 000000000..52b26b5d3 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt @@ -0,0 +1,45 @@ +package com.yandex.div.compose.views.modifiers + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import com.yandex.div.compose.views.evaluate +import com.yandex.div.compose.views.toDp +import com.yandex.div2.DivBase +import com.yandex.div2.DivEdgeInsets + +@Composable +internal fun Modifier.apply(data: DivBase): Modifier { + var modifier = this + .width(data.width) + .height(data.height) + + data.margins?.let { + modifier = modifier.padding(it) + } + + data.background?.let { + modifier = modifier.backgrounds(it) + } + + data.paddings?.let { + modifier = modifier.padding(it) + } + + data.id?.let { + modifier = modifier.testTag(it) + } + + return modifier +} + +@Composable +private fun Modifier.padding(value: DivEdgeInsets): Modifier { + return padding( + start = (value.start ?: value.left).evaluate().toDp(), + end = (value.end ?: value.right).evaluate().toDp(), + top = value.top.evaluate().toDp(), + bottom = value.bottom.evaluate().toDp() + ) +} diff --git a/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt new file mode 100644 index 000000000..f9febd6a4 --- /dev/null +++ b/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt @@ -0,0 +1,31 @@ +package com.yandex.div.compose.views.modifiers + +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.yandex.div.compose.views.evaluate +import com.yandex.div.compose.views.toDp +import com.yandex.div2.DivSize + +@Composable +internal fun Modifier.height(height: DivSize): Modifier { + return when (height) { + is DivSize.MatchParent -> fillMaxHeight() + is DivSize.WrapContent -> wrapContentHeight() + is DivSize.Fixed -> height(height.value.value.evaluate().toDp()) + } +} + +@Composable +internal fun Modifier.width(width: DivSize): Modifier { + return when (width) { + is DivSize.MatchParent -> fillMaxWidth() + is DivSize.WrapContent -> wrapContentWidth() + is DivSize.Fixed -> width(width.value.value.evaluate().toDp()) + } +} diff --git a/client/android/gradle/libs.versions.toml b/client/android/gradle/libs.versions.toml index f704ad30e..9206ee45b 100644 --- a/client/android/gradle/libs.versions.toml +++ b/client/android/gradle/libs.versions.toml @@ -88,6 +88,7 @@ androidx-work = { module = "androidx.work:work-runtime-ktx", version.ref = "andr androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } @@ -119,6 +120,7 @@ appmetrica-plugin = { module = "io.appmetrica.analytics:gradle", version.ref = " buildTimeTracker = { module = "com.asarkar.gradle:build-time-tracker", version.ref = "buildTimeTracker" } coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } +coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-gif = { module = "io.coil-kt.coil3:coil-gif", version.ref = "coil" } coil-network = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil" } coil-network-cachecontrol = { module = "io.coil-kt.coil3:coil-network-cache-control", version.ref = "coil" } @@ -194,8 +196,9 @@ zxing-embedded = { module = "com.journeyapps:zxing-android-embedded", version = [plugins] apiGenerator = { id = "com.yandex.divkit.api-generator", version = "unspecified" } android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.17.1" } -compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin-ksp" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/client/android/settings.gradle b/client/android/settings.gradle index bb67c9169..de490d334 100644 --- a/client/android/settings.gradle +++ b/client/android/settings.gradle @@ -25,6 +25,7 @@ include ':api-generator-test' include ':assertion' include ':beacon' include ':coil' +include ':compose' include ':compose-interop' include ':div' include ':div-core' @@ -45,6 +46,7 @@ include ':div-states' include ':div-storage' include ':div-svg' include ':div-video' +include ':div-video-m3' include ':divkit-demo-app' include ':divkit-perftests' include ':divkit-regression-testing' @@ -59,7 +61,6 @@ include ':screenshot-test-runtime' include ':ui-test-common' include ':utils' include ':video-custom' -include ':div-video-m3' def internalSettings = file("settings.internal.gradle") if (internalSettings.exists()) {