【Android】CanvasにPathで破線や点線を書くのに便利なPathEffectの紹介

## はじめに

AndroidでCanvasにPathを描画するときに、描画に対して効果を与えることのできる[PathEffect](https://developer.android.com/reference/android/graphics/PathEffect)の効果についてまとめてみました。
Pathをもとに作成した線に対して一定のルールで効果を与えたいときに便利なものになっています。
特に**破線**や**点線**なんかを便利に引くことが可能になります。

各PathEffectのサブクラスの効果・挙動は、以下のようなサンプルコードで見ていきます。

```java

private Paint mPaint;
private Path mPath;

private void init() {
    // 初期化処理
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    mPaint.setStyle(Paint.Style.STROKE);
    mPaint.setStrokeWidth(20);

    mPath = new Path();
}
@Override
protected void onDraw(Canvas canvas) {
    mPath.rewind();
    mPath.moveTo(100, 300);
    mPath.lineTo(200, 300);
    mPath.lineTo(300, 200);
    mPath.lineTo(400, 200);

    mPaint.setStyle(Paint.Style.STROKE);
    canvas.drawPath(mPath, mPaint);

    mPath.rewind();
    mPath.moveTo(500, 100);
    mPath.lineTo(550, 100);
    mPath.lineTo(700, 200);
    mPath.lineTo(550, 300);
    mPath.lineTo(500, 300);
    mPath.close();

    mPaint.setStyle(Paint.Style.FILL);
    canvas.drawPath(mPath, mPaint);
}
```

左側の線が`Paint.Style.STROKE`での描画、右の図形が`Paint.Style.FILL`となっています。

![device-2018-12-14-150426.png](https://qiita-image-store.s3.amazonaws.com/0/75828/c7b21a5a-9650-6100-bc83-87dd9fd43972.png)

## DashPathEffect

[DashPathEffect](https://developer.android.com/reference/android/graphics/DashPathEffect)は描写のON/OFFの間隔を指定することで、Pathを**破線**で描写することができます。
例では20px描画したあと、40pxの空白を入れたものになっています。
`Paint.Style.FILL`で描画されたものには効果がありません。

```java
mPaint.setPathEffect(new DashPathEffect(new float[]{20, 40}, 0));
```

![device-2018-12-14-150717.png](https://qiita-image-store.s3.amazonaws.com/0/75828/3159836d-efd9-e3f6-78b6-91b471560c5d.png)

## PathDashPathEffect

[PathDashPathEffect](https://developer.android.com/reference/android/graphics/PathDashPathEffect)は、**任意のPath**を等間隔に描写することが可能なPathEffectです。
たとえば、Pathに円形を指定することで点線を表現することが可能です。
指定したPathは、x:0, y:0の位置を基準に効果が適用されるため、作る図形は中心をx:0, y:0になるように作成すると意図通りの動きになると思います。
setStrokeWidthで指定した太さより大きいPathを指定した場合は途切れてしまうので注意してください。

PathDashPathEffect.Styleごとの変化を見ていきます。

### PathDashPathEffect.Style.TRANSLATE

`PathDashPathEffect.Style.TRANSLATE`では、**線の位置**に指定したPathの位置を**移動**したようになります。PathDashPathEffectを使う上で大抵これを選んでおけば問題ないと思います。

例では、20px x 20pxの円を30px間隔で配置しています。（つまり 20-30 = 10pxの空白ができます）

```java
Path shape = new Path(); // 丸型
shape.addOval(-10, -10, 10, 10, Path.Direction.CW);
mPaint.setPathEffect(new PathDashPathEffect(shape, 30, 0, PathDashPathEffect.Style.TRANSLATE));
```

![device-2018-12-14-163022.png](https://qiita-image-store.s3.amazonaws.com/0/75828/e45295e4-061c-754f-8238-da3a6c326d0e.png)

### PathDashPathEffect.Style.ROTATE

`PathDashPathEffect.Style.ROTATE`では、線の方向にPathが回転しているような挙動になります。

```java
Path shape = new Path(); // ◆型
shape.moveTo(-10, 0);
shape.lineTo(0, -10);
shape.lineTo(10, 0);
shape.lineTo(0, 10);
shape.close();
mPaint.setPathEffect(new PathDashPathEffect(shape, 30, 0, PathDashPathEffect.Style.ROTATE));
```

![device-2018-12-14-163231.png](https://qiita-image-store.s3.amazonaws.com/0/75828/cf4e3195-135e-4865-b1f2-f1cfe66132cb.png)

### PathDashPathEffect.Style.MORPH

`PathDashPathEffect.Style.MORPH`では、線の方向にPathが変形しているようです。

```java
Path shape = new Path(); // ◆型
shape.moveTo(-10, 0);
shape.lineTo(0, -10);
shape.lineTo(10, 0);
shape.lineTo(0, 10);
shape.close();
mPaint.setPathEffect(new PathDashPathEffect(shape, 30, 0, PathDashPathEffect.Style.MORPH));
```

![device-2018-12-14-163437.png](https://qiita-image-store.s3.amazonaws.com/0/75828/737663cb-9c01-859b-00e5-1d4eebc2e939.png)

## CornerPathEffect

[CornerPathEffect](https://developer.android.com/reference/android/graphics/CornerPathEffect)は**線の角を丸くする**効果が加わります。
一つ特徴的なのが、`Paint.Style.STROKE`での描画で線の始点と終点の角は丸くならないところです。
例ではradius: 10として作成しています。

```java
mPaint.setPathEffect(new CornerPathEffect(10));
```

![device-2018-12-14-151731.png](https://qiita-image-store.s3.amazonaws.com/0/75828/b1685e8f-227f-2bce-2dfe-4597801c2fc5.png)

## DiscretePathEffect

[DiscretePathEffect](https://developer.android.com/reference/android/graphics/DiscretePathEffect)は線を**ランダムに崩す**効果が加わります。
例の`10`の部分には崩しの長さ、`5`の部分には崩すレベルを指定します。

```java
mPaint.setPathEffect(new DiscretePathEffect(10, 5));
```

![device-2018-12-14-151920.png](https://qiita-image-store.s3.amazonaws.com/0/75828/e09dce08-2af6-d049-f033-82eea7864e2a.png)


## SumPathEffect

[SumPathEffect](https://developer.android.com/reference/android/graphics/SumPathEffect)は2つのPathEffectを**同時に適用**することが可能になります。
例えば、2つのPathDashPathEffectの`advance`と`phase`を適切に設定することで、円形と菱形が交互に入る点線を表現することが可能です。

`−−・−−・−−`のような線を書いて欲しいなどのデザイン要件が来た際に役立つと思います。

```java
Path circleShape = new Path(); // 丸型
circleShape.addOval(-10, -10, 10, 10, Path.Direction.CW);
PathDashPathEffect first = new PathDashPathEffect(circleShape, 80, 0, PathDashPathEffect.Style.TRANSLATE);

Path rectShape = new Path(); // ◆型
rectShape.moveTo(-10, 0);
rectShape.lineTo(0, -10);
rectShape.lineTo(10, 0);
rectShape.lineTo(0, 10);
rectShape.close();
PathDashPathEffect second = new PathDashPathEffect(rectShape, 80, 40, PathDashPathEffect.Style.ROTATE);

mPaint.setPathEffect(new SumPathEffect(first, second));
```

![device-2018-12-14-161947.png](https://qiita-image-store.s3.amazonaws.com/0/75828/3a4fc51c-0785-c6d4-6c22-c9b00b495d59.png)

## ComposePathEffect

[ComposePathEffect](https://developer.android.com/reference/android/graphics/ComposePathEffect)は2つのPathEffectを**順番に適用**することが可能になります。

`outer`, `inner`の2つのPathEffectを用意します。最初に`inner`の効果が適用されてから、その適用された図形に対して`outer`の効果が適用されるような挙動になります。

例では、まず最初に`inner`として指定した`PathDashPathEffect`の効果が適用されてから、その効果が適用された図形に対して`outer`として指定した`CornerPathEffect`の効果が加わり、菱形の角も含めて丸くなっていることが分かります。

```java
Path shape = new Path(); // ◆型
shape.moveTo(-10, 0);
shape.lineTo(0, -10);
shape.lineTo(10, 0);
shape.lineTo(0, 10);
shape.close();
PathDashPathEffect inner = new PathDashPathEffect(shape, 30, 0, PathDashPathEffect.Style.TRANSLATE);

CornerPathEffect outer = new CornerPathEffect(5);

mPaint.setPathEffect(new ComposePathEffect(outer, inner));
```

![device-2018-12-14-152133.png](https://qiita-image-store.s3.amazonaws.com/0/75828/2e48b873-93c0-81ee-f6e3-fd781034133b.png)