カスタムビューを作る

提供: tknotebook
移動: 案内検索

メインページ>コンピュータの部屋#Android>Android Tips


Android ではコ-ザインターフェースに標準で Button や TextView や LinearLayout など、たくさんのビューを利用できますが、ビューを自作することもできます。

以下に楕円を表示するだけの簡単なビューの実装例を示します。

モジュールを作成する

AndroidStudio で作成する場合は、Android Library Module を新規に作るのがよいでしょう。

カスタムViewだけが入った .aar ファイルができるので、他のプロジェクトで容易に再利用できます。

カスタム属性を定義する

モジュールに res/values/attr.xml というファイルを作り、カスタム属性を定義します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Ellipse">
        <attr name="strokeColor" format="color"/>
        <attr name="strokeWidth" format="dimension"/>
    </declare-styleable>
</resources>

Ellipseはカスタムビューのクラス名 strokeColor は楕円の輪郭の色、strokeWidth は楕円の輪郭の太さです。

formatの意味は 付録1 「カスタム属性の formatとその扱い方」を見てください。

カスタムビューのクラスを作成する

カスタムビューのクラスを View クラスを継承して作成します。ここでは名前は Ellipse とします。

class Ellipse extends View {
    // 輪郭色。既定値は赤
    private int mStrokeColor = Color.RED;
    // 輪郭幅。
    private float mStrokeWidth;
    // 楕円のサイズ
    private final RectF rect = new RectF();
    // 描画用Paint
    private final Paint mPaint = new Paint();
    // 画面の密度
    private float mDensity;
 :
 :
}

楕円の輪郭の属性を保持するフィールド等を加えます。

コンストラクタを書く

コンストラクタは2種類必要です。

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


    public Ellipse(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 160dpiとの比を求める。
        mDensity = getContext().getResources().getDisplayMetrics().density;
        // 線幅を既定値 2dp に。
        mStrokeWidth = (int) (mDensity * 2); 

         if (attrs == null) {
            return;
        }

        // 楕円の属性を取得
        TypedArray tArray = context.obtainStyledAttributes(attrs, R.styleable.Ellipse);
        try {
            // 色と高さを設定
            mStrokeColor = tArray.getColor(R.styleable.Ellipse_strokeColor, mStrokeColor);
            mStrokeWidth = tArray.getDimensionPixelSize(R.styleable.Ellipse_strokeWidth, mStrokeWidth);
        } finally {
            tArray.recycle();
        }
    }

最初のコンストラクタは全て既定値でビューを作る時のコンストラクタ。コードでビューを作る時などで利用されます。

2番目のコンストラクタはXMLで記述されたビューが作成されるときに呼び出されるコンストラクタです。

XMLで指定されたカスタム属性は AttributeSet attrs という引数で渡されますが、そのまま使ってはいけません。 コンテキストの obtainStyledAttributes メソッドで TypedArray 型に変換してください。これで属性はテーマやスタイルが反映された正しい値になります。

各カスタム属性値は TypedArray の getColorメソッドなどに、R.styleable に自動生成された識別子を指定して値を取り出します。 カスタム属性値がない場合、getColorなどのメソッドの最後のパラメータが既定値として戻るのに注意してください。従って mStrokeWidthやmStrokeColorは予め規定値で初期化しておく必要があります。

尚、ビューのレイアウトエディタで張り付けた時の初期サイズを決める目的で、コンストラクタで setLayoutParamsメソッドを呼びたくなりますが setLayoutParamsメソッドはコンストラクタで呼んでも効果はありません。layout:width や layout:height はコンストラクタ実行後 wrap_content が強制的に代入されてしまいます。レイアウトサイズが変更できるのはビューが生成された後です。

ビューのレイアウトエディタで張り付けた時の初期サイズは wrap_content の時のサイズを調整すればよいので、onMeasure でコンテンツサイズを示すことで実現してください。

カスタム属性の setter/getter を用意する

XMLで設定できる属性は通常コードからも動的に設定できるようにします。

    /**
     * 輪郭色を取得する
     * @return 輪郭色
     */
    public int getStrokeColor() {
        return mStrokeColor;
    }
    /**
     * 輪郭色を設定する
     * @param strokeColor 輪郭色
     */
    public void setStrokeColor(int strokeColor) {
        this.mStrokeColor = strokeColor;
        this.invalidate();
    }
    /**
     * 輪郭幅を取得する。
     * @return 輪郭幅(dp)
     */
    public float getStrokeWidth() {
        return mStrokeWidth / mDensity;
    }
    /**
     * 輪郭幅を設定する
     * @param strokeWidth 輪郭幅(dp)
     */
    public void setStrokeWidth(float strokeWidth) {
        this.mStrokeWidth = (int)(strokeWidth * mDensity);
        this.invalidate();
    }

属性の変更の結果、再描画が必要な場合は invalidate() を呼びます。 属性の変更の結果、子ビューの再配置が必要な場合は invalidate() と requestLayout() を呼びます。忘れると正常に描画されないので注意してください。

ビューのサイズが変わったときに備えてハンドラを用意する

ビューのサイズが変わったときに備えて、onSizeChanged メソッドを override します。 Ellipse では再描画を促すだけですが、大きさが変わったことで、描画を行う前にやっておきたいことがある場合はここに記述します。

    /**
     * ビューのサイズが変わったときのハンドラ
     * @param w 幅
     * @param h 高さ
     * @param oldw 古い幅
     * @param oldh 古い高さ
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.invalidate();
    }

カスタムビューのサイズのネゴ

カスタムビューは onMeasure を override しないと layout:width や layout:height が wrap_content でも 自分のサイズができるだけ大きくなるように親ビューとネゴします。

たいていこれでは具合が悪いので onMeasureメソッド を overrideします。

下記のコードは、 ビューがビューのコンテンツを表示するのに必要なサイズを計算し、親にそのサイズを要求します。 つまり、wrap_content の時のサイズを要求していると考えてほぼ間違いないでしょう。

サイズを要求の際、パディングを考慮することはビューの責任であることに注意してください。

Ellipse では決まったサイズのコンテンツはありませんが、コンテンツの描画に最低 30dp x 30dp 必要ということにして サイズを要求しています。

    /**
     * ビューの大きさの調整
     * @param widthMeasureSpec 幅の注文
     * @param heightMeasureSpec 高さの注文
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // コントロールの表示に必要な大きさを計算する。
        // このカスタムビューにはコンテンツの決まった大きさはないので、最小限 30dp必要ということで計算。
        int minWidth = this.getPaddingLeft() + this.getPaddingRight() + (int)(30 * mDensity);
        int minHeight = this.getPaddingTop() + this.getPaddingBottom() + (int)(30 * mDensity);

        // resolveSizeに渡すとよきに計らってくれる。
        setMeasuredDimension(resolveSize(minWidth, widthMeasureSpec), resolveSize(minHeight, heightMeasureSpec));
    }

カスタムビューを描画する

onDraw メソッドを override して ビューを描きます。

描画には canvas と paint を利用しますが、canvas が描画面、paint は線の太さなどの図形の属性を指定するのに用います。 canvas の座標系は左上が (0, 0) で単位はピクセルです。

パディングを考慮してコンテンツを描くのは onDraw の責任です。

    /**
     * ビューの描画
     * @param canvas キャンバス
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 指定輪郭色、指定輪郭幅でビュー一杯に楕円を描く。
        // 輪郭が外にはみ出ないように描く。
        int width = this.getWidth();
        int height = this.getHeight();
        mPaint.setStrokeWidth(mStrokeWidth);
        mPaint.setColor(this.mStrokeColor);
        mPaint.setStyle(Paint.Style.STROKE);

        rect.set(this.getPaddingLeft() + mStrokeWidth / 2,
                this.getPaddingTop() + mStrokeWidth / 2,
                width - mStrokeWidth / 2 - this.getPaddingRight(),
                height - mStrokeWidth / 2 - this.getPaddingBottom());
        canvas.drawOval(rect, mPaint);
    }

レイアウトエディタでカスタムビューを配置する

AndroidStudioのレイアウトエディタでは、パレットで CustomView を選ぶとカスタムビューの一覧が表示されます。 お望みのカスタムビューをダブルクリックし、プレビュー画面をクリックすればカスタムビューを配置できます。

CustomView一覧画面.png

レイアウト XML でカスタム属性を使えるようにする

レイアウトの XML に

xmlns:app="http://schemas.android.com/apk/res-auto"

という行を加えれば

app:strokeWidth="4dp"

というようにカスタム属性がXMLの中に書けます。

ちゃんとコード補完も効きますが、プロパティペインからは入力できないっぽいです。

編集時の表示例

カスタムビューの編集時イメージ.png

付録1 カスタム属性の formatとその扱い方

カスタムビューのカスタム属性の型( format )には私の知る限り10種類あります。

何故かSDKに一覧がみあたらないので、一覧を作ってみました。

それぞれ attr.xml での定義の仕方、XMLでの記述法と TypedArray からの受け取り方が違います。

format 意味 attr.xmlでの記述 レイアウトでの記述 TypedArrayからの受け取り
integer 整数値
<attr name="xxx" format="integer" />
app:xxx="1"
tArray.getInt(R.styleable.XXX, 既定値) 
float 実数値
<attr name="xxx" format="float" />
app:xxx="0.5"
tArray.getFloat(R.styleable.XXX, 既定値) 
string 文字列
<attr name="xxx" format="string" />
app:xxx="abc"
tArray.getString(R.styleable.XXX, 既定値) 
boolean 論理値
<attr name="xxx" format="boolean" />
app:xxx="false"
tArray.getBoolean(R.styleable.XXX, 既定値) 
enum 列挙値
<attr name="xxx">
    <enum name="aaa" value="0" />
    <enum name="bbb" value="1" />
</attr>
app:xxx="aaa"
tArray.getInt(R.styleable.XXX, 既定値) 
flag フラグ値
<attr name="xxx">
    <flag name="left" value="1" />
    <flag name="right" value="2" />
    <flag name="leftRight" value="3" />
</attr>
app:xxx="leftRight"
tArray.getInt(R.styleable.XXX, 既定値) 
dimension 単位付きの長さ。10dpとか 2pt など
<attr name="xxx" format="dimension" />
app:xxx="20dp"
tArray.getDimensionPixelSize(R.styleable.XXX, 既定値) 
color
<attr name="xxx" format="color" />
app:xxx="#0000ff"
app:xxx="@color/xxxx"
tArray.getColor(R.styleable.XXX, 既定値) 
reference リソースID
<attr name="xxx" format="reference" />
app:xxx="@id/yyyy" 
tArray.getResourceId(R.styleable.XXX, 0) 
fraction 割合?
<attr name="xxx" format="fraction" />
app:xxx="30%"
tArray.getFraction(R.styleable.XXX, 既定値) 

付録2 resolveSize/resolveSizeAndState の機能

カスタムビューの onMeasure メソッドでは 上位にネゴの要求を返すのに View#resolveSize メソッドを使いました。これとよく似た機能の View#resolveSizeAndState というメソッドもあります。ここではこの2つのメソッドの処理内容を解説します。

View#resolveSize メソッドは API-1の頃からあったメソッドですが、API-11(HoneyComb)からView#resolveSizeAndStateが加わりました。 機能的にはほとんど同じです。resolveSizeAndState から説明します。

resolveSizeAndStateメソッド

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState)

  まず引数ですが、

size はビューが親ビューに要求する自身のサイズす。ビューをまともに表示するのにこれだけはほしいという意味です。

measureSpec は整数値ですが、上位8ビットが「ステート」、下位24ビットがサイズ(ピクセル)なっており、親ビューから下された サイズの「制限」や、親ビューへ返す要求を表しています。 。 「ステート」の中のさらに上位2ビットが「モード」を表していて、親ビューが下す制限の内容を表しています。

  1. 「モード」MeasureSpec.AT_MOST(0x80000000)は「ビューのサイズをmeasureSpecのサイズ以下にせよ」
  2. 「モード」MeasureSpec.EXACTLY(0x40000000)は「ビューのサイズをmeasureSpecのサイズにせよ」
  3. 「モード」MeasureSpec.UNSPECIFIED(0x00000000)は「ビューのサイズをなんでもよい」

childMeasuredState は resolveSizeAndState が返す値に or される値で、用途はよくわかりません。 上位8ビットだけが 戻り値に or され、下位24ビットは無視されます。つまり「ステート亅の部分だけが使われます。

では具体的な動きを見てみましょう。

■measureSpecのモードが MeasureSpec.AT_MOSTの場合

size <= measureSpecのサイズ → size を返す。

size > 親ビューからのmeasureSpecのサイズ → measureSpecのサイズ | MEASURED_STATE_TOO_SMALL(0x01000000) を返す。

■measureSpecのモードが MeasureSpec.EXACTLYの場合

measureSpecのサイズを返す。

■measureSpecのモードが MeasureSpec.UNSPECIFIEDの場合

size を返す。

ここまでで明瞭にわかりますように、resolveSizeAndState はビューが必要としているサイズを極力要望し、 かつ余分な大きさを要望しないように親ビューと交渉するための単純なヘルパーメソッドであることがわかります。

尚、resolveSizeAndStateが実際に返す値は、繰り返しますが

 上で決まった値 | (childMeasuredState & MEASURED_STATE_MASK);

です。

resolveSizeメソッド

public static int resolveSize(int size, int measureSpec)

このメソッドは内部で resolveSizeAndState を呼び出しますが、childMeasuredState には 0 が指定され、結果の上位8ビット(「ステート」)が ゼロクリアされる点が異なります。つまり MEASURED_STATE_TOO_SMALL フラグはセットされません。おそらくこれがAPI-1 からあるresolveSize の仕様なのでしょう。

おそらくよりよいビューの配置のためにMEASURED_STATE_TOO_SMALL フラグが設けられたのだと思いますが、 これがセットされない場合の影響は未調査です。android2.Xのサポートがないならば、 resolveSizeAndState を使ったほうがよいかもしれません。