Pebble Timeでバッテリー残量を表示するWatch Faceを作ってみた

あむちょです。

実際に作ったのは半年以上前の話です。

PR アプリ作ってます
ad maruta

目次

Pebble Timeってなんですかって人はこちら。丸型スマートウォッチPebble Time Roundのレビュー

今回(半年前に)作ったのは、旧型Pebbleで割りとダウンロード数の多かったバッテリー残量を表示するWatch Faceです。旧型のPebbleはこちら。Pebbleのバッテリー残量を表示するWatch Face

今回も角形と丸型の両方に対応。

Watch Face 「Battery Info」

外側の円がバッテリー残量で、内側が秒をあらわしてます。

battery inforect battery inforound

周りの赤色の角は、余白をごまかすためのただの飾りです。

オープニングでアニメーションするけど、gifを作るのが大変なので動画で

こちらからダウンロードできます。Pebble Time Watch Face 「Battery Info」

ソースコード

バッテリー残量と秒はパスで直接描画してます。毎秒再描画させているのでバッテリーの消費がちょっと心配。

Pebbleは、textだったり描画レイヤー以外でも変更すると、システムから再描画がかかるので、バッテリーを気にするなら秒の表示はやめておく方がいいです。

#include 

static Window *window;
static TextLayer *text_layer;
static TextLayer *time_layer;

static Layer *round_layer;
static GPath *round_path;
static GPath *round_rect_path;
static GPath *round_sec_path;

static GBitmap *image;
static BitmapLayer *image_layer;

#define EMPTY_SLOT -1
int image_state = EMPTY_SLOT;

static int sec = 0;

static int battery = 0;
static AppTimer *timer;

static int animationCnt = 0;
static float delSec = 0.0;
static float delBt = 0.0;
static int line_x = 0.0;
#define STOP_CNT 30

#define COLOR_BACK GColorBlack
#define COLOR_BATTERY GColorCyan
#define COLOR_SEC GColorElectricBlue
#define COLOR_TIME GColorElectricBlue
#define COLOR_TEXT_BT GColorCyan

#if defined(PBL_RECT)
    #define WIDTH 144
    #define HEIGHT 168
#elif defined(PBL_ROUND)
    #define WIDTH 180
    #define HEIGHT 180
#endif

static const GPathInfo ROUND_POINTS = {
    4, (GPoint []){
        {-5, 70},
        {5, 70},
        {5, 55},
        {-5,55}
    }
};
static const GPathInfo ROUND_POINTS_RECT = {
    4, (GPoint []){
        {-20, 70},
        {20, 70},
        {20, 55},
        {-20,55}
    }
};
static const GPathInfo ROUND_POINTS_SEC = {
    4, (GPoint []){
        {-10, 58},
        {10, 58},
        {10, 45},
        {-10,45}
    }
};

static void handle_second_tick(struct tm* tick_time, TimeUnits units_changed);
static void handle_battery(BatteryChargeState charge_state);

static void round_update_proc(Layer *layer, GContext *ctx) {
    
    if(animationCnt == 0)return;
    
//battery
    graphics_context_set_fill_color(ctx, COLOR_BATTERY);
    graphics_fill_circle(ctx,GPoint(WIDTH/2,HEIGHT/2),65);
    
    graphics_context_set_fill_color(ctx, COLOR_BACK);
    graphics_fill_circle(ctx,GPoint(WIDTH/2,HEIGHT/2),57);
    
    int n=0;
    for(n=0;n<10;n++){
        gpath_rotate_to(round_path, (TRIG_MAX_ANGLE*n/10));
        gpath_draw_filled(ctx, round_path);
        if(n < battery){
            gpath_rotate_to(round_rect_path, (TRIG_MAX_ANGLE*(n+0.5+5)/10));
            gpath_draw_filled(ctx, round_rect_path);
        }
    }
    
//sec
    graphics_context_set_fill_color(ctx, COLOR_SEC);
    graphics_fill_circle(ctx,GPoint(WIDTH/2,HEIGHT/2),53);
    
    graphics_context_set_fill_color(ctx, COLOR_BACK);
    graphics_fill_circle(ctx,GPoint(WIDTH/2,HEIGHT/2),48);
    
    gpath_rotate_to(round_sec_path, (TRIG_MAX_ANGLE*(sec+30)/60));
    gpath_draw_filled(ctx, round_sec_path);
    
    graphics_context_set_stroke_color(ctx, GColorRed);
    graphics_context_set_stroke_width(ctx,2);
    //line length 64
    graphics_draw_line(ctx, GPoint((WIDTH-64)/2,HEIGHT/2-30+10),GPoint((WIDTH-64)/2+line_x,HEIGHT/2-30+10));
    graphics_draw_line(ctx, GPoint((WIDTH-64)/2+64-line_x,HEIGHT/2-30+55),GPoint((WIDTH-64)/2+64,HEIGHT/2-30+55));
    
}

static void update_layer(){
    
    animationCnt++;
    sec = delSec*animationCnt;
    battery = 10-delBt*animationCnt;
    line_x = 64*animationCnt/STOP_CNT;
    
    layer_mark_dirty(round_layer);

    if(animationCnt < STOP_CNT){ timer = app_timer_register(13 /* milli */, update_layer, NULL); } else{ tick_timer_service_subscribe(SECOND_UNIT, &handle_second_tick); battery_state_service_subscribe(&handle_battery); } } static void handle_battery(BatteryChargeState charge_state) { static char battery_text[] = "100% charged"; battery = 10-charge_state.charge_percent/10; if (charge_state.is_charging) { snprintf(battery_text, sizeof(battery_text), "charging"); } else { snprintf(battery_text, sizeof(battery_text), "%d%% charged", charge_state.charge_percent); } text_layer_set_text(text_layer, battery_text); } static void handle_second_tick(struct tm* tick_time, TimeUnits units_changed) { static char time_text[] = "08:22"; //strftime(time_text, sizeof(time_text), "%T", tick_time); text_layer_set_text(time_layer, time_text); handle_battery(battery_state_service_peek()); sec = tick_time->tm_sec;
    layer_mark_dirty(window_get_root_layer(window));
    
}

static void handle_battery(BatteryChargeState charge_state) {
    
    static char battery_text[] = "100% charged";
    
    battery = 10-charge_state.charge_percent/10;
    
    if (charge_state.is_charging) {
        snprintf(battery_text, sizeof(battery_text), "charging");
    } else {
        snprintf(battery_text, sizeof(battery_text), "%d%% charged", charge_state.charge_percent);
    }
    text_layer_set_text(text_layer, battery_text);
}

static void handle_second_tick(struct tm* tick_time, TimeUnits units_changed) {
    
    static char time_text[] = "08:22";
    
    strftime(time_text, sizeof(time_text), "%T", tick_time);
    text_layer_set_text(time_layer, time_text);
    
    handle_battery(battery_state_service_peek());
    
    sec = tick_time->tm_sec;
    layer_mark_dirty(window_get_root_layer(window));
    
}

static void window_load(Window *window) {
    Layer *window_layer = window_get_root_layer(window);
    GRect bounds = layer_get_bounds(window_layer);
    
//image
    image_state=1;
    image = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_BACK);
    image_layer = bitmap_layer_create(bounds);
    bitmap_layer_set_bitmap(image_layer, image);
    layer_add_child(window_layer, bitmap_layer_get_layer(image_layer));
    
//path
    round_path = gpath_create(&ROUND_POINTS);
    round_rect_path = gpath_create(&ROUND_POINTS_RECT);
    round_sec_path = gpath_create(&ROUND_POINTS_SEC);

    GRect centerRect =(GRect){.origin = {0,0}, .size = {bounds.size.w,bounds.size.h}};
    const GPoint center = grect_center_point(&centerRect);
    gpath_move_to(round_path, center);
    gpath_move_to(round_rect_path, center);
    gpath_move_to(round_sec_path, center);
    
    round_layer = layer_create(bounds);
    layer_set_update_proc(round_layer, round_update_proc);
    layer_add_child(window_layer, round_layer);
    
//text
    time_layer = text_layer_create((GRect) { .origin = { 0, HEIGHT/2-30+15+5 }, .size = { bounds.size.w, 40 } });
    //text_layer_set_text(text_layer, "Press a button");
    text_layer_set_text_color(time_layer,COLOR_TIME);
    text_layer_set_background_color(time_layer,GColorClear);
    text_layer_set_font(time_layer, fonts_get_system_font(FONT_KEY_BITHAM_30_BLACK));
    text_layer_set_text_alignment(time_layer, GTextAlignmentCenter);
    layer_add_child(window_layer, text_layer_get_layer(time_layer));

    text_layer = text_layer_create((GRect) { .origin = { 0, HEIGHT/2-40+15+6 }, .size = { bounds.size.w, 20 } });
  //text_layer_set_text(text_layer, "Press a button");
    text_layer_set_text_color(text_layer,COLOR_TEXT_BT);
    text_layer_set_background_color(text_layer,GColorClear);
    text_layer_set_text_alignment(text_layer, GTextAlignmentCenter);
    layer_add_child(window_layer, text_layer_get_layer(text_layer));
}

static void window_unload(Window *window) {
    text_layer_destroy(text_layer);
    text_layer_destroy(time_layer);
    layer_destroy(round_layer);
}

static void init(void) {
  window = window_create();
    window_set_background_color(window, COLOR_BACK);
  window_set_window_handlers(window, (WindowHandlers) {
    .load = window_load,
    .unload = window_unload,
  });
  const bool animated = true;
  window_stack_push(window, animated);
    
    time_t now = time(NULL);
    struct tm *current_time = localtime(&now);
    delSec = (float)(current_time->tm_sec+1)/STOP_CNT;
    
    BatteryChargeState charge_state = battery_state_service_peek();
    delBt = (float)(charge_state.charge_percent/10)/STOP_CNT;
    
    timer = app_timer_register(500 /* milli */, update_layer, NULL);
    
}

static void deinit(void) {
    tick_timer_service_unsubscribe();
    battery_state_service_unsubscribe();
    
//path
    gpath_destroy(round_path);
    gpath_destroy(round_rect_path);
    gpath_destroy(round_sec_path);
    
//image
    if (image_state != EMPTY_SLOT) {
        layer_remove_from_parent(bitmap_layer_get_layer(image_layer));
        bitmap_layer_destroy(image_layer);
        gbitmap_destroy(image);
        image_state = EMPTY_SLOT;
    }
    
    window_destroy(window);
}

int main(void) {
  init();


  app_event_loop();
  deinit();
}

オープニングアニメーションは13ミリ秒のタイマーを30フレーム分でまわしてますが、実際そこまで速度が出せないので、とりあえず早めにしてあります。

バッテリー残量の取り方は

変化があった時のイベント登録は

//init()で登録
battery_state_service_subscribe(&handle_battery);

//バッテリー残量や充電状態の変化で呼ばれる
static void handle_battery(BatteryChargeState charge_state) {
    
    //バッテリー残量 0~100で10、刻み
    int battery = charge_state.charge_percent;
    //充電中かどうか
    bool isCharging = charge_state.is_charging
    
}

イベントではなくバッテリー状態をすぐに知りたい時は

BatteryChargeState charge_state = battery_state_service_peek();
int battery = charge_state.charge_percent;

で求めれます。

自分のため

これでようやく3個リリースできました。

アプリ内のWatch Face管理画面はストアからダウンロードしないと画像が表示されない(gifだとちゃんと動く)のでちょっとさみしいですね。

pebble app

自分の管理画面のサムネイルを表示したいので、

そのためだけに公開してます。

コメント