352
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Organization

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

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

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

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

Viewにはコンストラクタが3種類存在します。(この辺を見るとわかります
基本的に全部オーバーライドしておけば問題ありませんでした。

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

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

また、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>を使って、どんな属性があるかを定義します。
中身の書き方についてはこちらの記事が詳しい。
公式の方にも多少書かれてます

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コード側

引数が2つ、もしくは3つあるコンストラクタの第二引数があれば、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というクラスを使います。
こちらのページがとてもわかりやすいので参照のこと。

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から( includemerge を使わずに、単一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するのがミソ

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

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

        // 第3引数に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の仕方はこちらの記事を、tools:parentTagの使い方はこちらのStackOverflowを参考にしました。

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

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

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

再生成されても状態を保持したい場合は、 View.BaseSavedState を実装したクラスを用意し、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とかも保存できる)、一度 ドキュメントのメソッド一覧 には目を通しておくと良いと思います。

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

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

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

Toolbar など、スタイルで指定した colorPrimarycolorAccent
を自動的に使用してくれる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

他にも何かあったら書き足す予定。 荒木さんのスライドでだいたいのことが分かりそうな予感

あわせて読みたい

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
352
Help us understand the problem. What are the problem?