The (Circle) Stopwatch sample application demonstrates how to create a simple circular stopwatch. The Ecore Animator API is used to show the rotating marks continuously.
The following figure illustrates the main screen of the (Circle) Stopwatch.
Figure: (Circle) Stopwatch screen
Source Files
You can create and view the sample application project, including the source files, in the IDE.
File name | Description |
---|---|
edje/images/ | This directory contains the image files used in the main.edc file. |
inc/stopwatch.h | This file contains information and definition of the variables and functions used in the C files, especially in the main.c file. |
inc/data.h | This file contains information and definition of the variables and functions used in the C files, especially in the data.c file. |
inc/view.h | This file contains information and definition of the variables and functions used in the C files, especially in the view.c file. |
res/edje/main.edc | This file is for the UI construction and contains layout information regarding images and UI components. |
res/image/ | This directory contains the image files used in the C files. |
src/data.c | This file contains the functions for retrieving and making data for the application. |
src/main.c | This file contains the functions related to the application life-cycle, callback functions, and view control. |
src/view.c | This file contains the functions for implementing the views and handling events. |
Implementation
Basic Layout
The following figure illustrates the basic view of the (Circle) Stopwatch. The red mark indicates the total elapsed time in seconds, and the light blue bar shows the total elapsed time in minutes. The blue mark indicates the lap time in seconds.
Figure: Basic view
To create the basic view:
- The application starts with the view_create() function:
/* @brief Creates essential objects: window, conformant, and layout */ Eina_Bool view_create(void) { /* Create the window */ s_info.win = view_create_win(PACKAGE); if (s_info.win == NULL) { dlog_print(DLOG_ERROR, LOG_TAG, "failed to create a window."); return EINA_FALSE; } /* Create the conformant */ s_info.conform = view_create_conformant_without_indicator(s_info.win); if (s_info.conform == NULL) { dlog_print(DLOG_ERROR, LOG_TAG, "failed to create a conformant"); return EINA_FALSE; } /* Show the window after the main view is set up */ evas_object_show(s_info.win); return EINA_TRUE; }
The win variable is an essential UI component in making the application UI. The conform variable is the main conformant object of this sample application.
- Create the stopwatch layout using the Edje file and make parts for the stopwatch marks:
static bool app_create(void *user_data) { data_get_full_path(EDJ_FILE, full_path, (int)PATH_MAX); view_stopwatch_create_layout(full_path); /* Set background image to "sw.number.bg" part of EDC */ image = data_get_image_path("sw.number.bg"); view_set_image(view_stopwatch_get_layout_object(), "sw.number.bg", image); view_set_color(view_stopwatch_get_layout_object(), "sw.number.bg", 249, 249, 249, 255); free(image); /* Create the lap mark part */ image = data_get_image_path("sw.mark.lap"); view_stopwatch_set_part(image, STOPWATCH_MARK_TYPE_LAP); free(image); /* Create the main mark part */ image = data_get_image_path("sw.mark.main"); view_stopwatch_set_part(image, STOPWATCH_MARK_TYPE_MAIN); free(image); }
- In the view_stopwatch_set_part() function, the parts are created according to the size and position predefined in the stopwatch.h header file:
/* @brief Creates the part and sets the color @param[in] parent Object to which you want to add this object @param[in] image_path Path of the image file you want to set @param[in] type Type of part to be created */ void view_stopwatch_set_part(const char *image_path, stopwatch_mark_type_e type) { if (type == STOPWATCH_MARK_TYPE_MAIN) { s_info.main_mark = view_create_part(s_info.layout, image_path, 0, 0, STOPWATCH_MARK_WIDTH, STOPWATCH_MARK_HEIGHT); evas_object_color_set(s_info.main_mark, 226, 0, 15, 255); } else if (type == STOPWATCH_MARK_TYPE_LAP) { s_info.lap_mark = view_create_part(s_info.layout, image_path, 0, 0, STOPWATCH_MARK_WIDTH, STOPWATCH_MARK_HEIGHT); evas_object_color_set(s_info.lap_mark, 0, 148, 255, 255); view_stopwatch_set_lapmark_visibility(false); } }
Managing Stopwatch States
The state machine is a good model for making the application structure simple. After designing the state machine, you can create modules and functions easily.
Figure: Stopwatch state diagram
The following 2 callback functions handle all user control events, because the stopwatch has only 2 transparent touch panels, which are the upper touch panel with the START, STOP, and RESUME buttons and the lower touch panel with the LAP and RESET buttons. They set the state machine status according to the current state and user input events.
/* @brief Function is operated when the clicked event of the upper touch area (start) is triggered @param[in] data Data to be passed to the callback function @param[in] obj Edje object where the signal comes from @param[in] emission Exact signal's emission string to be passed to the callback function @param[in] source Exact signal source */ static void _start_click_cb(void *data, Evas_Object *obj, const char *emission, const char *source) { if (s_info.state == STOPWATCH_STATE_READY) { /* Start the stopwatch */ stopwatch_set_stopwatch_state(STOPWATCH_STATE_RUNNING); } else if (s_info.state == STOPWATCH_STATE_RUNNING) { /* Stop the stopwatch */ stopwatch_set_stopwatch_state(STOPWATCH_STATE_STOP); } else if (s_info.state == STOPWATCH_STATE_STOP) { /* Resume the stopwatch */ stopwatch_set_stopwatch_state(STOPWATCH_STATE_RUNNING); } else if (s_info.state == STOPWATCH_STATE_MAX_STOP) { /* Ignore this event */ } else { dlog_print(DLOG_ERROR, LOG_TAG, "_start_click_cb Invalid Status [%d]", s_info.state); } } /* @brief Function is operated when the clicked event of the lower touch area (reset) is triggered @param[in] data Data to be passed to the callback function @param[in] obj Edje object where the signal comes from @param[in] emission Exact signal's emission string to be passed to the callback function @param[in] source Exact signal source */ static void _reset_click_cb(void *data, Evas_Object *obj, const char *emission, const char *source) { if (s_info.state == STOPWATCH_STATE_READY) { /* Ignore this event */ /* At ready state, start is available only */ return; } else if (s_info.state == STOPWATCH_STATE_RUNNING) { /* Record laps */ stopwatch_set_stopwatch_state(STOPWATCH_STATE_RUNNING); } else if (s_info.state == STOPWATCH_STATE_STOP || s_info.state == STOPWATCH_STATE_MAX_STOP) { /* Reset the stopwatch */ stopwatch_set_stopwatch_state(STOPWATCH_STATE_READY); } else { dlog_print(DLOG_ERROR, LOG_TAG, "_reset_click_cb Invalid Status [%d]", s_info.state); } }
The stopwatch_set_stopwatch_state() function is the main stopwatch control function. It controls all functionalities, such as the animator, the initialization process, and button views.
/* @brief Sets the stopwatch state machine @param[in] state The target state */ void stopwatch_set_stopwatch_state(stopwatch_state_e state) { dlog_print(DLOG_DEBUG, LOG_TAG, "stopwatch_set_state From [%d] To [%d]", s_info.state, state); switch (state) { case STOPWATCH_STATE_READY: if (s_info.state == STOPWATCH_STATE_STOP || s_info.state == STOPWATCH_STATE_MAX_STOP) { /* Reset the stopwatch */ _init_stopwatch(); } else { dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid STOPWATCH_STATE_READY at [%d]", state, s_info.state); return; } break; case STOPWATCH_STATE_RUNNING: if (s_info.state == STOPWATCH_STATE_READY) { /* Start the stopwatch */ s_info.time_ref = _get_current_ms_time(); stopwatch_handle_animator(STOPWATCH_ANIMATION_START); } else if (s_info.state == STOPWATCH_STATE_STOP) { /* Resume the stopwatch Set running from stop */ s_info.time_ref = _get_current_ms_time(); stopwatch_handle_animator(STOPWATCH_ANIMATION_RESUME); } else if (s_info.state == STOPWATCH_STATE_RUNNING && s_info.current_lap < STOPWATCH_LAP_MAX) { /* Record laps Set running from running */ s_info.time_lap_diff = _get_current_ms_time() - s_info.time_ref + s_info.time_elapse_sum; /* Keep lap records in a static array */ if (s_info.current_lap > 0) s_info.lap_record[s_info.current_lap] = s_info.time_lap_diff; /* Increase the lap indicator */ s_info.current_lap += 1; if (s_info.current_lap == 1) view_stopwatch_set_lapmark_visibility(true); /* Update the laps status view */ view_stopwatch_set_lap_number(s_info.current_lap); } else { dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid STOPWATCH_STATE_RUNNING at [%d]", state, s_info.state); return; } /* Change the buttons and state */ view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_STOP); view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_LAP); s_info.state = STOPWATCH_STATE_RUNNING; break; case STOPWATCH_STATE_STOP: if (s_info.state == STOPWATCH_STATE_RUNNING) { /* Stop the stopwatch Save the sum of the elapsed time and stop the animation */ s_info.time_elapse_sum += _get_current_ms_time() - s_info.time_ref; stopwatch_handle_animator(STOPWATCH_ANIMATION_STOP); /* Change the buttons and state */ view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_RESUME); view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_RESET); s_info.state = STOPWATCH_STATE_STOP; } else { dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid STOPWATCH_STATE_STOP at [%d]", state, s_info.state); return; } break; case STOPWATCH_STATE_MAX_STOP: if (s_info.state == STOPWATCH_STATE_RUNNING) { /* Stop the stopwatch Save the sum of the elapse time and stop the animation */ s_info.time_elapse_sum += _get_current_ms_time() - s_info.time_ref; stopwatch_handle_animator(STOPWATCH_ANIMATION_STOP); /* Change the buttons and state */ view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_MAX_STOP); view_stopwatch_set_button_released(STOPWATCH_BUTTON_TYPE_RESET); s_info.state = STOPWATCH_STATE_MAX_STOP; } else { dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid STOPWATCH_STATE_MAX_STOP at [%d]", state, s_info.state); return; } break; default: dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid [%d]", state); break; } }
Creating Continuously Rotating Marks
The Ecore Animator API is a helper to simplify animation creation. This sample application uses the ecore_animator_add() function to run an animation for an unspecified amount of time, because the marks must be moved continuously when the stopwatch is running.
The following animator is controlled by the control function. The stopwatch_update_animation callback is an actual animation drawing function, which is registered as an animator callback function.
/* @brief Handles the stopwatch animator @param[in] command Command to be executed */ void stopwatch_handle_animator(int command) { switch (command) { case STOPWATCH_ANIMATION_INIT: /* Initialize the animator */ if (s_info.animator) { ecore_animator_del(s_info.animator); s_info.animator = NULL; } /* Initialize the animation object position */ stopwatch_update_animation(NULL); view_stopwatch_rotate_mark(STOPWATCH_MARK_TYPE_LAP, 0.0, (STOPWATCH_MARK_WIDTH / 2), (STOPWATCH_MARK_HEIGHT / 2)); break; case STOPWATCH_ANIMATION_START: /* Start the animation */ s_info.animator = ecore_animator_add(stopwatch_update_animation, NULL); break; case STOPWATCH_ANIMATION_STOP: /* Pause the animation */ if (s_info.animator) ecore_animator_freeze(s_info.animator); break; case STOPWATCH_ANIMATION_RESUME: /* Resume the animation */ if (s_info.animator) ecore_animator_thaw(s_info.animator); break; default: dlog_print(DLOG_ERROR, LOG_TAG, "stopwatch_set_state Invalid Command [%d]"s, command); break; } }
When the stopwatch is started, the animator is created by the ecore_animator_add() function. After creating the animator, the application uses only the newly created animator until the stopwatch is reset. When the app_resume() callback function is called, the animator has to be resumed, and when the app_pause() callback function is called, the animator has to be stopped for saving application performance and device resources.
The stopwatch_update_animation() function redraws the hands at every animator frame. This function gets the current time and calculates the positions of each mark and finally redraws those at the calculated position.
/* @brief Updates the animation changing the object's position @param[in] user_data User data to be passed to the callback functions */ Eina_Bool stopwatch_update_animation(void *data) { double degree = 0.0f; double progress = 0.0f; int msec = 0; int sec = 0; int min = 0; char min_str[3] = {0,}; char sec_str[3] = {0,}; char msec_str[3] = {0,}; long long current_time = _get_current_ms_time(); long long current_sum; if (current_time == 0) { dlog_print(DLOG_ERROR, LOG_TAG, "Fail to get current time"); return ECORE_CALLBACK_RENEW; } /* Calculate the sum of elapsed time at this moment */ current_sum = current_time - s_info.time_ref + s_info.time_elapse_sum; /* Parse the millisecond time to various formats (minutes, seconds, milliseconds) */ sec = current_sum / 1000; msec = current_sum % 1000; min = sec / 60; sec = sec % 60; /* Stopwatch running time is limited to 60 minutes */ if (min >= STOPWATCH_RUNNING_TIME_MAX) { view_set_text(view_stopwatch_get_layout_object(), "text.main.minutes", "60"); view_set_text(view_stopwatch_get_layout_object(), "text.main.seconds", "00"); view_set_text(view_stopwatch_get_layout_object(), "text.main.milliseconds", "00"); view_stopwatch_rotate_mark(STOPWATCH_MARK_TYPE_MAIN, 360, (STOPWATCH_MARK_WIDTH / 2), (STOPWATCH_MARK_HEIGHT / 2)); view_stopwatch_set_progressbar_val(0); stopwatch_set_stopwatch_state(STOPWATCH_STATE_MAX_STOP); return ECORE_CALLBACK_CANCEL; } /* Update the stopwatch time number */ if (min < 10) sprintf(min_str, "0%d", min); else sprintf(min_str, "%d", min); if (sec < 10) sprintf(sec_str, "0%d", sec); else sprintf(sec_str, "%d", sec); if (msec < 10) sprintf(msec_str, "0%d", msec/10); else sprintf(msec_str, "%d", msec/10); view_set_text(view_stopwatch_get_layout_object(), "text.main.minutes", min_str); view_set_text(view_stopwatch_get_layout_object(), "text.main.seconds", sec_str); view_set_text(view_stopwatch_get_layout_object(), "text.main.milliseconds", msec_str); /* Rotate the stopwatch the main mark */ degree = sec * SEC_ANGLE; degree += msec * SEC_ANGLE / 1000.0; view_stopwatch_rotate_mark(STOPWATCH_MARK_TYPE_MAIN, degree, (STOPWATCH_MARK_WIDTH / 2), (STOPWATCH_MARK_HEIGHT / 2)); progress = (min % 10) * 10; progress += sec * PROGRESSBAR_SEC_ANGLE; progress += msec * PROGRESSBAR_SEC_ANGLE / 1000.0; view_stopwatch_set_progressbar_val(progress); if (s_info.current_lap > 0) { /* Calculate the sum of the currently lap elapse time */ current_sum = _get_current_ms_time() - s_info.time_ref + s_info.time_elapse_sum - s_info.time_lap_diff; sec = (int)current_sum / 1000; msec = (int)current_sum % 1000; sec = sec % 60; /* Rotate the stopwatch the lap mark */ degree = sec * SEC_ANGLE; degree += msec * SEC_ANGLE / 1000.0; view_stopwatch_rotate_mark(STOPWATCH_MARK_TYPE_LAP, degree, (STOPWATCH_MARK_WIDTH / 2), (STOPWATCH_MARK_HEIGHT / 2)); } return ECORE_CALLBACK_RENEW; }
Getting the Reference System Time
This sample application uses the clock_gettime() function to get the reference system time. Using the CLOCK_MONOTONIC option, the application can get the monotonic time since some unspecified system starting point.
The _get_current_ms_time() function returns the reference system time in the millisecond format. Therefore, the application can calculate the stopwatch time using this function.
/* @brief Gets the system time by milliseconds */ static long long _get_current_ms_time(void) { struct timespec tp; long long res = 0; if (clock_gettime(CLOCK_MONOTONIC, &tp) == -1) { /* Zero mean invalid time */ return 0; } else { /* Calculate milliseconds time */ res = tp.tv_sec * 1000 + tp.tv_nsec / 1000000; return res; } }