カスタムビューを作る
メインページ>コンピュータの部屋#Android>Android Tips
Android には標準で Button や TextView や LinearLayout など、多数のビューを利用できますが、ビューを自作することもできます。
以下に楕円を表示するだけの簡単なビューの実装例を示します。
目次
モジュールを作成する
AndroidStudio で作成する場合は、専用の Android Library Module を新規に作るのがよいでしょう。
カスタム属性を定義する
モジュールに 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>
strokeColor は楕円の輪郭の色、strokeWidth は楕円の輪郭の太さです。
カスタムビューのクラスを作成する
カスタムビューのクラスを 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(); : : }
コンストラクタを書く
コンストラクタは2種類必要です。
public Ellipse(Context context) { this(context, null); } public Ellipse(Context context, AttributeSet attrs) { super(context, attrs); // 160dpiとの比を求める。 float 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(); } }
XMLで指定されたカスタム属性は AttributeSet attrs という引数で渡されますが、そのまま使ってはいけません。 コンテキストの obtainStyledAttributes メソッドで TypedArray 型に変換してください。これで属性はテーマやスタイルが反映された正しい値になります。
各カスタム属性値は TypedArray の getColorメソッドなどに、R.styleable に自動生成された識別子を指定して値を取り出します。 カスタム属性値がない場合、getColorなどのメソッドの最後のパラメータが既定値として戻るのに注意してください。従って mStrokeWidthやmStrokeColorは予め規定値で初期化しておく必要があります。
カスタム属性の 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 30xp 必要ということにして サイズを要求しています。
/** * ビューの大きさの調整 * @param widthMeasureSpec 幅の注文 * @param heightMeasureSpec 高さの注文 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 画面密度取得(dpへの換算用) float density = getResources().getDisplayMetrics().density; // コントロールの表示に必要な大きさを計算する。 // このカスタムビューにはコンテンツの決まった大きさはないので、最小限 30dp必要ということで計算。 int minWidth = this.getPaddingLeft() + this.getPaddingRight() + (int)(30 * density); int minHeight = this.getPaddingTop() + this.getPaddingBottom() + (int)(30 * density); // 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); }