13 KiB
title | description | guide_group | order | related_docs | related_examples | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Unobstructed Area | Details on how to use the UnobstructedArea API to adapt your watchface layout when the screen is partially obstructed by a system overlay. | user-interfaces | 5 |
|
|
The UnobstructedArea
API, added in SDK 4.0, allows developers to dynamically
adapt their watchface design when an area of the screen is partially obstructed
by a system overlay. Currently, the Timeline Quick View feature is the only
system overlay.
Developers are not required to adjust their designs to cater for such system
overlays, but by using the UnobstructedArea
API they can detect changes to
the available screen real-estate and then move, scale, or hide their layers to
achieve an optimal layout while the screen is partially obscured.
Sample watchfaces with Timeline Quick View overlay
Potential versions of sample watchfaces using the UnobstructedArea API
Determining the Unobstructed Bounds
Prior to SDK 4.0, when displaying layers on screen you would calculate the
size of the display using layer_get_bounds()
and then scale and position
your layers accordingly. Developers can now calculate the size of a layer,
excluding system obstructions, using the new
layer_get_unobstructed_bounds()
.
static Layer *s_window_layer;
static TextLayer *s_text_layer;
static void main_window_load(Window *window) {
s_window_layer = window_get_root_layer(window);
GRect unobstructed_bounds = layer_get_unobstructed_bounds(s_window_layer);
s_text_layer = text_layer_create(GRect(0, unobstructed_bounds.size.h / 4, unobstructed_bounds.size.w, 50));
}
If you still want a fullscreen entities such as a background image, regardless of any obstructions, just combine both techniques as follows:
static Layer *s_window_layer;
static BitmapLayer *s_image_layer;
static TextLayer *s_text_layer;
static void main_window_load(Window *window) {
s_window_layer = window_get_root_layer(window);
GRect full_bounds = layer_get_bounds(s_window_layer);
GRect unobstructed_bounds = layer_get_unobstructed_bounds(s_window_layer);
s_image_layer = bitmap_layer_create(full_bounds);
s_text_layer = text_layer_create(GRect(0, unobstructed_bounds.size.h / 4, unobstructed_bounds.size.w, 50));
}
The approach outlined above is perfectly fine to use when your watchface is initially launched, but you’re also responsible for handling the obstruction appearing and disappearing while your watchface is running.
Rendering with LayerUpdateProc
If your application controls its own rendering process using a
LayerUpdateProc
you can just dynamically adjust your rendering
each time your layer updates.
In this example, we use layer_get_unobstructed_bounds()
instead of
layer_get_bounds()
. The graphics are then positioned or scaled based upon
the available screen real-estate, instead of the screen dimensions.
You must ensure you fill the entire window, not just the unobstructed area, when drawing the screen - failing to do so may cause unexpected graphics to be drawn behind the quick view, during animations.
static void hands_update_proc(Layer *layer, GContext *ctx) {
GRect bounds = layer_get_unobstructed_bounds(layer);
GPoint center = grect_center_point(&bounds);
const int16_t second_hand_length = (bounds.size.w / 2);
time_t now = time(NULL);
struct tm *t = localtime(&now);
int32_t second_angle = TRIG_MAX_ANGLE * t->tm_sec / 60;
GPoint second_hand = {
.x = (int16_t)(sin_lookup(second_angle) * (int32_t)second_hand_length / TRIG_MAX_RATIO) + center.x,
.y = (int16_t)(-cos_lookup(second_angle) * (int32_t)second_hand_length / TRIG_MAX_RATIO) + center.y,
};
// second hand
graphics_context_set_stroke_color(ctx, GColorWhite);
graphics_draw_line(ctx, second_hand, center);
// minute/hour hand
graphics_context_set_fill_color(ctx, GColorWhite);
graphics_context_set_stroke_color(ctx, GColorBlack);
gpath_rotate_to(s_minute_arrow, TRIG_MAX_ANGLE * t->tm_min / 60);
gpath_draw_filled(ctx, s_minute_arrow);
gpath_draw_outline(ctx, s_minute_arrow);
gpath_rotate_to(s_hour_arrow, (TRIG_MAX_ANGLE * (((t->tm_hour % 12) * 6) +
(t->tm_min / 10))) / (12 * 6));
gpath_draw_filled(ctx, s_hour_arrow);
gpath_draw_outline(ctx, s_hour_arrow);
// dot in the middle
graphics_context_set_fill_color(ctx, GColorBlack);
graphics_fill_rect(ctx, GRect(bounds.size.w / 2 - 1, bounds.size.h / 2 - 1, 3,
3), 0, GCornerNone);
}
Using Unobstructed Area Handlers
If you are not overriding the default rendering of a Layer
, you will need to
subscribe to one or more of the UnobstructedAreaHandlers
to adjust the sizes
and positions of layers.
There are 3 events available using UnobstructedAreaHandlers
.
These events will notify you when the unobstructed area is: about to change,
is currently changing, or has finished changing. You can use these handlers
to perform any necessary alterations to your layout.
.will_change
- an event to inform you that the unobstructed area size is about
to change. This provides a GRect
which lets you know the size of the screen
after the change has finished.
.change
- an event to inform you that the unobstructed area size is currently
changing. This event is called several times during the animation of an
obstruction appearing or disappearing. AnimationProgress
is provided to let
you know the percentage of progress towards completion.
.did_change
- an event to inform you that the unobstructed area size has
finished changing. This is useful for deinitializing or destroying anything
created or allocated in the will_change handler.
These handlers are optional, but at least one must be specified for a valid subscription. In the following example, we subscribe to two of the three available handlers.
NOTE: You must construct the
UnobstructedAreaHandlers
object before passing it to theunobstructed_area_service_subscribe()
method.
UnobstructedAreaHandlers handlers = {
.will_change = prv_unobstructed_will_change,
.did_change = prv_unobstructed_did_change
};
unobstructed_area_service_subscribe(handlers, NULL);
Hiding Layers
In this example, we’re going to hide a TextLayer
containing the current
date, while the screen is obstructed.
Just before the Timeline Quick View appears, we’re going to hide the
TextLayer
and we’ll show it again after the Timeline Quick View disappears.
static Window *s_main_window;
static Layer *s_window_layer;
static TextLayer *s_date_layer;
Subscribe to the .did_change
and .will_change
events:
static void main_window_load(Window *window) {
// Keep a handle on the root layer
s_window_layer = window_get_root_layer(window);
// Subscribe to the will_change and did_change events
UnobstructedAreaHandlers handlers = {
.will_change = prv_unobstructed_will_change,
.did_change = prv_unobstructed_did_change
};
unobstructed_area_service_subscribe(handlers, NULL);
}
The will_change
event fires before the size of the unobstructed area changes,
so we need to establish whether the screen is already obstructed, or about to
become obstructed. If there isn’t a current obstruction, that means the
obstruction must be about to appear, so we’ll need to hide our data layer.
static void prv_unobstructed_will_change(GRect final_unobstructed_screen_area,
void *context) {
// Get the full size of the screen
GRect full_bounds = layer_get_bounds(s_window_layer);
if (!grect_equal(&full_bounds, &final_unobstructed_screen_area)) {
// Screen is about to become obstructed, hide the date
layer_set_hidden(text_layer_get_layer(s_date_layer), true);
}
}
The did_change
event fires after the unobstructed size changes, so we can
perform the same check to see whether the screen is already obstructed, or
about to become obstructed. If the screen isn’t obstructed when this event
fires, then the obstruction must have just cleared and we’ll need to display
our date layer again.
static void prv_unobstructed_did_change(void *context) {
// Get the full size of the screen
GRect full_bounds = layer_get_bounds(s_window_layer);
// Get the total available screen real-estate
GRect bounds = layer_get_unobstructed_bounds(s_window_layer);
if (grect_equal(&full_bounds, &bounds)) {
// Screen is no longer obstructed, show the date
layer_set_hidden(text_layer_get_layer(s_date_layer), false);
}
}
Animating Layer Positions
The .change
event will fire several times while the unobstructed area is
changing size. This allows us to use this event to make our layers appear to
slide-in or slide-out of their initial positions.
In this example, we’re going to use percentages to position two text layers vertically. One layer at the top of the screen and one layer at the bottom. When the screen is obstructed, these two layers will shift to be closer together. Because we’re using percentages, it doesn’t matter if the unobstructed area is increasing or decreasing, our text layers will always be relatively positioned in the available space.
static const uint8_t s_offset_top_percent = 33;
static const uint8_t s_offset_bottom_percent = 10;
A simple helper function to simulate percentage based coordinates:
uint8_t relative_pixel(int16_t percent, int16_t max) {
return (max * percent) / 100;
}
Subscribe to the change event:
static void main_window_load(Window *window) {
UnobstructedAreaHandlers handler = {
.change = prv_unobstructed_change
};
unobstructed_area_service_subscribe(handler, NULL);
}
Move the text layer each time the unobstructed area size changes:
static void prv_unobstructed_change(AnimationProgress progress, void *context) {
// Get the total available screen real-estate
GRect bounds = layer_get_unobstructed_bounds(s_window_layer);
// Get the current position of our top text layer
GRect frame = layer_get_frame(text_layer_get_layer(s_top_text_layer));
// Shift the Y coordinate
frame.origin.y = relative_pixel(s_offset_top_percent, bounds.size.h);
// Apply the new location
layer_set_frame(text_layer_get_layer(s_top_text_layer), frame);
// Get the current position of our bottom text layer
GRect frame2 = layer_get_frame(text_layer_get_layer(s_top_text_layer));
// Shift the Y coordinate
frame2.origin.y = relative_pixel(s_offset_bottom_percent, bounds.size.h);
// Apply the new position
layer_set_frame(text_layer_get_layer(s_bottom_text_layer), frame2);
}
Toggling Timeline Quick View
The pebble
tool which shipped as part of SDK 4.0,
allows developers to enable and disable Timeline Quick View, which is
incredibly useful for debugging purposes.
To enable Timeline Quick View, you can use:
$ pebble emu-set-timeline-quick-view on
To disable Timeline Quick View, you can use:
$ pebble emu-set-timeline-quick-view off
CloudPebble does not currently support toggling Timeline Quick View, but it will be added as part of a future update.
Additional Considerations
If you're scaling or moving layers based on the unobstructed area, you must ensure you fill the entire window, not just the unobstructed area. Failing to do so may cause unexpected graphics to be drawn behind the quick view, during animations.
At present, Timeline Quick View is not currently planned for the Chalk platform.
For design reference, the height of the Timeline Quick View overlay will be 51px in total, which includes a 2px border, but this may vary on newer platforms and and the height should always be calculated at runtime.
// Calculate the actual height of the Timeline Quick View
s_window_layer = window_get_root_layer(window);
GRect fullscreen = layer_get_bounds(s_window_layer);
GRect unobstructed_bounds = layer_get_unobstructed_bounds(s_window_layer);
int16_t obstruction_height = fullscreen.size.h - unobstructed_bounds.size.h;