Wearable Appはじめました


先日Android Wear上でTwitterのタイムラインを見ることができるアプリ[Looking](https://play.google.com/store/apps/details?id=com.droibit.looking)をリリースしました。
Notificationを使用しないタイプのAndroid Wearアプリを実際に作成してみて、
得られた情報を簡単にまとめておきたいと思います。

![Image](http://cl.ly/image/010X1M382v2I/device-2014-11-10-174739.png)

## 開発環境
今回の開発は次の環境で行いました。

* Android Studio v0.9.2
* Nexus5
* LG G Watch

実機を使用したデバッグ方法はこちらを参考にさせていただきました。
[Android Wearアプリの作り方](http://www.buildinsider.net/mobile/androidwear/02)

## 開発前の準備
アプリを作り始める前にAndroid Wearでどんなことができるのか、公式のドキュメントにざっと目を通しますとわかるかと思います。
[http://developer.android.com/training/wearables/apps/index.html](http://developer.android.com/training/wearables/apps/index.html)

Lookingを作る上で必要になったのは主に次の2点でした。

### ・カスタムUI
Android Wearでは既存のButton等のUIも同様に使用することができますが、
build.gradleに次のライブラリを追加することでAndroid Wear用のカスタムUIが使用できます。

``` gradle
compile 'com.google.android.support:wearable:+
```

非常に便利なUIとして`GridViewPager`があります。

### ・データの同期
Android Wearとペアリングしている端末（スマートフォン等、以下Handheld）とデータをやりとするための方法が提供されています。
今回のアプリでは次の2つのAPIを利用しました（他にもNodeAPIがあります）

#### Message API
Android WearとHandheld間で一方的な通知を送るためのもの。合わせてバイト配列も送れます。
例えば、Android WearからHandheldにTwitterのタイムラインをリクエストする場合に使用できます。

#### Data Layer API
Android WearとHandheld間でデータを同期するためのもので、
データを作成、変更、削除すると相手側に通知されるようになっています。
現状Android Wear単体ではネットワーク通信ができないので、Handheld側でTwitterからタイムラインを取得し、このAPIを使用してデータを同期します。
	
これらの仕組みを利用することで、簡単にWearable Appを作ることができます。

### ・プロジェクト
Android StudioでAndroid Wear用アプリを作成すると、デフォルトでは *mobile* と *wear* モジュールがあります。
各モジュールで端末毎のアプリを作成していくことになります。


## 実装
主にAndroid Wear側のアプリについての内容です。

### ・カスタムUI
アプリを作成するにあたり使用したカスタムUIのビューについてです。

#### GridViewPager
行と列から構成されるビューのコンテナです、上下左右のスワイプでビューを切り替えることができます。
このレイアウトを使用してタイムラインの表示を行いました。
使用するにあたり、関係するクラスは次の3クラスがあります。

* **GridPagerAdapter**  
	GridViewPagerにビューを表示するためのアダプタ。  
	独自レイアウトのビューを使用したい場合はこちらを使用します。
* **FragmentGridPagerAdapter**  
	GridPargerAdapterから派生したアダプタ。用途は同じですが、グリッド毎のビューをフラグメントで作成します。  
	次のCardFragmentとセットで使用することが多いと思われます。
* **CardFragment**  
	冒頭のアプリ画像にもあるようなカードレイアウトのビューを表示するためのフラグメント。

個人的には、カードレイアウトを使用する場合は *FragmentGridPagerAdapter* を、使用しない場合は *GridPagerAdapter* を使用するのが簡単ではないかと思います。

GridViewPagerを使用してビューを表示する流れは次のようになります。
 **1. GridViewPagerのレイアウト定義**

``` xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.GridViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true"/>
```

ビューコンテナ毎に`android:keepScreenOn`フラグを`true`にすると、画面が常時点灯になります。

**2. CardFragmentの派生クラスの作成**
CardFragmentは派生クラスを作成しなくても使用できますが、レイアウトは固定的になってしまいます。
Twitterの用に文字ベースの情報だとデフォルトのままでは若干文字が大きいのと、テキストの色等カスタマイズしたかったので派生クラスを作成しました。
![イメージ](http://cl.ly/image/0f1X040s0q47/image.png)

このカスタム画面のレイアウトは次のようになります。定義するのはカード上のレイアウトのみになります。

``` content_tweet_card.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:paddingTop="@dimen/card_content_padding_rect_top"
    android:paddingRight="@dimen/card_content_padding_rect_right"
    android:paddingLeft="@dimen/card_content_padding_rect_left">

    <ImageView
        android:id="@android:id/icon"
        android:layout_width="26dp"
        android:layout_height="26dp"
        android:src="@drawable/ic_tweet_user" />

    <TextView
        android:id="@+id/tweet_user"
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:layout_toRightOf="@android:id/icon"
        android:layout_alignBottom="@android:id/icon"
        android:layout_marginLeft="6dp"
        android:text="itadakit@droibit"
        android:singleLine="true"
        android:ellipsize="end"
        android:textColor="@color/primary_color"
        android:textSize="16.5sp" />

    <TextView
        android:id="@+id/tweet_text"
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        android:layout_below="@id/tweet_user"
        android:paddingTop="4dp"
        android:text="Hello."
        android:textColor="@color/primary_text_light"
        android:textSize="@dimen/tweet_text"
        android:textColorLink="@color/primary_text_title_color"
        android:linksClickable="false"/>
</RelativeLayout>
```

RealtiveLayoutのpaddingの値はサポートライブラリに定義されているのでそれを使用しています。
ライブラリのresフォルダを見てみると他にも参考になるかもしれません。

このレイアウトを使用するためにはCardFragmentの派生クラスを作成します。
ポイントは **CardFragment#onCreateContentView(...)** メソッドをオーバーライドするところです。

``` TweetCardFragment.java
// タイムライン上の1ツイートを表示するためのフラグメント。
public class TweetCardFragment extends CardFragment {

	// ファクトリメソッドは省略

	/** {@inheritDoc} */
    @Override
    public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.content_tweet_card, container, false);
    }

    /** {@inheritDoc} */
    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        final TextView userView = (TextView) view.findViewById(R.id.tweet_user);
        userView.setText("ユーザ名");
        final TextView textView = (TextView) view.findViewById(R.id.tweet_text);
        textView.setText("テキスト");
        // カードの背景
        view.setBackgroundResource(R.drawable.tweet_background);
    }
}

```

背景をカスタマイズしたい場合は、コンテナビューに画像リソースを設定します。
ここで背景を設定した場合は1グリッド毎にこの画像が使用されます。

**3. FragmentGridPagerAdapter派生クラスの作成**
項目2で作成したフラグメントを使用する新しいアダプタを作成します。

``` TweetPagerAdapter.java
// ツイートを{@link GridViewPager}上に表示するためのアダプタ。
public class TweetPagerAdapter extends FragmentGridPagerAdapter {

    private List<Tweet> mTweets;

    /**
     * 新しいインスタンスを作成する。
     *
     * @param timeline Twitterのタイムライン
     * @param fm フラグメントマネージャ
     */
    public TweetPagerAdapter(Timeline timeline, FragmentManager fm) {
        super(fm);

        mTweets = timeline.getTweets();
    }

    /** {@inheritDoc} */
    @Override
    public Fragment getFragment(int row, int column) {
        final TinyTweet tweet = mTweets.get(row);
        return TweetCardFragment.newInstance(tweet);
        
        // デフォルトのCardFragmentを使用する場合
        //return CardFragment.create(tweet.user.name, tweet.text, R.drawable.ic_user);
    }

    /** {@inheritDoc} */
    @Override
    public int getRowCount() {
        return mTweets.size();
    }

    /** {@inheritDoc} */
    @Override
    public int getColumnCount(int row) {
        return 1;
    }

    /** {@inheritDoc} */
    @Override
    public ImageReference getBackground(int row, int column) {
        return super.getBackground(row, column);
    }
}

```

後は GridViewPager#setAdapter(...)で作成したアダプタを設定します。
このアダプタだと縦方向のスワイプのみですが、ツイートに対するアクション用のフラグメントを作成し、アダプタを修正すれば横方向にもスワイプできます。

#### misc
主に使用したのはGridViewPagerでしたが、それ以外のサポートライブラリのUIを使用してみて
いろいろ分かったこともありました。

**・CircledImageViewの問題点** 
これは丸いボタンのようなビューを簡単に作ることができます。
![イメージ](http://cl.ly/image/1x0o162T1E3Z/device-2014-11-10-174808.png)
通知系のアプリではこの見た目のものがボタンとして使われているため、アプリでもCircledImageView
を使おうとしました。
しかし、CircledImageViewをタッチしたところ、丸が **四角につぶれてしまった** ので使えませんでした。代替案として、青い丸の画像を用意して使用しました。
参考: [SDK付属サンプル:JumpingJack](http://developer.android.com/samples/JumpingJack/index.html)

画像のサイズはmdpi-104pxで作成しています（サポートライブラリのvalues.xml参考）。

**・DismissOverlayViewの問題点**
これは[×]のような外観のキャンセル用のビューで、×ボタンをタッチするとこのビューを表示しているActivityごと終了してくれるというものです。
このビューは合わせてメッセージを表示することもできますが、×ボタンのレイアウトが崩れてしまうため使用できませんでした。
代替案として、ConfirmationActivityの *FAILURE_ANIMATION* を使用しました。

### ・データの同期
データの同期にはGooglePlayサービスを使用します。
同期するためにはMessage, Data Layer APIのイベントリスナーを追加し、イベントが発生した際に色々と処理をします。
Lookingのようなタイムラインのやりとり（リクエストとレスポンス）を行う場合は、
Android WearとHandheld両方でイベントリスナーを実装する必要があります。その際、方法は主に2種類あります。

* Activityで実装
* WearableListenerServiceの派生クラスの作成

Activityを表示している間だけやりとりする場合はActivityで、Activityを表示しない場合はServiceを使用するのがいいのではないかと思います。
Lookingの場合は、Android Wear側はActivityで実装、Handheld側はServiceを作成しています。

実装はは[SDK付属:DataLayer](http://developer.android.com/samples/DataLayer/index.html)および[Android WearのData Layer APIを試してみた](http://bati11blog.hatenablog.com/entry/2014/07/15/113232)を参考にさせていただきました。

Data Layer APIをを使用して独自クラスのリストを同期するためにこちらを利用させていただきました。
[DataMapParcelableUtils](https://gist.github.com/MaciejCiemiega/0cd67c442f2d03c2ddac)
利用するにあたり、独自クラスはParcelableの実装が必要になります。

### ・おまけ
現在のAndroidでは画面を作成する際にActivityもしくはFragmentを利用できます。
同様にAndroid Wearでも両クラスを使用することができます。

``` java
// OK
// ※ Activityの場合はスワイプで元の画面に戻ることができます。
final Intent intent = new Intent(srcActivity, DestActivity.class);
srcActivity.startActivity(intent);

// OK
// ※ Fragmentの場合はスワイプで元の画面に戻ることができません。
final Fragment newFragment = new NewFragment();
srcActivity.getFragmentManager().beginTransaction()
	.replace(R.id.cotent, newFragment)
	// or
	.add(R.id.content, newFragment)
	.commit();
```

Lookingは当初Fragmentベースで画面を構成していましたが問題がありました。
画面が消灯しバックグラウンドに回った後、アプリをリスタートすると、Fragment上のビューが全て破棄され画面が真っ白になってしまいました。
また、ConfirmationActivity等のオーバーレイActivityを表示した場合も同様に画面が白くなりました。

スマートフォン/タブレットではこのようなことはありませんでしたが、Android Wear上ではこのような挙動になりますので、現状では **Activityベース**  で画面を作成するほうがいいのではないかと思われます。

## リリース
Wearable Appのapkを作成する方法は[Packaging Wearable Apps](http://developer.android.com/training/wearables/apps/packaging.html#Studio)に詳しく説明されています。
あとは、mobileおよびwearモジュールの **applicationId** が同一のものであれば問題無いと思います。
ビルドする際はmobileモジュールを選択し、作成されたapkをGoogle Playに提出します。

Google Playに提出する際に必要な画像はAndroid Studioから撮れます。
![Image](http://cl.ly/image/1t3o2O3b3M1d/screen_capture.png)

### アップデート
アプリをアップデートする際は、最低限mobileモジュール側のbuilde.gradleの *versionCode* 及び *versionName* 修正します。


以上、簡単ですがWearable Appを初めてリリースするまでの流れになります。