mirror of
https://github.com/google/pebble.git
synced 2025-07-21 06:44:49 -04:00
556 lines
26 KiB
C
556 lines
26 KiB
C
/*
|
|
* Copyright 2024 Google LLC
|
|
*
|
|
* 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
|
|
*
|
|
* http://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.
|
|
*/
|
|
|
|
#include "weather_app_layout.h"
|
|
|
|
#include "applib/fonts/fonts.h"
|
|
#include "applib/graphics/gcontext.h"
|
|
#include "applib/graphics/gdraw_command_image.h"
|
|
#include "applib/graphics/gpath.h"
|
|
#include "applib/graphics/graphics.h"
|
|
#include "applib/graphics/graphics_circle.h"
|
|
#include "applib/graphics/gtypes.h"
|
|
#include "applib/graphics/text.h"
|
|
#include "applib/ui/animation.h"
|
|
#include "applib/ui/animation_interpolate.h"
|
|
#include "applib/ui/animation_timing.h"
|
|
#include "applib/ui/content_indicator.h"
|
|
#include "applib/ui/kino/kino_layer.h"
|
|
#include "applib/ui/kino/kino_reel/morph_square.h"
|
|
#include "applib/ui/kino/kino_reel/transform.h"
|
|
#include "applib/ui/window.h"
|
|
#include "apps/system_apps/timeline/text_node.h"
|
|
#include "font_resource_keys.auto.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "resource/resource_ids.auto.h"
|
|
#include "services/common/i18n/i18n.h"
|
|
#include "services/normal/timeline/timeline_resources.h"
|
|
#include "services/normal/weather/weather_service.h"
|
|
#include "services/normal/weather/weather_types.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/size.h"
|
|
#include "util/trig.h"
|
|
|
|
#include <stdio.h>
|
|
|
|
#define WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT (18)
|
|
#define WEATHER_APP_LAYOUT_TOP_PADDING PBL_IF_RECT_ELSE(0, 15)
|
|
#define WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE (TimelineResourceSizeTiny)
|
|
#define WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET PBL_IF_RECT_ELSE(3, 23)
|
|
|
|
static int prv_draw_text(GPoint offset, int max_width, GContext *context,
|
|
const char *text, const GFont font,
|
|
GColor font_color, GTextAlignment alignment) {
|
|
const int height = fonts_get_font_height(font);
|
|
const GRect box = (GRect) {offset, GSize(max_width, height)};
|
|
|
|
graphics_context_set_text_color(context, font_color);
|
|
graphics_draw_text(context, text, font, box, GTextOverflowModeFill, alignment, NULL);
|
|
|
|
return height;
|
|
}
|
|
|
|
static void prv_draw_weather_background(const GRect *circle_bounding_box, GContext *context,
|
|
GColor background_color) {
|
|
if (!gcolor_is_invisible(background_color)) {
|
|
graphics_context_set_fill_color(context, background_color);
|
|
graphics_fill_oval(context, *circle_bounding_box, GOvalScaleModeFitCircle);
|
|
}
|
|
}
|
|
|
|
static void prv_fill_high_low_temp_buffer(const int high, const int low, char *buffer,
|
|
const size_t buffer_size, const void *i18n_owner) {
|
|
if ((high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) &&
|
|
(low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP)) {
|
|
/// Shown when neither high nor low temperature is known
|
|
const char *both_temps_no_data = i18n_get("--° / --°", i18n_owner);
|
|
strncpy(buffer, both_temps_no_data, strlen(both_temps_no_data));
|
|
buffer[buffer_size - 1] = '\0';
|
|
} else if (low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
|
/// Shown when only the day's high temperature is known, (e.g. "68° / --°")
|
|
snprintf(buffer, buffer_size, i18n_get("%i° / --°", i18n_owner), high);
|
|
} else if (high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
|
/// Shown when only the day's low temperature is known (e.g. "--° / 52°")
|
|
snprintf(buffer, buffer_size, i18n_get("--° / %i°", i18n_owner), low);
|
|
} else {
|
|
/// A day's high and low temperature, separated by a forward slash (e.g. "68° / 52°")
|
|
snprintf(buffer, buffer_size, i18n_get("%i° / %i°", i18n_owner), high, low);
|
|
}
|
|
}
|
|
|
|
#define GPS_ARROW_WIDTH (12)
|
|
#define GPS_ARROW_HEIGHT (14)
|
|
|
|
static const GPoint s_gps_arrow_path_points[] = {
|
|
{0, GPS_ARROW_HEIGHT},
|
|
{(GPS_ARROW_WIDTH / 2), 0},
|
|
{GPS_ARROW_WIDTH, GPS_ARROW_HEIGHT},
|
|
// This 6/7 height ratio for the arrow notch achieves the design spec
|
|
{(GPS_ARROW_WIDTH / 2), (GPS_ARROW_HEIGHT * 6 / 7)}
|
|
};
|
|
|
|
static void prv_draw_gps_arrow_node_callback(GContext *ctx, const GRect *rect,
|
|
UNUSED const GTextNodeDrawConfig *config, bool render,
|
|
GSize *size_out, UNUSED void *user_data) {
|
|
GPath gps_arrow_path = (GPath) {
|
|
.num_points = ARRAY_LENGTH(s_gps_arrow_path_points),
|
|
.points = (GPoint *)s_gps_arrow_path_points,
|
|
.offset = rect->origin,
|
|
// Ideal rotation would be 45 degrees, but the shape of the arrow matches the design best at
|
|
// 38 degrees
|
|
.rotation = DEG_TO_TRIGANGLE(38),
|
|
};
|
|
|
|
if (render) {
|
|
graphics_context_set_fill_color(ctx, GColorBlack);
|
|
gpath_draw_filled(ctx, &gps_arrow_path);
|
|
}
|
|
if (size_out) {
|
|
// Note that gpath_outer_rect() doesn't take into account the rotation; we'll add margin to the
|
|
// location text node to account for it
|
|
*size_out = gpath_outer_rect(&gps_arrow_path).size;
|
|
}
|
|
}
|
|
|
|
static GTextNode *prv_create_location_name_area_node(const WeatherLocationForecast *forecast,
|
|
GFont location_font) {
|
|
const GTextAlignment location_name_alignment = PBL_IF_RECT_ELSE(GTextAlignmentLeft,
|
|
GTextAlignmentCenter);
|
|
|
|
// One node for the location name text and one node for the possible GPS arrow
|
|
const size_t max_nodes = 2;
|
|
GTextNodeHorizontal *horizontal_node = graphics_text_node_create_horizontal(max_nodes);
|
|
horizontal_node->horizontal_alignment = location_name_alignment;
|
|
|
|
GTextNodeText *location_text_node = graphics_text_node_create_text(0);
|
|
location_text_node->text = forecast->location_name;
|
|
location_text_node->font = location_font;
|
|
location_text_node->color = GColorBlack;
|
|
location_text_node->overflow = GTextOverflowModeTrailingEllipsis;
|
|
if (forecast->is_current_location) {
|
|
// Horizontal spacing between location name and GPS arrow is spec'd by design to be 11 pixels
|
|
location_text_node->node.margin = GSize(11, 0);
|
|
}
|
|
graphics_text_node_container_add_child(&horizontal_node->container, &location_text_node->node);
|
|
|
|
if (forecast->is_current_location) {
|
|
GTextNodeCustom *arrow_node = graphics_text_node_create_custom(prv_draw_gps_arrow_node_callback,
|
|
NULL);
|
|
arrow_node->node.offset =
|
|
GPoint(0, (int16_t)(fonts_get_font_cap_offset(location_font) / 2));
|
|
graphics_text_node_container_add_child(&horizontal_node->container, &arrow_node->node);
|
|
}
|
|
|
|
return &horizontal_node->container.node;
|
|
}
|
|
|
|
static GSize prv_draw_location_name_area(GPoint offset, int max_width, GContext *ctx,
|
|
GFont location_font,
|
|
const WeatherLocationForecast *forecast) {
|
|
GTextNode *location_name_area_node = prv_create_location_name_area_node(forecast, location_font);
|
|
|
|
GRect location_name_area_rect = (GRect) {
|
|
.origin = offset,
|
|
.size = GSize(max_width, fonts_get_font_height(location_font)),
|
|
};
|
|
|
|
#if PBL_ROUND
|
|
// On round the location name text and arrow can be obscured by the edges of the bezel, so we
|
|
// horizontally inset the rectangle by a few pixels
|
|
const int16_t horizontal_inset = 5;
|
|
location_name_area_rect = grect_inset(location_name_area_rect,
|
|
GEdgeInsets(0, horizontal_inset, 0));
|
|
#endif
|
|
|
|
GSize location_name_area_size;
|
|
graphics_text_node_draw(location_name_area_node, ctx, &location_name_area_rect, NULL,
|
|
&location_name_area_size);
|
|
graphics_text_node_destroy(location_name_area_node);
|
|
return location_name_area_size;
|
|
}
|
|
// All text before the separator
|
|
static void prv_draw_top_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
|
|
int content_width,
|
|
GContext *context) {
|
|
const WeatherLocationForecast *forecast = layout->forecast;
|
|
|
|
current_offset->y += prv_draw_location_name_area(*current_offset, content_width, context,
|
|
layout->location_font, forecast).h;
|
|
|
|
const int location_and_today_temperature_vertical_spacing = 7;
|
|
current_offset->y += location_and_today_temperature_vertical_spacing;
|
|
|
|
char text_buffer[15] = {0};
|
|
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
|
|
|
|
if (forecast->current_temp == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
|
/// Shown when today's current temperature is unknown
|
|
const char *unknown_temp_string = i18n_get("--°", layout);
|
|
strncpy(text_buffer, unknown_temp_string, strlen(unknown_temp_string));
|
|
text_buffer[max_text_buff_size - 1] = '\0';
|
|
} else {
|
|
/// Today's current temperature (e.g. "68°")
|
|
snprintf(text_buffer, max_text_buff_size, i18n_get("%i°", layout), forecast->current_temp);
|
|
}
|
|
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
|
|
layout->temperature_font, GColorBlack, GTextAlignmentLeft);
|
|
|
|
prv_fill_high_low_temp_buffer(forecast->today_high, forecast->today_low, text_buffer,
|
|
max_text_buff_size, layout);
|
|
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
|
|
layout->high_low_phrase_font, GColorBlack,
|
|
GTextAlignmentLeft);
|
|
const int today_high_low_gap_vertical_spacing_reduction = 2;
|
|
current_offset->y -= today_high_low_gap_vertical_spacing_reduction;
|
|
|
|
current_offset->y += prv_draw_text(*current_offset, content_width, context,
|
|
forecast->current_weather_phrase, layout->high_low_phrase_font,
|
|
GColorBlack, GTextAlignmentLeft);
|
|
}
|
|
|
|
// All text after the separator
|
|
static void prv_draw_bottom_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
|
|
int content_width, GContext *context) {
|
|
const WeatherLocationForecast *forecast = layout->forecast;
|
|
|
|
const int separator_tomorrow_title_vertical_spacing = 6;
|
|
current_offset->y += separator_tomorrow_title_vertical_spacing;
|
|
current_offset->y += prv_draw_text(*current_offset, content_width, context,
|
|
/// Refers to the weather conditions for tomorrow
|
|
i18n_get("TOMORROW", layout), layout->tomorrow_font,
|
|
GColorBlack, GTextAlignmentLeft);
|
|
char text_buffer[15] = {0};
|
|
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
|
|
prv_fill_high_low_temp_buffer(forecast->tomorrow_high, forecast->tomorrow_low, text_buffer,
|
|
max_text_buff_size, layout);
|
|
prv_draw_text(*current_offset, content_width, context, text_buffer, layout->high_low_phrase_font,
|
|
GColorBlack, GTextAlignmentLeft);
|
|
}
|
|
|
|
static void prv_draw_weather_icon_backgrounds(const WeatherAppLayout *layout,
|
|
const GRect *content_bounds, GContext *context) {
|
|
const WeatherLocationForecast *forecast = layout->forecast;
|
|
// assume that both current and tomorrow weather icons are the same size
|
|
const GSize icon_size = layout->current_weather_icon_layer.layer.bounds.size;
|
|
const unsigned int weather_icon_bg_circle_diam = integer_sqrt(2 * icon_size.w * icon_size.h);
|
|
|
|
const int today_weather_bg_circle_top_margin = 28;
|
|
GRect bg_circle_bounding_box = GRect(grect_get_max_x(content_bounds) -
|
|
weather_icon_bg_circle_diam,
|
|
today_weather_bg_circle_top_margin,
|
|
weather_icon_bg_circle_diam, weather_icon_bg_circle_diam);
|
|
|
|
prv_draw_weather_background(&bg_circle_bounding_box, context,
|
|
weather_type_get_bg_color(forecast->current_weather_type));
|
|
|
|
const int weather_bg_circle_vertical_spacing = 40;
|
|
bg_circle_bounding_box.origin.y += weather_icon_bg_circle_diam +
|
|
weather_bg_circle_vertical_spacing;
|
|
prv_draw_weather_background(&bg_circle_bounding_box, context,
|
|
weather_type_get_bg_color(forecast->tomorrow_weather_type));
|
|
}
|
|
|
|
|
|
static void prv_render_layout(Layer *layer, GContext *context) {
|
|
// "Content" refers to everything except the dot separator
|
|
const GRect content_bounds =
|
|
grect_inset(layer->bounds, GEdgeInsets(0, WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET,
|
|
0));
|
|
const int content_x_offset = content_bounds.origin.x;
|
|
const int content_width = content_bounds.size.w;
|
|
|
|
const WeatherAppLayout *layout = window_get_user_data(layer_get_window(layer));
|
|
const WeatherLocationForecast *forecast = layout->forecast;
|
|
|
|
if (!forecast) {
|
|
// Nothing to draw.
|
|
return;
|
|
}
|
|
// start at 1 from the top to match design docs
|
|
GPoint current_offset = GPoint(content_x_offset, 1);
|
|
GPoint *offset = ¤t_offset;
|
|
|
|
prv_draw_top_half_text(layout, offset, content_width, context);
|
|
|
|
// dotted separator
|
|
const int phrase_separator_vertical_spacing = 10;
|
|
current_offset.y += phrase_separator_vertical_spacing;
|
|
|
|
const GPoint separator_start = GPoint(0, current_offset.y);
|
|
graphics_context_set_stroke_width(context, 5);
|
|
graphics_context_set_stroke_color(context, PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack));
|
|
graphics_draw_horizontal_line_dotted(context, separator_start,
|
|
layer->bounds.size.w);
|
|
|
|
if (!layout->animation_state.hide_bottom_half_text) {
|
|
prv_draw_bottom_half_text(layout, offset, content_width, context);
|
|
}
|
|
prv_draw_weather_icon_backgrounds(layout, &content_bounds, context);
|
|
}
|
|
|
|
static void prv_content_indicator_setup_direction(ContentIndicator *content_indicator,
|
|
Layer *indicator_layer,
|
|
ContentIndicatorDirection direction) {
|
|
content_indicator_configure_direction(content_indicator, direction, &(ContentIndicatorConfig) {
|
|
.layer = indicator_layer,
|
|
.colors.foreground = GColorBlack,
|
|
.colors.background = GColorLightGray,
|
|
});
|
|
}
|
|
|
|
void weather_app_layout_init(WeatherAppLayout *layout, const GRect *frame) {
|
|
layout->location_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
|
|
layout->temperature_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM);
|
|
layout->high_low_phrase_font = fonts_get_system_font(FONT_KEY_GOTHIC_18);
|
|
layout->tomorrow_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);
|
|
|
|
Layer *root_layer = &layout->root_layer;
|
|
layer_init(root_layer, frame);
|
|
|
|
Layer *down_arrow_layer = &layout->down_arrow_layer;
|
|
const GRect down_arrow_layer_frame = grect_inset(
|
|
*frame, GEdgeInsets(frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
|
|
layer_init(down_arrow_layer, &down_arrow_layer_frame);
|
|
layer_add_child(root_layer, down_arrow_layer);
|
|
|
|
const int content_layer_side_padding = PBL_IF_RECT_ELSE(5, 12);
|
|
const GRect content_layer_frame = grect_inset(*frame,
|
|
GEdgeInsets(WEATHER_APP_LAYOUT_TOP_PADDING,
|
|
content_layer_side_padding,
|
|
WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT));
|
|
|
|
Layer *content_layer = &layout->content_layer;
|
|
layer_init(content_layer, &content_layer_frame);
|
|
layer_set_update_proc(content_layer, prv_render_layout);
|
|
layer_add_child(root_layer, content_layer);
|
|
|
|
ContentIndicator *content_indicator = &layout->content_indicator;
|
|
content_indicator_init(content_indicator);
|
|
|
|
prv_content_indicator_setup_direction(content_indicator, down_arrow_layer,
|
|
ContentIndicatorDirectionDown);
|
|
|
|
const GSize icon_size =
|
|
timeline_resources_get_gsize(WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE);
|
|
|
|
const int icon_layer_margin_top = PBL_IF_RECT_ELSE(33, 18);
|
|
const int icon_layer_right_margin = 5;
|
|
GRect icon_layer_frame = (GRect) {
|
|
{
|
|
content_layer_frame.size.w - icon_size.w - WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET -
|
|
icon_layer_right_margin,
|
|
content_layer_frame.origin.y + icon_layer_margin_top
|
|
},
|
|
icon_size
|
|
};
|
|
|
|
KinoLayer *current_weather_icon_layer = &layout->current_weather_icon_layer;
|
|
kino_layer_init(current_weather_icon_layer, &icon_layer_frame);
|
|
layer_add_child(content_layer, kino_layer_get_layer(current_weather_icon_layer));
|
|
|
|
const int icon_layer_spacing = 50;
|
|
icon_layer_frame.origin.y += icon_size.h + icon_layer_spacing;
|
|
|
|
KinoLayer *tomorrow_weather_icon_layer = &layout->tomorrow_weather_icon_layer;
|
|
kino_layer_init(tomorrow_weather_icon_layer, &icon_layer_frame);
|
|
layer_add_child(content_layer, kino_layer_get_layer(tomorrow_weather_icon_layer));
|
|
}
|
|
|
|
static uint32_t prv_get_resource_id_for_weather_type(WeatherType type) {
|
|
const TimelineResourceInfo timeline_res = {
|
|
.res_id = weather_type_get_timeline_resource_id(type),
|
|
};
|
|
AppResourceInfo icon_res_info;
|
|
timeline_resources_get_id(&timeline_res, WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE,
|
|
&icon_res_info);
|
|
return icon_res_info.res_id;
|
|
}
|
|
|
|
void weather_app_layout_set_data(WeatherAppLayout *layout,
|
|
const WeatherLocationForecast *forecast) {
|
|
layout->forecast = forecast;
|
|
|
|
const uint32_t current_weather_res_id = forecast ?
|
|
prv_get_resource_id_for_weather_type(forecast->current_weather_type) : RESOURCE_ID_INVALID;
|
|
const uint32_t tomorrow_weather_res_id = forecast ?
|
|
prv_get_resource_id_for_weather_type(forecast->tomorrow_weather_type) : RESOURCE_ID_INVALID;
|
|
|
|
kino_layer_set_reel_with_resource(&layout->current_weather_icon_layer, current_weather_res_id);
|
|
kino_layer_set_reel_with_resource(&layout->tomorrow_weather_icon_layer, tomorrow_weather_res_id);
|
|
|
|
layer_mark_dirty(&layout->root_layer);
|
|
}
|
|
|
|
void weather_app_layout_set_down_arrow_visible(WeatherAppLayout *layout, bool is_down_visible) {
|
|
content_indicator_set_content_available(&layout->content_indicator, ContentIndicatorDirectionDown,
|
|
is_down_visible);
|
|
}
|
|
|
|
void weather_app_layout_deinit(WeatherAppLayout *layout) {
|
|
i18n_free_all(layout);
|
|
layer_deinit(&layout->root_layer);
|
|
}
|
|
|
|
// Down arrow layer grows until a point, after which the entire content teleports to a height
|
|
// slightly higher than its resting position, then relaxes into place
|
|
static void prv_down_animation_update(Animation *animation, AnimationProgress normalized) {
|
|
WeatherAppLayout *layout = animation_get_context(animation);
|
|
// Progress at which to switch from the down arrow growing to entire content relaxing downwards
|
|
const AnimationProgress animation_cut_frame_progress =
|
|
(interpolate_moook_in_duration() * ANIMATION_NORMALIZED_MAX) / interpolate_moook_duration();
|
|
// Progress at which to hide "TOMORROW" and tomorrow high / low temperature text
|
|
const AnimationProgress animation_hide_bottom_half_text_progress =
|
|
(animation_cut_frame_progress * 2) / 3;
|
|
|
|
int down_arrow_layer_height = WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT;
|
|
layout->animation_state.hide_bottom_half_text = false;
|
|
|
|
if (normalized <= animation_cut_frame_progress) {
|
|
if (normalized >= animation_hide_bottom_half_text_progress) {
|
|
layout->animation_state.hide_bottom_half_text = true;
|
|
}
|
|
// renormalize the progress so that interpolate_moook_in_only works as expected
|
|
int32_t new_normalized = animation_timing_scaled(normalized, ANIMATION_NORMALIZED_MIN,
|
|
animation_cut_frame_progress);
|
|
const int additional_down_arrow_height = 25;
|
|
// grow the down arrow layer
|
|
down_arrow_layer_height += interpolate_moook_in_only(new_normalized, 0,
|
|
additional_down_arrow_height);
|
|
} else {
|
|
// We've cut, so display the next forecast's data
|
|
if (layout->animation_state.next_forecast) {
|
|
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
|
|
layout->animation_state.next_forecast = NULL;
|
|
}
|
|
int32_t new_normalized = animation_timing_scaled(normalized, animation_cut_frame_progress,
|
|
ANIMATION_NORMALIZED_MAX);
|
|
|
|
// Relax the content by changing its top margin
|
|
const int animation_margin_top_from = WEATHER_APP_LAYOUT_TOP_PADDING - PBL_IF_RECT_ELSE(10, 15);
|
|
const int animation_margin_top_to = WEATHER_APP_LAYOUT_TOP_PADDING;
|
|
const int num_frames_from = 1;
|
|
const bool bounce_back = false;
|
|
int animation_margin_top = interpolate_moook_out(new_normalized, animation_margin_top_from,
|
|
animation_margin_top_to, num_frames_from,
|
|
bounce_back);
|
|
layout->content_layer.frame.origin.y = animation_margin_top;
|
|
|
|
// The down arrow's height follows the content margin. It starts off large, then goes back to
|
|
// its original size, as the content relaxes into place
|
|
down_arrow_layer_height += (-animation_margin_top + WEATHER_APP_LAYOUT_TOP_PADDING);
|
|
}
|
|
|
|
const GRect down_arrow_layer_frame = grect_inset(layout->root_layer.frame,
|
|
GEdgeInsets(layout->root_layer.frame.size.h - down_arrow_layer_height, 0, 0));
|
|
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
|
|
|
|
layer_mark_dirty(&layout->root_layer);
|
|
}
|
|
|
|
// moves the entire root layer up back into place
|
|
static void prv_up_animation_update(Animation *animation, AnimationProgress normalized) {
|
|
const int root_layer_top_margin_from = (WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT * 2) / 3;
|
|
const int root_layer_top_margin_to = 0;
|
|
const int num_frames_from = 1;
|
|
const bool bounce_back = false;
|
|
int root_layer_top_margin = interpolate_moook_out(normalized, root_layer_top_margin_from,
|
|
root_layer_top_margin_to, num_frames_from, bounce_back);
|
|
|
|
WeatherAppLayout *layout = animation_get_context(animation);
|
|
if (layout->animation_state.next_forecast) {
|
|
layout->forecast = layout->animation_state.next_forecast;
|
|
layout->animation_state.next_forecast = NULL;
|
|
}
|
|
|
|
layout->root_layer.frame.origin.y = root_layer_top_margin;
|
|
layer_set_frame(&layout->root_layer, &layout->root_layer.frame);
|
|
}
|
|
|
|
static void prv_animation_stopped(Animation *animation, bool finished, void *context) {
|
|
WeatherAppLayout *layout = context;
|
|
if (layout->animation_state.next_forecast) {
|
|
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
|
|
layout->animation_state.next_forecast = NULL;
|
|
}
|
|
layout->animation_state.hide_bottom_half_text = false;
|
|
|
|
GRect *root_layer_frame = &layout->root_layer.frame;
|
|
root_layer_frame->origin.y = 0;
|
|
layer_set_frame(&layout->root_layer, root_layer_frame);
|
|
|
|
GRect *content_layer_frame = &layout->content_layer.frame;
|
|
content_layer_frame->origin.y = WEATHER_APP_LAYOUT_TOP_PADDING;
|
|
layer_set_frame(&layout->content_layer, content_layer_frame);
|
|
|
|
const GRect down_arrow_layer_frame = grect_inset(*root_layer_frame,
|
|
GEdgeInsets(root_layer_frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
|
|
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
|
|
}
|
|
|
|
static const AnimationImplementation s_down_animation_implementation = {
|
|
.update = &prv_down_animation_update,
|
|
};
|
|
|
|
static const AnimationImplementation s_up_animation_implementation = {
|
|
.update = &prv_up_animation_update,
|
|
};
|
|
|
|
static const AnimationHandlers s_animation_handlers = {
|
|
.stopped = &prv_animation_stopped,
|
|
};
|
|
|
|
static void prv_morph_weather_icons(KinoLayer *icon_layer, WeatherType from, WeatherType to,
|
|
uint32_t duration) {
|
|
uint32_t from_image_res_id =
|
|
prv_get_resource_id_for_weather_type(from);
|
|
KinoReel *from_reel = kino_reel_create_with_resource(from_image_res_id);
|
|
KinoReel *to_reel = kino_reel_create_with_resource(prv_get_resource_id_for_weather_type(to));
|
|
|
|
KinoReel *icon_reel = kino_reel_morph_square_create(from_reel, true);
|
|
kino_reel_transform_set_to_reel(icon_reel, to_reel, true);
|
|
kino_reel_transform_set_transform_duration(icon_reel, duration);
|
|
|
|
kino_layer_set_reel(icon_layer, icon_reel, true);
|
|
kino_layer_play(icon_layer);
|
|
}
|
|
|
|
void weather_app_layout_animate(WeatherAppLayout *layout, WeatherLocationForecast *new_forecast,
|
|
bool animate_down) {
|
|
animation_unschedule_all();
|
|
|
|
const uint32_t anim_duration = animate_down ? interpolate_moook_duration() :
|
|
interpolate_moook_out_duration();
|
|
layout->animation_state.next_forecast = new_forecast;
|
|
Animation *animation = animation_create();
|
|
animation_set_duration(animation, anim_duration);
|
|
InterpolateInt64Function interpolation = animate_down ? interpolate_moook :
|
|
interpolate_moook_in_only;
|
|
animation_set_custom_interpolation(animation, interpolation);
|
|
animation_set_handlers(animation, s_animation_handlers, layout);
|
|
const AnimationImplementation *implementation = animate_down ? &s_down_animation_implementation :
|
|
&s_up_animation_implementation;
|
|
animation_set_implementation(animation, implementation);
|
|
animation_schedule(animation);
|
|
|
|
prv_morph_weather_icons(&layout->current_weather_icon_layer,
|
|
layout->forecast->current_weather_type,
|
|
new_forecast->current_weather_type, anim_duration);
|
|
|
|
prv_morph_weather_icons(&layout->tomorrow_weather_icon_layer,
|
|
layout->forecast->tomorrow_weather_type,
|
|
new_forecast->tomorrow_weather_type, anim_duration);
|
|
}
|