Buddy UI Sample Overview

The Buddy sample application demonstrates how you can design a list with animation for a Gear device to fit in the circular UI.

The following figure illustrates the main screens of the Buddy.

Figure: Buddy screens

Buddy screens

When the application opens, a list of buddies is displayed in screen.

To see more buddies, the user can rotate the bezel clockwise or counterclockwise. The list scrolls with animation to the direction the user rotates.

Source Files

You can create and view the sample application project including the source files in the IDE.

Table: Source files
File name Description
config.xml This file contains the application information for the platform to install and launch the application, including the view mode and the icon to be used in the device menu.
css/customIndicator.css This file contains the CSS styling for the custom list for the circular UI.
css/style.css This file contains the CSS styling for the application UI.
images/ This directory contains the images used to create the user interface.
index.html This is a starting file from which the application starts loading. It contains the layout of the application screens.
js/app.js This file contains the buddies' data and initializes the application.
js/customIndicator.js This file initializes the custom list for the circular UI and provides its control.

Implementation

Defining the Application Layout

The -webkit-mask styles create the circular icon of a buddy's picture:

<!--js/customIndicator.css-->
.list-thumbnail 
{
   -webkit-mask-image: url("../images/wc_contact_bg.png");
   -webkit-mask-size: contain;
   -webkit-mask-repeat: no-repeat;
}

Performing Basic Operations

The entry point for the application is the js/app.js module. The custom indicator module is required to run the application, and you must include both .js files.

<!--index.html-->
<script src="js/page.js"></script>
<script src="js/custom-indicator.js"></script>
<script src="js/app.js"></script>

The js/app.js module registers callbacks for user events, such as the back button or bezel rotation:

/* js/app.js */
function init() 
{
   listController.init("list-buddy");
   /* Push all data to the list */
   pushData();
   /* Add a hardware key event listener */
   window.addEventListener("tizenhwkey", keyEventHandler);
   /* Add both pages to the page controller */
   pageController.addPage("page-main");
   pageController.addPage("page-contact");
   /* Set callbacks for up and down scroll */
   /* These functions animate the header if needed */
   listController.setScrollUpCallback(scrollUpCallbackHeader);
   listController.setScrollDownCallback(scrollDownCallbackHeader);
}

The js/app.js module adds data to the circle list indicator:

/* js/app.js */
(function pushData() 
{
   var i;
   for (i = 0; i < PERSON_DATA_NAME.length; i++) 
   {
      if (PERSON_DATA_IMAGE[i]) 
      {
         listController.addData(PERSON_DATA_NAME[i], PERSON_DATA_IMAGE[i], createPageChangeFunc(i));
      } 
      else 
      {
         listController.addData(PERSON_DATA_NAME[i], null, createPageChangeFunc(i));
      }
   }
}());

Applying Animation Effects

To apply the animation effects:

  1. The event callback activates the header scroll animation effect. The HEADER_DATA variable holds the style data of the effect.

    The application uses the requestAnimationFrame() method to implement the animation effect.

    /* js/app.js */
    function setHeaderAnimation(start, end) 
    {
       if (animRequest) 
       {
          window.cancelAnimationFrame(animRequest);
       }
       animRequest = window.requestAnimationFrame(drawAnimationFrame.bind(this, start, end));
    }
    
    function scrollDownCallbackHeader(focusPos) 
    {
       /* Header disappears */
       switch (focusPos) 
       {
          case 0:
             if (diff === 1) 
             {
                /* Full header -> half header */
                setHeaderAnimation("FULL", "HALF");
             } 
             else
             {
                /* Full header -> no header */
                setHeaderAnimation("FULL", "NONE");
             }
             break;
          case 1:
             /* Half header -> no header */
             setHeaderAnimation("HALF", "NONE");
             break;
          default:
             break;
       }
    }
    
    function scrollUpCallbackHeader(focusPos) 
    {
       /* Header appears */
       switch (focusPos) 
       {
          case 1:
             /* Half header -> full header */
             setHeaderAnimation("HALF", "FULL");
             break;
          default:
             if (focusPos + diff === 1) 
             {
                /* No header -> half header */
                setHeaderAnimation("NONE", "HALF");
             } 
             else if (focusPos + diff === 0)
             {
                /* No header -> full header */
                setHeaderAnimation("NONE", "FULL");
             }
             break;
       }
    }
    
  2. Call the setAnimationStyle() method to render the frame. The method must set the new style for the frame.

    The value of the style is calculated by the ratio of the current progress in the animation effect duration.

    /* js/app.js */
    function setAnimationStyle(elm, origPos, destPos, ratio) 
    {
       var valOrigStyle,
           valDestStyle,
           valAnimStyle;
    
       if (ratio > 1) 
       {
          ratio = 1;
       }
    
       Object.keys(HEADER_DATA[origPos]).forEach(function(key) 
       {
          switch (key) 
          {
             case "top":
                valOrigStyle = parseFloat(HEADER_DATA[origPos][key]);
                valDestStyle = parseFloat(HEADER_DATA[destPos][key]);
                valAnimStyle = (valOrigStyle + (valDestStyle - valOrigStyle) * ratio) + "px";
                break;
             default:
                break;
          }
    
          elm.style[key] = valAnimStyle;
       });
    }
    
    function drawAnimationFrame(animStart, animEnd, timestamp) 
    {
       var elmHeader = document.querySelector(".header"),
           progress;
    
       if (!animStartTime) 
       {
          animStartTime = timestamp;
       }
       progress = timestamp - animStartTime;
    
       setAnimationStyle(elmHeader, animStart, animEnd, progress / ANIM_DURATION);
    
       if (progress < ANIM_DURATION) 
       {
          animRequest = window.requestAnimationFrame(drawAnimationFrame.bind(this, animStart, animEnd));
       } 
       else 
       {
          animRequest = 0;
          animStartTime = 0;
       }
    }
    
  3. The js/customIndicator.js module provides a list with animation for the circular UI.

    The code registers a rotary event listener to active the animation when a rotary event occurs.

    The callback changes the label text after the DELAY_TEXT_DURATION event, and starts the animation effect.

    /* js/customIndicator.js */
    listController.init = function init(listId) 
    {
       listElement = document.querySelector("#" + listId);
       labelElement = listElement.querySelector(".list-label");
       labelElement.addEventListener("click", labelClickEventHandler);
    
       document.addEventListener('rotarydetent', rotaryEventHandler);
    };
    
    function rotaryEventHandler(ev) 
    {
       var direction = ev.detail.direction;
    
       /* If the rotated direction is clockwise, scroll down the list */
       /* If the rotated direction is counterclockwise, scroll up the list */
       if (direction === "CW") 
       {
          listController.scroll(1);
       } 
       else if (direction === "CCW") 
       {
          listController.scroll(-1);
       }
    }
    
    listController.scroll = function scroll(diff) 
    {
       if (diff < 0) 
       {
          if (scrollUpCallback) 
          {
             scrollUpCallback(focusPos, diff);
          }
       } 
       else if (diff > 0) 
       {
          if (scrollDownCallback) 
          {
             scrollDownCallback(focusPos, diff);
          }
       }
    
       if ((focusPos + diff >= 0) && (focusPos + diff < listLength)) 
       {
          focusPos += diff;
          /* Set the animation state */
          setAnimation(-1 * diff);
          /* Label must be changed after the predefined duration */
          setTimeout(function() 
          {
             setLabel(dataName[focusPos]);
          }, DELAY_TEXT_DURATION);
       }
    };
    
  4. Set the animation with the setAnimation() method. The application uses the requestAnimationFrame() method to draw an animation frame.

    The drawAnimationFrame() method is called for each rendering of your rendering engine.

    If a new animation request is set before the previous animation is finished, this application stops the previous and starts the new one.

    /* js/customIndicator.js */
    function setAnimation(diff) 
    {
       animPosDiff = diff;
    
       if (animRequest) 
       {
          window.cancelAnimationFrame(animRequest);
       }
       animRequest = window.requestAnimationFrame(drawAnimationFrame);
    }
    
    function drawAnimationFrame(timestamp) 
    {
       var progress,
           i;
    
       if (!animStartTime) 
       {
          animStartTime = timestamp;
       }
       progress = timestamp - animStartTime;
    
       for (i = 0; i < listLength; i++) 
       {
          setAnimationStyle(containerElement[i], i - focusPos - animPosDiff, i - focusPos, progress / ANIM_SCROLL_DURATION);
       }
    
       if (progress < ANIM_SCROLL_DURATION) 
       {
          animRequest = window.requestAnimationFrame(drawAnimationFrame);
       } 
       else 
       {
          animRequest = 0;
          animStartTime = 0;
       }
    }
    
  5. The main code of the setAnimationStyle() method calculates a style value of a specific frame in a transitional animation.

    Apply the calculated style value to each element composing a list to implement the animation effect.

    /* js/customIndicator.js */
    /* function setAnimationStyle(elm, origPos, destPos, ratio) */
    Object.keys(CONTAINER_DATA["POS_" + origPos]).forEach(function(key) 
    {
       switch (key) 
       {
          case "transform":
             valOrigStyle = parseFloat(CONTAINER_DATA["POS_" + origPos][key].substring(7));
             valDestStyle = parseFloat(CONTAINER_DATA["POS_" + destPos][key].substring(7));
             valAnimStyle = "rotate(" + (valOrigStyle + (valDestStyle - valOrigStyle) * ratio) + "deg)";
             break;
          case "margin-left":
          case "width":
          case "height":
          case "font-size":
          case "line-height":
             valOrigStyle = parseFloat(CONTAINER_DATA["POS_" + origPos][key]);
             valDestStyle = parseFloat(CONTAINER_DATA["POS_" + destPos][key]);
             valAnimStyle = (valOrigStyle + (valDestStyle - valOrigStyle) * ratio) + "px";
             break;
          case "opacity":
             valOrigStyle = parseFloat(CONTAINER_DATA["POS_" + origPos][key]);
             valDestStyle = parseFloat(CONTAINER_DATA["POS_" + destPos][key]);
             valAnimStyle = (valOrigStyle + (valDestStyle - valOrigStyle) * ratio);
             break;
       }
    }