diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f69d3d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Ignore build generated files +build/ +dist/ +dist.zip + +# Ignore waf lock file +.lock-waf* + +# Ignore installed node modules +node_modules/ diff --git a/examples/demo/.gitignore b/examples/demo/.gitignore new file mode 100644 index 0000000..f69d3d2 --- /dev/null +++ b/examples/demo/.gitignore @@ -0,0 +1,10 @@ +# Ignore build generated files +build/ +dist/ +dist.zip + +# Ignore waf lock file +.lock-waf* + +# Ignore installed node modules +node_modules/ diff --git a/examples/demo/package.json b/examples/demo/package.json new file mode 100644 index 0000000..9c3ff8b --- /dev/null +++ b/examples/demo/package.json @@ -0,0 +1,28 @@ +{ + "name": "activity-indicator-layer-demo", + "author": "Alexsander Akers", + "version": "1.0.0", + "keywords": ["pebble-app"], + "private": true, + "dependencies": { + "pebble-activity-indicator-layer": "1.0.0" + }, + "pebble": { + "displayName": "Activity Indicator Demo", + "uuid": "5b603423-eaad-4186-b504-c14ed2dcd2cd", + "sdkVersion": "3", + "enableMultiJS": true, + "targetPlatforms": [ + "aplite", + "basalt", + "chalk" + ], + "watchapp": { + "watchface": false + }, + "messageKeys": [], + "resources": { + "media": [] + } + } +} diff --git a/examples/demo/src/demo.c b/examples/demo/src/demo.c new file mode 100644 index 0000000..eddd6ec --- /dev/null +++ b/examples/demo/src/demo.c @@ -0,0 +1,71 @@ +#include +#include + +static Window *s_window; +static ActivityIndicatorLayer *s_activity_indicator_layer; + +static void select_click_handler(ClickRecognizerRef recognizer, void *context) { + bool animating = activity_indicator_layer_get_animating(s_activity_indicator_layer); + activity_indicator_layer_set_animating(s_activity_indicator_layer, !animating); +} + +static void up_click_handler(ClickRecognizerRef recognizer, void *context) { + uint8_t thickness = activity_indicator_layer_get_thickness(s_activity_indicator_layer); + if (thickness >= 10) { + return; + } + + activity_indicator_layer_set_thickness(s_activity_indicator_layer, thickness + 1); +} + +static void down_click_handler(ClickRecognizerRef recognizer, void *context) { + uint8_t thickness = activity_indicator_layer_get_thickness(s_activity_indicator_layer); + if (thickness <= 1) { + return; + } + + activity_indicator_layer_set_thickness(s_activity_indicator_layer, thickness - 1); +} + +static void click_config_provider(void *context) { + window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler); + window_single_click_subscribe(BUTTON_ID_UP, up_click_handler); + window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler); +} + +static void window_load(Window *window) { + Layer *window_layer = window_get_root_layer(window); + const GRect bounds = layer_get_bounds(window_layer); + + GRect frame = GRect(0, 0, 50, 50); + grect_align(&frame, &bounds, GAlignCenter, false); + + s_activity_indicator_layer = activity_indicator_layer_create(frame); + activity_indicator_layer_set_animating(s_activity_indicator_layer, true); + layer_add_child(window_layer, (Layer *)s_activity_indicator_layer); +} + +static void window_unload(Window *window) { + activity_indicator_layer_destroy(s_activity_indicator_layer); +} + +static void init(void) { + s_window = window_create(); + window_set_click_config_provider(s_window, click_config_provider); + window_set_window_handlers(s_window, (WindowHandlers) { + .load = window_load, + .unload = window_unload, + }); + const bool animated = true; + window_stack_push(s_window, animated); +} + +static void deinit(void) { + window_destroy(s_window); +} + +int main(void) { + init(); + app_event_loop(); + deinit(); +} diff --git a/examples/demo/wscript b/examples/demo/wscript new file mode 100644 index 0000000..e721ba8 --- /dev/null +++ b/examples/demo/wscript @@ -0,0 +1,50 @@ +# +# This file is the default set of rules to compile a Pebble application. +# +# Feel free to customize this to your needs. +# +import os.path + +top = '.' +out = 'build' + + +def options(ctx): + ctx.load('pebble_sdk') + + +def configure(ctx): + """ + This method is used to configure your build. ctx.load(`pebble_sdk`) automatically configures + a build for each valid platform in `targetPlatforms`. Platform-specific configuration: add your + change after calling ctx.load('pebble_sdk') and make sure to set the correct environment first. + Universal configuration: add your change prior to calling ctx.load('pebble_sdk'). + """ + ctx.load('pebble_sdk') + + +def build(ctx): + ctx.load('pebble_sdk') + + build_worker = os.path.exists('worker_src') + binaries = [] + + cached_env = ctx.env + for platform in ctx.env.TARGET_PLATFORMS: + ctx.env = ctx.all_envs[platform] + ctx.set_group(ctx.env.PLATFORM_NAME) + app_elf = '{}/pebble-app.elf'.format(ctx.env.BUILD_DIR) + ctx.pbl_program(source=ctx.path.ant_glob('src/**/*.c'), target=app_elf) + + if build_worker: + worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) + binaries.append({'platform': platform, 'app_elf': app_elf, 'worker_elf': worker_elf}) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/**/*.c'), target=worker_elf) + else: + binaries.append({'platform': platform, 'app_elf': app_elf}) + ctx.env = cached_env + + ctx.set_group('bundle') + ctx.pbl_bundle(binaries=binaries, + js=ctx.path.ant_glob(['src/js/**/*.js', 'src/js/**/*.json']), + js_entry_file='src/js/app.js') diff --git a/include/activity-indicator-layer.h b/include/activity-indicator-layer.h new file mode 100644 index 0000000..b7fb645 --- /dev/null +++ b/include/activity-indicator-layer.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +struct ActivityIndicatorLayer; +typedef struct ActivityIndicatorLayer ActivityIndicatorLayer; + +ActivityIndicatorLayer *activity_indicator_layer_create(GRect frame); +void activity_indicator_layer_destroy(ActivityIndicatorLayer *activity_indicator_layer); + +Layer *activity_indicator_layer_get_layer(const ActivityIndicatorLayer *activity_indicator_layer); + +bool activity_indicator_layer_get_animating(const ActivityIndicatorLayer *activity_indicator_layer); +void activity_indicator_layer_set_animating(ActivityIndicatorLayer *activity_indicator_layer, bool animating); + +GColor activity_indicator_layer_get_color(const ActivityIndicatorLayer *activity_indicator_layer); +void activity_indicator_layer_set_color(ActivityIndicatorLayer *activity_indicator_layer, GColor color); + +uint8_t activity_indicator_layer_get_thickness(const ActivityIndicatorLayer *activity_indicator_layer); +void activity_indicator_layer_set_thickness(ActivityIndicatorLayer *activity_indicator_layer, uint8_t thickness); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9374855 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "pebble-activity-indicator-layer", + "description": "A \"material design\"-style activity indicator for Pebble.", + "author": "Alexsander Akers ", + "version": "1.0.0", + "license": "MIT", + "homepage": "https://github.com/a2/pebble-activity-indicator-layer", + "repository": { + "type": "git", + "url": "https://github.com/a2/pebble-activity-indicator-layer.git" + }, + "bugs": { + "url": "https://github.com/a2/pebble-activity-indicator-layer/issues" + }, + "files": ["dist.zip"], + "keywords": ["pebble-package"], + "dependencies": {}, + "pebble": { + "projectType": "package", + "sdkVersion": "3", + "targetPlatforms": [ + "aplite", + "basalt", + "chalk" + ], + "resources": { + "media": [] + } + } +} diff --git a/src/c/activity-indicator-layer.c b/src/c/activity-indicator-layer.c new file mode 100644 index 0000000..1a44310 --- /dev/null +++ b/src/c/activity-indicator-layer.c @@ -0,0 +1,191 @@ +#include "activity-indicator-layer.h" + +typedef struct ActivityIndicatorData { + Animation *animation; + GColor color; + uint8_t thickness; + uint32_t stroke_start; + uint32_t stroke_end; + uint32_t rotation; +} ActivityIndicatorData; + +#define activity_indicator_layer_animation_implementation(name, type, accessor_type) \ + static type activity_indicator_layer_get_##name(const Layer *layer) { \ + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); \ + return data->name; \ + } \ + \ + static void activity_indicator_layer_set_##name(Layer *layer, type name) { \ + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); \ + data->name = name; \ + layer_mark_dirty(layer); \ + } \ + \ + static const PropertyAnimationImplementation property_animation_implementation_##name = { \ + .base = { \ + .update = (AnimationUpdateImplementation)property_animation_update_##accessor_type, \ + }, \ + .accessors = { \ + .getter.accessor_type = (void *)activity_indicator_layer_get_##name, \ + .setter.accessor_type = (void *)activity_indicator_layer_set_##name, \ + }, \ + }; + +activity_indicator_layer_animation_implementation(stroke_start, uint32_t, uint32) +activity_indicator_layer_animation_implementation(stroke_end, uint32_t, uint32) +activity_indicator_layer_animation_implementation(rotation, uint32_t, uint32) + +static int32_t stroke_start_custom_curve(int32_t progress) { + if (progress > ANIMATION_NORMALIZED_MAX / 2) { + return ANIMATION_NORMALIZED_MAX * 4 / 5 + (progress - ANIMATION_NORMALIZED_MAX / 2) * 2 / 5; + } else { + return (2 * progress) * 4 / 5; + } +} + +static int32_t stroke_end_custom_curve(int32_t progress) { + if (progress > ANIMATION_NORMALIZED_MAX * 3 / 5) { + return ANIMATION_NORMALIZED_MAX; + } else { + return progress * 5 / 3; + } +} + +static Animation *activity_indicator_layer_create_animation(Layer *layer) { + Animation *animations[3]; + const uint32_t duration = 1000; + uint32_t from = 0, to = TRIG_MAX_ANGLE; + + animations[0] = ({ + PropertyAnimation *property_animation = property_animation_create(&property_animation_implementation_stroke_start, layer, NULL, NULL); + property_animation_from(property_animation, &from, sizeof(from), true); + property_animation_to(property_animation, &to, sizeof(to), true); + + Animation *animation = (Animation *)property_animation; + animation_set_custom_curve(animation, stroke_start_custom_curve); + animation_set_delay(animation, duration * 2 / 3); + animation_set_duration(animation, duration); + animation_set_play_count(animation, ANIMATION_PLAY_COUNT_INFINITE); + + animation; + }); + + animations[1] = ({ + PropertyAnimation *property_animation = property_animation_create(&property_animation_implementation_stroke_end, layer, NULL, NULL); + property_animation_from(property_animation, &from, sizeof(from), true); + property_animation_to(property_animation, &to, sizeof(to), true); + + Animation *animation = (Animation *)property_animation; + animation_set_custom_curve(animation, stroke_end_custom_curve); + animation_set_duration(animation, duration * 5 / 3); + animation_set_play_count(animation, ANIMATION_PLAY_COUNT_INFINITE); + + animation; + }); + + animations[2] = ({ + PropertyAnimation *property_animation = property_animation_create(&property_animation_implementation_rotation, layer, NULL, NULL); + property_animation_from(property_animation, &from, sizeof(from), true); + property_animation_to(property_animation, &to, sizeof(to), true); + + Animation *animation = (Animation *)property_animation; + animation_set_curve(animation, AnimationCurveLinear); + animation_set_duration(animation, duration * 3 / 2); + animation_set_play_count(animation, ANIMATION_PLAY_COUNT_INFINITE); + + animation; + }); + + return animation_spawn_create_from_array(animations, ARRAY_LENGTH(animations)); +} + +static void activity_indicator_layer_update_proc(Layer *layer, GContext *ctx) { + const GRect bounds = layer_get_bounds(layer); + + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); + if (data->animation == NULL) { + return; + } + + int32_t start = data->rotation + data->stroke_start; + int32_t end = data->rotation + data->stroke_end; + + while (start > end) { + end += TRIG_MAX_ANGLE; + } + + if (end - start < DEG_TO_TRIGANGLE(1)) { + end = start + DEG_TO_TRIGANGLE(1); + } + + uint8_t thickness = data->thickness; + graphics_context_set_stroke_color(ctx, data->color); + graphics_context_set_stroke_width(ctx, thickness); + graphics_draw_arc(ctx, grect_crop(bounds, (thickness + 1) / 2), GOvalScaleModeFitCircle, start, end); +} + +ActivityIndicatorLayer *activity_indicator_layer_create(GRect frame) { + Layer *layer = layer_create_with_data(frame, sizeof(ActivityIndicatorData)); + layer_set_update_proc(layer, activity_indicator_layer_update_proc); + + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); + data->color = GColorBlack; + data->thickness = 3; + + return (ActivityIndicatorLayer *)layer; +} + +void activity_indicator_layer_destroy(ActivityIndicatorLayer *activity_indicator_layer) { + Layer *layer = (Layer *)activity_indicator_layer; + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); + animation_destroy(data->animation); + layer_destroy(layer); +} + +Layer *activity_indicator_layer_get_layer(const ActivityIndicatorLayer *activity_indicator_layer) { + return (Layer *)activity_indicator_layer; +} + +bool activity_indicator_layer_get_animating(const ActivityIndicatorLayer *activity_indicator_layer) { + Layer *layer = (Layer *)activity_indicator_layer; + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); + return data->animation != NULL; +} + +void activity_indicator_layer_set_animating(ActivityIndicatorLayer *activity_indicator_layer, bool animating) { + Layer *layer = (Layer *)activity_indicator_layer; + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); + + bool is_animating = data->animation != NULL; + if (is_animating == animating) { + return; + } + + Animation *animation = data->animation; + if (animating) { + data->animation = animation = activity_indicator_layer_create_animation(layer); + animation_schedule(animation); + } else { + animation_destroy(animation); + data->animation = NULL; + + layer_mark_dirty(layer); + } +} + +#define activity_indicator_layer_accessors(name, type) \ + type activity_indicator_layer_get_##name(const ActivityIndicatorLayer *activity_indicator_layer) { \ + Layer *layer = (Layer *)activity_indicator_layer; \ + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); \ + return data->name; \ + } \ + \ + void activity_indicator_layer_set_##name(ActivityIndicatorLayer *activity_indicator_layer, type name) { \ + Layer *layer = (Layer *)activity_indicator_layer; \ + ActivityIndicatorData *data = (ActivityIndicatorData *)layer_get_data(layer); \ + data->name = name; \ + layer_mark_dirty(layer); \ + } + +activity_indicator_layer_accessors(color, GColor) +activity_indicator_layer_accessors(thickness, uint8_t) diff --git a/wscript b/wscript new file mode 100644 index 0000000..f5f986e --- /dev/null +++ b/wscript @@ -0,0 +1,48 @@ +# +# This file is the default set of rules to compile a Pebble project. +# +# Feel free to customize this to your needs. +# +import os +import shutil +import waflib + +top = '.' +out = 'build' + + +def distclean(ctx): + if os.path.exists('dist.zip'): + os.remove('dist.zip') + if os.path.exists('dist'): + shutil.rmtree('dist') + waflib.Scripting.distclean(ctx) + + +def options(ctx): + ctx.load('pebble_sdk_lib') + + +def configure(ctx): + ctx.load('pebble_sdk_lib') + + +def build(ctx): + ctx.load('pebble_sdk_lib') + + cached_env = ctx.env + for platform in ctx.env.TARGET_PLATFORMS: + ctx.env = ctx.all_envs[platform] + ctx.set_group(ctx.env.PLATFORM_NAME) + lib_name = '{}/{}'.format(ctx.env.BUILD_DIR, ctx.env.PROJECT_INFO['name']) + ctx.pbl_build(source=ctx.path.ant_glob('src/c/**/*.c'), target=lib_name, bin_type='lib') + ctx.env = cached_env + + ctx.set_group('bundle') + ctx.pbl_bundle(includes=ctx.path.ant_glob('include/**/*.h'), + js=ctx.path.ant_glob(['src/js/**/*.js', 'src/js/**/*.json']), + bin_type='lib') + + if ctx.cmd == 'clean': + for n in ctx.path.ant_glob(['dist/**/*', 'dist.zip'], quiet=True): + n.delete()