From e4ea51d5a39313a9c0056745fbca63de85a63406 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 24 Jun 2025 23:02:31 +0100 Subject: [PATCH 1/3] Snippets for some Wear OS Tile pages --- .../example/wear/snippets/tile/Animations.kt | 312 ++++++++++++++++++ .../com/example/wear/snippets/tile/Tile.kt | 167 +++++++++- 2 files changed, 476 insertions(+), 3 deletions(-) create mode 100644 wear/src/main/java/com/example/wear/snippets/tile/Animations.kt diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt new file mode 100644 index 000000000..b7c9e6d7a --- /dev/null +++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt @@ -0,0 +1,312 @@ +/* + * Copyright 2025 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 + * + * https://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 com.example.wear.snippets.tile + +import android.annotation.SuppressLint +import androidx.annotation.OptIn +import androidx.wear.protolayout.DeviceParametersBuilders +import androidx.wear.protolayout.DimensionBuilders.degrees +import androidx.wear.protolayout.DimensionBuilders.dp +import androidx.wear.protolayout.LayoutElementBuilders +import androidx.wear.protolayout.LayoutElementBuilders.Arc +import androidx.wear.protolayout.LayoutElementBuilders.ArcLine +import androidx.wear.protolayout.ModifiersBuilders +import androidx.wear.protolayout.ModifiersBuilders.AnimatedVisibility +import androidx.wear.protolayout.ModifiersBuilders.DefaultContentTransitions +import androidx.wear.protolayout.ModifiersBuilders.Modifiers +import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.TypeBuilders.FloatProp +import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters +import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec +import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat +import androidx.wear.protolayout.expression.ProtoLayoutExperimental +import androidx.wear.protolayout.material.CircularProgressIndicator +import androidx.wear.protolayout.material.Text +import androidx.wear.protolayout.material.layouts.EdgeContentLayout +import androidx.wear.tiles.RequestBuilders +import androidx.wear.tiles.TileBuilders.Tile +import androidx.wear.tiles.TileService +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + +private const val RESOURCES_VERSION = "1" +private const val someTileText = "Hello" +private val deviceParameters = DeviceParametersBuilders.DeviceParameters.Builder().build() + +private fun getTileTextToShow(): String { + return "Some text" +} + +/** Demonstrates a sweep transition animation on a [CircularProgressIndicator]. */ +class AnimationSweepTransition : TileService() { + // [START android_wear_tile_animations_sweep-transition] + private var startValue = 15f + private var endValue = 105f + private val animationDurationInMillis = 2000L // 2 seconds + + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + val circularProgressIndicator = + CircularProgressIndicator.Builder() + .setProgress( + FloatProp.Builder(/* static value */ 0.25f) + .setDynamicValue( + // Or you can use some other dynamic object, for example + // from the platform and then at the end of expression + // add animate(). + DynamicFloat.animate( + startValue, + endValue, + AnimationSpec.Builder() + .setAnimationParameters( + AnimationParameters.Builder() + .setDurationMillis(animationDurationInMillis) + .build() + ) + .build(), + ) + ) + .build() + ) + .build() + + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(Timeline.fromLayoutElement(circularProgressIndicator)) + .build() + ) + } + // [END android_wear_tile_animations_sweep_transition] +} + +/** Demonstrates setting the growth direction of an [Arc] and [ArcLine]. */ +@SuppressLint("RestrictedApi") +class AnimationArcDirection : TileService() { + // [START android_wear_tile_animations_set_arc_direction] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + EdgeContentLayout.Builder(deviceParameters) + .setResponsiveContentInsetEnabled(true) + .setEdgeContent( + Arc.Builder() + // Arc should always grow clockwise. + .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE) + .addContent( + ArcLine.Builder() + // Set color, length, thickness, and more. + // Arc should always grow clockwise. + .setArcDirection( + LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_set_arc_direction] +} + +/** Demonstrates smooth fade-in and fade-out transitions. */ +class AnimationFadeTransition : TileService() { + + @OptIn(ProtoLayoutExperimental::class) + // [START android_wear_tile_animations_smooth_fade_slide] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + // Assumes that you've defined a custom helper method called + // getTileTextToShow(). + val tileText = getTileTextToShow() + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, tileText) + .setModifiers( + Modifiers.Builder() + .setContentUpdateAnimation( + AnimatedVisibility.Builder() + .setEnterTransition(DefaultContentTransitions.fadeIn()) + .setExitTransition(DefaultContentTransitions.fadeOut()) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_smooth_fade_slide] +} + +/** Demonstrates smooth slide-in and slide-out transitions. */ +class AnimationSlideTransition : TileService() { + @OptIn(ProtoLayoutExperimental::class) + // [START android_wear_tile_animations_slide] + public override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + // Assumes that you've defined a custom helper method called + // getTileTextToShow(). + val tileText = getTileTextToShow() + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, tileText) + .setModifiers( + Modifiers.Builder() + .setContentUpdateAnimation( + AnimatedVisibility.Builder() + .setEnterTransition( + DefaultContentTransitions.slideIn( + ModifiersBuilders.SLIDE_DIRECTION_LEFT_TO_RIGHT + ) + ) + .setExitTransition( + DefaultContentTransitions.slideOut( + ModifiersBuilders.SLIDE_DIRECTION_LEFT_TO_RIGHT + ) + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + } + // [END android_wear_tile_animations_slide] +} + +/** Demonstrates a rotation transformation. */ +class AnimationRotation : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_rotation] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Set the pivot point 50 dp from the left edge + // and 100 dp from the top edge of the screen. + .setPivotX(dp(50f)) + .setPivotY(dp(100f)) + // Rotate the element 45 degrees clockwise. + .setRotation(degrees(45f)) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_rotation] + } +} + +/** Demonstrates a scaling transformation. */ +class AnimationScaling : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_scaling] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Set the pivot point 50 dp from the left edge + // and 100 dp from the top edge of the screen. + .setPivotX(dp(50f)) + .setPivotY(dp(100f)) + // Shrink the element by a scale factor + // of 0.5 horizontally and 0.75 vertically. + .setScaleX(FloatProp.Builder(0.5f).build()) + .setScaleY( + FloatProp.Builder(0.75f).build() + ) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_scaling] + } +} + +/** Demonstrates a geometric translation. */ +class AnimationGeometricTranslation : TileService() { + override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture { + // [START android_wear_tile_animations_geometric_translation] + return Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder(this, someTileText) + .setModifiers( + Modifiers.Builder() + .setTransformation( + ModifiersBuilders.Transformation.Builder() + // Translate (move) the element 60 dp to the right + // and 80 dp down. + .setTranslationX(dp(60f)) + .setTranslationY(dp(80f)) + .build() + ) + .build() + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_animations_geometric_translation] + } +} diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt index 58cbaa758..86e59398c 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -16,9 +16,19 @@ package com.example.wear.snippets.tile +import android.Manifest +import android.content.Context +import androidx.annotation.RequiresPermission import androidx.wear.protolayout.ColorBuilders.argb +import androidx.wear.protolayout.DimensionBuilders +import androidx.wear.protolayout.LayoutElementBuilders import androidx.wear.protolayout.ResourceBuilders.Resources +import androidx.wear.protolayout.TimelineBuilders import androidx.wear.protolayout.TimelineBuilders.Timeline +import androidx.wear.protolayout.TypeBuilders +import androidx.wear.protolayout.expression.DynamicBuilders +import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString +import androidx.wear.protolayout.expression.PlatformHealthSources import androidx.wear.protolayout.material.Text import androidx.wear.protolayout.material.Typography import androidx.wear.tiles.RequestBuilders @@ -26,6 +36,7 @@ import androidx.wear.tiles.RequestBuilders.ResourcesRequest import androidx.wear.tiles.TileBuilders.Tile import androidx.wear.tiles.TileService import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture private const val RESOURCES_VERSION = "1" @@ -48,10 +59,160 @@ class MyTileService : TileService() { ) override fun onTileResourcesRequest(requestParams: ResourcesRequest) = + Futures.immediateFuture(Resources.Builder().setVersion(RESOURCES_VERSION).build()) +} + +// [END android_wear_tile_mytileservice] + +fun simpleLayout(context: Context) = + Text.Builder(context, "Hello World!") + .setTypography(Typography.TYPOGRAPHY_BODY1) + .setColor(argb(0xFFFFFFFF.toInt())) + .build() + +class PeriodicUpdatesSingleEntry : TileService() { + // [START android_wear_tile_periodic_single_entry] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + val tile = + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + // We add a single timeline entry when our layout is fixed, and + // we don't know in advance when its contents might change. + .setTileTimeline(Timeline.fromLayoutElement(simpleLayout(this))) + .build() + return Futures.immediateFuture(tile) + } + // [END android_wear_tile_periodic_single_entry] +} + +fun emptySpacer(): LayoutElementBuilders.LayoutElement { + return LayoutElementBuilders.Spacer.Builder() + .setWidth(DimensionBuilders.dp(0f)) + .setHeight(DimensionBuilders.dp(0f)) + .build() +} + +fun getNoMeetingsLayout(): LayoutElementBuilders.Layout { + return LayoutElementBuilders.Layout.Builder().setRoot(emptySpacer()).build() +} + +fun getMeetingLayout(meeting: Meeting): LayoutElementBuilders.Layout { + return LayoutElementBuilders.Layout.Builder().setRoot(emptySpacer()).build() +} + +data class Meeting(val name: String, val dateTimeMillis: Long) + +object MeetingsRepo { + fun getMeetings(): List { + // TODO: Replace with actual meeting data + val now = System.currentTimeMillis() + return listOf( + Meeting("Meeting 1", now + 1 * 60 * 60 * 1000), // 1 hour from now + Meeting("Meeting 2", now + 3 * 60 * 60 * 1000), // 3 hours from now + ) + } +} + +class PeriodicUpdatesTimebound : TileService() { + // [START android_wear_tile_periodic_timebound] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture { + val timeline = Timeline.Builder() + + // Add fallback "no meetings" entry + // Use the version of TimelineEntry that's in androidx.wear.protolayout. + timeline.addTimelineEntry( + TimelineBuilders.TimelineEntry.Builder().setLayout(getNoMeetingsLayout()).build() + ) + + // Retrieve a list of scheduled meetings + val meetings = MeetingsRepo.getMeetings() + // Add a timeline entry for each meeting + meetings.forEach { meeting -> + timeline.addTimelineEntry( + TimelineBuilders.TimelineEntry.Builder() + .setLayout(getMeetingLayout(meeting)) + .setValidity( + // The tile should disappear when the meeting begins + // Use the version of TimeInterval that's in + // androidx.wear.protolayout. + TimelineBuilders.TimeInterval.Builder() + .setEndMillis(meeting.dateTimeMillis) + .build() + ) + .build() + ) + } + + val tile = + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setTileTimeline(timeline.build()) + .build() + return Futures.immediateFuture(tile) + } + // [END android_wear_tile_periodic_timebound] +} + +fun getWeatherLayout() = emptySpacer() + +class PeriodicUpdatesRefresh : TileService() { + // [START android_wear_tile_periodic_refresh] + override fun onTileRequest( + requestParams: RequestBuilders.TileRequest + ): ListenableFuture = Futures.immediateFuture( - Resources.Builder() - .setVersion(RESOURCES_VERSION) + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes + .setTileTimeline(Timeline.fromLayoutElement(getWeatherLayout())) .build() ) + // [END android_wear_tile_periodic_refresh] +} + +class DynamicHeartRate : TileService() { + @RequiresPermission(Manifest.permission.BODY_SENSORS) + // [START android_wear_tile_dynamic_heart_rate] + override fun onTileRequest(requestParams: RequestBuilders.TileRequest) = + Futures.immediateFuture( + Tile.Builder() + .setResourcesVersion(RESOURCES_VERSION) + .setFreshnessIntervalMillis(60 * 60 * 1000) // 60 minutes + .setTileTimeline( + Timeline.fromLayoutElement( + Text.Builder( + this, + TypeBuilders.StringProp.Builder("--") + .setDynamicValue( + PlatformHealthSources.heartRateBpm() + .format() + .concat(DynamicBuilders.DynamicString.constant(" bpm")) + ) + .build(), + TypeBuilders.StringLayoutConstraint.Builder("000").build(), + ) + .build() + ) + ) + .build() + ) + // [END android_wear_tile_dynamic_heart_rate] +} + +@RequiresPermission( + allOf = [Manifest.permission.ACTIVITY_RECOGNITION, Manifest.permission.BODY_SENSORS] +) +fun dynamicExpression1() { + // [START android_wear_tile_dynamic_expressions1] + val personHealthInfo = + DynamicString.constant("This person has walked ") + .concat(PlatformHealthSources.dailySteps().div(1000).format()) + .concat(DynamicString.constant("thousands of steps and has a current heart rate ")) + .concat(PlatformHealthSources.heartRateBpm().format()) + .concat(DynamicString.constant(" beats per minute")) + // [END android_wear_tile_dynamic_expressions2] } -// [END android_wear_tile_mytileservice] From c498a0b214f29051ff649676cb9a145c9b137d79 Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 24 Jun 2025 23:06:07 +0100 Subject: [PATCH 2/3] Fix tags, imports --- .../com/example/wear/snippets/tile/Animations.kt | 2 +- .../java/com/example/wear/snippets/tile/Tile.kt | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt index b7c9e6d7a..6572a1f97 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt @@ -53,7 +53,7 @@ private fun getTileTextToShow(): String { /** Demonstrates a sweep transition animation on a [CircularProgressIndicator]. */ class AnimationSweepTransition : TileService() { - // [START android_wear_tile_animations_sweep-transition] + // [START android_wear_tile_animations_sweep_transition] private var startValue = 15f private var endValue = 105f private val animationDurationInMillis = 2000L // 2 seconds diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt index 86e59398c..ee92f1bf1 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -27,7 +27,6 @@ import androidx.wear.protolayout.TimelineBuilders import androidx.wear.protolayout.TimelineBuilders.Timeline import androidx.wear.protolayout.TypeBuilders import androidx.wear.protolayout.expression.DynamicBuilders -import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString import androidx.wear.protolayout.expression.PlatformHealthSources import androidx.wear.protolayout.material.Text import androidx.wear.protolayout.material.Typography @@ -202,17 +201,3 @@ class DynamicHeartRate : TileService() { ) // [END android_wear_tile_dynamic_heart_rate] } - -@RequiresPermission( - allOf = [Manifest.permission.ACTIVITY_RECOGNITION, Manifest.permission.BODY_SENSORS] -) -fun dynamicExpression1() { - // [START android_wear_tile_dynamic_expressions1] - val personHealthInfo = - DynamicString.constant("This person has walked ") - .concat(PlatformHealthSources.dailySteps().div(1000).format()) - .concat(DynamicString.constant("thousands of steps and has a current heart rate ")) - .concat(PlatformHealthSources.heartRateBpm().format()) - .concat(DynamicString.constant(" beats per minute")) - // [END android_wear_tile_dynamic_expressions2] -} From a622eeafddc88f575665e23d33de0e919e3e417d Mon Sep 17 00:00:00 2001 From: Michael Stillwell Date: Tue, 24 Jun 2025 23:23:58 +0100 Subject: [PATCH 3/3] Fixes --- .../main/java/com/example/wear/snippets/tile/Animations.kt | 4 ++-- wear/src/main/java/com/example/wear/snippets/tile/Tile.kt | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt index 6572a1f97..b941cb53d 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Animations.kt @@ -135,7 +135,7 @@ class AnimationArcDirection : TileService() { class AnimationFadeTransition : TileService() { @OptIn(ProtoLayoutExperimental::class) - // [START android_wear_tile_animations_smooth_fade_slide] + // [START android_wear_tile_animations_fade] public override fun onTileRequest( requestParams: RequestBuilders.TileRequest ): ListenableFuture { @@ -164,7 +164,7 @@ class AnimationFadeTransition : TileService() { .build() ) } - // [END android_wear_tile_animations_smooth_fade_slide] + // [END android_wear_tile_animations_fade] } /** Demonstrates smooth slide-in and slide-out transitions. */ diff --git a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt index ee92f1bf1..9e2a9508e 100644 --- a/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt +++ b/wear/src/main/java/com/example/wear/snippets/tile/Tile.kt @@ -105,7 +105,6 @@ data class Meeting(val name: String, val dateTimeMillis: Long) object MeetingsRepo { fun getMeetings(): List { - // TODO: Replace with actual meeting data val now = System.currentTimeMillis() return listOf( Meeting("Meeting 1", now + 1 * 60 * 60 * 1000), // 1 hour from now