Skip to content

Commit d542acf

Browse files
committed
Update README.md
1 parent 28d16bd commit d542acf

1 file changed

Lines changed: 311 additions & 2 deletions

File tree

README.md

Lines changed: 311 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,311 @@
1-
# ComposeOClock
2-
Draw Wear OS Watch Faces with Compose Canvas (& runtime)
1+
# Compose O'Clock
2+
3+
Draw Wear OS Watch Faces with Compose Canvas (& runtime)
4+
5+
Compose O'Clock is the fastest way to develop a good looking and efficient Watch Face for Wear OS.
6+
7+
Using this SDK, knowing how to use Jetpack Compose is about 96% of the knowledge you need to know to
8+
successfully get a proper watch face that you can also see live in a phone app.
9+
10+
The 4% remaining to develop a good watch face app is documented here.
11+
12+
## Features
13+
14+
-**Live-preview** your Watch Face in Android Studio, a phone, or a watch
15+
-**Adaptive refresh-rate** out of the box (up to 60fps on watches, and 120fps on phones)
16+
- ✅ Compose Canvas and text APIs (**Kotlin-friendly!**)
17+
- ✅ Shader effects (Wear OS 4+)
18+
- ✅ Auto recomposition or re-render on state change
19+
- ✅ Convenience **time APIs** for clocks
20+
- ✅ Watch Face **specific optimizations**
21+
-**Instant** style changes, including while in ambient mode
22+
- ✅ Render your Watch Faces **in any Composable** in the phone or watch app
23+
- ✅ Full freedom for **complications** rendering (custom data like weather, step counts, heart rate…)
24+
-**Technical support** available for pro licenses
25+
26+
## Getting started
27+
28+
This repository contains a starter project.
29+
After cloning the project and opening it in Android Studio, you can navigate to the Watch Face
30+
entry point: [SampleWatchFaceService.kt](app-watch/src/main/kotlin/SampleWatchFaceService.kt).
31+
32+
It all starts with this function inside `ComposeWatchFaceService`:
33+
34+
```kotlin
35+
@Composable
36+
override fun Content(complicationData: Map<Int, StateFlow<ComplicationData>>) {
37+
MyWatchFace()
38+
}
39+
```
40+
41+
Here's how a simple, typical Watch Face looks like:
42+
43+
```kotlin
44+
@Composable
45+
fun MyWatchFace() {
46+
FancyBackground()
47+
CoolHourPips()
48+
SomeExtraStuff()
49+
HourHand()
50+
MinutesHand()
51+
SecondsHand()
52+
}
53+
```
54+
55+
Inside those other composables, you need to use `OClockCanvas` at some point to draw things.
56+
57+
It's exactly like `Canvas` from Compose, except that it also takes an optional `onTap` parameter,
58+
and that it will work in a Watch Face.
59+
60+
Here's how `SecondsHand()` from above could look like:
61+
62+
```kotlin
63+
@Composable
64+
private fun SecondsHand() {
65+
val time = LocalTime.current
66+
val isAmbient by LocalIsAmbient.current // We use delegation to get a Boolean here
67+
OClockCanvas {
68+
if (isAmbient) return@OClockCanvas // Don't read seconds in ambient mode, return early.
69+
// Because we read the time just below,
70+
// this will auto-re-render when the second changes.
71+
val degrees = time.seconds * 6f // 60s × 6 -> 360°
72+
val top = size.height / 32f
73+
drawLine( // Can use anything from Compose DrawScope
74+
someColor,
75+
start = center.copy(y = top).rotate(degrees),
76+
end = center,
77+
strokeWidth = 2.dp.toPx(),
78+
blendMode = BlendMode.Lighten, // Because why not! More on that below.
79+
cap = StrokeCap.Round
80+
)
81+
}
82+
}
83+
```
84+
85+
## `OClockCanvas`
86+
87+
### One canvas for all by default
88+
89+
`OClockCanvas` is actually a bit more powerful than `Canvas`:
90+
All `OClockCanvas` will share the same underlying canvas, without layering by default,
91+
which means that if you use a `BlendMode`, it will apply over everything that has been drawn yet.
92+
93+
It can have multiple use cases, like:
94+
- Tinting what's been drawn before (without having to touch those components), using `BlendMode.SrcIn`
95+
- Drawing on the pixels that haven't been drawn yet (e.g. draw a background last), using `BlendMode.DstOver`
96+
- Avoid overlaps, using `BlendMode.Xor` for example.
97+
- Make overlaps visible, using `BlendMode.Plus` or `BlendMode.Lighten` for example.
98+
99+
### Taps
100+
101+
Wear OS Watch Faces support taps (but not swipes, which the system already uses for tiles, notifications, and quick settings).
102+
103+
When displaying an `OClockCanvas` element in a regular compose hierarchy (on the phone, in a watch, or in Android Studio preview),
104+
the behavior will be exactly the same as when set as a Watch Face: a swipe will cause the tap to be cancelled. Proper taps will be registered.
105+
106+
Make sure your `onTap` handlers return `true` if the tap occurs in the region where you support actions, and `false` otherwise,
107+
**regardless of whether it's a tap down or up**.
108+
109+
Also make sure tap actions are fired only on tap-up, and make sure you're not leaving broken/unwanted state on tap cancel.
110+
111+
In addition to the `event`, the `onTap` lambda has an `OnTapScope` receiver, which contains the `size`, `center`, and density info, so it can be used along with the `position` `Offset` from the event to determine whether the tap should be handled or not.
112+
113+
## Complications
114+
115+
The complications APIs from `androidx.wear.watchface` are… well… complicated.
116+
Compose O'Clock provides several Compose friendly extensions that cover all types of complications
117+
that Wear OS supports.
118+
119+
_Complication related code won't crash when running on a phone. Showing placeholder data is supported,
120+
and it's possible to forward live the actual complication data from the watch face to the phone app._
121+
122+
### For `ComplicationText?` & `ComplicationText`
123+
124+
```kotlin
125+
@Composable
126+
fun ComplicationText?.rememberMeasuredAsState(
127+
default: String,
128+
callMeasure: TextMeasurer.(string: String) -> TextLayoutResult
129+
): State<TextLayoutResult>
130+
131+
@Composable
132+
fun ComplicationText.rememberMeasuredAsState(
133+
callMeasure: TextMeasurer.(string: String) -> TextLayoutResult
134+
): State<TextLayoutResult>
135+
```
136+
137+
Note that `TextLayoutResult` can directly be used in `drawText` inside `OClockCanvas`.
138+
139+
### For images/icons in `ComplicationData` subclasses
140+
141+
```kotlin
142+
@Composable
143+
inline fun NoPermissionComplicationData.rememberDrawableAsState(
144+
preferSmallImage: Boolean = false,
145+
tint: Color = Color.Unspecified,
146+
tintBlendMode: BlendMode = BlendMode.SrcIn,
147+
ambientTint: Color = tint,
148+
ambientTintBlendMode: BlendMode = tintBlendMode
149+
): State<Drawable?>
150+
151+
@Composable
152+
inline fun LongTextComplicationData.rememberDrawableAsState(
153+
preferSmallImage: Boolean = false,
154+
tint: Color = Color.Unspecified,
155+
tintBlendMode: BlendMode = BlendMode.SrcIn,
156+
ambientTint: Color = tint,
157+
ambientTintBlendMode: BlendMode = tintBlendMode
158+
): State<Drawable?>
159+
160+
@Composable
161+
inline fun MonochromaticImageComplicationData.rememberDrawableAsState(
162+
tint: Color = Color.Unspecified,
163+
tintBlendMode: BlendMode = BlendMode.SrcIn,
164+
ambientTint: Color = tint,
165+
ambientTintBlendMode: BlendMode = tintBlendMode
166+
): State<Drawable?>
167+
168+
@Composable
169+
inline fun PhotoImageComplicationData.rememberDrawableAsState(
170+
tint: Color = Color.Unspecified,
171+
tintBlendMode: BlendMode = BlendMode.SrcIn,
172+
): State<Drawable?>
173+
174+
@Composable
175+
inline fun RangedValueComplicationData.rememberDrawableAsState(
176+
preferSmallImage: Boolean = false,
177+
tint: Color = Color.Unspecified,
178+
tintBlendMode: BlendMode = BlendMode.SrcIn,
179+
ambientTint: Color = tint,
180+
ambientTintBlendMode: BlendMode = tintBlendMode
181+
): State<Drawable?>
182+
183+
@Composable
184+
inline fun ShortTextComplicationData.rememberDrawableAsState(
185+
preferSmallImage: Boolean = false,
186+
tint: Color = Color.Unspecified,
187+
tintBlendMode: BlendMode = BlendMode.SrcIn,
188+
ambientTint: Color = tint,
189+
ambientTintBlendMode: BlendMode = tintBlendMode
190+
): State<Drawable?>
191+
192+
@Composable
193+
inline fun SmallImageComplicationData.rememberDrawableAsState(
194+
tint: Color = Color.Unspecified,
195+
tintBlendMode: BlendMode = BlendMode.SrcIn,
196+
ambientTint: Color = tint,
197+
ambientTintBlendMode: BlendMode = tintBlendMode
198+
): State<Drawable?>
199+
200+
@RequiresApi(33)
201+
@Composable
202+
inline fun GoalProgressComplicationData.rememberDrawableAsState(
203+
preferSmallImage: Boolean = false,
204+
tint: Color = Color.Unspecified,
205+
tintBlendMode: BlendMode = BlendMode.SrcIn,
206+
ambientTint: Color = tint,
207+
ambientTintBlendMode: BlendMode = tintBlendMode
208+
): State<Drawable?>
209+
210+
@RequiresApi(33)
211+
@Composable
212+
inline fun WeightedElementsComplicationData.rememberDrawableAsState(
213+
preferSmallImage: Boolean = false,
214+
tint: Color = Color.Unspecified,
215+
tintBlendMode: BlendMode = BlendMode.SrcIn,
216+
ambientTint: Color = tint,
217+
ambientTintBlendMode: BlendMode = tintBlendMode
218+
): State<Drawable?>
219+
```
220+
221+
### Other complications related convenience extensions for advanced use cases
222+
223+
```kotlin
224+
fun Drawable.setTintBlendMode(blendMode: BlendMode) // BlendMode from Compose UI graphics
225+
fun Drawable.setTint(color: Color) // Color from Compose UI graphics
226+
227+
@Composable
228+
fun SmallImage.rememberDrawableAsState(
229+
tint: Color = Color.Unspecified,
230+
tintBlendMode: BlendMode = BlendMode.SrcIn,
231+
ambientTint: Color = tint,
232+
ambientTintBlendMode: BlendMode = tintBlendMode,
233+
): State<Drawable?>
234+
235+
@Composable
236+
fun MonochromaticImage.rememberDrawableAsState(
237+
tint: Color = Color.Unspecified,
238+
tintBlendMode: BlendMode = BlendMode.SrcIn,
239+
ambientTint: Color = tint,
240+
ambientTintBlendMode: BlendMode = tintBlendMode,
241+
): State<Drawable?>
242+
243+
@Composable
244+
fun rememberDrawableAsState(
245+
monochromaticImage: MonochromaticImage?,
246+
smallImage: SmallImage?,
247+
preferSmallImage: Boolean = false,
248+
tint: Color = Color.Unspecified,
249+
tintBlendMode: BlendMode = BlendMode.SrcIn,
250+
ambientTint: Color = tint,
251+
ambientTintBlendMode: BlendMode = tintBlendMode,
252+
): State<Drawable?>
253+
```
254+
255+
## License
256+
257+
Compose O'Clock has a dual license:
258+
259+
It's free to use in debug apps, and you are free to use the dependency in any project that is compatible with the Apache 2.0 license.
260+
261+
However, releasing an app using Compose O'Clock requires a commercial license.
262+
263+
If you want to release a Watch Face app for Wear OS that uses Compose O'Clock, please send an email to [composeoclock@splitties.org](mailto:composeoclock@splitties.org)
264+
265+
## Why Compose O'Clock?
266+
267+
Currently, there are 3 ways to make a Watch Face for Wear OS:
268+
269+
1. `androidx.wear.watchface`
270+
2. Samsung Watch Face Studio (WFS)
271+
3. XML based Watch Face Format (WFF)
272+
273+
`androidx.wear.watchface` gives a lot of flexibility, but the APIs require a lot of boilerplate,
274+
which can steer you away from finishing and refining your watch face idea.
275+
Also, you get no IDE preview.
276+
277+
Compose O'Clock uses `androidx.wear.watchface` internally, to ensure the best compatibility.
278+
However, the developer/designer experience is much improved because the API is simplified,
279+
many optimizations require much fewer code, or no code, and you get super fast live-preview in
280+
Android Studio, or on a phone app. You get both WYSIWYG (What You See is What You Get),
281+
and the power of Kotlin code.
282+
283+
Samsung Watch Face Studio (WFS) is a WYSIWYG editor.
284+
Features are limited, making it likely your Watch Face will look like yet another basic one.
285+
Samsung WFS outputs XML based Watch Face Format (WFF), so all WFF limitations apply to WFS.
286+
287+
XML based Watch Face Format (WFF) has many limitations. Here's a non exhaustive list:
288+
- Works only on Wear OS 4: many non Samsung and non Google watches are still on Wear OS 2 or 3. 🚫
289+
- Capped at 15fps: sensor based animations, and other animations never look smooth. 🐌
290+
- No live-preview: you need to re-build and re-deploy each time you want to see your changes ⏳
291+
- Settings are very limited (numbers and gradients are not supported, unless you hack deeply)
292+
- You can't control the order in which settings will appear 🤷🏻🤪
293+
- No support for in-app purchases(!) 💸
294+
- No usage statistics possible
295+
- Near-zero connection with your users
296+
- No interactivity apart from switching raster images, and launching complications 🔒
297+
- Doesn't support all types of complications
298+
- No runtime logic except for basic arithmetic expressions
299+
- No support for shaders
300+
- No support for blend modes (except masking)
301+
- On phone editor is slow and hard to discover 🐌
302+
- As of Wear OS 4, the WFF renderer is no better than what's possible with `androidx.wear.watchface`
303+
- Publishing on the Play Store is as long as any other app, if not more, despite reduced risks
304+
305+
If, for some reason, you still want to try WFF, without the inconvenience of writing XML by hand,
306+
we made a Kotlin DSL based on kotlinx.html: https://github.com/Splitties/WffDsl
307+
We decided to not publish it because the dev UX and product potential isn't great at the moment.
308+
309+
We are waiting for the Wear OS team at Google to make an on-device API, so executable watch faces,
310+
like ones made with Compose O'Clock, or `androidx.wear.watchface`, can update the WFF that
311+
will be used for ambient mode or energy saver mode (which will make sense once the WFF renderer is improved).

0 commit comments

Comments
 (0)