Add width and height parameters to Glance @Preview
Relnote: Add width and height parameters to Glance @Preview
Bug: 325459978
Test: GlanceAppWidgetViewAdapterTest
Change-Id: Ibabe8a1ac71ff0a34c888956fc134b2fdda317d9
diff --git a/glance/glance-appwidget-preview/build.gradle b/glance/glance-appwidget-preview/build.gradle
index 5075396..b9d7f35 100644
--- a/glance/glance-appwidget-preview/build.gradle
+++ b/glance/glance-appwidget-preview/build.gradle
@@ -43,8 +43,10 @@
api(project(":glance:glance-appwidget"))
androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.kotlinTest)
androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
}
android {
diff --git a/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetPreviews.kt b/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetPreviews.kt
index 1e997e2..b81e66a 100644
--- a/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetPreviews.kt
+++ b/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetPreviews.kt
@@ -17,9 +17,11 @@
package androidx.glance.appwidget.preview
import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.glance.Button
import androidx.glance.GlanceModifier
+import androidx.glance.LocalSize
import androidx.glance.action.Action
import androidx.glance.appwidget.appWidgetBackground
import androidx.glance.layout.Alignment
@@ -42,7 +44,9 @@
.padding(16.dp)
) {
Text(
- text = "First Glance widget",
+ text = "First Glance widget, LocalSize = ${LocalSize.current.let {
+ if (it == DpSize.Unspecified) "Unspecified" else "${it.width} x ${it.height}"
+ }}",
modifier = GlanceModifier
.fillMaxWidth()
.padding(bottom = 8.dp),
diff --git a/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapterTest.kt b/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapterTest.kt
index a41f02ec..762784c 100644
--- a/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapterTest.kt
+++ b/glance/glance-appwidget-preview/src/androidTest/kotlin/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapterTest.kt
@@ -25,9 +25,12 @@
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.preview.test.R
import androidx.test.filters.MediumTest
-import org.junit.Assert.assertNotNull
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -73,9 +76,10 @@
private fun initAndInflate(
className: String,
methodName: String,
+ size: DpSize,
) {
activityTestRule.runOnUiThread {
- glanceAppWidgetViewAdapter.init(className, methodName)
+ glanceAppWidgetViewAdapter.init(className, methodName, size)
glanceAppWidgetViewAdapter.requestLayout()
}
}
@@ -98,27 +102,60 @@
"Could not find the $viewTypeName View matching $composableName"
@Test
- fun testFirstGlancePreview() {
+ fun glanceAppWidgetPreview_unspecifiedSize() {
initAndInflate(
- "androidx.glance.appwidget.preview.GlanceAppWidgetPreviewsKt",
- "FirstGlancePreview")
+ className = "androidx.glance.appwidget.preview.GlanceAppWidgetPreviewsKt",
+ methodName = "FirstGlancePreview",
+ size = DpSize.Unspecified
+ )
activityTestRule.runOnUiThread {
val rootComposable = glanceAppWidgetViewAdapter.getChildAt(0) as ViewGroup
val linearLayoutColumn = rootComposable.getChildOfType<LinearLayout>()
- assertNotNull(viewNotFoundMsg("LinearLayout", "Column"), linearLayoutColumn)
- val textView = linearLayoutColumn!!.getChildOfType<TextView>()
- assertNotNull(viewNotFoundMsg("TextView", "Text"), textView)
+ assertNotNull(linearLayoutColumn, viewNotFoundMsg("LinearLayout", "Column"))
+ val textView = linearLayoutColumn.getChildOfType<TextView>()
+ assertNotNull(textView, viewNotFoundMsg("TextView", "Text"))
+ assertThat(textView.text.toString())
+ .isEqualTo("First Glance widget, LocalSize = Unspecified")
val linearLayoutRow = linearLayoutColumn.getChildOfType<LinearLayout>()
- assertNotNull(viewNotFoundMsg("LinearLayout", "Row"), linearLayoutRow)
+ assertNotNull(linearLayoutRow, viewNotFoundMsg("LinearLayout", "Row"))
// Backport button are implemented using FrameLayout and depending on the API version
// Button might be wrapped in the RelativeLayout.
- val button1 = linearLayoutRow!!.getChildOfType<Button>()
+ val button1 = linearLayoutRow.getChildOfType<Button>()
?: linearLayoutRow.getChildOfType<RelativeLayout>()!!.getChildOfType<FrameLayout>()
val button2 = linearLayoutRow.getChildOfType<Button>(1)
?: linearLayoutRow.getChildOfType<RelativeLayout>(1)!!.getChildOfType<FrameLayout>()
- assertNotNull(viewNotFoundMsg("FrameLayout", "Button"), button1)
- assertNotNull(viewNotFoundMsg("FrameLayout", "Button"), button2)
+ assertNotNull(button1, viewNotFoundMsg("FrameLayout", "Button"))
+ assertNotNull(button2, viewNotFoundMsg("FrameLayout", "Button"))
+ }
+ }
+
+ @Test
+ fun glanceAppWidgetPreview_withSize() {
+ initAndInflate(
+ className = "androidx.glance.appwidget.preview.GlanceAppWidgetPreviewsKt",
+ methodName = "FirstGlancePreview",
+ size = DpSize(Dp(123.0f), Dp(456.0f))
+ )
+
+ activityTestRule.runOnUiThread {
+ val rootComposable = glanceAppWidgetViewAdapter.getChildAt(0) as ViewGroup
+ val linearLayoutColumn = rootComposable.getChildOfType<LinearLayout>()
+ assertNotNull(linearLayoutColumn, viewNotFoundMsg("LinearLayout", "Column"))
+ val textView = linearLayoutColumn.getChildOfType<TextView>()
+ assertNotNull(textView, viewNotFoundMsg("TextView", "Text"))
+ assertThat(textView.text.toString())
+ .isEqualTo("First Glance widget, LocalSize = 123.0.dp x 456.0.dp")
+ val linearLayoutRow = linearLayoutColumn.getChildOfType<LinearLayout>()
+ assertNotNull(linearLayoutRow, viewNotFoundMsg("LinearLayout", "Row"))
+ // Backport button are implemented using FrameLayout and depending on the API version
+ // Button might be wrapped in the RelativeLayout.
+ val button1 = linearLayoutRow.getChildOfType<Button>()
+ ?: linearLayoutRow.getChildOfType<RelativeLayout>()!!.getChildOfType<FrameLayout>()
+ val button2 = linearLayoutRow.getChildOfType<Button>(1)
+ ?: linearLayoutRow.getChildOfType<RelativeLayout>(1)!!.getChildOfType<FrameLayout>()
+ assertNotNull(button1, viewNotFoundMsg("FrameLayout", "Button"))
+ assertNotNull(button2, viewNotFoundMsg("FrameLayout", "Button"))
}
}
diff --git a/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapter.kt b/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapter.kt
index d484f5f..a2fd62b 100644
--- a/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapter.kt
+++ b/glance/glance-appwidget-preview/src/main/java/androidx/glance/appwidget/preview/GlanceAppWidgetViewAdapter.kt
@@ -21,6 +21,7 @@
import android.util.AttributeSet
import androidx.compose.runtime.Composable
import androidx.compose.runtime.currentComposer
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi
import androidx.glance.appwidget.GlanceRemoteViews
@@ -28,6 +29,7 @@
import kotlinx.coroutines.runBlocking
private const val TOOLS_NS_URI = "http://schemas.android.com/tools"
+private const val ANDROID_NS_URI = "http://schemas.android.com/apk/res/android"
/**
* View adapter that renders a glance `@Composable`. The `@Composable` is found by reading the
@@ -51,6 +53,7 @@
internal fun init(
className: String,
methodName: String,
+ size: DpSize,
) {
val content = @Composable {
val composer = currentComposer
@@ -63,7 +66,7 @@
val remoteViews = runBlocking {
GlanceRemoteViews().compose(
context = context,
- size = DpSize.Unspecified,
+ size = size,
content = content).remoteViews
}
val view = remoteViews.apply(context, this)
@@ -75,6 +78,13 @@
val className = composableName.substringBeforeLast('.')
val methodName = composableName.substringAfterLast('.')
- init(className, methodName)
+ val width = attrs.getAttributeValue(ANDROID_NS_URI, "layout_width")
+ ?.removeSuffix("dp")?.toFloatOrNull()
+ val height = attrs.getAttributeValue(ANDROID_NS_URI, "layout_height")
+ ?.removeSuffix("dp")?.toFloatOrNull()
+ var size = DpSize.Unspecified
+ if (width != null && height != null) size = DpSize(Dp(width), Dp(height))
+
+ init(className, methodName, size)
}
}
diff --git a/glance/glance-preview/api/current.txt b/glance/glance-preview/api/current.txt
index fc59a6d..acbb7f5 100644
--- a/glance/glance-preview/api/current.txt
+++ b/glance/glance-preview/api/current.txt
@@ -4,12 +4,16 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlancePreviewApi {
}
- @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Preview {
+ @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Preview {
+ method public abstract int heightDp() default -1;
method public abstract String surface();
+ method public abstract int widthDp() default -1;
+ property public abstract int heightDp;
property public abstract String surface;
+ property public abstract int widthDp;
}
- @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface Preview.Container {
+ @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface Preview.Container {
method public abstract androidx.glance.preview.Preview[] value();
}
diff --git a/glance/glance-preview/api/restricted_current.txt b/glance/glance-preview/api/restricted_current.txt
index fc59a6d..acbb7f5 100644
--- a/glance/glance-preview/api/restricted_current.txt
+++ b/glance/glance-preview/api/restricted_current.txt
@@ -4,12 +4,16 @@
@SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlancePreviewApi {
}
- @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Preview {
+ @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface Preview {
+ method public abstract int heightDp() default -1;
method public abstract String surface();
+ method public abstract int widthDp() default -1;
+ property public abstract int heightDp;
property public abstract String surface;
+ property public abstract int widthDp;
}
- @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface Preview.Container {
+ @SuppressCompatibility @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public static @interface Preview.Container {
method public abstract androidx.glance.preview.Preview[] value();
}
diff --git a/glance/glance-preview/src/main/java/androidx/glance/preview/Preview.kt b/glance/glance-preview/src/main/java/androidx/glance/preview/Preview.kt
index fb20c35..beb418ae 100644
--- a/glance/glance-preview/src/main/java/androidx/glance/preview/Preview.kt
+++ b/glance/glance-preview/src/main/java/androidx/glance/preview/Preview.kt
@@ -17,13 +17,29 @@
package androidx.glance.preview
/**
- * The annotation that marks glance components (functions) that should have visual preview.
+ * The annotation to be used on a Glance composable for displaying visual previews in Android
+ * Studio.
+ *
+ * The [widthDp] and [heightDp] parameters correspond to the size of the widget, i.e. the size
+ * available in the LocalSize composition local. When [widthDp] and [heightDp] aren't specified,
+ * the visual preview wraps its content. In this case, LocalSize should not be read within the
+ * composable.
+ *
+ * @param surface indicates which of the possible glance [Surfaces] to use in this preview.
+ * @param widthDp width in DP that will be used when rendering the annotated Glance @[Composable]
+ * and that will be set as the widget's LocalSize width.
+ * @param heightDp height in DP that will be used when rendering the annotated Glance @[Composable]
+ * and that will be set as the widget's LocalSize height.
*/
-@Retention(AnnotationRetention.SOURCE)
+@Retention(AnnotationRetention.BINARY)
@Target(
AnnotationTarget.ANNOTATION_CLASS,
AnnotationTarget.FUNCTION
)
@ExperimentalGlancePreviewApi
@Repeatable
-annotation class Preview(@Surface val surface: String)
+annotation class Preview(
+ @Surface val surface: String,
+ val widthDp: Int = -1,
+ val heightDp: Int = -1
+)