Canvas オブジェクトのクリッピング

この Codelab は、Kotlin での高度な Android 開発コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できますが、これは必須ではありません。すべてのコース Codelab は Kotlin での Codelab の高度な Codelab のランディング ページに掲載されています。

はじめに

この Codelab では、選択的に描画される、または画面に描画されない画像、キャンバス、ビットマップの領域を定義する手段としてクリッピングを使用します。クリッピングの 1 つの目的は、オーバードローを減らすことです。オーバードローとは、最終的なピクセルを表示するために、画面上のピクセルを複数回描画することを意味します。オーバードローを減らすと、描画のパフォーマンスを最大化するために、ディスプレイのピクセルまたは領域が描画される回数を最小限に抑えられます。また、クリッピングを使用して、ユーザー インターフェースのデザインやアニメーションにさまざまな効果を加えることもできます。

たとえば、下に重ねたカードを何枚も描画する場合、各カードを下から完全に描画するよりも、通常、表示されている部分のみを描画する方が効率的です。(通常、クリッピング オペレーションにはコストがかかるため、Android システムは全体的に多数の描画最適化を行います)。

カードの表示部分のみ描画するには、カードごとに切り取り領域を指定します。たとえば下の図では、クリッピングの長方形が画像に適用されると、その長方形内の部分のみが表示されます。

クリッピング領域は通常長方形ですが、テキストだけでなく、任意の形状または形状の組み合わせを使用できます。クリッピング領域内のリージョンを含めるか除外するかを指定することもできます。たとえば、円形のクリッピング領域を作成して、円の外側にある要素のみを表示できます。

この Codelab では、さまざまなクリッピングを試してみます。

前提となる知識

以下について把握しておく必要があります。

  • Activity を使用してアプリを作成し、Android Studio を使用して実行する方法。
  • Canvas を作成して描画する方法。
  • カスタムの View を作成し、onDraw()onSizeChanged() をオーバーライドする方法

学習内容

  • Canvas で描画するオブジェクトをクリップする方法。
  • キャンバスの描画状態を保存、復元する方法。
  • キャンバスとテキストに変換を適用する方法

演習内容

  • さまざまなクリップ方法を示し、その結果がそれらの形状の可視性を示して、クリップした図形を画面に描画するアプリを作成します。
  • また、訳文やゆがんだテキストを描画します。

ClippingExample アプリでは、図形を組み合わせて、キャンバスのどの部分をビューに表示するか指定することができます。完成したアプリは下のスクリーンショットのようになります。

このアプリをゼロから作成するので、プロジェクトを設定し、ディメンションと文字列を定義して、いくつかの変数を宣言する必要があります。

ステップ 1: ClippingExample プロジェクトを作成する

  1. Empty Activity テンプレートを使用して、ClippingExample という名前の Kotlin プロジェクトを作成します。パッケージ名の接頭辞には com.example.android を使用します。
  2. MainActivity.kt を開きます。
  3. onCreate() メソッドで、デフォルトのコンテンツ ビューを置き換え、コンテンツ ビューを ClippedView の新しいインスタンスに設定します。これは、次に作成するクリップのサンプルのカスタムビューです。
setContentView(ClippedView(this))
  1. MainActivity.kt と同じレベルで、View を拡張する ClippedView というカスタムビューの新しい Kotlin ファイルとクラスを作成します。下記の署名を入力してください。残りの作業はすべて、このClippedView内に収まります。@JvmOverloads アノテーションは、デフォルトのパラメータ値を置き換えるこの関数に対してオーバーロードを生成するよう Kotlin コンパイラに指示します。
class ClippedView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

ステップ 2: ディメンションと文字列リソースを追加する

  1. res/values/dimens.xml の新しいリソース ファイルで、クリップされたビューに使用するディメンションを定義します。これらのデフォルトのディメンションはハードコードされており、小さい画面に収まるようにサイズ設定されています。
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">90dp</dimen>
   <dimen name="clipRectBottom">90dp</dimen>
   <dimen name="clipRectTop">0dp</dimen>
   <dimen name="clipRectLeft">0dp</dimen>

   <dimen name="rectInset">8dp</dimen>
   <dimen name="smallRectOffset">40dp</dimen>

   <dimen name="circleRadius">30dp</dimen>
   <dimen name="textOffset">20dp</dimen>
   <dimen name="strokeWidth">4dp</dimen>

   <dimen name="textSize">18sp</dimen>
</resources>

アプリを大画面で適切に表示し、詳細を確認しやすくするために、大画面にのみ適用される大きな値を持つ dimens ファイルを作成できます。

  1. Android Studio で [values] フォルダを右クリックし、[New > Values resource file] を選択します。
  2. [New Resource File] ダイアログで、dimens ファイルを呼び出します。[Available qualifiers] で [Smallest screen Width] を選択し、[>>] ボタンをクリックして [選択した修飾子] に追加します。[画面の幅の最小値] ボックスに「480」と入力し、[OK] をクリックします。

  1. このファイルは、次のように values フォルダにあります。

  1. ファイルが表示されない場合は、アプリの [Project Files] ビューに切り替えます。新しいファイルのフルパスは ClippingExample/app/src/main/res/values-sw480dp/dimens.xml のようになります。

  1. values-sw480dp/dimens.xml ファイルのデフォルト コンテンツを下記のディメンションに置き換えます。
<?xml version="1.0" encoding="utf-8"?>
<resources>
   <dimen name="clipRectRight">120dp</dimen>
   <dimen name="clipRectBottom">120dp</dimen>

   <dimen name="rectInset">10dp</dimen>
   <dimen name="smallRectOffset">50dp</dimen>

   <dimen name="circleRadius">40dp</dimen>
   <dimen name="textOffset">25dp</dimen>
   <dimen name="strokeWidth">6dp</dimen>
</resources>
  1. strings.xml に次の文字列を追加します。キャンバスにテキストを表示するために使用します。
<string name="clipping">Clipping</string>
<string name="translated">translated text</string>
<string name="skewed">"Skewed and "</string>

ステップ 3: Paint オブジェクトと Path オブジェクトを作成して初期化する

  1. プロジェクトの [Android] ビューに戻ります。
  2. ClippedView では、描画の際に使用する Paint 変数を定義します。アンチエイリアスを有効にして、次のように、ディメンションで定義されたストロークの幅とテキストサイズを使用します。
private val paint = Paint().apply {
   // Smooth out edges of what is drawn without affecting shape.
   isAntiAlias = true
   strokeWidth = resources.getDimension(R.dimen.strokeWidth)
   textSize = resources.getDimension(R.dimen.textSize)
}
  1. ClippedViewPath を作成して初期化し、描画したもののパスをローカルに保存します。android.graphics.Path をインポートします。
private val path = Path()

ステップ 4: シェイプを設定する

このアプリでは、複数の行と 2 列のシェイプがさまざまな方法でクリップされています。

これらはすべて共通しています。

  • コンテナとして機能する大きな長方形(正方形)
  • 大きいレクタングル(対角線)
  • 短い文字列

このステップでは、リソースからそれらのシェイプのディメンションを設定し、後でそれを使用するときに一度だけディメンションを取得するようにします。

  1. ClippedView で、path の下に、図形セット全体を囲むクリッピング長方形の寸法の変数を追加します。
private val clipRectRight = resources.getDimension(R.dimen.clipRectRight)
private val clipRectBottom = resources.getDimension(R.dimen.clipRectBottom)
private val clipRectTop = resources.getDimension(R.dimen.clipRectTop)
private val clipRectLeft = resources.getDimension(R.dimen.clipRectLeft)
  1. 長方形のインセットと小さな長方形のオフセットの変数を追加します。
private val rectInset = resources.getDimension(R.dimen.rectInset)
private val smallRectOffset = resources.getDimension(R.dimen.smallRectOffset)
  1. 円の半径の変数を追加します。これは、長方形の内側に描画される円の半径です。
private val circleRadius = resources.getDimension(R.dimen.circleRadius)
  1. 長方形内に描画されるテキストのオフセットとテキストサイズを追加します。
private val textOffset = resources.getDimension(R.dimen.textOffset)
private val textSize = resources.getDimension(R.dimen.textSize)

手順 4: 行と列の場所を設定する

このアプリのシェイプは、上記で設定したディメンションの値に基づいて、2 列 4 行で表示されます。この計算は、この Codelab には含まれていませんが、このステップで提供されているコードにコピーするときに確認してください。

  1. 2 つの列の座標を設定します。
private val columnOne = rectInset
private val columnTwo = columnOne + rectInset + clipRectRight
  1. 各行の座標(変換されたテキストの最後の行を含む)を追加します。
private val rowOne = rectInset
private val rowTwo = rowOne + rectInset + clipRectBottom
private val rowThree = rowTwo + rectInset + clipRectBottom
private val rowFour = rowThree + rectInset + clipRectBottom
private val textRow = rowFour + (1.5f * clipRectBottom)
  1. アプリを実行します。アプリ名の下に空白の画面が表示されます。

onDraw() では、以下のアプリのスクリーンショットに示すように、7 つの異なるクリップされた長方形を描画するメソッドを呼び出します。長方形はすべて同じ方法で描画されます。唯一の違いは、定義されたクリップ領域と画面上の位置です。

長方形の描画に使用するアルゴリズムは、次の図と説明のように動作します。要約すると、Canvas の原点を移動して一連の長方形を描画します。概念的には、次の手順で構成されます。

(1)まず、Canvas を長方形の描画先に変換します。つまり、次の長方形と他のすべての図形を描画する必要がある場所を計算するのではなく、Canvas の原点、つまり座標系を移動します。

(2)次に、キャンバスの新たな起点に長方形を描画します。つまり、変換した座標系内の同じ場所に図形を描画します。これは、はるかにシンプルで効率的です。

(3)最後に Canvas を元の Origin に復元します。

実装するアルゴリズムは次のとおりです。

  1. onDraw() で、Canvas をグレーの背景色で塗りつぶし、元の図形を描画する関数を呼び出します。
  2. クリップされる長方形と描画するテキストごとに関数を呼び出します。

長方形またはテキストごとに、次の操作を行います。

  1. Canvas の現在の状態を保存して、その初期状態にリセットできるようにします。
  2. キャンバスの Origin を描画する場所に翻訳します。
  3. クリッピングのシェイプとパスを適用します。
  4. 長方形またはテキストを描画します。
  5. Canvas の状態を復元します。

ステップ: onDraw() をオーバーライドする

  1. 次のコードのように onDraw() をオーバーライドします。描画するシェイプごとに関数を呼び出します。これは後で実装します。
 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawBackAndUnclippedRectangle(canvas)
        drawDifferenceClippingExample(canvas)
        drawCircularClippingExample(canvas)
        drawIntersectionClippingExample(canvas)
        drawCombinedClippingExample(canvas)
        drawRoundedRectangleClippingExample(canvas)
        drawOutsideClippingExample(canvas)
        drawSkewedTextExample(canvas)
        drawTranslatedTextExample(canvas)
        // drawQuickRejectExample(canvas)
    }
  1. 描画関数ごとにスタブを作成し、コードのコンパイルを続けます。以下のコードをコピーします。
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
}
private fun drawDifferenceClippingExample(canvas: Canvas){
}
private fun drawCircularClippingExample(canvas: Canvas){
}
private fun drawIntersectionClippingExample(canvas: Canvas){
}
private fun drawCombinedClippingExample(canvas: Canvas){
}
private fun drawRoundedRectangleClippingExample(canvas: Canvas){
}
private fun drawOutsideClippingExample(canvas: Canvas){
}
private fun drawTranslatedTextExample(canvas: Canvas){
}
private fun drawSkewedTextExample(canvas: Canvas){
}
private fun drawQuickRejectExample(canvas: Canvas){
}

アプリは同じ長方形とシェイプを 7 回描画し(最初にクリッピングなし)、次に 6 回、さまざまなクリッピング パスを適用しました。次に示すように、drawClippedRectangle() メソッドは 1 つの長方形を描画するコードを分析します。

ステップ 1: drawClippedRectangle() メソッドを作成する

  1. Canvas 型の引数 canvas を取る drawClippedRectangle() メソッドを作成します。
private fun drawClippedRectangle(canvas: Canvas) {
}
  1. drawClippedRectangle() メソッド内で、クリッピングの長方形の境界をシェイプ全体に設定します。正方形のみを描画するクリッピング長方形を適用します。
canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
)

Canvas.clipRect(...) メソッドは、将来の描画オペレーションが書き込み可能な画面の領域を縮小します。クリッピング境界を、現在のクリッピング長方形と clipRect() に渡される長方形の空間交差に設定します。clipRect() メソッドにはさまざまなバリエーションがあり、領域ごとに異なるフォームを受け入れ、クリッピング長方形で異なる操作が可能です。

  1. canvas の色は白で塗りつぶします。もちろん!キャンバス全体を囲んでいます。長方形を描画していないため、クリップされることになります。クリッピングの矩形があるため、クリッピングの長方形で定義された領域のみが塗りつぶされ、白い長方形が作成されます。その他のサーフェスは灰色のままです。
canvas.drawColor(Color.WHITE)
  1. 色を赤に変更し、クリッピング レクタングル内に対角線を描画します。
paint.color = Color.RED
canvas.drawLine(
   clipRectLeft,clipRectTop,
   clipRectRight,clipRectBottom,paint
)
  1. 色を緑色に設定し、クリップする長方形内に円を描画します。
paint.color = Color.GREEN
canvas.drawCircle(
   circleRadius,clipRectBottom - circleRadius,
   circleRadius,paint
)
  1. 色を青色に設定し、テキストを切り取る長方形の右端に沿って描画します。canvas.drawText() を使ってテキストを描画します。
paint.color = Color.BLUE
// Align the RIGHT side of the text with the origin.
paint.textSize = textSize
paint.textAlign = Paint.Align.RIGHT
canvas.drawText(
   context.getString(R.string.clipping),
   clipRectRight,textOffset,paint
)

ステップ 2: drawBackAndUnclippedRectangle() メソッドを実装する

  1. drawClippedRectangle() メソッドの動作を確認するには、次のように drawBackAndUnclippedRectangle() メソッドを実装して、最初のクリップされていない長方形を描画します。canvas を保存して、最初の行と列の位置に変換し、drawClippedRectangle() を呼び出して描画した後、canvas を以前の状態に復元します。
private fun drawBackAndUnclippedRectangle(canvas: Canvas){
   canvas.drawColor(Color.GRAY)
   canvas.save()
   canvas.translate(columnOne,rowOne)
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. アプリを実行すると、最初の白い長方形が赤い背景と灰色の背景に表示されます。

次のクリッピング サンプル メソッドでは、クリッピング領域のさまざまな組み合わせを適用してグラフィック効果を実現し、クリッピング領域を組み合わせて必要なシェイプを作成する方法を学びます。

それぞれの方法は同じパターンに従います。

  1. キャンバスの現在の状態を保存します。canvas.save()

アクティビティのコンテキストは、描画状態のスタックを保持します。描画状態は、現在の変換マトリックスと現在のクリッピング領域で構成されます。現在の状態を保存し、描画状態を変更するアクション(キャンバスの回転や回転など)を実行して、保存された状態を復元することができます。(注: これは git の「stash」コマンドに似ています)。

描画に変換が含まれている場合、変換を連鎖させて元に戻すことはエラーが起こりやすくなります。たとえば、平行移動、伸縮、回転を行うと、すばやく複雑になります。代わりに、キャンバスの状態を保存し、変換を適用して描画し、以前の状態を復元します。

たとえば、クリッピング領域を定義してその状態を保存します。キャンバスを翻訳し、クリッピング領域を追加して回転させます。描画した後、元のクリッピング状態を復元して、図に示すように別の変換とスキュー変換に進むことができます。

  1. キャンバスの原点を行/列座標に変換: canvas.translate()

描画するすべての要素を移動するより、キャンバスの原点を移動して同じ要素を新しい座標系で描画する方がはるかに簡単です。(ヒント: 要素の回転についても同じ手法を使用できます)。

  1. 変換がある場合は、path に適用します。
  2. クリッピングを適用: canvas.clipPath(path)
  3. 図形を描画します: drawClippedRectangle() or drawText()
  4. 以前のキャンバスの状態(canvas.restore())を復元します

ステップ 1: drawDifferenceClippingExample(canvas) を実装する

2 つ目のクリップの長方形の差を使って画像フレーム効果を作成する 2 つ目の長方形を描画するコードを追加します。

以下の処理を行うコードを使用します。

  1. キャンバスを保存します。
  2. キャンバスの原点を、最初の矩形の右側にある最初の行(2 列目)への空きスペースに変換します。
  3. クリップ用の長方形を 2 つ適用します。DIFFERENCE 演算子は、1 番目の長方形から 2 番目の長方形を減算します。
  1. drawClippedRectangle() メソッドを呼び出して、変更したキャンバスを描画します。
  2. キャンバスの状態を復元します。
private fun drawDifferenceClippingExample(canvas: Canvas) {
   canvas.save()
   // Move the origin to the right for the next rectangle.
   canvas.translate(columnTwo,rowOne)
   // Use the subtraction of two clipping rectangles to create a frame.
   canvas.clipRect(
       2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .DIFFERENCE) was deprecated in API level 26. The recommended
   // alternative method is clipOutRect(float, float, float, float),
   // which is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
       canvas.clipRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset,
            Region.Op.DIFFERENCE
       )
   } else {
       canvas.clipOutRect(
           4 * rectInset,4 * rectInset,
           clipRectRight - 4 * rectInset,
           clipRectBottom - 4 * rectInset
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}
  1. アプリを実行すると、次のようになります。

ステップ 2: drawCircularClippingExample(canvas) を実装する

次に、円形のパスから作成された円形のクリッピング領域を使用する長方形を描画するコードを追加します。基本的に、円を描画しない(描画しない)ため、代わりに灰色の背景を表示します。

private fun drawCircularClippingExample(canvas: Canvas) {

   canvas.save()
   canvas.translate(columnOne, rowTwo)
   // Clears any lines and curves from the path but unlike reset(),
   // keeps the internal data structure for faster reuse.
   path.rewind()
   path.addCircle(
       circleRadius,clipRectBottom - circleRadius,
       circleRadius,Path.Direction.CCW
   )
   // The method clipPath(path, Region.Op.DIFFERENCE) was deprecated in
   // API level 26. The recommended alternative method is
   // clipOutPath(Path), which is currently available in
   // API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipPath(path, Region.Op.DIFFERENCE)
   } else {
       canvas.clipOutPath(path)
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

ステップ 3: drawIntersectionClippingExample(canvas) を実装する

次に、2 行目と 2 つ目のクリッピングする長方形の交差を描画するコードを追加します。

この領域の外観は、画面の解像度に応じて異なります。表示領域のサイズを変更するには、smallRectOffset ディメンションをテストします。smallRectOffset が小さいほど、画面上の領域が広くなります。

private fun drawIntersectionClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowTwo)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight - smallRectOffset,
       clipRectBottom - smallRectOffset
   )
   // The method clipRect(float, float, float, float, Region.Op
   // .INTERSECT) was deprecated in API level 26. The recommended
   // alternative method is clipRect(float, float, float, float), which
   // is currently available in API level 26 and higher.
   if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom,
           Region.Op.INTERSECT
       )
   } else {
       canvas.clipRect(
           clipRectLeft + smallRectOffset,
           clipRectTop + smallRectOffset,
           clipRectRight,clipRectBottom
       )
   }
   drawClippedRectangle(canvas)
   canvas.restore()
}

ステップ 4: drawCombinedClippingExample(canvas) を実装する

次に、シェイプ、円、長方形を組み合わせて、任意のパスを描画してクリッピング領域を定義します。

private fun drawCombinedClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne, rowThree)
   path.rewind()
   path.addCircle(
       clipRectLeft + rectInset + circleRadius,
       clipRectTop + circleRadius + rectInset,
       circleRadius,Path.Direction.CCW
   )
   path.addRect(
       clipRectRight / 2 - circleRadius,
       clipRectTop + circleRadius + rectInset,
       clipRectRight / 2 + circleRadius,
       clipRectBottom - rectInset,Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

ステップ 5: drawRoundedRectangleClippingExample(canvas) を実装する

次に、一般的に使用されているクリップ形状である、角丸の長方形を追加します。

  1. トップレベルで、長方形の変数を作成して初期化します。RectF は、浮動小数点値で長方形の座標を保持するクラスです。
private var rectF = RectF(
   rectInset,
   rectInset,
   clipRectRight - rectInset,
   clipRectBottom - rectInset
)
  1. 関数 drawRoundedRectangleClippingExample() を実装します。addRoundRect() 関数は、長方形、角の丸みの x 値と y 値、丸い長方形を巻く方向を指定します。Path.Direction は、パスに追加されたときの閉じた形状(長方形、楕円など)の向きを指定します。CCW は反時計回りを意味します。
private fun drawRoundedRectangleClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnTwo,rowThree)
   path.rewind()
   path.addRoundRect(
       rectF,clipRectRight / 4,
       clipRectRight / 4, Path.Direction.CCW
   )
   canvas.clipPath(path)
   drawClippedRectangle(canvas)
   canvas.restore()
}

ステップ 6: drawOutsideClippingExample(canvas) を実装する

クリップの長方形の外側をクリップします。クリッピングする長方形のインセットを 2 倍にします。

private fun drawOutsideClippingExample(canvas: Canvas) {
   canvas.save()
   canvas.translate(columnOne,rowFour)
   canvas.clipRect(2 * rectInset,2 * rectInset,
       clipRectRight - 2 * rectInset,
       clipRectBottom - 2 * rectInset)
   drawClippedRectangle(canvas)
   canvas.restore()
}

ステップ 7: drawTranslatedTextExample(キャンバス)を実装する

テキストの描画は他の図形とそれほど変わりないため、テキストに変換を適用できます。たとえば、キャンバスを翻訳してテキストを描画することで、テキストを翻訳できます。

  1. 以下の関数を実装します。
private fun drawTranslatedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.GREEN
   // Align the RIGHT side of the text with the origin.
   paint.textAlign = Paint.Align.LEFT
   // Apply transformation to canvas.
   canvas.translate(columnTwo,textRow)
   // Draw text.
   canvas.drawText(context.getString(R.string.translated),
       clipRectLeft,clipRectTop,paint)
   canvas.restore()
}
  1. アプリを実行すると、翻訳されたテキストが表示されます。

ステップ 8: drawSkewedTextExample(canvas) を実装する

テキストを歪めることもできます。さまざまな形で歪めます。

  1. 以下の ClippedView で関数を作成します。
private fun drawSkewedTextExample(canvas: Canvas) {
   canvas.save()
   paint.color = Color.YELLOW
   paint.textAlign = Paint.Align.RIGHT
   // Position text.
   canvas.translate(columnTwo, textRow)
   // Apply skew transformation.
   canvas.skew(0.2f, 0.3f)
   canvas.drawText(context.getString(R.string.skewed),
       clipRectLeft, clipRectTop, paint)
   canvas.restore()
}
  1. アプリを実行すると、翻訳されたテキストの前にスキューされたテキストが描画されます。

Canvas メソッド quickReject() を使用すると、すべての変換が適用された後に、指定された長方形またはパスが現在表示されている領域の外側にあるかどうかを確認できます。

複雑な描画を作成する場合、quickReject() メソッドは非常に高速に処理する必要があります。quickReject() を使用すると、描画不要のオブジェクトを効率的に決定でき、独自の交差ロジックを記述する必要がなくなります。

  • quickReject() メソッドは、画面に長方形またはパスがまったく表示されない場合、true を返します。部分的に重複している場合も、ご自身で確認する必要があります。
  • EdgeType は、AAアンチエイリアス: アンチエイリアスとなる場合があるため、角を丸めて処理します)または BW(黒ピクセルは最も近いピクセル境界に丸めて処理します)のいずれかです。最も近いピクセルに丸めます。

quickReject() には複数のバージョンがあり、ドキュメントでも確認できます。

boolean

quickReject(float left, float top, float right, float bottom, Canvas.EdgeType type)

boolean

quickReject(RectF rect, Canvas.EdgeType type)

boolean

quickReject(パス path, Canvas.EdgeType type)

この演習では、前と同様に、テキストの下、clipRect 内に新しい行を描画します。

  • まず、quickReject() を長方形 inClipRectangle で呼び出し、これが clipRect と重なります。したがって、quickReject() は false を返し、clipRectBLACK で塗りつぶされ、inClipRectangle の長方形が描画されます。

  • 次に、コードを変更し、notInClipRectangle を指定して quickReject() を呼び出します。quickReject() が true を返すようになり、clipRectWHITE で塗りつぶされ、notInClipRectangle は描画されなくなります。

複雑な描画がある場合、どの領域がクリッピング領域から完全に外されているか、クリッピング領域の一部または全部が内側であるため、追加の計算と描画が必要になる可能性があるかをすばやく確認できます。

ステップ: quickReject() を試してみる

  1. 最上位で、追加の行の y 座標を表す変数を作成します。
   private val rejectRow = rowFour + rectInset + 2*clipRectBottom
  1. 次の drawQuickRejectExample() 関数を ClippedView に追加します。コードには、quickReject() の使用に必要なすべての情報が記載されていますので、お読みください。
private fun drawQuickRejectExample(canvas: Canvas) {
   val inClipRectangle = RectF(clipRectRight / 2,
       clipRectBottom / 2,
       clipRectRight * 2,
       clipRectBottom * 2)

   val notInClipRectangle = RectF(RectF(clipRectRight+1,
       clipRectBottom+1,
       clipRectRight * 2,
       clipRectBottom * 2))

   canvas.save()
   canvas.translate(columnOne, rejectRow)
   canvas.clipRect(
       clipRectLeft,clipRectTop,
       clipRectRight,clipRectBottom
   )
   if (canvas.quickReject(
           inClipRectangle, Canvas.EdgeType.AA)) {
       canvas.drawColor(Color.WHITE)
   }
   else {
       canvas.drawColor(Color.BLACK)
       canvas.drawRect(inClipRectangle, paint
       )
   }
       canvas.restore()
}
  1. onDraw() で、drawQuickRejectExample() の呼び出しのコメントを解除します。
  2. アプリを実行すると、黒い長方形(塗りつぶしのクリッピング領域)と inClipRectangle の一部が表示されます。これは、2 つの長方形が重なり、quickReject()false を返し、inClipRectangle が描画されるためです。

  1. drawQuickRejectExample() で、notInClipRectangle. に対して quickReject() を実行するようにコードを変更します。quickReject()true を返し、クリッピング領域が白で塗りつぶされます。

完成した Codelab のコードをダウンロードします。

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-clipping


リポジトリを ZIP ファイルとしてダウンロードして解凍し、Android Studio で開くこともできます。

ZIP をダウンロード

  • アクティビティの Context は、Canvas の変換とクリッピング領域を保持する状態を維持します。
  • canvas.save()canvas.restore() を使って描画し、キャンバスを元の状態に戻します。
  • キャンバスに複数の図形を描画するには、描画位置を計算するか、描画サーフェスの原点を移動(変換)します。後者を使用すると、描画シーケンスを繰り返すユーティリティ メソッドを簡単に作成できます。
  • クリッピング領域は任意の形状にすることができ、形状やパスの組み合わせです。
  • クリッピング リージョンを追加、減算、交差させて、必要なリージョンを正確に取得できます。
  • キャンバスを変換することで、テキストに変換を適用することができます。
  • CanvasquickReject() メソッドを使用すると、指定した長方形またはパスが現在表示されている領域の外側に配置されているか確認できます。

Udacity コース:

Android デベロッパー ドキュメント:

Android フレームワークが画面に描画される仕組みの詳細については、グラフィック アーキテクチャ シリーズの記事をご覧ください。

このセクションでは、インストラクターが主導するコースの一環として、この Codelab に取り組む生徒の課題について説明します。教師は以下のことを行えます。

  • 必要に応じて課題を割り当てます。
  • 宿題の提出方法を生徒に伝える。
  • 宿題を採点します。

教師はこれらの提案を少しだけ使うことができます。また、他の課題は自由に割り当ててください。

この Codelab にご自分で取り組む場合は、これらの課題を使用して知識をテストしてください。

次の質問に答えてください。

問題 1

図形を描画から効率的に除外するには、次のどのメソッドを呼び出しますか。

excludeFromDrawing()

quickReject()

onDraw()

clipRect()

質問 2

Canvas.save()Canvas.restore()が保存、復元する情報はどれですか。

▢ 色、線の太さなど

▢ 現在の変換のみ

▢ 現在の変換とクリッピング領域

▢ 現在のクリッピング地域のみ

問題 3

Paint.Align には以下を指定します。

▢ 次の図形図形の配置方法

▢ テキストの原点のどちら側から抜粋するか

▢ クリッピング領域内の位置

▢ テキストのどの辺を原文に合わせるか

このコースの他の Codelab へのリンクについては、Kotlin Codelab の高度な Codelab のランディング ページをご覧ください。