ターゲットを狙うようなカスタムViewを作成してみた。

カスタムView(カスタムViewだとなんのことか分からなくなりそうなので、TargetViewと呼びます。) の作り方とアニメーションのやり方を学ぶための備忘録になります。
とりあえず見た目重視の勉強用で作成したため、画面回転時などは何も考えていおりません。

#今回作ったもの
![TargetView.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/dfbdd93d-5e7d-dbe9-e220-1a1b379d2f5c.gif)

QRコードを読み込む際に使えそうな感じですね。（といいつつも用途不明です） 

#TargetView
今回作成したカスタムViewはxmlに定義したFrameLayoutの中に、

- 鍵かっこを表示するView
- グレーの矩形を表示するView
- 真ん中の十字を表示するView

を重ねて表示しています。

Viewそれぞれにはxml内でマージンを設定し、特に鍵かっこViewとグレーの矩形がアニメーションした際に重ならないように調整しています。

3つの`View`はそれぞれのクラス内の`onDraw`で`Canvas`のメソッドを使用して横線、縦線、矩形を描画しています。

鍵かっことグレーの矩形のアニメーションは縮小・拡大を交互に行うようにしています。

##鍵かっこを表示するView

鍵かっこViewの作成手順
1. `View`を継承したクラスを作成
2. `Canvas`クラスと`Paint`クラスを使用して縦線と横線を描画
3. 拡大→縮小のアニメーションを作成

###Viewを継承する

```kotlin
class TargetFrameView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    // xmlに定義するため、このコンストラクタは必須となります。
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)
   
    // 省略...
}
```

`View`を継承する際はxmlから呼び出す場合とコードから呼び出す場合とで必要となるコンストラクタが違うため、用途に合わせて継承する必要があります。
今回はxmlから呼び出すことを想定しているため、最低限`Context`と`AttributeSet`のコンストラクタを追加する必要があります。

###鍵かっこ描画

```kotlin
class TargetFrameView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 省略...

    private val c: Context by lazy {
        context
    }

    // 線の長さ
    private val margin: Float = 50f

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

        drawLeftTopLine(canvas, setupLinePaint())
        drawLeftBottomLine(canvas, setupLinePaint())
        drawRightTopLine(canvas, setupLinePaint())
        drawRightBottomLine(canvas, setupLinePaint())

        // 省略...
    }

    private fun drawLeftTopLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(0.0f, 0.0f, margin, 0.0f, paint)
        // 縦線
        canvas?.drawLine(0.0f, 0.0f, 0.0f, margin, paint)
    }

    private fun drawLeftBottomLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(0.0f, height.toFloat(), margin, height.toFloat(), paint)
        // 縦線
        canvas?.drawLine(0.0f, height.toFloat() - margin, 0.0f, height.toFloat(), paint)
    }

    private fun drawRightTopLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(width.toFloat() - margin, 0.0f, width.toFloat(), 0.0f, paint)
        // 縦線
        canvas?.drawLine(width.toFloat(), 0.0f, width.toFloat(), margin, paint)
    }

    private fun drawRightBottomLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(width.toFloat() - margin, height.toFloat(), width.toFloat(), height.toFloat(), paint)
        // 縦線
        canvas?.drawLine(width.toFloat(), height.toFloat() - margin, width.toFloat(), height.toFloat(), paint)
    }

    private fun setupPaint(): Paint {
        return Paint().apply {
            isAntiAlias = true // アンチエイリアスを有効にするかの設定。基本的にはtrueしておくと良いと思います。
            color = ContextCompat.getColor(c, android.R.color.holo_green_dark)
            strokeWidth = 10f
        }
    }

    // 省略...
}
```

鍵かっこ描画での登場人物は、`Canvas`と`Paint`です。
`Canvas`とは簡単に言うとView上に図形や画像を描画する際に使われるAPIです。4つの基本的なコンポーネントを使用して、渡された情報を元に描画を行います。
`Paint`とは`Canvas`に線を作成したりする際などに、色や太さなどの情報を渡すために使用します。

実際の線の描画については、`Canvas#drawLine`を使用しています。
`Canvas#drawLine`の使い方は始点の座標と終点の座標、さらに線を描画する際の設定を渡すだけです。

描画される線の長さは、50.0固定で設定しています。（marginという変数に格納しています。) 

最終的には以下のように描画されます。
![鍵かっこ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/8458d6ec-22cf-ab4e-a442-32ba79c46f50.png)

###拡大→縮小アニメーション作成

```kotlin
class TargetFrameView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 省略...

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

        // 省略...

        expandedAnimation()
    }

    private fun expandedAnimation() {
        val expandedScaleX = PropertyValuesHolder.ofFloat("scaleX", 1.05f, 0.98f)
        val expandedScaleY = PropertyValuesHolder.ofFloat("scaleY", 1.05f, 0.98f)

        ObjectAnimator.ofPropertyValuesHolder(this, expandedScaleX, expandedScaleY).apply {
            duration = 1100
            repeatCount = ValueAnimator.INFINITE // アニメーションはループさせる
            repeatMode = ValueAnimator.REVERSE   // 拡大後は縮小させる
        }.start()
    }
}
```

拡大→縮小アニメーションは、`ObjectAnimator`を使用して行っています。
`ObjectAnimator`は渡したObjectに対してアニメーションさせるためのAPIです。
`PropertyValuesHolder`というアニメーション情報を保持するAPIを使用して、複数のアニメーション情報を作成しています。
アニメーション自体はX軸方向、Y軸方向に大きくなるというアニメーションです。

###鍵かっこ完成
以上で鍵かっこは完成です。

![鍵かっこ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/5956aac2-79ca-ecfb-24d2-0e399a7785d7.gif)


コード全文

```kotlin
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat

class TargetFrameView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)

    private val c: Context by lazy {
        context
    }

    private val margin: Float = 50f

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

        drawLeftTopLine(canvas, setupLinePaint())
        drawLeftBottomLine(canvas, setupLinePaint())
        drawRightTopLine(canvas, setupLinePaint())
        drawRightBottomLine(canvas, setupLinePaint())

        expandedAnimation()
    }

    private fun drawLeftTopLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(0.0f, 0.0f, margin, 0.0f, paint)
        // 縦線
        canvas?.drawLine(0.0f, 0.0f, 0.0f, margin, paint)
    }

    private fun drawLeftBottomLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(0.0f, height.toFloat(), margin, height.toFloat(), paint)
        // 縦線
        canvas?.drawLine(0.0f, height.toFloat() - margin, 0.0f, height.toFloat(), paint)
    }

    private fun drawRightTopLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(width.toFloat() - margin, 0.0f, width.toFloat(), 0.0f, paint)
        // 縦線
        canvas?.drawLine(width.toFloat(), 0.0f, width.toFloat(), margin, paint)
    }

    private fun drawRightBottomLine(canvas: Canvas?, paint: Paint) {
        // 横線
        canvas?.drawLine(width.toFloat() - margin, height.toFloat(), width.toFloat(), height.toFloat(), paint)
        // 縦線
        canvas?.drawLine(width.toFloat(), height.toFloat() - margin, width.toFloat(), height.toFloat(), paint)
    }

    private fun setupPaint(): Paint {
        return Paint().apply {
            isAntiAlias = true // アンチエイリアスを有効にするかの設定。基本的にはtrueしておくと良いと思います。
            color = ContextCompat.getColor(c, android.R.color.holo_green_dark)
            strokeWidth = 10f
        }
    }

    private fun expandedAnimation() {
        val expandedScaleX = PropertyValuesHolder.ofFloat("scaleX", 1.05f, 0.98f)
        val expandedScaleY = PropertyValuesHolder.ofFloat("scaleY", 1.05f, 0.98f)

        ObjectAnimator.ofPropertyValuesHolder(this, expandedScaleX, expandedScaleY).apply {
            duration = 1100
            repeatCount = ValueAnimator.INFINITE // アニメーションはループさせる
            repeatMode = ValueAnimator.REVERSE   // 拡大後は縮小させる
        }.start()
    }
}
```


##グレーの矩形を表示するView

グレー矩形作成手順
1. Viewを継承したクラスを作成
2. CanvasクラスとPaintクラスを使用して矩形を描画
3. 縮小→拡大のアニメーションを作成

###Viewを継承する

```kotlin
class TargetRectangleView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    // xmlに定義するため、このコンストラクタは必須となります。
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)
   
    // 省略...
}
```

鍵かっこと同じなので解説は飛ばします。

###グレー矩形描画

```kotlin
class TargetRectangleView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 省略...

    private val c: Context by lazy {
        context
    }

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

        drawCenterRectangle(canvas, setupRectPaint())

        // 省略...
    }

    private fun drawCenterRectangle(canvas: Canvas?, paint: Paint) {
        val rect = Rect().apply {
            set(0, 0, width, height)
        }
        canvas?.drawRect(rect, paint)
    }

    private fun setupRectPaint(): Paint {
        return Paint().apply {
            isAntiAlias = true
            color = ContextCompat.getColor(c, R.color.rectangle_color)
        }
    }

    // 省略...
}
```

今回は矩形を表示するため、`Canvas#drawRect`を使用しています。
`Rect`を使用して矩形の大きさを`Canvas`に渡しています。
`Paint`に関しては鍵かっこで説明したので省かせていただきます。

![矩形.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/d4f3677b-eea1-f9cf-00a9-c107e8ea9ceb.png)

###縮小→拡大アニメーション作成

```kotlin
class TargetRectangleView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    // 省略...

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

        // 省略...

        shrinkAnimation()
    }

    // 省略...

    private fun shrinkAnimation() {
        val shrunkScaleX = PropertyValuesHolder.ofFloat("scaleX", 0.94f, 1.03f)
        val shrunkScaleY = PropertyValuesHolder.ofFloat("scaleY", 0.94f, 1.03f)

        ObjectAnimator.ofPropertyValuesHolder(this, shrunkScaleX, shrunkScaleY).apply {
            duration = 1100 // 拡大→縮小アニメーションと同じ秒数
            repeatCount = ValueAnimator.INFINITE //拡大→縮小アニメーションと同じ
            repeatMode = ValueAnimator.REVERSE //拡大→縮小アニメーションと同じ
        }.start()
    }
}
```

こちらも鍵かっことほとんど同じです。
矩形に適応するアニメーションは縮小→拡大なので、`PropertyValuesHolder#ofFloat`の第二引数と第三引数の大小関係が違っています。

###グレー矩形完成
わかりづらいですが、縮小→拡大アニメーションがついています。
![矩形完成.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/9bc760f8-46c6-50cd-e050-57e429b0211e.gif)

コード全文

```kotlin
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat
import com.example.targetviewsample.R

class TargetRectangleView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)

    private val c: Context by lazy {
        context
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        drawCenterRectangle(canvas, setupRectPaint())

        shrinkAnimation()
    }

    private fun drawCenterRectangle(canvas: Canvas?, paint: Paint) {
        val rect = Rect().apply {
            set(0, 0, width, height)
        }
        canvas?.drawRect(rect, paint)
    }

    private fun setupRectPaint(): Paint {
        return Paint().apply {
            isAntiAlias = true
            color = ContextCompat.getColor(c, R.color.rectangle_color)
        }
    }

    private fun shrinkAnimation() {
        val shrunkScaleX = PropertyValuesHolder.ofFloat("scaleX", 0.94f, 1.03f)
        val shrunkScaleY = PropertyValuesHolder.ofFloat("scaleY", 0.94f, 1.03f)
        ObjectAnimator.ofPropertyValuesHolder(this, shrunkScaleX, shrunkScaleY).apply {
            duration = 1100
            repeatCount = ValueAnimator.INFINITE
            repeatMode = ValueAnimator.REVERSE
        }.start()
    }
}
```


##真ん中の十字を表示するView
真ん中の十字作成手順
1. Viewを継承したクラスを作成
2. CanvasクラスとPaintクラスを使用して線を描画

十字に関しては鍵かっことかなり似ているため、全文を載せます。

基本的には、幅と高さを半分にして十字Viewの中心を出し、そこから計算を始点と終点を計算し縦線横線を描画しています。

``` kotlin
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import androidx.core.content.ContextCompat

class TargetCrossView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : View(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)

    private val c: Context by lazy {
        context
    }

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

        drawCrossLine(canvas, setupCrossLinePaint())
    }

    private fun drawCrossLine(canvas: Canvas?, paint: Paint) {
        val halfWidth = (width / 2).toFloat()
        val halfHeight= (height / 2).toFloat()
        val margin = 25f

        // 横線
        canvas?.drawLine(halfWidth - margin, halfHeight, halfWidth + margin, halfHeight, paint)
        // 縦線
        canvas?.drawLine(halfWidth, halfHeight - margin, halfWidth, halfHeight + margin, paint)
    }

    private fun setupCrossLinePaint(): Paint {
        return Paint().apply {
            isAntiAlias = true
            color = ContextCompat.getColor(c, android.R.color.white)
            strokeWidth = 5f
        }
    }
}
```

###十字View完成
コード上では十字の色は白色だったのですが、完成画像を見せる際に見えないため黒くしています。

![十字.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/9ed840b1-1e25-11a5-b256-bd4866919fa9.png)

##TargetView作り
###レイアウトファイルに定義
最後に今まで作成してきた3つのViewをFrameLayoutを親として定義したxmlレイアウトに配置し、FrameLayoutを継承したカスタムViewGourpのクラスで全てのViewを組み合わせます。 

```xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">

    <view class="com.example.targetviewsample.targetview.TargetFrameView"
        android:id="@+id/indicatorView"
        android:layout_margin="16dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <view
        android:id="@+id/rectangleView"
        class="com.example.targetviewsample.targetview.TargetRectangleView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="24dp" />

    <view class="com.example.targetviewsample.targetview.TargetCrossView"
        android:id="@+id/crossView"
        android:layout_margin="24dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"/>
</FrameLayout>
```

Viewを配置していく順番ですが、 最前面から十字→矩形→鍵かっこという順番になるように配置していきます。

###FrameLayoutを継承したクラスを作成

```kotlin
import android.content.Context
import android.graphics.Point
import android.util.AttributeSet
import android.util.Size
import android.view.ViewGroup
import android.widget.FrameLayout
import com.example.targetviewsample.R

class TargetView(
    context: Context,
    attrs: AttributeSet?,
    defStyleAttr: Int,
    defStyleRes: Int
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {

    constructor(context: Context): this(context, null, 0, 0)
    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): this(context, attrs, defStyleAttr, 0)

    private val c: Context by lazy {
        context
    }

    init {
        inflate(c, R.layout.item_target_view, this)
    }
}
```

作成したカスタムViewGroup内で先ほど3つのViewを定義したレイアウトファイルをinflateし、Activityなどで使用します。

##TargetViewの完成
最初のGIFを再掲
![TargetView.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/dfbdd93d-5e7d-dbe9-e220-1a1b379d2f5c.gif)


#使用感
GIFが用意できずわかりづらいのですが、こんな感じとういうのが伝われば幸いです。

![Screenshot_20200305-062735.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236467/fff18651-08d6-3147-aa3e-f83c1a51b481.png)

#最後に
過去に何回かカスタムViewを作成する機会はあったのですが、アニメーションこみでは作ったことがなかったので勉強になりました。
今回作成したものはまだまだ改良の余地がありますので、さらに勉強していこうと思います。

朝まで記事を書いてしまったため、誤字脱字や見づらい箇所があると思います。。。

見ていただきありがとうございます。

##参考にさせていただいた記事
[Custom Viewsで複数のviewを１つにまとめて部品化する
](https://qiita.com/yst_i/items/5cc766dc95e2cfc97a40)
[AndroidでもiPhoneに負けないようなアニメーションを実装してみよう
](https://techblog.yahoo.co.jp/programming/androidiphone/)

##公式リファレンス
[カスタムViewの作り方](https://developer.android.com/training/custom-views/create-view?hl=ja)
[View](https://developer.android.com/reference/android/view/View#public-constructors_1)
[ObjectAnimator](https://developer.android.com/reference/android/animation/ObjectAnimator)
[PropertyValuesHolder](https://developer.android.com/reference/android/animation/PropertyValuesHolder)
[Canvas](https://developer.android.com/reference/android/graphics/Canvas)
[Canvas#drawLineのリファレンス](https://developer.android.com/reference/android/graphics/Canvas#drawLine\(float,%20float,%20float,%20float,%20android.graphics.Paint)
[Paint](https://developer.android.com/reference/android/graphics/Paint)
[Rect](https://developer.android.com/reference/android/graphics/Rect)
[Canvas#drawRect](https://developer.android.com/reference/android/graphics/Canvas#drawRect\(float,%2520float,%2520float,%2520float,%2520android.graphics.Paint))