3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-04

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

今回作ったもの

TargetView.gif

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

TargetView

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

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

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

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

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

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

鍵かっこを表示するView

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

Viewを継承する

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から呼び出すことを想定しているため、最低限ContextAttributeSetのコンストラクタを追加する必要があります。

鍵かっこ描画

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
        }
    }

    // 省略...
}

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

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

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

最終的には以下のように描画されます。
鍵かっこ.png

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

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

コード全文

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を継承する

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)

    // 省略...
}

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

グレー矩形描画

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

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

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

コード全文

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の中心を出し、そこから計算を始点と終点を計算し縦線横線を描画しています。

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

TargetView作り

レイアウトファイルに定義

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

<?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を継承したクラスを作成

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

使用感

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

Screenshot_20200305-062735.png

最後に

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

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

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

参考にさせていただいた記事

Custom Viewsで複数のviewを1つにまとめて部品化する

AndroidでもiPhoneに負けないようなアニメーションを実装してみよう

公式リファレンス

カスタムViewの作り方
View
ObjectAnimator
PropertyValuesHolder
Canvas
Canvas#drawLineのリファレンス
Paint
Rect
Canvas#drawRect)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2