| /* |
| * Copyright 2020 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.wear.watchface |
| |
| import android.graphics.Color |
| import androidx.annotation.ColorInt |
| import androidx.annotation.RestrictTo |
| import androidx.wear.watchface.RenderParameters.HighlightLayer |
| import androidx.wear.watchface.data.IdAndTapEventWireFormat |
| import androidx.wear.watchface.data.RenderParametersWireFormat |
| import androidx.wear.watchface.style.WatchFaceLayer |
| import androidx.wear.watchface.style.UserStyleSetting |
| import java.time.Instant |
| |
| /* Used to parameterize watch face drawing based on the current system state. */ |
| public enum class DrawMode { |
| /** |
| * This mode is used when the user is interacting with the watch face. |
| * |
| * This is currently the only mode that is supported when editing the watch face. |
| */ |
| INTERACTIVE, |
| |
| /** |
| * This mode is used when the user is interacting with the watch face but the battery is |
| * low, the watch face should render fewer pixels, ideally with darker colors. |
| */ |
| LOW_BATTERY_INTERACTIVE, |
| |
| /** |
| * This mode is used when there's an interruption filter. The watch face should look muted. |
| */ |
| MUTE, |
| |
| /** |
| * In this mode as few pixels as possible should be turned on, ideally with darker colors. |
| * |
| * Typically watch faces switch to a simplified display when in ambient mode. E.g. if the watch |
| * face displays seconds, it should hide them in ambient mode. |
| * |
| * Note: In ambient mode the watch face will be rendered once per minute. |
| */ |
| AMBIENT |
| } |
| |
| /** |
| * Used to parameterize watch face rendering. |
| * |
| * Watch face rendering is split up in a number of layers: the base layer [WatchFaceLayer.BASE], the |
| * [ComplicationSlot]s layer [WatchFaceLayer.COMPLICATIONS], and the layer above the |
| * complicationSlots [WatchFaceLayer.COMPLICATIONS_OVERLAY]. These layers are always drawn in |
| * this order, one on top of the previous one. These are the layers that are used to render the |
| * watch face itself. |
| * |
| * An additional layer, the highlight layer, can be drawn during editing the watch face to highlight |
| * different elements of the watch face, namely a set of [ComplicationSlot]s (which may be a single |
| * ComplicationSlot) or the area of the watch face that is affected by a single user style setting. |
| * |
| * The watch face should provide a way to highlight any of the above combinations so that its own |
| * editor, or the one provided by the Wear OS companion phone app is able to highlight the editable |
| * part of the watch face to the user. If an individual user style setting is meant to affect the |
| * entire watch face (e.g. an overall color setting),then the entire watch face should be |
| * highlighted. |
| * |
| * The watch face layers and highlight layer can be configured independently, so that it is possible |
| * to draw only the highlight layer by passing an empty set of [watchFaceLayers]. |
| * |
| * The semantics of rendering different layers is such that if each individual layer is rendered |
| * independently and the resulting images are composited with alpha blending, the result is |
| * identical to rendering all of the layers in a single request. |
| * |
| * @param drawMode The overall drawing parameters based on system state. |
| * @param watchFaceLayers The parts of the watch face to draw. |
| * @param highlightLayer Optional [HighlightLayer] used by editors to visually highlight an |
| * aspect of the watch face. Rendered last on top of [watchFaceLayers]. If highlighting isn't needed |
| * this will be `null`. |
| * @param lastComplicationTapDownEvents Map of [ComplicationSlot] id to the latest [TapType.DOWN] |
| * [TapEvent] that ComplicationSlot received, if any. |
| */ |
| public class RenderParameters @JvmOverloads constructor( |
| public val drawMode: DrawMode, |
| public val watchFaceLayers: Set<WatchFaceLayer>, |
| public val highlightLayer: HighlightLayer? = null, |
| public val lastComplicationTapDownEvents: Map<Int, TapEvent> = emptyMap() |
| ) { |
| init { |
| require(watchFaceLayers.isNotEmpty() || highlightLayer != null) { |
| "Either watchFaceLayers must be non empty or " + |
| "renderParameters.highlightLayer must be non-null." |
| } |
| } |
| |
| /** An element of the watch face to highlight. */ |
| public sealed class HighlightedElement { |
| /** All [ComplicationSlot]s will be highlighted. */ |
| public object AllComplicationSlots : HighlightedElement() |
| |
| /** |
| * A single [androidx.wear.watchface.ComplicationSlot] with the specified [id] will be |
| * highlighted. |
| */ |
| public class ComplicationSlot(public val id: Int) : HighlightedElement() { |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as ComplicationSlot |
| |
| if (id != other.id) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| return id |
| } |
| } |
| |
| /** |
| * A [UserStyleSetting] to highlight. E.g. for a setting that controls watch hands, the |
| * location of the hands should be highlighted. |
| * |
| * @param id The [UserStyleSetting.Id] of the [UserStyleSetting] to highlight. |
| */ |
| public class UserStyle(public val id: UserStyleSetting.Id) : HighlightedElement() { |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as UserStyle |
| |
| if (id != other.id) return false |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| return id.hashCode() |
| } |
| } |
| } |
| |
| /** |
| * The definition of what to include in the highlight layer. |
| * |
| * The highlight layer is used by editors to show the parts of the watch face affected by a |
| * setting. E.g. a set of [ComplicationSlot]s or a user style setting. |
| * |
| * The highlight layer is composited on top of the watch face with an alpha blend. It should |
| * be cleared with [backgroundTint]. The solid or semi-transparent outline around |
| * [highlightedElement] should be rendered using the provided [highlightTint]. The highlighted |
| * element itself should be rendered as fully transparent (an alpha value of 0) to leave it |
| * unaffected. |
| * |
| * @param highlightedElement The [HighlightedElement] to draw highlighted with [highlightTint]. |
| * @param highlightTint The highlight tint to apply to [highlightedElement]. |
| * @param backgroundTint The tint to apply to everything other than [highlightedElement]. |
| * Typically this will darken everything else to increase contrast. |
| */ |
| public class HighlightLayer( |
| public val highlightedElement: HighlightedElement, |
| |
| @ColorInt |
| @get:ColorInt |
| public val highlightTint: Int, |
| |
| @ColorInt |
| @get:ColorInt |
| public val backgroundTint: Int |
| ) { |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as HighlightLayer |
| |
| if (highlightedElement != other.highlightedElement) return false |
| if (highlightTint != other.highlightTint) return false |
| if (backgroundTint != other.backgroundTint) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = highlightedElement.hashCode() |
| result = 31 * result + highlightTint |
| result = 31 * result + backgroundTint |
| return result |
| } |
| } |
| |
| public companion object { |
| /** Default RenderParameters which draws everything in interactive mode. */ |
| @JvmField |
| public val DEFAULT_INTERACTIVE: RenderParameters = |
| RenderParameters(DrawMode.INTERACTIVE, WatchFaceLayer.ALL_WATCH_FACE_LAYERS, null) |
| } |
| |
| /** @hide */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) |
| public constructor(wireFormat: RenderParametersWireFormat) : this( |
| DrawMode.values()[wireFormat.drawMode], |
| HashSet<WatchFaceLayer>().apply { |
| WatchFaceLayer.values().forEachIndexed { index, layer -> |
| if (wireFormat.watchFaceLayerSetBitfield and 1.shl(index) != 0) { |
| add(layer) |
| } |
| } |
| }, |
| when (wireFormat.elementType) { |
| RenderParametersWireFormat.ELEMENT_TYPE_NONE -> null |
| |
| RenderParametersWireFormat.ELEMENT_TYPE_ALL_COMPLICATIONS -> { |
| HighlightLayer( |
| HighlightedElement.AllComplicationSlots, |
| wireFormat.highlightTint, |
| wireFormat.backgroundTint |
| ) |
| } |
| |
| RenderParametersWireFormat.ELEMENT_TYPE_COMPLICATION -> { |
| HighlightLayer( |
| HighlightedElement.ComplicationSlot(wireFormat.elementComplicationSlotId), |
| wireFormat.highlightTint, |
| wireFormat.backgroundTint |
| ) |
| } |
| |
| RenderParametersWireFormat.ELEMENT_TYPE_USER_STYLE -> { |
| HighlightLayer( |
| HighlightedElement.UserStyle( |
| UserStyleSetting.Id(wireFormat.elementUserStyleSettingId!!) |
| ), |
| wireFormat.highlightTint, |
| wireFormat.backgroundTint |
| ) |
| } |
| |
| else -> null |
| }, |
| wireFormat.idAndTapEventWireFormat?.associate { |
| Pair(it.id, TapEvent(it.x, it.y, Instant.ofEpochMilli(it.calendarTapTimeMillis))) |
| } ?: emptyMap() |
| ) |
| |
| /** @hide */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) |
| public fun toWireFormat(): RenderParametersWireFormat { |
| val idAndTapEventWireFormats = lastComplicationTapDownEvents.map { |
| IdAndTapEventWireFormat( |
| it.key, |
| it.value.xPos, |
| it.value.yPos, |
| it.value.tapTime.toEpochMilli() |
| ) |
| } |
| return when (val thingHighlighted = highlightLayer?.highlightedElement) { |
| is HighlightedElement.AllComplicationSlots -> RenderParametersWireFormat( |
| drawMode.ordinal, |
| computeLayersBitfield(), |
| RenderParametersWireFormat.ELEMENT_TYPE_ALL_COMPLICATIONS, |
| 0, |
| null, |
| highlightLayer!!.highlightTint, |
| highlightLayer.backgroundTint, |
| idAndTapEventWireFormats |
| ) |
| |
| is HighlightedElement.ComplicationSlot -> RenderParametersWireFormat( |
| drawMode.ordinal, |
| computeLayersBitfield(), |
| RenderParametersWireFormat.ELEMENT_TYPE_COMPLICATION, |
| thingHighlighted.id, |
| null, |
| highlightLayer!!.highlightTint, |
| highlightLayer.backgroundTint, |
| idAndTapEventWireFormats |
| ) |
| |
| is HighlightedElement.UserStyle -> RenderParametersWireFormat( |
| drawMode.ordinal, |
| computeLayersBitfield(), |
| RenderParametersWireFormat.ELEMENT_TYPE_USER_STYLE, |
| 0, |
| thingHighlighted.id.value, |
| highlightLayer!!.highlightTint, |
| highlightLayer.backgroundTint, |
| idAndTapEventWireFormats |
| ) |
| |
| else -> RenderParametersWireFormat( |
| drawMode.ordinal, |
| computeLayersBitfield(), |
| RenderParametersWireFormat.ELEMENT_TYPE_NONE, |
| 0, |
| null, |
| Color.BLACK, |
| Color.BLACK, |
| idAndTapEventWireFormats |
| ) |
| } |
| } |
| |
| private fun computeLayersBitfield(): Int { |
| var bitfield = 0 |
| WatchFaceLayer.values().forEachIndexed { index, layer -> |
| if (watchFaceLayers.contains(layer)) { |
| bitfield += 1.shl(index) |
| } |
| } |
| return bitfield |
| } |
| |
| internal fun dump(writer: IndentingPrintWriter) { |
| writer.println("RenderParameters:") |
| writer.increaseIndent() |
| writer.println("drawMode=${drawMode.name}") |
| writer.println("watchFaceLayers=${watchFaceLayers.joinToString()}") |
| writer.println( |
| "lastComplicationTapDownEvents=" + lastComplicationTapDownEvents.map { |
| it.key.toString() + "->" + it.value |
| }.joinToString(", ") |
| ) |
| |
| highlightLayer?.let { |
| writer.println("HighlightLayer:") |
| writer.increaseIndent() |
| when (it.highlightedElement) { |
| is HighlightedElement.AllComplicationSlots -> { |
| writer.println("HighlightedElement.AllComplicationSlots:") |
| } |
| |
| is HighlightedElement.ComplicationSlot -> { |
| writer.println("HighlightedElement.ComplicationSlot:") |
| writer.increaseIndent() |
| writer.println("id=${it.highlightedElement.id}") |
| writer.decreaseIndent() |
| } |
| |
| is HighlightedElement.UserStyle -> { |
| writer.println("HighlightedElement.UserStyle:") |
| writer.increaseIndent() |
| writer.println("id=${it.highlightedElement.id}") |
| writer.decreaseIndent() |
| } |
| } |
| writer.println("highlightTint=${it.highlightTint}") |
| writer.println("backgroundTint=${it.backgroundTint}") |
| writer.decreaseIndent() |
| } |
| writer.decreaseIndent() |
| } |
| |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as RenderParameters |
| |
| if (drawMode != other.drawMode) return false |
| if (watchFaceLayers != other.watchFaceLayers) return false |
| if (highlightLayer != other.highlightLayer) return false |
| if (lastComplicationTapDownEvents != other.lastComplicationTapDownEvents) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = drawMode.hashCode() |
| result = 31 * result + watchFaceLayers.hashCode() |
| result = 31 * result + (highlightLayer?.hashCode() ?: 0) |
| result = 31 * result + lastComplicationTapDownEvents.hashCode() |
| return result |
| } |
| } |