Androidで独自Viewを作るときの4つのTips +3


独自のViewを作るときに困ったことがたくさんあったので、まとめておこうと思います。

（ところで独自Viewのことは何て呼ぶんでしょうか？　Custom View? Custom Component? Custom Widget?）


# コンストラクタの作りかた

Viewにはコンストラクタが３種類存在します。（[この辺を見るとわかります](http://developer.android.com/reference/android/view/View.html#View(android.content.Context))）
基本的に全部オーバーライドしておけば問題ありませんでした。

<追記 2016/01/19>
API Level 21 からコンストラクタが４種類に増えたようです。
引数が４つのコンストラクタを Lolipop 未満のOSから呼び出すと `InvocationTargetException` を起こすので、オーバーライドの際にはバージョン分岐などが必要そうです。
</追記 2016/01/19>

XMLで定義したLayoutからインスタンス化された時には、引数が２つのコンストラクタが呼ばれるようです。

また、AndroidのSDKのソースを見ると流儀があるようで、引数の多いコンストラクタに初期化処理を全部任せるようです。
具体的にはこんな感じ。

```OriginalView.java

public class OriginalView extends View {

	public OriginalView(Context context) {
		this(context, null);
	}

	public OriginalView(Context context, AttributeSet attrs) {
		this(context, attrs, R.attr.originalViewStyle);
	}

	public OriginalView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);

		// このへんで初期化処理

	}

// 以下略
```

# 独自のXML属性を作って、セットされた値を取得する

resourceのXML(`attrs.xml`)側とJavaのコード側の両方で作業が必要になります。

## XML側

`<declare-styleable>`を使って、どんな属性があるかを定義します。
中身の書き方については[こちらの記事](http://y-anz-m.blogspot.jp/2011/06/declare-styleable.html)が詳しい。
[公式の方にも多少書かれてます](http://developer.android.com/training/custom-views/create-view.html)。

`format`属性の中身は`|`で区切れるらしいのですが、どうやって使えばいいのかイマイチよくわからないです。
詳しい人がいたら教えて下さい。

```attrs.xml
<resources>
	<declare-styleable name="OriginalView">
		<attr name="hoge_int" format="integer" />
		<attr name="fuga_color" format="color" />
		<attr name="moge_str" format="string" />

		<!-- 多分、colorとreferenceどちらも指定できるんだと思う -->
		<attr name="foo" format="color|reference" />
	</declare-styleable>

	<declare-styleable name="Themes">
		<attr name="originalViewStyle" format="reference" />
	</declare-styleable>
</resources>
```

## Javaコード側

引数が２つ、もしくは３つあるコンストラクタの第二引数があれば、XMLの属性値を取得できるようです。
`TypedArray`として値を取り出して使用します。

```OriginalView.java
	public OriginalView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);

		TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
                defStyleAttr, 0);

		int hogeInt = a.getInteger(R.stylable.OriginalView_hoge_int, 0);
		int fugaColor = a.getInteger(R.stylable.OriginalView_fuga_color, Color.BLACK);
		String mogeStr = a.getInteger(R.stylable.OriginalView_moge_str);

		a.recycle();
	}
```

## layoutで使うとき

実際にlayoutでオリジナルの属性を使うときは、`xmlns`の拡張が必要です。

```HogeHogeActivity.xml
<LinearLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:original="http://schemas.android.com/apk/res-auto">
```

これで`original`名前空間使って独自要素を記述できます。

```HogeHogeActivity.xml
<View
	class="net.kikuchy.MyViewSample.OriginamView"
	original:hoge_int="30"
	original:fuga_color="#ff000000"
	original:moge_str="定時だ帰るぞ！！！！！！" />
```

# `layer-list`に入れたid付きの`item`を取得したい

`ProgressBar`の見た目を変えたいときには、背景部分の見た目と動く棒の見た目を別個のDrawableにせず、
`layer-list`に入れたid付きの`item`を使って一つのDrawableにまとめることができます。

文章だとわかりづらいのでソースで。

```drawable/progress.xml
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">

	<!-- 背景の方の見た目 -->
    <item android:id="@android:id/background">
        <shape android:shape="rectangle">
            <solid android:color="#efefef" />
        </shape>
    </item>

	<!-- 動く棒の方の見た目 -->
    <item android:id="@android:id/progress">
        <shape android:shape="rectangle">
            <solid android:color="#222222" />
        </shape>
    </item>
</layer-list>
```

こんな風に、一つのDrawableにいくつかの見た目を格納しておいて、後で`id`で取り出したいときにはどうしたらいいのか、という話。

## 取り出し方

`findDrawableByLayerId()`を使います。
事前に、どうにかして`layer-list`をソースコード側で取得しておきます。

```OriginalView.java
LayerDrawable layer = (LayerDrawable)getResources().getDrawable(R.drawable.progress);
Drawable progress = layer.findDrawableByLayerId(android.R.id.progress);
```

引数のResource IDは適当なものにしておきます。
SDKでも使われているようなIDを使いたければ`android`名前空間にあるidを使えば上記のようにできます。
独自のidが使いたければ、適当に定義して使ってください。

# Drawableを使ってCanvasに描画したい

Drawableを取得できていれば、サイズの指定を行うだけですぐに描画できます。

```OriginalView.java
@Override
protected void onDraw(Canvas canvas) {
	Drawable d = (どうにかして取得してくる);
	d.setBounds(x, y, width, height);
	d.draw(canvas);
}
```

# 文字列がぴったり収まるような図形を描きたい

`Paint.FontMetrics`というクラスを使います。
[こちらのページ](http://wikiwiki.jp/android/?%A5%C6%A5%AD%A5%B9%A5%C8%A4%CE%C9%C1%B2%E8(FontMetrics))がとてもわかりやすいので参照のこと。


# xmlで書いたlayoutを一つのViewとして扱いたい (2015/9/12追記、2017/11/22修正)

例えばこんな複雑なlayoutファイルがあったとき。

```res/layout/complex_layout.xml
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <ImageView
        android:id="@+id/face_picture" />
    <TextView ... />
    <Button ... />
    ...
</FrameLayout>
```

このレイアウトを一つのViewとして扱えるようにして、

他のlayoutから( `include` や `merge` を使わずに、単一Viewとして)使いたいとき、

```res/layout/main_activity.xml
...
    <net.kikuchy.MyViewSample.ComplexView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
```

あるいは独自のセッターを持たせたりしたいときにどうしたらいいか。

```OriginalListAdapter.java
...

@Override
public View getView (int position, View convertView, ViewGroup parent) {
    SomeEntity entityData = mEntities.get(position);
    ComplexView singleRow = (ComplexView) convertView;
    if (singleRow == null) singleRow = new ComplexView(mContext);
    singleRow.setEntity(entityData);    // <- こんなセッターが欲しい
    return singleRow;
}

...
```

独自Viewクラスの引数が三つあるコンストラクタの中で、layoutファイルをinflateしてしまえばお行儀よく実現できます。

```res/layout/main_activity.xml
<!-- root要素を merge に変更。 -->
<!-- 元のroot要素はtools:parentTag属性に記述しておきます -->
<marge
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:parentTag="LinearLayout" >
    <ImageView
        android:id="@+id/face_picture" />
    <TextView ... />
    <Button ... />
    ...
</merge>
```

```ComplexView.java
public class ComplexView extends FrameLayout {   // layoutファイルの元のroot要素をextendするのがミソ

    ...
    // 残りの２つのコンストラクタは、この記事上部の「コンストラクタの作り方」の通りになっているとします。

    public ComplexView(ontext context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        // 第３引数にtrueを渡すと、このComplexViewの直下にレイアウトのmergeの子要素を展開してくれる
        LayoutInflater.from(context).inflate(R.layout.complex_layout, this, true);

        // あらかじめ、後で使うViewのインスタンスはメンバとして掴んでおく
        mFacePicture = (ImageView) findViewById(R.id.face_picture);
        ...
    }

    // エンティティの要素を独自ビューのパーツに流し込む
    public void setEntity(SomeEntity entity) {
        mFacePicture.setImageUri(entity.getFaceImageUri());
        ...
    }

...
```

inflateの仕方は[こちらの記事](http://zawapro.com/?p=1534)を、tools:parentTagの使い方は[こちらのStackOverflow](https://stackoverflow.com/a/39539735)を参考にしました。

<追記 2017/11/22>
`merge` を使ってView階層がフラットになるようにしました。
</追記 2017/11/22>


# 画面回転とかしてもインスタンスの状態を保持したい (2016/04/08追記)

"Don't keep Activity" （アクティビティを保持しない）がONになっていたり低スペック端末だったりすると、画面を回転したりアプリをバックグラウンドに入れたりするとViewは破棄され、再生成されます。

再生成されても状態を保持したい場合は、 [`View.BaseSavedState`](http://developer.android.com/intl/ja/reference/android/view/View.BaseSavedState.html) を実装したクラスを用意し、Viewの `onSaveInstanceState` で状態の保存、 `onRestoreInstanceState` で状態の復元を行う必要があります。

AOSPのクラスを見ると、大まかに以下のように実装すれば良さそうです。

```OriginalView.java
public class OriginalView extends View {
    // 保持したいパラメーターたち
    private int paramA;
    private int paramB;
    private String[] paramC;

    ...

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        // 以下で作成する SavedState クラスに保持したいパラメーターを移しておく。
        SavedState ss = new SavedState(superState);
        ss.a = paramA;
        ss.b = paramB;
        ss.c = paramC;
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        // この辺はボイラープレート。以下で作成する SavedSate だったらそれにキャストする。
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState)state;
        super.onRestoreInstanceState(ss.getSuperState());

        // 保持していた値を取り出す
        paramA = ss.a;
        paramB = ss.b;
        paramC = ss.c;
        // 表示の更新とかが必要ならここでやってしまう
    }

    ...

    public static class SavedState extends BaseSavedState {
        // 親クラス（対象のView）の保持したいパラメーターと同じ型のメンバを持っておく
        int a;
        int b;
        String[] c;

        // コンストラクタは二つ
        // Parcelable が引数のコンストラクタはsuperを呼び出すだけ
        SavedState(Parcelable superState) {
            super(superState);
        }

        // Parcel が引数のコンストラクタでは、保存した値を取り出す
        // 以下の CREATOR からしか使われないっぽいので、このコンストラクタは private でも良いらしい
        SavedState(Parcel source) {
            super(source);

            // 読み出す順番は writeToParcel で書き込む順番と同じにする
            a = source.readInt();
            b = source.readInt();
            // createXXXArray は、配列の「領域確保と読み出し」を行うメソッド
            c = source.createStringArray();
            // readXXXArray は、すでに領域確保済みの配列に対して読み出しを行うメソッド
            // c = new String[10];
            // source.readStringArray(c);
        }

        // writeToParcel で値を保存する
        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);

            out.writeInt(a);
            out.writeInt(b);
            out.writeStringArray(c);
        }

        // BaseSavedState は Parcelable を継承しているので、non-null な Parcelable.Creator<SavedState> CREATOR をstaticフィールドに持っている必要がある
        @SuppressWarnings("hiding")
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}
```

`Parcel` に書き込める型は意外と種類が豊富なので（FileDescriptorとかも保存できる）、一度 [ドキュメントのメソッド一覧](https://developer.android.com/intl/ja/reference/android/os/Parcel.html#pubmethods) には目を通しておくと良いと思います。

ただドキュメントの説明はけっこう不親切で、 `createStringArray()` と `readStringArray()` の違いなどが書かれていません。

`createXXXArray` 系は配列の作成と値の読み込みを同時に行ってくれるもので、
`readXXXArray` 系は、すでに作成済みの配列に値の読み込みを行ってくれます。
長さが足りない配列に `readXXXArray`  を使おうとすると `RuntimeException: bad array lengths` の例外が出るのでご注意。
（参考： [StackOverflow](http://stackoverflow.com/a/24607882 )）


# スタイルで宣言されているあの値を使いたい（2017/11/23追記）

`Toolbar` など、スタイルで指定した `colorPrimary` や `colorAccent`
 を自動的に使用してくれるViewがありますよね。
柔軟性を高めるためとか、自分でもそうしたスタイルに指定されたリソースを使用したいことがあります。


コードからは以下のようにして取得することができます。

```ThemeUsingView.java
TypedValue d = new TypedValue();
TypedArray a = getContext().obtainStyledAttributes(
                   d.data,
                   new int[] { R.attr.colorPrimary }
               );

// このprimaryColorが使いたい色
int primaryColor = a.getColor(0, 0);

// TypedArrayは忘れずにrecycleしましょう。
a.recycle();
```

`R.attr` にはスタイルで設定した属性名などが反映されています。
これを使えば好きな値を取得することが可能です。
色さえ得られれば、あとは背景色に反映するなりtintに使うなり、いろいろできます！

（参考：[StackOverflow](https://stackoverflow.com/a/27611244)）





<del>他にも何かあったら書き足す予定。</del> [荒木さんのスライドでだいたいのことが分かりそうな予感](http://yaraki.github.io/slides/gdm01/#1)

# あわせて読みたい

- [iOSで独自Viewを作る時の5つのTips](http://qiita.com/kikuchy/items/f1d6731d804b63cf7a29)
- [ActivityをスリムにするObject Compositionパターン](http://qiita.com/kikuchy/items/e9900db1dd791fc19dc2)
- [Androidで位置情報を扱うときの3つのTips](http://qiita.com/kikuchy/items/c79241b0488cb40c1da6)