カスタム描画

カスタムビューの最も重要な要素は外観です。カスタム描画では、アプリのニーズに応じて簡単な図形も複雑な図形も作成できます。このレッスンでは、非常によく利用される処理について説明します。

関連情報については、キャンバスとドローアブルをご覧ください。

onDraw() をオーバーライドする

カスタムビューの描画で最も重要なステップは、onDraw() メソッドのオーバーライドです。onDraw() のパラメータは、ビューが自身を描画する際に使用できる Canvas オブジェクトです。Canvas クラスは、テキストや、線、ビットマップ、その他さまざまなグラフィック プリミティブを描画するためのメソッドを定義します。このような onDraw() のメソッドを使用することにより、カスタム ユーザー インターフェース(UI)を作成できます。

ただし、描画メソッドを呼び出す前に、Paint オブジェクトを作成する必要があります。次のセクションでは、Paint について説明します。

描画オブジェクトを作成する

android.graphics フレームワークでは、描画は 2 つの領域に分かれています。

  • 何を描画するか(処理担当は Canvas
  • どのように描画するか(処理担当は Paint

たとえば、Canvas は、線を描画するメソッドを提供し、Paint は、その線の色を定義するメソッドを提供します。Canvas には、四角形を描画するメソッドがあり、Paint は、その四角形を特定の色で塗りつぶすか空白のままにするかを定義します。簡単に言うと、Canvas は、画面上に描画できる図形を定義し、Paint は、描画する図形の色やスタイル、フォントなどを定義します。

したがって、何かを描画する前に、1 つまたは複数の Paint オブジェクトを作成する必要があります。下記の PieChart の例の場合、Java では、コンストラクタから呼び出される init というメソッドを使用してこの処理を行っていますが、Kotlin では、インラインで初期化しています。

Kotlin

    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 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() メソッドをオーバーライドするだけで足ります。

onSizeChanged() は、ビューに最初にサイズが割り当てられたときに呼び出され、ビューのサイズがなんらかの理由で変更されると再度呼び出されます。描画のたびに再計算するのではなく、onSizeChanged() 内で、ビューのサイズに関連する位置やディメンション、その他各種の値を計算します。PieChart の例の場合、PieChart ビューは、円グラフの境界をなす四角形の計算と、テキストラベルや各種視覚要素の相対位置の計算を、onSizeChanged() で行っています。

ビューにサイズが割り当てられたとき、レイアウト マネージャーは、そのサイズにビューのパディングがすべて含まれていると想定します。ビューのサイズを計算する際は、このパディング値を処理する必要があります。この処理を行う方法を、PieChart.onSizeChanged() から抜粋した次のスニペットに示します。

Kotlin

    // Account for padding
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label
    if (showText) xpad += textWidth

    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big we can make the pie.
    val diameter = Math.min(ww, hh)
    

Java

    // 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 we can make the pie.
    float diameter = Math.min(ww, hh);
    

ビューのレイアウト パラメータをきめ細かく制御する必要がある場合は、onMeasure() を実装します。このメソッドのパラメータは、ビューの親がビューに要求する大きさと、そのサイズが実際の最大値であるのか単なる提案であるのかを示す View.MeasureSpec 値です。最適化のために、各値はパッケージ化整数として格納されます。各整数に格納された情報を取り出すには、View.MeasureSpec の静的メソッドを使用します。

onMeasure() の実装例を以下に示します。この実装では、PieChart は、円グラフのスライスをそのラベルと同じサイズにするのに十分な大きさの領域を作成しようと試みます。

Kotlin

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        // Try for a width based on our minimum
        val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
        val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

        // Whatever the width ends up being, ask for a height that would let the pie
        // get as big as it can
        val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
        val h: Int = View.resolveSizeAndState(
                View.MeasureSpec.getSize(w) - textWidth.toInt(),
                heightMeasureSpec,
                0
        )

        setMeasuredDimension(w, h)
    }
    

Java

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       // Try for a width based on our minimum
       int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
       int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

       // Whatever the width ends up being, ask for a height that would let the pie
       // get as big as it can
       int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
       int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, 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 オブジェクトを作成して、グラデーションの塗りつぶしを定義します。setShader() を呼び出して、塗りつぶし図形に対して LinearGradient を使用します。
  • drawBitmap() を使用してビットマップを描画します。

PieChart を描画するコードの例を以下に示します。この例では、テキスト、線、図形の組み合わせを使用しています。

Kotlin

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.apply {
            // Draw the shadow
            drawOval(shadowBounds, shadowPaint)

            // Draw the label text
            drawText(data[mCurrentItem].mLabel, textX, textY, textPaint)

            // Draw the pie slices
            data.forEach {
                piePaint.shader = it.mShader
                drawArc(bounds,
                        360 - it.endAngle,
                        it.endAngle - it.startAngle,
                        true, piePaint)
            }

            // Draw the pointer
            drawLine(textX, pointerY, pointerX, pointerY, textPaint)
            drawCircle(pointerX, pointerY, pointerSize, mTextPaint)
        }
    }
    

Java

    protected void onDraw(Canvas canvas) {
       super.onDraw(canvas);

       // Draw the shadow
       canvas.drawOval(
               shadowBounds,
               shadowPaint
       );

       // Draw the label text
       canvas.drawText(data.get(currentItem).mLabel, 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, pointerSize, mTextPaint);
    }