とりあえず試してみたアニメーションの覚書
高レベルアニメーションAPI / 低レベルアニメーションAPI があります
https://developer.android.com/jetpack/compose/animation?hl=ja
API
・高レベルアニメーションAPI
AnimationContent
AnimatedVisibility
animateContentSize
・低レベルアニメーションAPI
animate*AsState
updateTransition
Animatable
AnimeState
Animetion
複数のイメージを入れ替えるアニメーション
俗に言うぱたぱたアニメーション
pixelart town様から青色スライムと緑髪女の子の画像をお借りしました
https://pixelarttown.net/terms/
画像はassetsにimgフォルダーを作成してそこに置く
assetsからBitmap読込むのは以前紹介した以下
/**
* Assetsにあるイメージファイルを読込みBitmapを返す
*/
fun Context.assetsToBitmap(fileName: String): Bitmap? {
return try {
with(assets.open(fileName)) {
BitmapFactory.decodeStream(this)
}
} catch (e: IOException) { null }
}
キャラクター全体を管理するinterface Characterを作成してrememberSaveableに対応する
コンストラクタでloadImageすると失敗するので別に呼ぶ形にした
画像リネームなど面倒でしたくなかったので不要なファイルも読込んでいます
画像表示するImageとアニメーションを無限に繰り返すためinfiniteTransitionにkeyframesを設定
import androidx.compose.foundation.Image
interface Character {
val name: String
val count: Int
val animation: List<Int>
val images: MutableList<Bitmap>
val interval: Int
val beforeDelay: Int
val afterDelay: Int
open fun loadImage(context: Context) {
if (images.isNotEmpty()) return
repeat(count) { count ->
val bitmap = context.assetsToBitmap("img/${name}/${name}_${count + 1}.png")
bitmap?.let {
images.add(it)
}
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable open fun drawCharacter(modifier: Modifier = Modifier) {
val infiniteTransition = rememberInfiniteTransition()
val anime = infiniteTransition.animateValue(
initialValue = 0,
targetValue = animation.count() - 1,
typeConverter = Int.VectorConverter,
animationSpec = infiniteRepeatable(
animation = keyframes {
delayMillis = beforeDelay
durationMillis = (animation.count() + 1) * interval + afterDelay
repeat(animation.count()) {
it at (it + 1) * interval with LinearEasing
}
},
repeatMode = RepeatMode.Restart
)
)
AnimatedContent(
targetState = anime,
) { targetCount ->
val index: Int = targetCount.value
val bitmap = images[animation[index]]
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = modifier,
contentScale = ContentScale.Fit,
)
}
}
companion object {
inline fun <reified T: Character> Server() = listSaver<T, Any>(
save = { listOf(it.images) },
restore = {
val instance = Class.forName(T::class.qualifiedName!!).newInstance() as T
@Suppress("UNCHECKED_CAST")
instance.images.addAll(it[0] as List<Bitmap>)
return@listSaver instance
}
)
@Composable inline fun <reified T: Character> remember(context: Context) = rememberSaveable(saver = Server<T>()) {
val instance = Class.forName(T::class.qualifiedName!!).newInstance() as T
instance.loadImage(context)
return@rememberSaveable instance
}
}
}
今回は青色スライムと緑髪女の子を描画したいのでSlimeクラスとGirlクラスを実装して
画像ファイル情報とアニメーションしたい画像のindexをセット(indexはファイルの番号-1なので注意)
class Slime: Character {
override val name = "slime03_blue01"
override val count = 23
override val animation = listOf(0, 10, 11, 4, 5, 20, 19, 18)
override val images: MutableList<Bitmap> = mutableListOf()
override val interval = 100
override val beforeDelay = 1500
override val afterDelay = 0
}
class Girl: Character {
override val name = "girl07_longhair02"
override val count = 23
override val animation = listOf(0, 6, 7, 8, 9, 10, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 18, 19, 20)
override val images: MutableList<Bitmap> = mutableListOf()
override val interval = 100
override val beforeDelay = 1500
override val afterDelay = 1500
}
描画座標やサイズ変更をして描画
@Composable fun AppContent() {
val context = LocalContext.current
val slime = Character.remember<Slime>(context)
val girl = Character.remember<Girl>(context)
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
with(LocalDensity.current) {
// 320 * 320
slime.drawCharacter(modifier = Modifier.offset(10f.toDp(), 10f.toDp()))
slime.drawCharacter(modifier = Modifier.offset(340f.toDp(), 10f.toDp()))
girl.drawCharacter(modifier = Modifier.offset(170f.toDp(), 330f.toDp()).size(420f.toDp()))
}
}
}
}
}
移動と画像の反転を追加
graphicsLayerで色々できます
以下の動画が参考になりました
https://www.youtube.com/watch?v=u9CX9BqyOKU
AnimatedContentをネストしてアニメーションの追加もできます
interface Character {
var reverse: Boolean
...
@Composable open fun drawCharacter(modifier: Modifier = Modifier) {
...
AnimatedContent(
targetState = anime,
) { targetCount ->
val index: Int = targetCount.value
val bitmap = images[animation[index]]
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = modifier.graphicsLayer {
translationX = (targetCount.value * 10).toFloat()
if (reverse) rotationY = 180f
},
contentScale = ContentScale.Fit,
)
}
}
}
ボタンでアニメーション開始
ボタンを押すと星が一回転(左右反転)するアニメーション
(アニメーション開始タイミングを任意にする)
Star自身をrememberするのでクラス内の処理はrememberつけないで実装(問題ないはず)
アニメーションを元に戻すためduration 0msで実行
finishedListenerでanimatingを元に戻す
(animatingはStateなので戻したタイミングでアニメーションが再度実行されます)
アイコンの種類は以下で確認
https://developer.android.com/jetpack/compose/graphics/images/material?hl=ja
https://fonts.google.com/icons?hl=ja
標準に無いアイコンを使う場合
リリース時はリソースを減らすためsvgを使う分だけダウンロードした方が良いそうです
androidx.compose.material:material-icons-extended
class Star {
private var animating by mutableStateOf(false)
fun animation() {
animating = true
}
@Composable fun animateRotation(): Float {
return animateFloatAsState(
targetValue = if (animating) 180f else 0f,
animationSpec = tween(durationMillis = if (animating) 500 else 0),
finishedListener = { animating = false }
).value
}
@Composable fun draw() {
val rotation = animateRotation()
Icon(
imageVector = Icons.Default.StarBorder,
contentDescription = null,
modifier = Modifier.size(100.dp).graphicsLayer {
rotationY = rotation
}
)
}
}
@Composable fun AppContent() {
MaterialTheme {
val star by remember { mutableStateOf(Star()) }
Surface(modifier = Modifier.fillMaxSize()) {
Column {
Button(onClick = { star.animation() }) {
Text("Click!")
}
star.draw()
}
}
}
}
*** メモ ***
・SVGをリソース(drawable)に追加する方法
Vector Asset Studioでファイルを取り込んでxmlに変換
https://developer.android.com/studio/write/vector-asset-studio?hl=ja
painterResourceで読み込む
・ImageのContentScale
https://developer.android.com/jetpack/compose/graphics/images/customize?hl=a
Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022