カスタムビューの最も重要な要素は外観です。カスタム描画は、アプリのニーズに応じて簡単なものから複雑なものまでさまざまです。このドキュメントでは、最も一般的なオペレーションについて説明します。
詳しくは、ドローアブルの概要をご覧ください。
onDraw() をオーバーライドする
カスタムビューの描画で最も重要なステップは、onDraw()
メソッドのオーバーライドです。onDraw()
のパラメータは、ビューが自身の描画に使用できる Canvas
オブジェクトです。Canvas
クラスは、テキスト、線、ビットマップ、その他多くのグラフィック プリミティブを描画するためのメソッドを定義します。onDraw()
でこれらのメソッドを使用すると、カスタム ユーザー インターフェース(UI)を作成できます。
まず、Paint
オブジェクトを作成します。次のセクションでは、Paint
について説明します。
描画オブジェクトを作成する
android.graphics
フレームワークは、描画を 2 つの領域に分割します。
- 描画する対象(
Canvas
で処理)。 - どのように描画するか(処理担当は
Paint
)
たとえば、Canvas
は線を描画するメソッド、Paint
は線の色を定義するメソッドです。Canvas
には長方形を描画するメソッドがあり、Paint
はその長方形を色で塗りつぶすか空のままにするかを定義します。Canvas
は画面上に描画できるシェイプを定義し、Paint
は描画する各シェイプの色、スタイル、フォントなどを定義します。
何かを描画する前に、1 つ以上の Paint
オブジェクトを作成します。次の例では、init
というメソッドでこの処理を行っています。このメソッドは、Java のコンストラクタから呼び出されますが、Kotlin ではインラインで初期化できます。
Kotlin
@ColorInt private var textColor // Obtained from style attributes. @Dimension private var textHeight // Obtained from style attributes. private val textPaint = Paint(ANTI_ALIAS_FLAG).apply { color = textColor if (textHeight == 0f) { textHeight = textSize } else { textSize = textHeight } } private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL textSize = textHeight } private val shadowPaint = Paint(0).apply { color = 0x101010 maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL) }
Java
private Paint textPaint; private Paint piePaint; private Paint shadowPaint; @ColorInt private int textColor; // Obtained from style attributes. @Dimension private float textHeight; // Obtained from style attributes. private void init() { textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); textPaint.setColor(textColor); if (textHeight == 0) { textHeight = textPaint.getTextSize(); } else { textPaint.setTextSize(textHeight); } piePaint = new Paint(Paint.ANTI_ALIAS_FLAG); piePaint.setStyle(Paint.Style.FILL); piePaint.setTextSize(textHeight); shadowPaint = new Paint(0); shadowPaint.setColor(0xff101010); shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); ... }
事前にオブジェクトを作成することは、最適化するうえで重要です。ビューは頻繁に再描画され、描画オブジェクトの多くは高コストの初期化を必要とします。onDraw()
メソッド内で描画オブジェクトを作成すると、パフォーマンスが大幅に低下し、UI の動作が遅くなることがあります。
レイアウト イベントを処理する
カスタムビューを適切に描画するには、そのサイズを確認します。複雑なカスタムビューでは、多くの場合、画面上の領域のサイズと形状に応じて、複数のレイアウト計算を実行する必要があります。画面上のビューのサイズについて想定しないでください。ビューを使用するアプリが 1 つだけであっても、そのアプリは、さまざまな画面サイズ、複数の画面密度、さまざまなアスペクト比を、縦向きと横向きの両方で処理する必要があります。
View
には測定を処理するための多数のメソッドがありますが、そのほとんどはオーバーライドする必要はありません。ビューのサイズを特別に制御する必要がない場合は、onSizeChanged()
という 1 つのメソッドのみをオーバーライドします。
onSizeChanged()
は、ビューに最初にサイズが割り当てられたときと、なんらかの理由でビューのサイズが変更されたときに呼び出されます。描画のたびに再計算するのではなく、onSizeChanged()
でビューのサイズに関連する位置、寸法、その他の値を計算します。次の例の onSizeChanged()
では、ビューがグラフの境界四角形と、テキストラベルやその他の視覚要素の相対位置を計算します。
ビューにサイズが割り当てられると、レイアウト マネージャーは、そのサイズにビューのパディングが含まれていると想定します。ビューのサイズを計算するときに、パディング値を処理します。以下の onSizeChanged()
のスニペットは、その方法を示しています。
Kotlin
private val showText // Obtained from styled attributes. private val textWidth // Obtained from styled attributes. override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) // Account for padding. var xpad = (paddingLeft + paddingRight).toFloat() val ypad = (paddingTop + paddingBottom).toFloat() // Account for the label. if (showText) xpad += textWidth.toFloat() val ww = w.toFloat() - xpad val hh = h.toFloat() - ypad // Figure out how big you can make the pie. val diameter = Math.min(ww, hh) }
Java
private Boolean showText; // Obtained from styled attributes. private int textWidth; // Obtained from styled attributes. @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Account for padding. float xpad = (float)(getPaddingLeft() + getPaddingRight()); float ypad = (float)(getPaddingTop() + getPaddingBottom()); // Account for the label. if (showText) xpad += textWidth; float ww = (float)w - xpad; float hh = (float)h - ypad; // Figure out how big you can make the pie. float diameter = Math.min(ww, hh); }
ビューのレイアウト パラメータをきめ細かく制御する必要がある場合は、onMeasure()
を実装します。このメソッドのパラメータは、ビューの親がビューに求める大きさと、そのサイズがハード最大値であるか単なる提案であるかを示す View.MeasureSpec
値です。最適化として、これらの値はパック整数として格納されます。各整数に格納されている情報を展開するには、View.MeasureSpec
の静的メソッドを使用します。
onMeasure()
の実装例を次に示します。この実装では、グラフをラベルと同じ大きさにするのに十分な大きさに領域を合わせようとします。
Kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // Try for a width based on your minimum. val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1) // Whatever the width is, ask for a height that lets the pie get as big as // it can. val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0) setMeasuredDimension(w, h) }
Java
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on your minimum. int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = resolveSizeAndState(minw, widthMeasureSpec, 1); // Whatever the width is, ask for a height that lets the pie get as big as it // can. int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop(); int h = resolveSizeAndState(minh, heightMeasureSpec, 0); setMeasuredDimension(w, h); }
このコードには、3 つの重要な注意点があります。
- 計算では、ビューのパディングが考慮されます。前述のように、これはビューの役割です。
- ヘルパー メソッド
resolveSizeAndState()
を使用して、最終的な幅と高さの値を作成します。このヘルパーは、ビューに必要なサイズとonMeasure()
に渡された値を比較して、適切なView.MeasureSpec
値を返します。 onMeasure()
は戻り値を返しません。代わりに、このメソッドはsetMeasuredDimension()
を呼び出して結果を伝えます。このメソッドの呼び出しは必須です。この呼び出しを省略すると、View
クラスからランタイム例外がスローされます。
引き分け
オブジェクトの作成と測定コードを定義したら、onDraw()
を実装できます。ビューによって onDraw()
は異なる方法で実装されますが、ほとんどのビューで共通する一般的なオペレーションがいくつかあります。
drawText()
を使用してテキストを描画します。setTypeface()
を呼び出して書体を指定し、setColor()
を呼び出してテキストの色を指定します。drawRect()
、drawOval()
、drawArc()
を使用して、プリミティブ シェイプを描画します。setStyle()
を呼び出して、シェイプを塗りつぶすか、枠線で囲むか、その両方を行うかを変更します。Path
クラスを使用して、より複雑なシェイプを描画します。Path
オブジェクトに線と曲線を追加してシェイプを定義し、drawPath()
を使用してシェイプを描画します。 プリミティブ シェイプと同様に、パスはsetStyle()
に応じて、枠線、塗りつぶし、またはその両方が可能です。-
LinearGradient
オブジェクトを作成してグラデーション塗りつぶしを定義します。塗りつぶしシェイプでLinearGradient
を使用するには、setShader()
を呼び出します。 drawBitmap()
を使用してビットマップを描画します。
次のコードは、テキスト、線、図形の組み合わせを描画します。
Kotlin
private val data = mutableListOf<Item>() // A list of items that are displayed. private var shadowBounds = RectF() // Calculated in onSizeChanged. private var pointerRadius: Float = 2f // Obtained from styled attributes. private var pointerX: Float = 0f // Calculated in onSizeChanged. private var pointerY: Float = 0f // Calculated in onSizeChanged. private var textX: Float = 0f // Calculated in onSizeChanged. private var textY: Float = 0f // Calculated in onSizeChanged. private var bounds = RectF() // Calculated in onSizeChanged. private var currentItem: Int = 0 // The index of the currently selected item. override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.apply { // Draw the shadow. drawOval(shadowBounds, shadowPaint) // Draw the label text. drawText(data[currentItem].label, textX, textY, textPaint) // Draw the pie slices. data.forEach {item -> piePaint.shader = item.shader drawArc( bounds, 360 - item.endAngle, item.endAngle - item.startAngle, true, piePaint ) } // Draw the pointer. drawLine(textX, pointerY, pointerX, pointerY, textPaint) drawCircle(pointerX, pointerY, pointerRadius, textPaint) } } // Maintains the state for a data item. private data class Item( var label: String, var value: Float = 0f, @ColorInt var color: Int = 0, // Computed values. var startAngle: Float = 0f, var endAngle: Float = 0f, var shader: Shader )
Java
private List<Item> data = new ArrayList<Item>(); // A list of items that are displayed. private RectF shadowBounds; // Calculated in onSizeChanged. private float pointerRadius; // Obtained from styled attributes. private float pointerX; // Calculated in onSizeChanged. private float pointerY; // Calculated in onSizeChanged. private float textX; // Calculated in onSizeChanged. private float textY; // Calculated in onSizeChanged. private RectF bounds; // Calculated in onSizeChanged. private int currentItem = 0; // The index of the currently selected item. protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the shadow. canvas.drawOval( shadowBounds, shadowPaint ); // Draw the label text. canvas.drawText(data.get(currentItem).label, textX, textY, textPaint); // Draw the pie slices. for (int i = 0; i < data.size(); ++i) { Item it = data.get(i); piePaint.setShader(it.shader); canvas.drawArc( bounds, 360 - it.endAngle, it.endAngle - it.startAngle, true, piePaint ); } // Draw the pointer. canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint); canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint); } // Maintains the state for a data item. private class Item { public String label; public float value; @ColorInt public int color; // Computed values. public int startAngle; public int endAngle; public Shader shader; }
グラフィック効果を適用する
Android 12(API レベル 31)では、一般的なグラフィック効果(ぼかし、カラーフィルタ、Android シェーダー効果など)を View
オブジェクトとレンダリング階層に適用する RenderEffect
クラスが追加されました。エフェクトは、チェーン エフェクトとして組み合わせることができます。チェーン エフェクトは、インナーエフェクトとアウターエフェクト、またはブレンドエフェクトです。この機能のサポートは、デバイスの処理能力によって異なります。
また、View.setRenderEffect(RenderEffect)
を呼び出して、View
の基になる RenderNode
に効果を適用することもできます。
RenderEffect
オブジェクトを実装する手順は次のとおりです。
view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))
ビューはプログラムで作成することも、XML レイアウトからインフレートしてビュー バインディング または
findViewById()
を使用して取得することもできます。