blob: 8142f15e101b8f51ceaa3fa773fb19d3f27f06d7 [file] [log] [blame]
/*
* 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 android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.glance.BackgroundModifier
import androidx.glance.Emittable
import androidx.glance.Modifier
import androidx.glance.action.ActionModifier
import androidx.glance.action.LaunchActivityAction
import androidx.glance.action.LaunchActivityClassAction
import androidx.glance.action.LaunchActivityComponentAction
import androidx.glance.findModifier
import androidx.glance.layout.Alignment
import androidx.glance.layout.Dimension
import androidx.glance.layout.EmittableBox
import androidx.glance.layout.EmittableButton
import androidx.glance.layout.EmittableColumn
import androidx.glance.layout.EmittableRow
import androidx.glance.layout.EmittableText
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.layout.toEmittableText
import androidx.glance.text.FontStyle
import androidx.glance.text.FontWeight
import androidx.glance.text.TextDecoration
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import androidx.glance.unit.FixedColorProvider
import androidx.glance.unit.ResourceColorProvider
import androidx.glance.unit.dp
import androidx.glance.unit.resolve
import androidx.glance.wear.layout.AnchorType
import androidx.glance.wear.layout.CurvedTextStyle
import androidx.glance.wear.layout.EmittableAndroidLayoutElement
import androidx.glance.wear.layout.EmittableCurvedRow
import androidx.glance.wear.layout.EmittableCurvedText
import androidx.glance.wear.layout.RadialAlignment
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_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
@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 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 =
ModifiersBuilders.Background.Builder()
.setColor(argb(this.colorProvider.getColor(context).value.toInt()))
.build()
private fun ColorProvider.getColor(context: Context) = when (this) {
is FixedColorProvider -> color
is ResourceColorProvider -> resolve(context)
else -> error("Unsupported color provider: $this")
}
private fun LaunchActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
ActionBuilders.LaunchAction.Builder()
.setAndroidActivity(
ActionBuilders.AndroidActivity.Builder()
.setPackageName(
when (this) {
is LaunchActivityComponentAction -> componentName.packageName
is LaunchActivityClassAction -> context.packageName
else -> error("Action type not defined in wear package: $this")
}
)
.setClassName(
when (this) {
is LaunchActivityComponentAction -> componentName.className
is LaunchActivityClassAction -> activityClass.name
else -> error("Action type not defined in wear package: $this")
})
.build()
)
.build()
private fun ActionModifier.toProto(context: Context): ModifiersBuilders.Clickable {
val builder = ModifiersBuilders.Clickable.Builder()
val onClick = when (val action = this.action) {
is LaunchActivityAction -> action.toProto(context)
else -> throw IllegalArgumentException("Unknown Action $this")
}
builder.setOnClick(onClick)
return builder.build()
}
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 -> throw IllegalArgumentException("Unknown arc anchor type $this")
}
@VerticalAlignment
private fun RadialAlignment.toProto(): Int =
when (this) {
RadialAlignment.Outer -> VERTICAL_ALIGN_TOP
RadialAlignment.Center -> VERTICAL_ALIGN_CENTER
RadialAlignment.Inner -> VERTICAL_ALIGN_BOTTOM
else -> throw IllegalArgumentException("Unknown radial alignment $this")
}
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 Modifier.getWidth(
context: Context,
default: Dimension = Dimension.Wrap
): Dimension = findModifier<WidthModifier>()?.width?.resolve(context) ?: default
private fun Modifier.getHeight(
context: Context,
default: Dimension = Dimension.Wrap
): Dimension = findModifier<HeightModifier>()?.height?.resolve(context) ?: default
private fun translateEmittableBox(
context: Context,
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, it)) } }
.build()
private fun translateEmittableRow(
context: Context,
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, 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,
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, 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(style: TextStyle): LayoutElementBuilders.FontStyle {
val fontStyleBuilder = LayoutElementBuilders.FontStyle.Builder()
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 -> throw IllegalArgumentException("Unknown font weight $it")
}
)
}
style.textDecoration?.let {
fontStyleBuilder.setUnderline(TextDecoration.Underline in it)
}
return fontStyleBuilder.build()
}
private fun translateTextStyle(style: CurvedTextStyle): LayoutElementBuilders.FontStyle {
val fontStyleBuilder = LayoutElementBuilders.FontStyle.Builder()
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 -> throw IllegalArgumentException("Unknown font weight $it")
}
)
}
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)
element.style?.let { textBuilder.setFontStyle(translateTextStyle(it)) }
return if (width !is Dimension.Wrap || height !is Dimension.Wrap) {
LayoutElementBuilders.Box.Builder()
.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 translateEmittableCurvedRow(
context: Context,
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 { arcBuilder.addContent(translateCompositionInArc(context, it)) }
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(
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.textStyle?.let { arcTextBuilder.setFontStyle(translateTextStyle(it)) }
return arcTextBuilder.build()
}
private fun translateEmittableElementInArc(
context: Context,
element: Emittable
): LayoutElementBuilders.ArcLayoutElement = LayoutElementBuilders.ArcAdapter.Builder()
.setContent(translateComposition(context, element))
.build()
private fun translateModifiers(
context: Context,
modifier: Modifier
): ModifiersBuilders.Modifiers =
modifier.foldIn(ModifiersBuilders.Modifiers.Builder()) { builder, element ->
when (element) {
is BackgroundModifier -> builder.setBackground(element.toProto(context))
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
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())
}
}
.build()
private fun translateCompositionInArc(
context: Context,
element: Emittable
): LayoutElementBuilders.ArcLayoutElement {
return when (element) {
is EmittableCurvedText -> translateEmittableCurvedText(element)
else -> translateEmittableElementInArc(context, element)
}
}
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,
element: Emittable
): LayoutElementBuilders.LayoutElement {
return when (element) {
is EmittableBox -> translateEmittableBox(context, element)
is EmittableRow -> translateEmittableRow(context, element)
is EmittableColumn -> translateEmittableColumn(context, element)
is EmittableText -> translateEmittableText(context, element)
is EmittableCurvedRow -> translateEmittableCurvedRow(context, element)
is EmittableAndroidLayoutElement -> translateEmittableAndroidLayoutElement(element)
is EmittableButton -> translateEmittableText(context, element.toEmittableText())
else -> throw IllegalArgumentException("Unknown element $element")
}
}