カスタムView(カスタムViewだとなんのことか分からなくなりそうなので、TargetViewと呼びます。) の作り方とアニメーションのやり方を学ぶための備忘録になります。
とりあえず見た目重視の勉強用で作成したため、画面回転時などは何も考えていおりません。
QRコードを読み込む際に使えそうな感じですね。(といいつつも用途不明です)
#TargetView
今回作成したカスタムViewはxmlに定義したFrameLayoutの中に、
- 鍵かっこを表示するView
- グレーの矩形を表示するView
- 真ん中の十字を表示するView
を重ねて表示しています。
Viewそれぞれにはxml内でマージンを設定し、特に鍵かっこViewとグレーの矩形がアニメーションした際に重ならないように調整しています。
3つのView
はそれぞれのクラス内のonDraw
でCanvas
のメソッドを使用して横線、縦線、矩形を描画しています。
鍵かっことグレーの矩形のアニメーションは縮小・拡大を交互に行うようにしています。
##鍵かっこを表示するView
鍵かっこViewの作成手順
-
View
を継承したクラスを作成 -
Canvas
クラスとPaint
クラスを使用して縦線と横線を描画 - 拡大→縮小のアニメーションを作成
###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から呼び出すことを想定しているため、最低限Context
とAttributeSet
のコンストラクタを追加する必要があります。
###鍵かっこ描画
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という変数に格納しています。)
###拡大→縮小アニメーション作成
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軸方向に大きくなるというアニメーションです。
###鍵かっこ完成
以上で鍵かっこは完成です。
コード全文
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
グレー矩形作成手順
- Viewを継承したクラスを作成
- CanvasクラスとPaintクラスを使用して矩形を描画
- 縮小→拡大のアニメーションを作成
###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
に関しては鍵かっこで説明したので省かせていただきます。
###縮小→拡大アニメーション作成
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
の第二引数と第三引数の大小関係が違っています。
###グレー矩形完成
わかりづらいですが、縮小→拡大アニメーションがついています。
コード全文
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
真ん中の十字作成手順
- Viewを継承したクラスを作成
- 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完成
コード上では十字の色は白色だったのですが、完成画像を見せる際に見えないため黒くしています。
##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などで使用します。
#使用感
GIFが用意できずわかりづらいのですが、こんな感じとういうのが伝われば幸いです。
#最後に
過去に何回かカスタムViewを作成する機会はあったのですが、アニメーションこみでは作ったことがなかったので勉強になりました。
今回作成したものはまだまだ改良の余地がありますので、さらに勉強していこうと思います。
朝まで記事を書いてしまったため、誤字脱字や見づらい箇所があると思います。。。
見ていただきありがとうございます。
##参考にさせていただいた記事
Custom Viewsで複数のviewを1つにまとめて部品化する
AndroidでもiPhoneに負けないようなアニメーションを実装してみよう
##公式リファレンス
カスタムViewの作り方
View
ObjectAnimator
PropertyValuesHolder
Canvas
Canvas#drawLineのリファレンス
Paint
Rect
Canvas#drawRect)