pebble/devsite/source/_guides/user-interfaces/unobstructed-area.md
2025-02-24 18:58:29 -08:00

13 KiB
Raw Permalink Blame History

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
Graphics
LayerUpdateProc
UnobstructedArea
title url
Simple Example https://github.com/pebble-examples/unobstructed-area-example
title url
Watchface Tutorial https://github.com/pebble-examples/watchface-tutorial-unobstructed

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.

Unobstructed-watchfaces

Sample watchfaces with Timeline Quick View overlay

Obstructed-watchfaces

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 youre 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 the unobstructed_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, were going to hide a TextLayer containing the current date, while the screen is obstructed.

Just before the Timeline Quick View appears, were going to hide the TextLayer and well 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 isnt a current obstruction, that means the obstruction must be about to appear, so well 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 isnt obstructed when this event fires, then the obstruction must have just cleared and well 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, were 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 were using percentages, it doesnt 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.

Unobstructed animation >{pebble-screenshot,pebble-screenshot--time-black}

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;