Lottie はデザイナーさんがオーサリングツールで作成したアニメーションを埋め込むことができるので非常に便利ですよね。
Android Developer 公式の Compose のアニメーションの選択のフローチャートでも、複数の要素で構成された複雑なイラストのアニメーションについては Lottie のようなフレームワークを使うことが提案されていて、実際のところこの手のライブラリとしては第一の選択肢として挙がるのではないでしょうか:
ところで、ツールで作られたアニメーションについて、一部を動的に変更したくなることはないでしょうか。
たとえば、Theme にあわせて色を変更したいというのは真っ先におもいつくことかと思います。Light / Dark の切り替えもそうですし、デザインシステムに調和するように色をあわせたいことはよくあるユースケースかと思います。
今回はサンプルのアニメーションを使って、アニメーション内の色をあわせる方法を紹介します。
サンプル
まず、サンプルのアニメーションを用意します。LottieFiles の Lottie Creator で適当なアニメーションを作成してみました:
これを素直に再生するとこうなります:
@Composable
fun MyAnimation(modifier: Modifier = Modifier) {
val composition by rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.my_animation),
)
LottieAnimation(
composition = composition,
modifier = modifier,
iterations = LottieConstants.IterateForever,
)
}
色をつける
このアニメーションでたとえばチェックマークの丸の背景色を動的に変更したいとします。
その場合は Dynamic Property を使ってこう指定します:
composition = composition,
modifier = modifier,
iterations = LottieConstants.IterateForever,
+ dynamicProperties = rememberLottieDynamicProperties(
+ rememberLottieDynamicProperty(
+ property = LottieProperty.COLOR,
+ value = Color.Red.toArgb(),
+ keyPath = arrayOf("Icon Asset", "Background", "Background", "Fill"),
+ ),
+ ),
)
この変更により、丸の色が赤に変化しました:
Dynamic Property については公式のドキュメントで解説されています:
この dynamicProperties
で目的のオブジェクトまでを KeyPath で指定し、その値を変更することで値を動的に変更できます。今回でいうと Icon Asset
> Background
> Background
> Fill
という階層の KeyPath を指定しています。 (KeyPath の決め方は後述します)
同様に、円周を動くプログレスの色を変更するとします。これは種別としてはフィルではなくストロークなので、プロパティの種別を LottieProperty.COLOR
ではなく LottieProperty.STROKE_COLOR
を指定する必要があるでしょう:
rememberLottieDynamicProperty(
property = LottieProperty.STROKE_COLOR,
value = Color.Red.toArgb(),
keyPath = arrayOf("Progress Track", "Stroke"),
),
最後にまだ青いままのリップルを同様に指定して完成です:
dynamicProperties = rememberLottieDynamicProperties(
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = Color.Red.toArgb(),
keyPath = arrayOf("Icon Asset", "Background", "Background", "Fill"),
),
rememberLottieDynamicProperty(
property = LottieProperty.STROKE_COLOR,
value = Color.Red.toArgb(),
keyPath = arrayOf("Progress Track", "Stroke"),
),
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = Color.Red.toArgb(),
keyPath = arrayOf("Ripple", "Fill"),
),
),
Theme との調和
アプリの Theme の色を指定することで、Light/Dark にも柔軟に対応できますし、調和をとることもできそうです:
Light | Dark |
---|---|
![]() |
![]() |
dynamicProperties = rememberLottieDynamicProperties(
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = MaterialTheme.colorScheme.primary.toArgb(),
keyPath = arrayOf("Icon Asset", "Background", "Background", "Fill"),
),
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = MaterialTheme.colorScheme.onPrimary.toArgb(),
keyPath = arrayOf("Icon Asset", "Icon", "Icon", "Fill"),
),
rememberLottieDynamicProperty(
property = LottieProperty.STROKE_COLOR,
value = MaterialTheme.colorScheme.primary.toArgb(),
keyPath = arrayOf("Progress Track", "Stroke"),
),
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = MaterialTheme.colorScheme.primary.toArgb(),
keyPath = arrayOf("Ripple", "Fill"),
),
),
そういえば、LottieFiles にはテーマがあって、これを使うと JSON 内で slot
に定義したトークンが使えそうで、これを変更できるようになるとよりやりやすそうなのですが……調べた範囲では方法が見つかりませんでした。
対象の KeyPath を知る方法
さて、色を変更する方法がわかったところで、肝心の KeyPath をどうやって見つけるかというところが明らかじゃないと思います。
これが結構大変でオーサリングツールのレイヤーの構造と一致しているかといわれるとそうでない場合もあるので、なかなか理解に苦しむかと思います。
実際、今回のアニメーションは LottieFiles で作成して構造は以下ですが、なかなかここからパッと想像はしがたいのかなと思います:
今回のケースを例にして説明します。チェックマークの背景の丸のフィルの部分までの Key Path をみつけましょう。
JSON を開いて nm
をキーを辿っていくと確実なのですが…とはいえ JSON の中身を追っていくのも大変です…
今回のアニメーションの JSON
{
"nm": "Main Scene",
"ddd": 0,
"h": 500,
"w": 500,
"meta": { "g": "@lottiefiles/creator 1.45.0" },
"layers": [
{
"ty": 0,
"nm": "Icon Asset",
"sr": 1,
"st": 48,
"op": 90,
"ip": 48,
"hd": false,
"ddd": 0,
"bm": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [250, 250] },
"s": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [74.422, 74.422],
"t": 48
},
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [200.309, 200.309],
"t": 56
},
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [143.0236, 143.0236],
"t": 63
},
{ "s": [148.7659, 148.7659], "t": 68 }
]
},
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [250, 250] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [0],
"t": 48
},
{ "s": [100], "t": 56 }
]
}
},
"w": 500,
"h": 500,
"refId": "precomp_newScene_d3dd3211-476a-4493-aaa1-08613e963887_fd99ed59-2b14-42b6-894e-407e605fe6cc",
"ind": 1
},
{
"ty": 4,
"nm": "Ripple",
"sr": 1,
"st": 48,
"op": 90,
"ip": 48,
"hd": false,
"ddd": 0,
"bm": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [0, 0] },
"s": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [102.1023, 102.1023],
"t": 63
},
{ "s": [165.2173, 165.2173], "t": 90 }
]
},
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [250, 250] },
"r": { "a": 0, "k": -360 },
"sa": { "a": 0, "k": 0 },
"o": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [0],
"t": 56
},
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [80],
"t": 63
},
{ "s": [1], "t": 90 }
]
}
},
"shapes": [
{
"ty": "el",
"bm": 0,
"hd": false,
"nm": "Ellipse Path 1",
"d": 1,
"p": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [296, 296] }
},
{
"ty": "fl",
"bm": 0,
"hd": false,
"nm": "Fill",
"c": { "a": 0, "k": [0.2275, 0.5255, 1] },
"r": 2,
"o": { "a": 0, "k": 100 }
}
],
"ind": 2
},
{
"ty": 4,
"nm": "Progress Track",
"sr": 1,
"st": 0,
"op": 150,
"ip": 0,
"hd": false,
"ddd": 0,
"bm": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [0, 0] },
"s": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [100, 100],
"t": 48
},
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [104.911, 104.911],
"t": 50
},
{ "s": [90.3795, 90.3795], "t": 61 }
]
},
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [250, 250] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"shapes": [
{
"ty": "sh",
"bm": 0,
"hd": false,
"nm": "Ellipse Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"i": [
[-86.92424999999999, 0],
[0, -86.92424999999999],
[86.92424999999999, 0],
[0, 86.92424999999999]
],
"o": [
[86.92424999999999, 0],
[0, 86.92424999999999],
[-86.92424999999999, 0],
[0, -86.92424999999999]
],
"v": [
[0, -157.5],
[157.5, 0],
[0, 157.5],
[-157.5, 0]
]
}
}
},
{
"ty": "st",
"bm": 0,
"hd": false,
"nm": "Stroke",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 10 },
"c": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [0, 0.7765, 1],
"t": 48
},
{ "s": [0.2275, 0.5255, 1], "t": 61 }
]
}
},
{
"ty": "tm",
"bm": 0,
"hd": false,
"nm": "Trim Path",
"e": {
"a": 1,
"k": [
{
"o": { "x": 0.65, "y": 0 },
"i": { "x": 0.36, "y": 1 },
"s": [0],
"t": 0
},
{ "s": [100], "t": 48 }
]
},
"o": { "a": 0, "k": 0 },
"s": { "a": 0, "k": 0 },
"m": 1
}
],
"ind": 3
}
],
"v": "5.7.0",
"fr": 30,
"op": 90,
"ip": 0,
"assets": [
{
"nm": "Icon Asset",
"id": "precomp_newScene_d3dd3211-476a-4493-aaa1-08613e963887_fd99ed59-2b14-42b6-894e-407e605fe6cc",
"layers": [
{
"ty": 4,
"nm": "Icon",
"sr": 1,
"st": 0,
"op": 90,
"ip": 0,
"hd": false,
"ddd": 0,
"bm": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [83.00074768066405, 86.04505157470705] },
"s": { "a": 0, "k": [124.2456, 124.2456] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [250.00092896032712, 253.7833425993042] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"shapes": [
{
"ty": "gr",
"bm": 0,
"hd": false,
"nm": "Icon",
"it": [
{
"ty": "sh",
"bm": 0,
"hd": false,
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[31.2285, 89.0905],
[67.7751, 125.635],
[134.773, 58.6362],
[122.592, 46.4551],
[67.7751, 101.273],
[43.4096, 76.9094],
[31.2285, 89.0905]
]
}
}
},
{
"ty": "fl",
"bm": 0,
"hd": false,
"nm": "Fill",
"c": { "a": 0, "k": [1, 1, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [82.99999999999999, 86.21942990335272] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [82.99999999999999, 86.21942990335272] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
}
],
"ind": 1
},
{
"ty": 4,
"nm": "Background",
"sr": 1,
"st": 0,
"op": 90,
"ip": 0,
"hd": false,
"ddd": 0,
"bm": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [82.99999999999999, 82.99999999999999] },
"s": { "a": 0, "k": [124.2456, 124.2456] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [249.99999999999997, 249.99999999999997] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"shapes": [
{
"ty": "gr",
"bm": 0,
"hd": false,
"nm": "Background",
"it": [
{
"ty": "sh",
"bm": 0,
"hd": false,
"nm": "Path 2",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"i": [
[0, 0],
[-45.8376, 0],
[0, -45.8392],
[45.84299999999999, 0],
[0, 45.839]
],
"o": [
[0, -45.8392],
[45.84100000000001, 0],
[0, 45.839],
[-45.8376, 0],
[0, 0]
],
"v": [
[0, 83],
[83, 0],
[166, 83],
[83, 166],
[0, 83]
]
}
}
},
{
"ty": "fl",
"bm": 0,
"hd": false,
"nm": "Fill",
"c": { "a": 0, "k": [0.2275, 0.5255, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [82.99999999999999, 82.99999999999999] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [82.99999999999999, 82.99999999999999] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
}
],
"ind": 2
}
]
}
]
}
jq
を使うと多少リストアップができますが、階層構造を示しながらダンプするのは困難です:
jq '.. | .nm? | strings' my_animation.json
したがって、KeyPath はワイルドカードを指定できるので、まずあたりをつけて大まかに指定が適用されるか確認して近づけていくのがよいでしょう:
rememberLottieDynamicProperty(
property = LottieProperty.COLOR,
value = Color.Red.toArgb(),
keyPath = arrayOf("Icon Asset", "**", "Fill"),
)
楽な KeyPath を見つける手段
とはいえ、いい手段がないかというといくつか方法が思い付きます。
まず現代では AI (GitHub Copilot) にやってもらうのはお手軽手段としてオススメです:
正しい結果を出すのか微妙なところがあり、やや信頼性にかける印象はありますが…… (実際この例でも Icon Asset > Background > Background が解釈できていないわけで)
とはいえこれはシステムに組み込むようなものではなくて、エンジニアの手作業をサポートするのに使っていて、誤りがあっても人間が容易に気づいて修正できて取り返しがつくので気軽に使ってもいいと思えます。
楽な KeyPath を見つける手段 2
あとはスクリプトで用意しておくことでしょう。今回、仕様に合っている実装か不明ですが、雰囲気で手書きしてスクリプトを書いてみました:
Web 版も用意したので JSON ファイルをぶちこめばダンプできます:
See the Pen Lottie KeyPath Tree Generator by マンガーノ・伊藤 (@mangano_ito) on CodePen.
こっちを書くほうがだんぜん時間がかかりました。一度用意すれば再利用できるのでめっちゃ使うならアリかもしれませんが。
以上です!