/*
* 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.tiles
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
import androidx.glance.AndroidResourceImageProvider
import androidx.glance.BackgroundModifier
import androidx.glance.BitmapImageProvider
import androidx.glance.Emittable
import androidx.glance.EmittableButton
import androidx.glance.EmittableImage
import androidx.glance.GlanceModifier
import androidx.glance.semantics.SemanticsModifier
import androidx.glance.VisibilityModifier
import androidx.glance.action.Action
import androidx.glance.action.ActionModifier
import androidx.glance.wear.tiles.action.RunCallbackAction
import androidx.glance.action.StartActivityAction
import androidx.glance.action.StartActivityClassAction
import androidx.glance.action.StartActivityComponentAction
import androidx.glance.findModifier
import androidx.glance.layout.Alignment
import androidx.glance.layout.ContentScale
import androidx.glance.layout.EmittableBox
import androidx.glance.layout.EmittableColumn
import androidx.glance.layout.EmittableRow
import androidx.glance.layout.EmittableSpacer
import androidx.glance.layout.HeightModifier
import androidx.glance.layout.PaddingInDp
import androidx.glance.layout.PaddingModifier
import androidx.glance.layout.WidthModifier
import androidx.glance.layout.collectPaddingInDp
import androidx.glance.semantics.SemanticsProperties
import androidx.glance.text.EmittableText
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.TextAlign
import androidx.glance.text.TextDecoration
import androidx.glance.text.TextStyle
import androidx.glance.toEmittableText
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.Dimension
import androidx.glance.wear.tiles.curved.AnchorType
import androidx.glance.wear.tiles.curved.ActionCurvedModifier
import androidx.glance.wear.tiles.curved.CurvedTextStyle
import androidx.glance.wear.tiles.curved.EmittableCurvedChild
import androidx.glance.wear.tiles.curved.EmittableCurvedLine
import androidx.glance.wear.tiles.curved.EmittableCurvedRow
import androidx.glance.wear.tiles.curved.EmittableCurvedSpacer
import androidx.glance.wear.tiles.curved.EmittableCurvedText
import androidx.glance.wear.tiles.curved.GlanceCurvedModifier
import androidx.glance.wear.tiles.curved.RadialAlignment
import androidx.glance.wear.tiles.curved.SemanticsCurvedModifier
import androidx.glance.wear.tiles.curved.SweepAngleModifier
import androidx.glance.wear.tiles.curved.ThicknessModifier
import androidx.glance.wear.tiles.curved.findModifier
import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders.argb
import androidx.wear.tiles.DimensionBuilders
import androidx.wear.tiles.DimensionBuilders.degrees
import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.DimensionBuilders.expand
import androidx.wear.tiles.DimensionBuilders.sp
import androidx.wear.tiles.DimensionBuilders.wrap
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_CENTER
import androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_END
import androidx.wear.tiles.LayoutElementBuilders.ARC_ANCHOR_START
import androidx.wear.tiles.LayoutElementBuilders.ArcAnchorType
import androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_BOLD
import androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_MEDIUM
import androidx.wear.tiles.LayoutElementBuilders.FONT_WEIGHT_NORMAL
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_END
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_LEFT
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_RIGHT
import androidx.wear.tiles.LayoutElementBuilders.HORIZONTAL_ALIGN_START
import androidx.wear.tiles.LayoutElementBuilders.HorizontalAlignment
import androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_CENTER
import androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_END
import androidx.wear.tiles.LayoutElementBuilders.TEXT_ALIGN_START
import androidx.wear.tiles.LayoutElementBuilders.TextAlignment
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 androidx.wear.tiles.ResourceBuilders
import java.io.ByteArrayOutputStream
import java.util.Arrays
internal const val GlanceWearTileTag = "GlanceWearTile"
@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 -> {
Log.w(
GlanceWearTileTag,
"Unknown vertical alignment type $this, align to Top instead"
)
VERTICAL_ALIGN_TOP
}
}
@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 -> {
Log.w(
GlanceWearTileTag,
"Unknown horizontal alignment type $this, align to Start instead"
)
HORIZONTAL_ALIGN_START
}
}
private fun PaddingInDp.toProto(): ModifiersBuilders.Padding =
ModifiersBuilders.Padding.Builder()
.setStart(dp(start.value))
.setTop(dp(top.value))
.setEnd(dp(end.value))
.setBottom((dp(bottom.value)))
.setRtlAware(true)
.build()
private fun BackgroundModifier.toProto(context: Context): ModifiersBuilders.Background? =
this.colorProvider?.let { provider ->
ModifiersBuilders.Background.Builder()
.setColor(argb(provider.getColorAsArgb(context)))
.build()
}
private fun BorderModifier.toProto(context: Context): ModifiersBuilders.Border =
ModifiersBuilders.Border.Builder()
.setWidth(dp(this.width.toDp(context.resources).value))
.setColor(argb(this.color.getColorAsArgb(context)))
.build()
private fun SemanticsModifier.toProto(): ModifiersBuilders.Semantics? =
this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
ModifiersBuilders.Semantics.Builder()
.setContentDescription(it.joinToString())
.build()
}
private fun SemanticsCurvedModifier.toProto(): ModifiersBuilders.Semantics? =
this.configuration.getOrNull(SemanticsProperties.ContentDescription)?.let {
ModifiersBuilders.Semantics.Builder()
.setContentDescription(it.joinToString())
.build()
}
private fun ColorProvider.getColorAsArgb(context: Context) = getColor(context).toArgb()
// TODO: handle parameters
private fun StartActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(
ActionBuilders.AndroidActivity.Builder()
.setPackageName(
when (this) {
is StartActivityComponentAction -> componentName.packageName
is StartActivityClassAction -> context.packageName
else -> error("Action type not defined in wear package: $this")
}
)
.setClassName(
when (this) {
is StartActivityComponentAction -> componentName.className
is StartActivityClassAction -> activityClass.name
else -> error("Action type not defined in wear package: $this")
}
)
.build()
)
.build()
private fun Action.toClickable(context: Context): ModifiersBuilders.Clickable {
val builder = ModifiersBuilders.Clickable.Builder()
when (this) {
is StartActivityAction -> {
builder.setOnClick(toProto(context))
}
is RunCallbackAction -> {
builder.setOnClick(ActionBuilders.LoadAction.Builder().build())
.setId(callbackClass.canonicalName!!)
}
else -> {
Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
}
}
return builder.build()
}
private fun ActionModifier.toProto(context: Context): ModifiersBuilders.Clickable =
this.action.toClickable(context)
private fun ActionCurvedModifier.toProto(context: Context): ModifiersBuilders.Clickable =
this.action.toClickable(context)
private fun Dimension.toContainerDimension(): DimensionBuilders.ContainerDimension =
when (this) {
is Dimension.Wrap -> wrap()
is Dimension.Expand -> expand()
is Dimension.Fill -> expand()
is Dimension.Dp -> dp(this.dp.value)
else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
}
@ArcAnchorType
private fun AnchorType.toProto(): Int =
when (this) {
AnchorType.Start -> ARC_ANCHOR_START
AnchorType.Center -> ARC_ANCHOR_CENTER
AnchorType.End -> ARC_ANCHOR_END
else -> {
Log.w(GlanceWearTileTag, "Unknown arc anchor type $this, anchor to center instead")
ARC_ANCHOR_CENTER
}
}
@VerticalAlignment
private fun RadialAlignment.toProto(): Int =
when (this) {
RadialAlignment.Outer -> VERTICAL_ALIGN_TOP
RadialAlignment.Center -> VERTICAL_ALIGN_CENTER
RadialAlignment.Inner -> VERTICAL_ALIGN_BOTTOM
else -> {
Log.w(
GlanceWearTileTag,
"Unknown radial alignment $this, align to center instead"
)
VERTICAL_ALIGN_CENTER
}
}
@TextAlignment
private fun TextAlign.toTextAlignment(isRtl: Boolean): Int =
when (this) {
TextAlign.Center -> TEXT_ALIGN_CENTER
TextAlign.End -> TEXT_ALIGN_END
TextAlign.Left -> if (isRtl) TEXT_ALIGN_END else TEXT_ALIGN_START
TextAlign.Right -> if (isRtl) TEXT_ALIGN_START else TEXT_ALIGN_END
TextAlign.Start -> TEXT_ALIGN_START
else -> {
Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
TEXT_ALIGN_START
}
}
@HorizontalAlignment
private fun TextAlign.toHorizontalAlignment(): Int =
when (this) {
TextAlign.Center -> HORIZONTAL_ALIGN_CENTER
TextAlign.End -> HORIZONTAL_ALIGN_END
TextAlign.Left -> HORIZONTAL_ALIGN_LEFT
TextAlign.Right -> HORIZONTAL_ALIGN_RIGHT
TextAlign.Start -> HORIZONTAL_ALIGN_START
else -> {
Log.w(GlanceWearTileTag, "Unknown text alignment $this, align to Start instead")
HORIZONTAL_ALIGN_START
}
}
private fun Dimension.resolve(context: Context): Dimension {
if (this !is Dimension.Resource) return this
val sizePx = context.resources.getDimension(res)
return when (sizePx.toInt()) {
ViewGroup.LayoutParams.MATCH_PARENT -> Dimension.Fill
ViewGroup.LayoutParams.WRAP_CONTENT -> Dimension.Wrap
else -> Dimension.Dp((sizePx / context.resources.displayMetrics.density).dp)
}
}
private fun GlanceModifier.getWidth(
context: Context,
default: Dimension = Dimension.Wrap
): Dimension = findModifier<WidthModifier>()?.width?.resolve(context) ?: default
private fun GlanceModifier.getHeight(
context: Context,
default: Dimension = Dimension.Wrap
): Dimension = findModifier<HeightModifier>()?.height?.resolve(context) ?: default
private fun translateEmittableBox(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: EmittableBox
) = LayoutElementBuilders.Box.Builder()
.setVerticalAlignment(element.contentAlignment.vertical.toProto())
.setHorizontalAlignment(element.contentAlignment.horizontal.toProto())
.setModifiers(translateModifiers(context, element.modifier))
.setWidth(element.modifier.getWidth(context).toContainerDimension())
.setHeight(element.modifier.getHeight(context).toContainerDimension())
.also { box ->
element.children.forEach {
box.addContent(translateComposition(context, resourceBuilder, it))
}
}
.build()
private fun translateEmittableRow(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: EmittableRow
): LayoutElementBuilders.LayoutElement {
val width = element.modifier.getWidth(context)
val height = element.modifier.getHeight(context)
val baseRowBuilder = LayoutElementBuilders.Row.Builder()
.setHeight(height.toContainerDimension())
.setVerticalAlignment(element.verticalAlignment.toProto())
.also { row ->
element.children.forEach {
row.addContent(translateComposition(context, resourceBuilder, it))
}
}
// Do we need to wrap it in a column to set the horizontal alignment?
return if (element.horizontalAlignment != Alignment.Horizontal.Start &&
width !is Dimension.Wrap
) {
LayoutElementBuilders.Column.Builder()
.setHorizontalAlignment(element.horizontalAlignment.toProto())
.setModifiers(translateModifiers(context, element.modifier))
.setWidth(width.toContainerDimension())
.setHeight(height.toContainerDimension())
.addContent(baseRowBuilder.setWidth(wrap()).build())
.build()
} else {
baseRowBuilder
.setModifiers(translateModifiers(context, element.modifier))
.setWidth(width.toContainerDimension())
.build()
}
}
private fun translateEmittableColumn(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: EmittableColumn
): LayoutElementBuilders.LayoutElement {
val width = element.modifier.getWidth(context)
val height = element.modifier.getHeight(context)
val baseColumnBuilder = LayoutElementBuilders.Column.Builder()
.setWidth(width.toContainerDimension())
.setHorizontalAlignment(element.horizontalAlignment.toProto())
.also { column ->
element.children.forEach {
column.addContent(translateComposition(context, resourceBuilder, it))
}
}
// Do we need to wrap it in a row to set the vertical alignment?
return if (element.verticalAlignment != Alignment.Vertical.Top &&
height !is Dimension.Wrap
) {
LayoutElementBuilders.Row.Builder()
.setVerticalAlignment(element.verticalAlignment.toProto())
.setModifiers(translateModifiers(context, element.modifier))
.setWidth(width.toContainerDimension())
.setHeight(height.toContainerDimension())
.addContent(baseColumnBuilder.setHeight(wrap()).build())
.build()
} else {
baseColumnBuilder
.setModifiers(translateModifiers(context, element.modifier))
.setHeight(height.toContainerDimension())
.build()
}
}
private fun translateTextStyle(
context: Context,
style: TextStyle,
): LayoutElementBuilders.FontStyle {
val fontStyleBuilder = LayoutElementBuilders.FontStyle.Builder()
style.color?.let { fontStyleBuilder.setColor(argb(it.getColorAsArgb(context))) }
// TODO(b/203656358): Can we support Em here too?
style.fontSize?.let {
if (!it.isSp) {
throw IllegalArgumentException("Only Sp is supported for font size")
}
fontStyleBuilder.setSize(sp(it.value))
}
style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
style.fontWeight?.let {
fontStyleBuilder.setWeight(
when (it) {
FontWeight.Normal -> FONT_WEIGHT_NORMAL
FontWeight.Medium -> FONT_WEIGHT_MEDIUM
FontWeight.Bold -> FONT_WEIGHT_BOLD
else -> {
Log.w(
GlanceWearTileTag,
"Unknown font weight $it, use Normal weight instead"
)
FONT_WEIGHT_NORMAL
}
}
)
}
style.textDecoration?.let {
fontStyleBuilder.setUnderline(TextDecoration.Underline in it)
}
return fontStyleBuilder.build()
}
private fun translateTextStyle(
context: Context,
style: CurvedTextStyle,
): LayoutElementBuilders.FontStyle {
val fontStyleBuilder = LayoutElementBuilders.FontStyle.Builder()
style.color?.let { fontStyleBuilder.setColor(argb(it.getColorAsArgb(context))) }
style.fontSize?.let { fontStyleBuilder.setSize(sp(it.value)) }
style.fontStyle?.let { fontStyleBuilder.setItalic(it == FontStyle.Italic) }
style.fontWeight?.let {
fontStyleBuilder.setWeight(
when (it) {
FontWeight.Normal -> FONT_WEIGHT_NORMAL
FontWeight.Medium -> FONT_WEIGHT_MEDIUM
FontWeight.Bold -> FONT_WEIGHT_BOLD
else -> {
Log.w(
GlanceWearTileTag,
"Unknown font weight $it, use Normal weight instead"
)
FONT_WEIGHT_NORMAL
}
}
)
}
return fontStyleBuilder.build()
}
private fun translateEmittableText(
context: Context,
element: EmittableText
): LayoutElementBuilders.LayoutElement {
// Does it have a width or height set? If so, we need to wrap it in a Box.
val width = element.modifier.getWidth(context)
val height = element.modifier.getHeight(context)
val textBuilder = LayoutElementBuilders.Text.Builder()
.setText(element.text)
.setMaxLines(element.maxLines)
element.style?.let { textBuilder.setFontStyle(translateTextStyle(context, it)) }
val textAlign: TextAlign? = element.style?.textAlign
if (textAlign != null) {
val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
textBuilder.setMultilineAlignment(textAlign.toTextAlignment(isRtl))
}
return if (width !is Dimension.Wrap || height !is Dimension.Wrap) {
val boxBuilder = LayoutElementBuilders.Box.Builder()
if (textAlign != null) {
boxBuilder.setHorizontalAlignment(textAlign.toHorizontalAlignment())
}
boxBuilder.setWidth(width.toContainerDimension())
.setHeight(height.toContainerDimension())
.setModifiers(translateModifiers(context, element.modifier))
.addContent(textBuilder.build())
.build()
} else {
textBuilder.setModifiers(translateModifiers(context, element.modifier)).build()
}
}
private fun Dimension.toImageDimension(): DimensionBuilders.ImageDimension =
when (this) {
is Dimension.Expand -> expand()
is Dimension.Fill -> expand()
is Dimension.Dp -> dp(this.dp.value)
else -> throw IllegalArgumentException("The dimension should be fully resolved, not $this.")
}
private fun translateEmittableImage(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: EmittableImage
): LayoutElementBuilders.LayoutElement {
var mappedResId: String
when (element.provider) {
is AndroidResourceImageProvider -> {
val resId = (element.provider as AndroidResourceImageProvider).resId
mappedResId = "android_$resId"
resourceBuilder.addIdToImageMapping(
mappedResId,
ResourceBuilders.ImageResource.Builder().setAndroidResourceByResId(
ResourceBuilders.AndroidImageResourceByResId.Builder()
.setResourceId(resId)
.build()
).build()
)
}
is BitmapImageProvider -> {
val bitmap = (element.provider as BitmapImageProvider).bitmap
val buffer = ByteArrayOutputStream().apply {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, this)
}.toByteArray()
mappedResId = "android_${Arrays.hashCode(buffer)}"
resourceBuilder.addIdToImageMapping(
mappedResId,
ResourceBuilders.ImageResource.Builder().setInlineResource(
ResourceBuilders.InlineImageResource.Builder()
.setWidthPx(bitmap.width)
.setHeightPx(bitmap.height)
.setData(buffer)
.build()
).build()
)
}
else ->
throw IllegalArgumentException("An unsupported ImageProvider type was used")
}
val imageBuilder = LayoutElementBuilders.Image.Builder()
.setWidth(element.modifier.getWidth(context).toImageDimension())
.setHeight(element.modifier.getHeight(context).toImageDimension())
.setModifiers(translateModifiers(context, element.modifier, element.contentDescription))
.setResourceId(mappedResId)
.setContentScaleMode(
when (element.contentScale) {
ContentScale.Crop -> LayoutElementBuilders.CONTENT_SCALE_MODE_CROP
ContentScale.Fit -> LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
ContentScale.FillBounds -> LayoutElementBuilders.CONTENT_SCALE_MODE_FILL_BOUNDS
// Defaults to CONTENT_SCALE_MODE_FIT
else -> LayoutElementBuilders.CONTENT_SCALE_MODE_FIT
}
)
return imageBuilder.build()
}
private fun translateEmittableCurvedRow(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: EmittableCurvedRow
): LayoutElementBuilders.LayoutElement {
// Does it have a width or height set? If so, we need to wrap it in a Box.
val width = element.modifier.getWidth(context)
val height = element.modifier.getHeight(context)
// Note: Wear Tiles uses 0 degrees = 12 o clock, but Glance / Wear Compose use 0 degrees = 3
// o clock. Tiles supports wraparound etc though, so just add on the 90 degrees here.
val arcBuilder = LayoutElementBuilders.Arc.Builder()
.setAnchorAngle(degrees(element.anchorDegrees + 90f))
.setAnchorType(element.anchorType.toProto())
.setVerticalAlign(element.radialAlignment.toProto())
// Add all the children first...
element.children.forEach { curvedChild ->
if (curvedChild is EmittableCurvedChild) {
curvedChild.children.forEach {
arcBuilder.addContent(
translateEmittableElementInArc(
context,
resourceBuilder,
it,
curvedChild.rotateContent)
)
}
} else {
arcBuilder.addContent(translateCurvedCompositionInArc(context, curvedChild))
}
}
return if (width is Dimension.Dp || height is Dimension.Dp) {
LayoutElementBuilders.Box.Builder()
.setWidth(width.toContainerDimension())
.setHeight(height.toContainerDimension())
.setModifiers(translateModifiers(context, element.modifier))
.addContent(arcBuilder.build())
.build()
} else {
arcBuilder
.setModifiers(translateModifiers(context, element.modifier))
.build()
}
}
private fun translateEmittableCurvedText(
context: Context,
element: EmittableCurvedText
): LayoutElementBuilders.ArcLayoutElement {
// Modifiers are currently ignored for this element; we'll have to add CurvedScope modifiers in
// future which can be used with ArcModifiers, but we don't have any of those added right now.
val arcTextBuilder = LayoutElementBuilders.ArcText.Builder()
.setText(element.text)
element.style?.let {
arcTextBuilder.setFontStyle(translateTextStyle(context, it))
}
arcTextBuilder.setModifiers(translateCurvedModifiers(context, element.curvedModifier))
return arcTextBuilder.build()
}
private fun translateEmittableCurvedLine(
context: Context,
element: EmittableCurvedLine
): LayoutElementBuilders.ArcLayoutElement {
var sweepAngleDegrees =
element.curvedModifier.findModifier<SweepAngleModifier>() ?. degrees ?: 0f
var thickness = element.curvedModifier.findModifier<ThicknessModifier>() ?. thickness ?: 0.dp
return LayoutElementBuilders.ArcLine.Builder()
.setLength(degrees(sweepAngleDegrees))
.setThickness(dp(thickness.value))
.setColor(argb(element.color.getColorAsArgb(context)))
.setModifiers(translateCurvedModifiers(context, element.curvedModifier))
.build()
}
private fun translateEmittableCurvedSpacer(
context: Context,
element: EmittableCurvedSpacer
): LayoutElementBuilders.ArcLayoutElement {
var sweepAngleDegrees =
element.curvedModifier.findModifier<SweepAngleModifier>() ?. degrees ?: 0f
var thickness = element.curvedModifier.findModifier<ThicknessModifier>() ?. thickness ?: 0.dp
return LayoutElementBuilders.ArcSpacer.Builder()
.setLength(degrees(sweepAngleDegrees))
.setThickness(dp(thickness.value))
.setModifiers(translateCurvedModifiers(context, element.curvedModifier))
.build()
}
private fun translateEmittableElementInArc(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: Emittable,
rotateContent: Boolean
): LayoutElementBuilders.ArcLayoutElement = LayoutElementBuilders.ArcAdapter.Builder()
.setContent(translateComposition(context, resourceBuilder, element))
.setRotateContents(rotateContent)
.build()
private fun translateCurvedModifiers(
context: Context,
curvedModifier: GlanceCurvedModifier
): ModifiersBuilders.ArcModifiers =
curvedModifier.foldIn(ModifiersBuilders.ArcModifiers.Builder()) { builder, element ->
when (element) {
is ActionCurvedModifier -> builder.setClickable(element.toProto(context))
is ThicknessModifier -> builder /* Skip for now, handled elsewhere. */
is SweepAngleModifier -> builder /* Skip for now, handled elsewhere. */
is SemanticsCurvedModifier -> {
element.toProto()?.let { builder.setSemantics(it) } ?: builder
}
else -> throw IllegalArgumentException("Unknown curved modifier type")
}
}.build()
private fun translateModifiers(
context: Context,
modifier: GlanceModifier,
contentDescription: String? = null
): ModifiersBuilders.Modifiers =
modifier.foldIn(ModifiersBuilders.Modifiers.Builder()) { builder, element ->
when (element) {
is BackgroundModifier -> {
element.toProto(context)?.let { builder.setBackground(it) } ?: builder
}
is WidthModifier -> builder /* Skip for now, handled elsewhere. */
is HeightModifier -> builder /* Skip for now, handled elsewhere. */
is ActionModifier -> builder.setClickable(element.toProto(context))
is PaddingModifier -> builder // Processing that after
is VisibilityModifier -> builder // Already processed
is BorderModifier -> builder.setBorder(element.toProto(context))
is SemanticsModifier -> {
element.toProto()?.let { builder.setSemantics(it) } ?: builder
}
else -> throw IllegalArgumentException("Unknown modifier type")
}
}
.also { builder ->
val isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
modifier.collectPaddingInDp(context.resources)
?.toRelative(isRtl)
?.let {
builder.setPadding(it.toProto())
}
contentDescription?.let { contentDescription ->
builder.setSemantics(
ModifiersBuilders.Semantics.Builder()
.setContentDescription(contentDescription)
.build()
)
}
}
.build()
private fun translateCurvedCompositionInArc(
context: Context,
element: Emittable
): LayoutElementBuilders.ArcLayoutElement {
return when (element) {
is EmittableCurvedText -> translateEmittableCurvedText(context, element)
is EmittableCurvedLine -> translateEmittableCurvedLine(context, element)
is EmittableCurvedSpacer -> translateEmittableCurvedSpacer(context, element)
else -> throw IllegalArgumentException(
"Unknown curved Element: $element"
)
}
}
private fun Dimension.toSpacerDimension(): DimensionBuilders.SpacerDimension =
when (this) {
is Dimension.Dp -> dp(this.dp.value)
else -> throw IllegalArgumentException(
"The spacer dimension should be with dp value, not $this."
)
}
private fun translateEmittableSpacer(
context: Context,
element: EmittableSpacer
) = LayoutElementBuilders.Spacer.Builder()
.setWidth(element.modifier.getWidth(context, Dimension.Dp(0.dp)).toSpacerDimension())
.setHeight(element.modifier.getHeight(context, Dimension.Dp(0.dp)).toSpacerDimension())
.build()
private fun translateEmittableAndroidLayoutElement(element: EmittableAndroidLayoutElement) =
element.layoutElement
/**
* 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).
*/
internal fun translateComposition(
context: Context,
resourceBuilder: ResourceBuilders.Resources.Builder,
element: Emittable
): LayoutElementBuilders.LayoutElement {
return when (element) {
is EmittableBox -> translateEmittableBox(context, resourceBuilder, element)
is EmittableRow -> translateEmittableRow(context, resourceBuilder, element)
is EmittableColumn ->
translateEmittableColumn(context, resourceBuilder, element)
is EmittableText -> translateEmittableText(context, element)
is EmittableCurvedRow ->
translateEmittableCurvedRow(context, resourceBuilder, element)
is EmittableAndroidLayoutElement -> translateEmittableAndroidLayoutElement(element)
is EmittableButton ->
translateEmittableText(context, element.toEmittableText())
is EmittableSpacer -> translateEmittableSpacer(context, element)
is EmittableImage ->
translateEmittableImage(context, resourceBuilder, element)
else -> throw IllegalArgumentException("Unknown element $element")
}
}
internal class CompositionResult(
val layout: LayoutElementBuilders.LayoutElement,
val resources: ResourceBuilders.Resources.Builder
)
internal fun translateTopLevelComposition(
context: Context,
element: Emittable
): CompositionResult {
val resourceBuilder = ResourceBuilders.Resources.Builder()
val layout = translateComposition(context, resourceBuilder, element)
return CompositionResult(layout, resourceBuilder)
}