17 KiB
title | description | guide_group | order | related_docs | related_examples | platforms | |||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Round App UI | Details on how to use the Pebble SDK to create layouts specifically for round displays. | user-interfaces | 4 |
|
|
|
This guide is about creating round apps in code. For advice on designing a round app, read {% guide_link design-and-interaction/in-the-round %}.
With the addition of Pebble Time Round (the Chalk platform) to the Pebble family, developers face a new challenge - circular apps! With this display shape, traditional layouts will not display properly due to the obscuring of the corners. Another potential issue is the increased display resolution. Any UI elements that were not previously centered correctly (or drawn with hardcoded coordinates) will also display incorrectly.
However, the Pebble SDK provides additions and functionality to help developers cope with this way of thinking. In many cases, a round display can be an aesthetic advantage. An example of this is the traditional circular dial watchface, which has been emulated on Pebble many times, but also wastes corner space. With a round display, these watchfaces can look better than ever.
Detecting Display Shape
The first step for any app wishing to correctly support both display shapes is to use the available compiler directives to conditionally create the UI. This can be done as shown below:
#if defined(PBL_RECT)
printf("This code is run on a rectangular display!");
/* Rectangular UI code */
#elif defined(PBL_ROUND)
printf("This code is run on a round display!");
/* Round UI code */
#endif
Another approach for single value selection is the PBL_IF_RECT_ELSE()
and
PBL_IF_ROUND_ELSE()
macros, which accept two parameters for each of the
respective round and rectangular cases. For example, PBL_IF_RECT_ELSE()
will
compile the first parameter on a rectangular display, and the second one
otherwise:
// Conditionally print out the shape of the display
printf("This is a %s display!", PBL_IF_RECT_ELSE("rectangular", "round"));
Circular Drawing
In addition to the older graphics_draw_circle()
and
graphics_fill_circle()
functions, the Pebble SDK for the chalk platform
contains additional functions to help draw shapes better suited for a round
display. These include:
-
graphics_draw_arc()
- Draws a line arc clockwise between two angles within a givenGRect
area, where 0° is the top of the circle. -
graphics_fill_radial()
- Fills a circle clockwise between two angles within a givenGRect
area, with adjustable inner inset radius allowing the creation of 'doughnut-esque' shapes. -
gpoint_from_polar()
- Returns aGPoint
object describing a point given by a specified angle within a centeredGRect
.
In the Pebble SDK angles between 0
and 360
degrees are specified as values
scaled between 0
and TRIG_MAX_ANGLE
to preserve accuracy and avoid
floating point math. These are most commonly used when dealing with drawing
circles. To help with this conversion, developers can use the
DEG_TO_TRIGANGLE()
macro.
An example function to draw the letter 'C' in a yellow color is shown below for
use in a LayerUpdateProc
.
static void draw_letter_c(GRect bounds, GContext *ctx) {
GRect frame = grect_inset(bounds, GEdgeInsets(30));
graphics_context_set_fill_color(ctx, GColorYellow);
graphics_fill_radial(ctx, frame, GOvalScaleModeFitCircle, 30,
DEG_TO_TRIGANGLE(-225), DEG_TO_TRIGANGLE(45));
}
This produces the expected result, drawn with a smooth antialiased filled circle arc between the specified angles.
Adaptive Layouts
With not only a difference in display shape, but also in resolution, it is very
important that an app's layout not be created using hardcoded coordinates.
Consider the examples below, designed to create a child Layer
to fill the
size of the parent layer.
// Bad - only works on Aplite and Basalt rectangular displays
Layer *layer = layer_create(GRect(0, 0, 144, 168));
// Better - uses the native display size
GRect bounds = layer_get_bounds(parent_layer);
Layer *layer = layer_create(bounds);
Using this style, the child layer will always fill the parent layer, regardless of its actual dimensions.
In a similar vein, when working with the Pebble Time Round display it can be important that the layout is centered correctly. A set of layout values that are in the center of the classic 144 x 168 pixel display will not be centered when displayed on a 180 x 180 display. The undesirable effect of this can be seen in the example shown below:
By using the technique described above, the layout's GRect
objects can
specify their origin
and size
as a function of the dimensions of the layer
they are drawn into, solving this problem.
Text Flow and Pagination
A chief concern when working with a circular display is the rendering of large amounts of text. As demonstrated by an animation in {% guide_link design-and-interaction/in-the-round#pagination %}, continuous reflowing of text makes it much harder to read.
A solution to this problem is to render text while flowing within the
constraints of the shape of the display, and to scroll/animate it one page at a
time. There are three approaches to this available to developers, which are
detailed below. For full examples of each, see the
text-flow-techniques
example app.
Using TextLayer
Additions to the TextLayer
API allow text rendered within it to be
automatically flowed according to the curve of the display, and paged correctly
when the layer is moved or animated further. After a TextLayer
is created in
the usual way, text flow can then be enabled:
// Create TextLayer
TextLayer *s_text_layer = text_layer_create(bounds);
/* other properties set up */
// Add to parent Window
layer_add_child(window_layer, text_layer_get_layer(s_text_layer));
// Enable paging and text flow with an inset of 5 pixels
text_layer_enable_screen_text_flow_and_paging(s_text_layer, 5);
Note: The
text_layer_enable_screen_text_flow_and_paging()
function must be called after theTextLayer
is added to the view heirachy (i.e.: after usinglayer_add_child()
), or else it will have no effect.
An example of two TextLayer
elements flowing their text within the
constraints of the display shape is shown below:
Using ScrollLayer
The ScrollLayer
UI component also contains round-friendly functionality,
allowing it to scroll its child Layer
elements in pages of the same height
as its frame (usually the size of the parent Window
). This allows consuming
long content to be a more consistent experience, whether it is text, images, or
some other kind of information.
// Enable ScrollLayer paging
scroll_layer_set_paging(s_scroll_layer, true);
When combined with a TextLayer
as the main child layer, it becomes easy to
display long pieces of textual content on a round display. The TextLayer
can
be set up to handle the reflowing of text to follow the display shape, and the
ScrollLayer
handles the paginated scrolling.
// Add the TextLayer and ScrollLayer to the view heirachy
scroll_layer_add_child(s_scroll_layer, text_layer_get_layer(s_text_layer));
layer_add_child(window_layer, scroll_layer_get_layer(s_scroll_layer));
// Set the ScrollLayer's content size to the total size of the text
scroll_layer_set_content_size(s_scroll_layer,
text_layer_get_content_size(s_text_layer));
// Enable TextLayer text flow and paging
const int inset_size = 2;
text_layer_enable_screen_text_flow_and_paging(s_text_layer, inset_size);
// Enable ScrollLayer paging
scroll_layer_set_paging(s_scroll_layer, true);
Manual Text Drawing
The drawing of text into a [Graphics Context
](Drawing Text
) can also be
performed with awareness of text flow and paging preferences. This can be used
to emulate the behavior of the two previous approaches, but with more
flexibility. This approach involves the use of the GTextAttributes
object,
which is given to the Graphics API to allow it to flow text and paginate when
being animated.
When initializing the Window
that will do the drawing:
// Create the attributes object used for text rendering
GTextAttributes *s_attributes = graphics_text_attributes_create();
// Enable text flow with an inset of 5 pixels
graphics_text_attributes_enable_screen_text_flow(s_attributes, 5);
// Enable pagination with a fixed reference point and bounds, used for animating
graphics_text_attributes_enable_paging(s_attributes, bounds.origin, bounds);
When drawing some text in a LayerUpdateProc
:
static void update_proc(Layer *layer, GContext *ctx) {
GRect bounds = layer_get_bounds(layer);
// Calculate size of the text to be drawn with current attribute settings
GSize text_size = graphics_text_layout_get_content_size_with_attributes(
s_sample_text, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD), bounds,
GTextOverflowModeWordWrap, GTextAlignmentCenter, s_attributes
);
// Draw the text in this box with the current attribute settings
graphics_context_set_text_color(ctx, GColorBlack);
graphics_draw_text(ctx, s_sample_text, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD),
GRect(bounds.origin.x, bounds.origin.y, text_size.w, text_size.h),
GTextOverflowModeWordWrap, GTextAlignmentCenter, s_attributes
);
}
Once this setup is complete, the text will display correctly when moved or
scrolled via a PropertyAnimation
, such as one that moves the Layer
that
draws the text upwards, and at the same time extending its height to display
subsequent pages. An example animation is shown below:
GRect window_bounds = layer_get_bounds(window_get_root_layer(s_main_window));
const int duration_ms = 1000;
// Animate the Layer upwards, lengthening it to allow the next page to be drawn
GRect start = layer_get_frame(s_layer);
GRect finish = GRect(start.origin.x, start.origin.y - window_bounds.size.h,
start.size.w, start.size.h * 2);
// Create and scedule the PropertyAnimation
PropertyAnimation *prop_anim = property_animation_create_layer_frame(
s_layer, &start, &finish);
Animation *animation = property_animation_get_animation(prop_anim);
animation_set_duration(animation, duration_ms);
animation_schedule(animation);
Working With a Circular Framebuffer
The traditional rectangular Pebble app framebuffer is a single continuous memory
segment that developers could access with gbitmap_get_data()
. With a round
display, Pebble saves memory by clipping sections of each line of difference
between the display area and the rectangle it occupies. The resulting masking
pattern looks like this:
Download this mask by saving the PNG image above, or get it as a Photoshop PSD layer.
This has an important implication - the memory segment of the framebuffer can no
longer be accessed using classic y * row_width + x
formulae. Instead,
developers should use the gbitmap_get_data_row_info()
API. When used with a
given y coordinate, this will return a GBitmapDataRowInfo
object containing
a pointer to the row's data, as well as values for the minumum and maximum
visible values of x coordinate on that row. For example:
static void round_update_proc(Layer *layer, GContext *ctx) {
// Get framebuffer
GBitmap *fb = graphics_capture_frame_buffer(ctx);
GRect bounds = layer_get_bounds(layer);
// Write a value to all visible pixels
for(int y = 0; y < bounds.size.h; y++) {
// Get the min and max x values for this row
GBitmapDataRowInfo info = gbitmap_get_data_row_info(fb, y);
// Iterate over visible pixels in that row
for(int x = info.min_x; x < info.max_x; x++) {
// Set the pixel to black
memset(&info.data[x], GColorBlack.argb, 1);
}
}
// Release framebuffer
graphics_release_frame_buffer(ctx, fb);
}
Displaying More Content
When more content is available than fits on the screen at any one time, the user
should be made aware using visual clues. The best way to do this is to use the
ContentIndicator
UI component.
A ContentIndicator
can be obtained in two ways. It can be created from
scratch with content_indicator_create()
and manually managed to determine
when the arrows should be shown, or a built-in instance can be obtained from a
ScrollLayer
, as shown below:
// Get the ContentIndicator from the ScrollLayer
s_indicator = scroll_layer_get_content_indicator(s_scroll_layer);
In order to draw the arrows indicating more information in each direction, the
ContentIndicator
must be supplied with two new Layer
elements that will
be used to do the drawing. These should also be added as children to the main
Window
root Layer
such that they are visible on top of all other
Layer
elements:
static void window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
/* ... */
// Create two Layers to draw the arrows
s_indicator_up_layer = layer_create(
GRect(0, 0, bounds.size.w, STATUS_BAR_LAYER_HEIGHT));
s_indicator_down_layer = layer_create(
GRect(0, bounds.size.h - STATUS_BAR_LAYER_HEIGHT,
bounds.size.w, STATUS_BAR_LAYER_HEIGHT));
/* ... */
// Add these Layers as children after all other components to appear below
layer_add_child(window_layer, s_indicator_up_layer);
layer_add_child(window_layer, s_indicator_down_layer);
}
Once the indicator Layer
elements have been created, each of the up and down
directions for conventional vertical scrolling must be configured with data to
control its behavior. Aspects such as the color of the arrows and background,
whether or not the arrows time out after being brought into view, and the
alignment of the drawn arrow within the Layer
itself are configured with a
const
ContentIndicatorConfig
object when each direction is being
configured:
// Configure the properties of each indicator
const ContentIndicatorConfig up_config = (ContentIndicatorConfig) {
.layer = s_indicator_up_layer,
.times_out = false,
.alignment = GAlignCenter,
.colors = {
.foreground = GColorBlack,
.background = GColorWhite
}
};
content_indicator_configure_direction(s_indicator, ContentIndicatorDirectionUp,
&up_config);
const ContentIndicatorConfig down_config = (ContentIndicatorConfig) {
.layer = s_indicator_down_layer,
.times_out = false,
.alignment = GAlignCenter,
.colors = {
.foreground = GColorBlack,
.background = GColorWhite
}
};
content_indicator_configure_direction(s_indicator, ContentIndicatorDirectionDown,
&down_config);
Unless the ContentIndicator
has been retrieved from another Layer
type
that includes an instance, it should be destroyed along with its parent
Window
:
// Destroy a manually created ContentIndicator
content_indicator_destroy(s_indicator);
For layouts that use the StatusBarLayer
, the ContentIndicatorDirectionUp
.layer
in the ContentIndicatorConfig
object can be given the status bar's
Layer
with status_bar_layer_get_layer()
, and the drawing routines for
each will be managed automatically.