Android Wear / ViewStub

Match Timer – Part 5

Previously in this series we’ve looked at the timer engine, the event engine, and the mechanism for notifying the user in our app for timing football matches. In this article we’ll turn our attention to the UI and look at the Activities in the app.

matchtimerOn a mobile or tablet notifications can be a useful mechanism to alert a user to new or updated information in an app. However on Wear notifications are much more front and centre and are the primary mechanism for user interaction. Whereas on mobile & tablet you have a launcher app which is the primary mechanism for launching apps, on Wear the launcher has been deliberately pushed away to encourage developers to use alternate mechanisms to launch Wear apps (we’ll cover this in a future series). For Match Timer, we’ll need to use voice control or the launcher to initially display a Notification. The main Launcher Activity is actually pretty simple:

public class MatchTimerWearActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MatchTimerReceiver.setUpdate(this);
        finish();
    }
}

All this does is call MatchTimerReceiver.setUpdate() which we defined previously which will create a Notification which the user can then interact with.

There is a second Activity in our app which displays the detailed information within the Notification (we discussed how this gets attached to the notification in the previous article). This Activity has a UI associated with it and our main layout contains a single WatchViewStub:

<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WatchViewStub 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/watch_view_stub"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:rectLayout="@layout/rect_activity_match_timer"
  app:roundLayout="@layout/round_activity_match_timer"
  tools:context=".MatchTimerNotificationActivity"
  tools:deviceIds="wear" />

This is a custom ViewStub for Wear which will automatically inflate one of two distinct layouts depending on the shape of the Wear device. There are also two further layouts which contain the same controls, but in layouts optimised for the different from factors. Here’s the rectangular layout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  android:id="@+id/timer"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MatchTimerNotificationActivity"
  tools:deviceIds="wear_square">

  <TextView
    android:id="@+id/elapsed"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerVertical="true"
    android:layout_alignParentLeft="true"
    style="@style/CardText.Elapsed"
    android:text="@string/zero_time" />

  <TextView
    android:id="@+id/played"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignTop="@id/elapsed"
    android:layout_alignParentRight="true"
    style="@style/CardText"
    android:text="@string/zero_time" />

  <TextView
    android:id="@+id/all_stoppages"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignBottom="@id/elapsed"
    android:layout_alignParentRight="true"
    style="@style/CardText"
    android:text="@string/zero_time" />
</RelativeLayout>

This has three TextViews which will display the total elapsed time, total played time, and total stoppage time. The round layout has the same Views, with the same IDs but just in a slightly different layout which will work better on a round display. I won’t bother listing the round layout here, it will be available in the source.

Finally we have MatchTimerNotificationActivity itself:

public class MatchTimerNotificationActivity extends Activity implements View.OnClickListener {
    public static final int MINUTE_MILLIS = 60000;
    public static final int SECOND_MILLIS = 1000;
    private Handler handler = null;

    private TextView elapsed;
    private TextView played;
    private TextView totalStoppages;
    private View timer;

    private MatchTimer matchTimer;

    private Runnable updateRunnable = new Runnable() {
        @Override
        public void run() {
            update();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_match_timer);
        handler = new Handler();
        final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
        stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
            @Override
            public void onLayoutInflated(WatchViewStub stub) {
                elapsed = (TextView) stub.findViewById(R.id.elapsed);
                played = (TextView) stub.findViewById(R.id.played);
                totalStoppages = (TextView) stub.findViewById(R.id.all_stoppages);
                timer = stub.findViewById(R.id.timer);
                timer.setOnClickListener(MatchTimerNotificationActivity.this);
                updateWithoutTimer();
            }
        });
    }

    @Override
    protected void onResume() {
        super.onResume();
        matchTimer = MatchTimer.newInstance(this);
        matchTimer.registerForUpdates();
        update();
    }

    @Override
    protected void onPause() {
        matchTimer.unregisterForUpdates();
        handler.removeCallbacks(updateRunnable);
        super.onPause();
    }

    private void nextTimer() {
        handler.removeCallbacks(updateRunnable);
        handler.postDelayed(updateRunnable, SECOND_MILLIS);
    }

    private void update() {
        updateWithoutTimer();
        nextTimer();
    }

    private void updateWithoutTimer() {
        long playedMillis = matchTimer.getPlayed();
        long stoppedMillis = matchTimer.getTotalStoppages();
        if (elapsed != null) {
            elapsed.setText(format(playedMillis + stoppedMillis));
        }
        if (played != null) {
            played.setText(format(playedMillis));
        }
        if (totalStoppages != null) {
            totalStoppages.setText(format(stoppedMillis));
        }
    }

    private String format(long elapsedMillis) {
        long mins = elapsedMillis / MINUTE_MILLIS;
        long secs = (elapsedMillis - (mins * MINUTE_MILLIS)) / SECOND_MILLIS;
        return String.format(Locale.getDefault(), "%2d:%02d", mins, secs);
    }

    @Override
    public void onClick(View v) {
        if (matchTimer.isPaused()) {
           sendBroadcast(MatchTimerReceiver.RESUME_INTENT);
        } else if (matchTimer.isRunning()) {
            sendBroadcast(MatchTimerReceiver.PAUSE_INTENT);
        } else {
            sendBroadcast(MatchTimerReceiver.START_INTENT);
        }
    }
}

This is pretty straightforward, we use a Handler to fire every second while the Activity is running. There’s a couple of aspects that are worthy of a little explanation, though. Firstly, because we’re using a ViewStub, the actual layout containing the controls that we need to update will not exist after we inflate the activity_match_timer layout. So we need to use a ViewStub.OnLayoutInflatedListener so that we receive a callback once the ViewStub‘s layout has been inflated. At that point we’re able to get our View handles and perform an initial update.

The second thing worth mentioning is that initially all of the user interaction was done via the Notification actions. While this worked quite well, I found during testing that in the excitement of a live football match having to swipe to get to the actions was a little cumbersome. I therefore decided to add an OnClickListener which would perform the most common action (i.e. the action on the first page to the right of the main activity) whenever the user taps on the Activity. This made thing much better, but I also discovered that by adding this, it also handled taps on the Notification itself (i.e. when just the notification title is displayed), and this made things easier still!

That’s pretty much it for the code, all that we need is the manifest and we’re good to go:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.stylingandroid.matchtimer">

  <uses-feature android:name="android.hardware.type.watch" />

  <uses-permission android:name="android.permission.VIBRATE" />

  <application
    android:allowBackup="true"
    android:icon="@drawable/ic_football"
    android:label="@string/app_name"
    android:theme="@android:style/Theme.DeviceDefault">

    <activity
      android:name=".MatchTimerWearActivity"
      android:label="@string/app_name"
      android:theme="@android:style/Theme.DeviceDefault.Light">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity>

    <activity
      android:name=".MatchTimerNotificationActivity"
      android:allowEmbedded="true"
      android:exported="true"
      android:taskAffinity=""
      android:theme="@android:style/Theme.DeviceDefault.Light" />

    <receiver
      android:name=".MatchTimerReceiver"
      android:enabled="true"
      android:exported="false">
      <intent-filter>
        <action android:name="com.stylingandroid.matchtimer.ACTION_START" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_STOP" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_PAUSE" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_RESUME" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_RESET" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_UPDATE" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_ELAPSED_ALARM" />
        <action android:name="com.stylingandroid.matchtimer.ACTION_FULL_TIME_ALARM" />
      </intent-filter>
    </receiver>
  </application>
</manifest>

As this is a Wear app we must declare that in a uses-feature element. We also need to declare that we require VIBRATE permission otherwise our vibrating alarms will not work. There are declarations for both Activities. We don’t need to do anything in order to enable voice launch capabilities, other than adhere to the normal rules (i.e. the appropriate intent-filter)for creating a launch-able Activity – the framework adds voice launching based upon the name used in the android:label attribute.

Finally we declare MatchTimerReceiver with the supported actions.

When I originally planned this series of articles, I felt that this would be the appropriate place to conclude the series. However in releasing the app to Google Play I encountered one or two issues in packaging the app appropriately for upload to the Play developer console. So in the concluding article in this series we’ll take a look at how to package the app ready for distribution.

Match Timer is available on Google Play.


Get it on Google Play

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.