1 /*
<lambda>null2 * Copyright 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 @file:Suppress("deprecation")
17
18 package androidx.glance.wear.tiles
19
20 import android.content.Context
21 import android.graphics.Bitmap
22 import android.util.Log
23 import android.view.View
24 import android.view.ViewGroup
25 import androidx.compose.ui.graphics.toArgb
26 import androidx.compose.ui.unit.dp
27 import androidx.glance.AndroidResourceImageProvider
28 import androidx.glance.BackgroundModifier
29 import androidx.glance.BitmapImageProvider
30 import androidx.glance.Emittable
31 import androidx.glance.EmittableButton
32 import androidx.glance.EmittableImage
33 import androidx.glance.GlanceModifier
34 import androidx.glance.TintColorFilterParams
35 import androidx.glance.VisibilityModifier
36 import androidx.glance.action.Action
37 import androidx.glance.action.ActionModifier
38 import androidx.glance.action.LambdaAction
39 import androidx.glance.action.StartActivityAction
40 import androidx.glance.action.StartActivityClassAction
41 import androidx.glance.action.StartActivityComponentAction
42 import androidx.glance.findModifier
43 import androidx.glance.layout.Alignment
44 import androidx.glance.layout.ContentScale
45 import androidx.glance.layout.EmittableBox
46 import androidx.glance.layout.EmittableColumn
47 import androidx.glance.layout.EmittableRow
48 import androidx.glance.layout.EmittableSpacer
49 import androidx.glance.layout.HeightModifier
50 import androidx.glance.layout.PaddingInDp
51 import androidx.glance.layout.PaddingModifier
52 import androidx.glance.layout.WidthModifier
53 import androidx.glance.layout.collectPaddingInDp
54 import androidx.glance.semantics.SemanticsModifier
55 import androidx.glance.semantics.SemanticsProperties
56 import androidx.glance.text.EmittableText
57 import androidx.glance.text.FontStyle
58 import androidx.glance.text.FontWeight
59 import androidx.glance.text.TextAlign
60 import androidx.glance.text.TextDecoration
61 import androidx.glance.text.TextStyle
62 import androidx.glance.toEmittableText
63 import androidx.glance.unit.ColorProvider
64 import androidx.glance.unit.Dimension
65 import androidx.glance.wear.tiles.action.RunCallbackAction
66 import androidx.glance.wear.tiles.curved.ActionCurvedModifier
67 import androidx.glance.wear.tiles.curved.AnchorType
68 import androidx.glance.wear.tiles.curved.CurvedTextStyle
69 import androidx.glance.wear.tiles.curved.EmittableCurvedChild
70 import androidx.glance.wear.tiles.curved.EmittableCurvedLine
71 import androidx.glance.wear.tiles.curved.EmittableCurvedRow
72 import androidx.glance.wear.tiles.curved.EmittableCurvedSpacer
73 import androidx.glance.wear.tiles.curved.EmittableCurvedText
74 import androidx.glance.wear.tiles.curved.GlanceCurvedModifier
75 import androidx.glance.wear.tiles.curved.RadialAlignment
76 import androidx.glance.wear.tiles.curved.SemanticsCurvedModifier
77 import androidx.glance.wear.tiles.curved.SweepAngleModifier
78 import androidx.glance.wear.tiles.curved.ThicknessModifier
79 import androidx.glance.wear.tiles.curved.findModifier
80 import androidx.wear.tiles.ColorBuilders
81 import java.io.ByteArrayOutputStream
82 import java.util.Arrays
83
84 internal const val GlanceWearTileTag = "GlanceWearTile"
85
86 @Suppress("deprecation") // for backward compatibility
87 @androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
88 private fun Alignment.Vertical.toProto(): Int =
89 when (this) {
90 Alignment.Vertical.Top -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
91 Alignment.Vertical.CenterVertically ->
92 androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
93 Alignment.Vertical.Bottom -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
94 else -> {
95 Log.w(GlanceWearTileTag, "Unknown vertical alignment type $this, align to Top instead")
96 androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
97 }
98 }
99
100 @Suppress("deprecation") // for backward compatibility
101 @androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
Alignmentnull102 private fun Alignment.Horizontal.toProto(): Int =
103 when (this) {
104 Alignment.Horizontal.Start ->
105 androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
106 Alignment.Horizontal.CenterHorizontally ->
107 androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
108 Alignment.Horizontal.End -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
109 else -> {
110 Log.w(
111 GlanceWearTileTag,
112 "Unknown horizontal alignment type $this, align to Start instead"
113 )
114 androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
115 }
116 }
117
118 @Suppress("deprecation") // for backward compatibility
toProtonull119 private fun PaddingInDp.toProto(): androidx.wear.tiles.ModifiersBuilders.Padding =
120 androidx.wear.tiles.ModifiersBuilders.Padding.Builder()
121 .setStart(androidx.wear.tiles.DimensionBuilders.dp(start.value))
122 .setTop(androidx.wear.tiles.DimensionBuilders.dp(top.value))
123 .setEnd(androidx.wear.tiles.DimensionBuilders.dp(end.value))
124 .setBottom((androidx.wear.tiles.DimensionBuilders.dp(bottom.value)))
125 .setRtlAware(true)
126 .build()
127
128 @Suppress("deprecation") // for backward compatibility
129 private fun BackgroundModifier.toProto(
130 context: Context
131 ): androidx.wear.tiles.ModifiersBuilders.Background? =
132 when (this) {
133 is BackgroundModifier.Color ->
134 androidx.wear.tiles.ModifiersBuilders.Background.Builder()
135 .setColor(ColorBuilders.argb(this.colorProvider.getColorAsArgb(context)))
136 .build()
137 else -> error("Unexpected modifier $this")
138 }
139
140 @Suppress("deprecation") // for backward compatibility
toProtonull141 private fun BorderModifier.toProto(context: Context): androidx.wear.tiles.ModifiersBuilders.Border =
142 androidx.wear.tiles.ModifiersBuilders.Border.Builder()
143 .setWidth(
144 androidx.wear.tiles.DimensionBuilders.dp(this.width.toDp(context.resources).value)
145 )
146 .setColor(androidx.wear.tiles.ColorBuilders.argb(this.color.getColorAsArgb(context)))
147 .build()
148
149 @Suppress("deprecation") // for backward compatibility
150 private fun SemanticsModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
151 this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
152 androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
153 .setContentDescription(it.joinToString())
154 .build()
155 }
156
157 @Suppress("deprecation") // for backward compatibility
toProtonull158 private fun SemanticsCurvedModifier.toProto(): androidx.wear.tiles.ModifiersBuilders.Semantics? =
159 this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
160 androidx.wear.tiles.ModifiersBuilders.Semantics.Builder()
161 .setContentDescription(it.joinToString())
162 .build()
163 }
164
165 @Suppress("deprecation") // for backward compatibility
ColorProvidernull166 private fun ColorProvider.getColorAsArgb(context: Context) = getColor(context).toArgb()
167
168 // TODO: handle parameters
169 @Suppress("deprecation") // for backward compatibility
170 private fun StartActivityAction.toProto(
171 context: Context
172 ): androidx.wear.tiles.ActionBuilders.LaunchAction =
173 androidx.wear.tiles.ActionBuilders.LaunchAction.Builder()
174 .setAndroidActivity(
175 androidx.wear.tiles.ActionBuilders.AndroidActivity.Builder()
176 .setPackageName(
177 when (this) {
178 is StartActivityComponentAction -> componentName.packageName
179 is StartActivityClassAction -> context.packageName
180 else -> error("Action type not defined in wear package: $this")
181 }
182 )
183 .setClassName(
184 when (this) {
185 is StartActivityComponentAction -> componentName.className
186 is StartActivityClassAction -> activityClass.name
187 else -> error("Action type not defined in wear package: $this")
188 }
189 )
190 .build()
191 )
192 .build()
193
194 @Suppress("deprecation") // for backward compatibility
Actionnull195 private fun Action.toClickable(context: Context): androidx.wear.tiles.ModifiersBuilders.Clickable {
196 val builder = androidx.wear.tiles.ModifiersBuilders.Clickable.Builder()
197
198 when (this) {
199 is StartActivityAction -> {
200 builder.setOnClick(toProto(context))
201 }
202 is RunCallbackAction -> {
203 builder
204 .setOnClick(androidx.wear.tiles.ActionBuilders.LoadAction.Builder().build())
205 .setId(callbackClass.canonicalName!!)
206 }
207 is LambdaAction -> {
208 Log.e(
209 GlanceWearTileTag,
210 "Lambda actions are not currently supported on Wear Tiles. Use " +
211 "actionRunCallback actions instead."
212 )
213 }
214 else -> {
215 Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
216 }
217 }
218
219 return builder.build()
220 }
221
222 @Suppress("deprecation") // for backward compatibility
toProtonull223 private fun ActionModifier.toProto(
224 context: Context
225 ): androidx.wear.tiles.ModifiersBuilders.Clickable = this.action.toClickable(context)
226
227 @Suppress("deprecation") // for backward compatibility
228 private fun ActionCurvedModifier.toProto(
229 context: Context
230 ): androidx.wear.tiles.ModifiersBuilders.Clickable = this.action.toClickable(context)
231
232 @Suppress("deprecation") // for backward compatibility
233 private fun Dimension.toContainerDimension():
234 androidx.wear.tiles.DimensionBuilders.ContainerDimension =
235 when (this) {
236 is Dimension.Wrap -> androidx.wear.tiles.DimensionBuilders.wrap()
237 is Dimension.Expand -> androidx.wear.tiles.DimensionBuilders.expand()
238 is Dimension.Fill -> androidx.wear.tiles.DimensionBuilders.expand()
239 is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
240 else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
241 }
242
243 @Suppress("deprecation") // for backward compatibility
244 @androidx.wear.tiles.LayoutElementBuilders.ArcAnchorType
toProtonull245 private fun AnchorType.toProto(): Int =
246 when (this) {
247 AnchorType.Start -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_START
248 AnchorType.Center -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_CENTER
249 AnchorType.End -> androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_END
250 else -> {
251 Log.w(GlanceWearTileTag, "Unknown arc anchor type $this, anchor to center instead")
252 androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_CENTER
253 }
254 }
255
256 @Suppress("deprecation") // for backward compatibility
257 @androidx.wear.tiles.LayoutElementBuilders.VerticalAlignment
toProtonull258 private fun RadialAlignment.toProto(): Int =
259 when (this) {
260 RadialAlignment.Outer -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_TOP
261 RadialAlignment.Center -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
262 RadialAlignment.Inner -> androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_BOTTOM
263 else -> {
264 Log.w(GlanceWearTileTag, "Unknown radial alignment $this, align to center instead")
265 androidx.wear.tiles.LayoutElementBuilders.VERTICAL_ALIGN_CENTER
266 }
267 }
268
269 @Suppress("deprecation") // for backward compatibility
270 @androidx.wear.tiles.LayoutElementBuilders.TextAlignment
TextAlignnull271 private fun TextAlign.toTextAlignment(isRtl: Boolean): Int =
272 when (this) {
273 TextAlign.Center -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_CENTER
274 TextAlign.End -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
275 TextAlign.Left ->
276 if (isRtl) androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
277 else androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
278 TextAlign.Right ->
279 if (isRtl) androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
280 else androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
281 TextAlign.Start -> androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
282 else -> {
283 Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
284 androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
285 }
286 }
287
288 @Suppress("deprecation") // for backward compatibility
289 @androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
TextAlignnull290 private fun TextAlign.toHorizontalAlignment(): Int =
291 when (this) {
292 TextAlign.Center -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
293 TextAlign.End -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
294 TextAlign.Left -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_LEFT
295 TextAlign.Right -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_RIGHT
296 TextAlign.Start -> androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
297 else -> {
298 Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
299 androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
300 }
301 }
302
resolvenull303 private fun Dimension.resolve(context: Context): Dimension {
304 if (this !is Dimension.Resource) return this
305 val sizePx = context.resources.getDimension(res)
306 return when (sizePx.toInt()) {
307 ViewGroup.LayoutParams.MATCH_PARENT -> Dimension.Fill
308 ViewGroup.LayoutParams.WRAP_CONTENT -> Dimension.Wrap
309 else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
310 }
311 }
312
GlanceModifiernull313 private fun GlanceModifier.getWidth(
314 context: Context,
315 default: Dimension = Dimension.Wrap
316 ): Dimension = findModifier<WidthModifier>()?.width?.resolve(context) ?: default
317
318 private fun GlanceModifier.getHeight(
319 context: Context,
320 default: Dimension = Dimension.Wrap
321 ): Dimension = findModifier<HeightModifier>()?.height?.resolve(context) ?: default
322
323 @Suppress("deprecation") // for backward compatibility
324 private fun translateEmittableBox(
325 context: Context,
326 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
327 element: EmittableBox
328 ) =
329 androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
330 .setVerticalAlignment(element.contentAlignment.vertical.toProto())
331 .setHorizontalAlignment(element.contentAlignment.horizontal.toProto())
332 .setModifiers(translateModifiers(context, element.modifier))
333 .setWidth(element.modifier.getWidth(context).toContainerDimension())
334 .setHeight(element.modifier.getHeight(context).toContainerDimension())
335 .also { box ->
336 element.children.forEach {
337 box.addContent(translateComposition(context, resourceBuilder, it))
338 }
339 }
340 .build()
341
342 @Suppress("deprecation") // for backward compatibility
translateEmittableRownull343 private fun translateEmittableRow(
344 context: Context,
345 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
346 element: EmittableRow
347 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
348 val width = element.modifier.getWidth(context)
349 val height = element.modifier.getHeight(context)
350
351 val baseRowBuilder =
352 androidx.wear.tiles.LayoutElementBuilders.Row.Builder()
353 .setHeight(height.toContainerDimension())
354 .setVerticalAlignment(element.verticalAlignment.toProto())
355 .also { row ->
356 element.children.forEach {
357 row.addContent(translateComposition(context, resourceBuilder, it))
358 }
359 }
360
361 // Do we need to wrap it in a column to set the horizontal alignment?
362 return if (
363 element.horizontalAlignment != Alignment.Horizontal.Start && width !is Dimension.Wrap
364 ) {
365 androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
366 .setHorizontalAlignment(element.horizontalAlignment.toProto())
367 .setModifiers(translateModifiers(context, element.modifier))
368 .setWidth(width.toContainerDimension())
369 .setHeight(height.toContainerDimension())
370 .addContent(
371 baseRowBuilder.setWidth(androidx.wear.tiles.DimensionBuilders.wrap()).build()
372 )
373 .build()
374 } else {
375 baseRowBuilder
376 .setModifiers(translateModifiers(context, element.modifier))
377 .setWidth(width.toContainerDimension())
378 .build()
379 }
380 }
381
382 @Suppress("deprecation") // for backward compatibility
translateEmittableColumnnull383 private fun translateEmittableColumn(
384 context: Context,
385 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
386 element: EmittableColumn
387 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
388 val width = element.modifier.getWidth(context)
389 val height = element.modifier.getHeight(context)
390
391 val baseColumnBuilder =
392 androidx.wear.tiles.LayoutElementBuilders.Column.Builder()
393 .setWidth(width.toContainerDimension())
394 .setHorizontalAlignment(element.horizontalAlignment.toProto())
395 .also { column ->
396 element.children.forEach {
397 column.addContent(translateComposition(context, resourceBuilder, it))
398 }
399 }
400
401 // Do we need to wrap it in a row to set the vertical alignment?
402 return if (element.verticalAlignment != Alignment.Vertical.Top && height !is Dimension.Wrap) {
403 androidx.wear.tiles.LayoutElementBuilders.Row.Builder()
404 .setVerticalAlignment(element.verticalAlignment.toProto())
405 .setModifiers(translateModifiers(context, element.modifier))
406 .setWidth(width.toContainerDimension())
407 .setHeight(height.toContainerDimension())
408 .addContent(
409 baseColumnBuilder.setHeight(androidx.wear.tiles.DimensionBuilders.wrap()).build()
410 )
411 .build()
412 } else {
413 baseColumnBuilder
414 .setModifiers(translateModifiers(context, element.modifier))
415 .setHeight(height.toContainerDimension())
416 .build()
417 }
418 }
419
420 @Suppress("deprecation") // for backward compatibility
translateTextStylenull421 private fun translateTextStyle(
422 context: Context,
423 style: TextStyle,
424 ): androidx.wear.tiles.LayoutElementBuilders.FontStyle {
425 val fontStyleBuilder = androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder()
426
427 style.color.let {
428 fontStyleBuilder.setColor(
429 androidx.wear.tiles.ColorBuilders.argb(it.getColorAsArgb(context))
430 )
431 }
432 // TODO(b/203656358): Can we support Em here too?
433 style.fontSize?.let {
434 if (!it.isSp) {
435 throw IllegalArgumentException("Only Sp is supported for font size")
436 }
437 fontStyleBuilder.setSize(androidx.wear.tiles.DimensionBuilders.sp(it.value))
438 }
439 style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
440 style.fontWeight?.let {
441 fontStyleBuilder.setWeight(
442 when (it) {
443 FontWeight.Normal -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
444 FontWeight.Medium -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
445 FontWeight.Bold -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
446 else -> {
447 Log.w(GlanceWearTileTag, "Unknown font weight $it, use Normal weight instead")
448 androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
449 }
450 }
451 )
452 }
453 style.textDecoration?.let { fontStyleBuilder.setUnderline(TextDecoration.Underline in it) }
454
455 return fontStyleBuilder.build()
456 }
457
458 @Suppress("deprecation") // for backward compatibility
translateTextStylenull459 private fun translateTextStyle(
460 context: Context,
461 style: CurvedTextStyle,
462 ): androidx.wear.tiles.LayoutElementBuilders.FontStyle {
463 val fontStyleBuilder = androidx.wear.tiles.LayoutElementBuilders.FontStyle.Builder()
464
465 style.color?.let {
466 fontStyleBuilder.setColor(
467 androidx.wear.tiles.ColorBuilders.argb(it.getColorAsArgb(context))
468 )
469 }
470 style.fontSize?.let {
471 fontStyleBuilder.setSize(androidx.wear.tiles.DimensionBuilders.sp(it.value))
472 }
473 style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
474 style.fontWeight?.let {
475 fontStyleBuilder.setWeight(
476 when (it) {
477 FontWeight.Normal -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
478 FontWeight.Medium -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
479 FontWeight.Bold -> androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
480 else -> {
481 Log.w(GlanceWearTileTag, "Unknown font weight $it, use Normal weight instead")
482 androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
483 }
484 }
485 )
486 }
487
488 return fontStyleBuilder.build()
489 }
490
491 @Suppress("deprecation") // for backward compatibility
translateEmittableTextnull492 private fun translateEmittableText(
493 context: Context,
494 element: EmittableText
495 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
496 // Does it have a width or height set? If so, we need to wrap it in a Box.
497 val width = element.modifier.getWidth(context)
498 val height = element.modifier.getHeight(context)
499
500 val textBuilder =
501 androidx.wear.tiles.LayoutElementBuilders.Text.Builder()
502 .setText(element.text)
503 .setMaxLines(element.maxLines)
504
505 element.style?.let { textBuilder.setFontStyle(translateTextStyle(context, it)) }
506
507 val textAlign: TextAlign? = element.style?.textAlign
508 if (textAlign != null) {
509 val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
510 textBuilder.setMultilineAlignment(textAlign.toTextAlignment(isRtl))
511 }
512
513 return if (width !is Dimension.Wrap || height !is Dimension.Wrap) {
514 val boxBuilder = androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
515 if (textAlign != null) {
516 boxBuilder.setHorizontalAlignment(textAlign.toHorizontalAlignment())
517 }
518 boxBuilder
519 .setWidth(width.toContainerDimension())
520 .setHeight(height.toContainerDimension())
521 .setModifiers(translateModifiers(context, element.modifier))
522 .addContent(textBuilder.build())
523 .build()
524 } else {
525 textBuilder.setModifiers(translateModifiers(context, element.modifier)).build()
526 }
527 }
528
529 @Suppress("deprecation") // for backward compatibility
toImageDimensionnull530 private fun Dimension.toImageDimension(): androidx.wear.tiles.DimensionBuilders.ImageDimension =
531 when (this) {
532 is Dimension.Expand -> androidx.wear.tiles.DimensionBuilders.expand()
533 is Dimension.Fill -> androidx.wear.tiles.DimensionBuilders.expand()
534 is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
535 else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
536 }
537
538 @Suppress("deprecation") // for backward compatibility
translateEmittableImagenull539 private fun translateEmittableImage(
540 context: Context,
541 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
542 element: EmittableImage
543 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
544 var mappedResId: String
545 when (element.provider) {
546 is AndroidResourceImageProvider -> {
547 val resId = (element.provider as AndroidResourceImageProvider).resId
548 mappedResId = "android_$resId"
549 resourceBuilder.addIdToImageMapping(
550 mappedResId,
551 androidx.wear.tiles.ResourceBuilders.ImageResource.Builder()
552 .setAndroidResourceByResId(
553 androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId.Builder()
554 .setResourceId(resId)
555 .build()
556 )
557 .build()
558 )
559 }
560 is BitmapImageProvider -> {
561 val bitmap = (element.provider as BitmapImageProvider).bitmap
562 val buffer =
563 ByteArrayOutputStream()
564 .apply { bitmap.compress(Bitmap.CompressFormat.PNG, 100, this) }
565 .toByteArray()
566 mappedResId = "android_${Arrays.hashCode(buffer)}"
567 resourceBuilder.addIdToImageMapping(
568 mappedResId,
569 androidx.wear.tiles.ResourceBuilders.ImageResource.Builder()
570 .setInlineResource(
571 androidx.wear.tiles.ResourceBuilders.InlineImageResource.Builder()
572 .setWidthPx(bitmap.width)
573 .setHeightPx(bitmap.height)
574 .setData(buffer)
575 .build()
576 )
577 .build()
578 )
579 }
580 else -> throw IllegalArgumentException("An unsupported ImageProvider type was used")
581 }
582
583 val imageBuilder =
584 androidx.wear.tiles.LayoutElementBuilders.Image.Builder()
585 .setWidth(element.modifier.getWidth(context).toImageDimension())
586 .setHeight(element.modifier.getHeight(context).toImageDimension())
587 .setModifiers(translateModifiers(context, element.modifier))
588 .setResourceId(mappedResId)
589 .setContentScaleMode(
590 when (element.contentScale) {
591 ContentScale.Crop ->
592 androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_CROP
593 ContentScale.Fit ->
594 androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
595 ContentScale.FillBounds ->
596 androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FILL_BOUNDS
597 // Defaults to CONTENT_SCALE_MODE_FIT
598 else -> androidx.wear.tiles.LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
599 }
600 )
601
602 element.colorFilterParams?.let { colorFilterParams ->
603 when (colorFilterParams) {
604 is TintColorFilterParams -> {
605 imageBuilder.setColorFilter(
606 androidx.wear.tiles.LayoutElementBuilders.ColorFilter.Builder()
607 .setTint(
608 androidx.wear.tiles.ColorBuilders.argb(
609 colorFilterParams.colorProvider.getColorAsArgb(context)
610 )
611 )
612 .build()
613 )
614 }
615 else -> throw IllegalArgumentException("An unsupported ColorFilter was used.")
616 }
617 }
618 return imageBuilder.build()
619 }
620
621 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedRownull622 private fun translateEmittableCurvedRow(
623 context: Context,
624 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
625 element: EmittableCurvedRow
626 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
627 // Does it have a width or height set? If so, we need to wrap it in a Box.
628 val width = element.modifier.getWidth(context)
629 val height = element.modifier.getHeight(context)
630
631 // Note: Wear Tiles uses 0 degrees = 12 o clock, but Glance / Wear Compose use 0 degrees = 3
632 // o clock. Tiles supports wraparound etc though, so just add on the 90 degrees here.
633 val arcBuilder =
634 androidx.wear.tiles.LayoutElementBuilders.Arc.Builder()
635 .setAnchorAngle(
636 androidx.wear.tiles.DimensionBuilders.degrees(element.anchorDegrees + 90f)
637 )
638 .setAnchorType(element.anchorType.toProto())
639 .setVerticalAlign(element.radialAlignment.toProto())
640
641 // Add all the children first...
642 element.children.forEach { curvedChild ->
643 if (curvedChild is EmittableCurvedChild) {
644 curvedChild.children.forEach {
645 arcBuilder.addContent(
646 translateEmittableElementInArc(
647 context,
648 resourceBuilder,
649 it,
650 curvedChild.rotateContent
651 )
652 )
653 }
654 } else {
655 arcBuilder.addContent(translateCurvedCompositionInArc(context, curvedChild))
656 }
657 }
658
659 return if (width is Dimension.Dp || height is Dimension.Dp) {
660 androidx.wear.tiles.LayoutElementBuilders.Box.Builder()
661 .setWidth(width.toContainerDimension())
662 .setHeight(height.toContainerDimension())
663 .setModifiers(translateModifiers(context, element.modifier))
664 .addContent(arcBuilder.build())
665 .build()
666 } else {
667 arcBuilder.setModifiers(translateModifiers(context, element.modifier)).build()
668 }
669 }
670
671 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedTextnull672 private fun translateEmittableCurvedText(
673 context: Context,
674 element: EmittableCurvedText
675 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
676 // Modifiers are currently ignored for this element; we'll have to add CurvedScope modifiers in
677 // future which can be used with ArcModifiers, but we don't have any of those added right now.
678 val arcTextBuilder =
679 androidx.wear.tiles.LayoutElementBuilders.ArcText.Builder().setText(element.text)
680
681 element.style?.let { arcTextBuilder.setFontStyle(translateTextStyle(context, it)) }
682
683 arcTextBuilder.setModifiers(translateCurvedModifiers(context, element.curvedModifier))
684
685 return arcTextBuilder.build()
686 }
687
688 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedLinenull689 private fun translateEmittableCurvedLine(
690 context: Context,
691 element: EmittableCurvedLine
692 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
693 var sweepAngleDegrees = element.curvedModifier.findModifier<SweepAngleModifier>()?.degrees ?: 0f
694 var thickness = element.curvedModifier.findModifier<ThicknessModifier>()?.thickness ?: 0.dp
695
696 return androidx.wear.tiles.LayoutElementBuilders.ArcLine.Builder()
697 .setLength(androidx.wear.tiles.DimensionBuilders.degrees(sweepAngleDegrees))
698 .setThickness(androidx.wear.tiles.DimensionBuilders.dp(thickness.value))
699 .setColor(androidx.wear.tiles.ColorBuilders.argb(element.color.getColorAsArgb(context)))
700 .setModifiers(translateCurvedModifiers(context, element.curvedModifier))
701 .build()
702 }
703
704 @Suppress("deprecation") // for backward compatibility
translateEmittableCurvedSpacernull705 private fun translateEmittableCurvedSpacer(
706 context: Context,
707 element: EmittableCurvedSpacer
708 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
709 var sweepAngleDegrees = element.curvedModifier.findModifier<SweepAngleModifier>()?.degrees ?: 0f
710 var thickness = element.curvedModifier.findModifier<ThicknessModifier>()?.thickness ?: 0.dp
711
712 return androidx.wear.tiles.LayoutElementBuilders.ArcSpacer.Builder()
713 .setLength(androidx.wear.tiles.DimensionBuilders.degrees(sweepAngleDegrees))
714 .setThickness(androidx.wear.tiles.DimensionBuilders.dp(thickness.value))
715 .setModifiers(translateCurvedModifiers(context, element.curvedModifier))
716 .build()
717 }
718
719 @Suppress("deprecation") // for backward compatibility
translateEmittableElementInArcnull720 private fun translateEmittableElementInArc(
721 context: Context,
722 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
723 element: Emittable,
724 rotateContent: Boolean
725 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement =
726 androidx.wear.tiles.LayoutElementBuilders.ArcAdapter.Builder()
727 .setContent(translateComposition(context, resourceBuilder, element))
728 .setRotateContents(rotateContent)
729 .build()
730
731 @Suppress("deprecation") // for backward compatibility
732 private fun translateCurvedModifiers(
733 context: Context,
734 curvedModifier: GlanceCurvedModifier
735 ): androidx.wear.tiles.ModifiersBuilders.ArcModifiers =
736 curvedModifier
737 .foldIn(androidx.wear.tiles.ModifiersBuilders.ArcModifiers.Builder()) { builder, element ->
738 when (element) {
739 is ActionCurvedModifier -> builder.setClickable(element.toProto(context))
740 is ThicknessModifier -> builder /* Skip for now, handled elsewhere. */
741 is SweepAngleModifier -> builder /* Skip for now, handled elsewhere. */
742 is SemanticsCurvedModifier -> {
743 element.toProto()?.let { builder.setSemantics(it) } ?: builder
744 }
745 else -> throw IllegalArgumentException("Unknown curved modifier type")
746 }
747 }
748 .build()
749
750 @Suppress("deprecation") // for backward compatibility
translateModifiersnull751 private fun translateModifiers(
752 context: Context,
753 modifier: GlanceModifier,
754 ): androidx.wear.tiles.ModifiersBuilders.Modifiers =
755 modifier
756 .foldIn(androidx.wear.tiles.ModifiersBuilders.Modifiers.Builder()) { builder, element ->
757 when (element) {
758 is BackgroundModifier -> {
759 element.toProto(context)?.let { builder.setBackground(it) } ?: builder
760 }
761 is WidthModifier -> builder /* Skip for now, handled elsewhere. */
762 is HeightModifier -> builder /* Skip for now, handled elsewhere. */
763 is ActionModifier -> builder.setClickable(element.toProto(context))
764 is PaddingModifier -> builder // Processing that after
765 is VisibilityModifier -> builder // Already processed
766 is BorderModifier -> builder.setBorder(element.toProto(context))
767 is SemanticsModifier -> {
768 element.toProto()?.let { builder.setSemantics(it) } ?: builder
769 }
770 else -> throw IllegalArgumentException("Unknown modifier type")
771 }
772 }
buildernull773 .also { builder ->
774 val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
775 modifier.collectPaddingInDp(context.resources)?.toRelative(isRtl)?.let {
776 builder.setPadding(it.toProto())
777 }
778 }
779 .build()
780
781 @Suppress("deprecation") // for backward compatibility
translateCurvedCompositionInArcnull782 private fun translateCurvedCompositionInArc(
783 context: Context,
784 element: Emittable
785 ): androidx.wear.tiles.LayoutElementBuilders.ArcLayoutElement {
786 return when (element) {
787 is EmittableCurvedText -> translateEmittableCurvedText(context, element)
788 is EmittableCurvedLine -> translateEmittableCurvedLine(context, element)
789 is EmittableCurvedSpacer -> translateEmittableCurvedSpacer(context, element)
790 else -> throw IllegalArgumentException("Unknown curved Element: $element")
791 }
792 }
793
794 @Suppress("deprecation") // for backward compatibility
toSpacerDimensionnull795 private fun Dimension.toSpacerDimension(): androidx.wear.tiles.DimensionBuilders.SpacerDimension =
796 when (this) {
797 is Dimension.Dp -> androidx.wear.tiles.DimensionBuilders.dp(this.dp.value)
798 else ->
799 throw IllegalArgumentException(
800 "The spacer dimension should be with dp value, not $this."
801 )
802 }
803
804 @Suppress("deprecation") // for backward compatibility
translateEmittableSpacernull805 private fun translateEmittableSpacer(context: Context, element: EmittableSpacer) =
806 androidx.wear.tiles.LayoutElementBuilders.Spacer.Builder()
807 .setWidth(element.modifier.getWidth(context, Dimension.Dp(0.dp)).toSpacerDimension())
808 .setHeight(element.modifier.getHeight(context, Dimension.Dp(0.dp)).toSpacerDimension())
809 .build()
810
811 private fun translateEmittableAndroidLayoutElement(element: EmittableAndroidLayoutElement) =
812 element.layoutElement
813
814 /**
815 * Translates a Glance Composition to a Wear Tile.
816 *
817 * @throws IllegalArgumentException If the provided Emittable is not recognised (e.g. it is an
818 * element which this translator doesn't understand).
819 */
820 @Suppress("deprecation") // for backward compatibility
821 internal fun translateComposition(
822 context: Context,
823 resourceBuilder: androidx.wear.tiles.ResourceBuilders.Resources.Builder,
824 element: Emittable
825 ): androidx.wear.tiles.LayoutElementBuilders.LayoutElement {
826 return when (element) {
827 is EmittableBox -> translateEmittableBox(context, resourceBuilder, element)
828 is EmittableRow -> translateEmittableRow(context, resourceBuilder, element)
829 is EmittableColumn -> translateEmittableColumn(context, resourceBuilder, element)
830 is EmittableText -> translateEmittableText(context, element)
831 is EmittableCurvedRow -> translateEmittableCurvedRow(context, resourceBuilder, element)
832 is EmittableAndroidLayoutElement -> translateEmittableAndroidLayoutElement(element)
833 is EmittableButton -> translateEmittableText(context, element.toEmittableText())
834 is EmittableSpacer -> translateEmittableSpacer(context, element)
835 is EmittableImage -> translateEmittableImage(context, resourceBuilder, element)
836 else -> throw IllegalArgumentException("Unknown element $element")
837 }
838 }
839
840 @Suppress("deprecation") // for backward compatibility
841 internal class CompositionResult(
842 val layout: androidx.wear.tiles.LayoutElementBuilders.LayoutElement,
843 val resources: androidx.wear.tiles.ResourceBuilders.Resources.Builder
844 )
845
846 @Suppress("deprecation") // for backward compatibility
translateTopLevelCompositionnull847 internal fun translateTopLevelComposition(context: Context, element: Emittable): CompositionResult {
848 val resourceBuilder = androidx.wear.tiles.ResourceBuilders.Resources.Builder()
849 val layout = translateComposition(context, resourceBuilder, element)
850 return CompositionResult(layout, resourceBuilder)
851 }
852