独自の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のソースを見ると流儀があるようで、引数の多いコンストラクタに初期化処理を全部任せるようです。
具体的にはこんな感じ。
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
属性の中身は|
で区切れるらしいのですが、どうやって使えばいいのかイマイチよくわからないです。
詳しい人がいたら教えて下さい。
<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
として値を取り出して使用します。
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
の拡張が必要です。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:original="http://schemas.android.com/apk/res-auto">
これでoriginal
名前空間使って独自要素を記述できます。
<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にまとめることができます。
文章だとわかりづらいのでソースで。
<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
をソースコード側で取得しておきます。
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を取得できていれば、サイズの指定を行うだけですぐに描画できます。
@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ファイルがあったとき。
<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として)使いたいとき、
...
<net.kikuchy.MyViewSample.ComplexView
android:layout_width="match_parent"
android:layout_height="match_parent" />
あるいは独自のセッターを持たせたりしたいときにどうしたらいいか。
...
@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してしまえばお行儀よく実現できます。
<!-- 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>
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のクラスを見ると、大まかに以下のように実装すれば良さそうです。
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
など、スタイルで指定した colorPrimary
や colorAccent
を自動的に使用してくれるViewがありますよね。
柔軟性を高めるためとか、自分でもそうしたスタイルに指定されたリソースを使用したいことがあります。
コードからは以下のようにして取得することができます。
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)
他にも何かあったら書き足す予定。 荒木さんのスライドでだいたいのことが分かりそうな予感