Add initial composition translator, with tests, for Wear.
Relnote: n/a
Test: included (./gradlew :glance:glance-wear:test)
Change-Id: Ibe56089d8230f627ba4c7f3a64fb48273087f8b6
diff --git a/glance/glance-wear/api/current.txt b/glance/glance-wear/api/current.txt
index e6f50d0..93c7b08 100644
--- a/glance/glance-wear/api/current.txt
+++ b/glance/glance-wear/api/current.txt
@@ -1 +1,8 @@
// Signature format: 4.0
+package androidx.glance.wear {
+
+ public final class WearCompositionTranslatorKt {
+ }
+
+}
+
diff --git a/glance/glance-wear/api/public_plus_experimental_current.txt b/glance/glance-wear/api/public_plus_experimental_current.txt
index e6f50d0..8544895 100644
--- a/glance/glance-wear/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.glance.wear {
+
+ public final class WearCompositionTranslatorKt {
+ method @androidx.glance.GlanceInternalApi public static androidx.wear.tiles.LayoutElementBuilders.LayoutElement translateComposition(androidx.glance.Emittable element);
+ }
+
+}
+
diff --git a/glance/glance-wear/api/restricted_current.txt b/glance/glance-wear/api/restricted_current.txt
index e6f50d0..93c7b08 100644
--- a/glance/glance-wear/api/restricted_current.txt
+++ b/glance/glance-wear/api/restricted_current.txt
@@ -1 +1,8 @@
// Signature format: 4.0
+package androidx.glance.wear {
+
+ public final class WearCompositionTranslatorKt {
+ }
+
+}
+
diff --git a/glance/glance-wear/build.gradle b/glance/glance-wear/build.gradle
index c831070..335d2ea 100644
--- a/glance/glance-wear/build.gradle
+++ b/glance/glance-wear/build.gradle
@@ -33,18 +33,21 @@
kotlinPlugin(project(":compose:compiler:compiler"))
api(project(":glance:glance"))
-
+ api(project(":wear:tiles:tiles"))
+
implementation(libs.kotlinStdlib)
implementation(project(":compose:runtime:runtime"))
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
+ testImplementation(libs.truth)
testImplementation(libs.junit)
+ testImplementation(libs.kotlinCoroutinesTest)
}
android {
defaultConfig {
- minSdkVersion 23
+ minSdkVersion 26
}
// Use Robolectric 4.+
testOptions.unitTests.includeAndroidResources = true
diff --git a/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
new file mode 100644
index 0000000..e24f14c
--- /dev/null
+++ b/glance/glance-wear/src/androidMain/kotlin/androidx/glance/wear/WearCompositionTranslator.kt
@@ -0,0 +1,91 @@
+@file:OptIn(GlanceInternalApi::class)
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.wear
+
+import androidx.glance.Emittable
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.EmittableBox
+import androidx.glance.layout.PaddingModifier
+import androidx.wear.tiles.DimensionBuilders.dp
+import androidx.wear.tiles.LayoutElementBuilders
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
+import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
+import androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
+import androidx.wear.tiles.ModifiersBuilders
+import java.lang.IllegalArgumentException
+
+@VerticalAlignment private fun Alignment.Vertical.toProto(): Int =
+ when (this) {
+ Alignment.Vertical.Top -> VERTICAL_ALIGN_TOP
+ Alignment.Vertical.CenterVertically -> VERTICAL_ALIGN_CENTER
+ Alignment.Vertical.Bottom -> VERTICAL_ALIGN_BOTTOM
+ else -> throw IllegalArgumentException("Unknown vertical alignment type $this")
+ }
+
+@HorizontalAlignment private fun Alignment.Horizontal.toProto(): Int =
+ when (this) {
+ Alignment.Horizontal.Start -> HORIZONTAL_ALIGN_START
+ Alignment.Horizontal.CenterHorizontally -> HORIZONTAL_ALIGN_CENTER
+ Alignment.Horizontal.End -> HORIZONTAL_ALIGN_END
+ else -> throw IllegalArgumentException("Unknown horizontal alignment type $this")
+ }
+
+private fun PaddingModifier.toProto(): ModifiersBuilders.Padding =
+ ModifiersBuilders.Padding.Builder()
+ .setStart(dp(this.start.value))
+ .setTop(dp(this.top.value))
+ .setEnd(dp(this.end.value))
+ .setBottom(dp(this.bottom.value))
+ .setRtlAware(this.rtlAware)
+ .build()
+
+private fun translateEmittableBox(element: EmittableBox) = LayoutElementBuilders.Box.Builder()
+ .setVerticalAlignment(element.contentAlignment.vertical.toProto())
+ .setHorizontalAlignment(element.contentAlignment.horizontal.toProto())
+ .setModifiers(translateModifiers(element.modifier))
+ .also { box -> element.children.forEach { box.addContent(translateComposition(it)) } }
+ .build()
+
+private fun translateModifiers(modifier: Modifier): ModifiersBuilders.Modifiers = modifier
+ .foldOut(ModifiersBuilders.Modifiers.Builder()) { element, builder ->
+ when (element) {
+ is PaddingModifier -> builder.setPadding(element.toProto())
+ else -> throw IllegalArgumentException("Unknown modifier type")
+ }
+ }.build()
+
+/**
+ * Translates a Glance Composition to a Wear Tile.
+ *
+ * @throws IllegalArgumentException If the provided Emittable is not recognised (e.g. it is an
+ * element which this translator doesn't understand).
+ */
+@GlanceInternalApi
+public fun translateComposition(element: Emittable): LayoutElementBuilders.LayoutElement {
+ return when (element) {
+ is EmittableBox -> translateEmittableBox(element)
+ else -> throw IllegalArgumentException("Unknown element $element")
+ }
+}
\ No newline at end of file
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt
new file mode 100644
index 0000000..857465b
--- /dev/null
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/Utils.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.wear
+
+import androidx.compose.runtime.BroadcastFrameClock
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
+import androidx.compose.runtime.Recomposer
+import androidx.glance.Applier
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.EmittableBox
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.launch
+
+inline fun <reified T> Modifier.findModifier(): T? = this.foldOut<T?>(null) { cur, acc ->
+ if (cur is T) {
+ cur
+ } else {
+ acc
+ }
+}
+
+@OptIn(GlanceInternalApi::class)
+suspend fun runTestingComposition(content: @Composable () -> Unit): EmittableBox = coroutineScope {
+ val root = EmittableBox()
+ val applier = Applier(root)
+ val recomposer = Recomposer(currentCoroutineContext())
+ val composition = Composition(applier, recomposer)
+ val frameClock = BroadcastFrameClock()
+
+ composition.setContent { content() }
+
+ launch(frameClock) { recomposer.runRecomposeAndApplyChanges() }
+
+ recomposer.close()
+ recomposer.join()
+
+ root
+}
diff --git a/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
new file mode 100644
index 0000000..68e07bf
--- /dev/null
+++ b/glance/glance-wear/src/test/kotlin/androidx/glance/wear/WearCompositionTranslatorTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.wear
+
+import androidx.compose.runtime.Composable
+import androidx.glance.GlanceInternalApi
+import androidx.glance.Modifier
+import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
+import androidx.glance.layout.padding
+import androidx.glance.unit.dp
+import androidx.wear.tiles.LayoutElementBuilders
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
+import androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(GlanceInternalApi::class, ExperimentalCoroutinesApi::class)
+class WearCompositionTranslatorTest {
+ private lateinit var fakeCoroutineScope: TestCoroutineScope
+
+ @Before
+ fun setUp() {
+ fakeCoroutineScope = TestCoroutineScope()
+ }
+
+ @Test
+ fun canTranslateBox() = fakeCoroutineScope.runBlockingTest {
+ val content = runAndTranslate {
+ Box {}
+ }
+
+ // runAndTranslate wraps the result in a Box...ensure that the layout generated two Boxes
+ val outerBox = content as LayoutElementBuilders.Box
+ assertThat(outerBox.contents).hasSize(1)
+
+ assertThat(outerBox.contents[0]).isInstanceOf(LayoutElementBuilders.Box::class.java)
+ }
+
+ @Test
+ fun canTranslateAlignment() = fakeCoroutineScope.runBlockingTest {
+ val content = runAndTranslate {
+ Box(contentAlignment = Alignment.Center) {}
+ }
+
+ val innerBox =
+ (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+
+ assertThat(innerBox.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_CENTER)
+ assertThat(innerBox.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_CENTER)
+ }
+
+ @Test
+ fun canTranslateBoxWithChildren() = fakeCoroutineScope.runBlockingTest {
+ val content = runAndTranslate {
+ Box {
+ Box(contentAlignment = Alignment.TopCenter) {}
+ Box(contentAlignment = Alignment.BottomEnd) {}
+ }
+ }
+
+ val innerBox =
+ (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+ val leaf0 = innerBox.contents[0] as LayoutElementBuilders.Box
+ val leaf1 = innerBox.contents[1] as LayoutElementBuilders.Box
+
+ assertThat(leaf0.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_TOP)
+ assertThat(leaf0.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_CENTER)
+
+ assertThat(leaf1.verticalAlignment!!.value).isEqualTo(VERTICAL_ALIGN_BOTTOM)
+ assertThat(leaf1.horizontalAlignment!!.value).isEqualTo(HORIZONTAL_ALIGN_END)
+ }
+
+ @Test
+ fun canTranslatePaddingModifier() = fakeCoroutineScope.runBlockingTest {
+ val content = runAndTranslate {
+ Box(modifier = Modifier.padding(start = 1.dp, top = 2.dp, end = 3.dp, bottom = 4.dp)) {}
+ }
+
+ val innerBox =
+ (content as LayoutElementBuilders.Box).contents[0] as LayoutElementBuilders.Box
+ val padding = requireNotNull(innerBox.modifiers!!.padding)
+
+ assertThat(padding.start!!.value).isEqualTo(1f)
+ assertThat(padding.top!!.value).isEqualTo(2f)
+ assertThat(padding.end!!.value).isEqualTo(3f)
+ assertThat(padding.bottom!!.value).isEqualTo(4f)
+ }
+
+ private suspend fun runAndTranslate(
+ content: @Composable () -> Unit
+ ): LayoutElementBuilders.LayoutElement {
+ val root = runTestingComposition(content)
+
+ return translateComposition(root)
+ }
+}
\ No newline at end of file