Android
 Computer >> コンピューター >  >> システム >> Android

Kotlinで実装するAndroidアニメーションのアクセシビリティ対応ガイド

初めてのAndroidコントリビューション向けにサンプルコードを調査していたところ、Kotlinで書かれたアニメーションの事例はほとんど存在せず、ネイティブなアニメーションにおけるアクセシビリティへの配慮を示すコード例も同様に少ないことが分かりました。

そこで本記事では、Kotlinでネイティブな「展開(expand)」アニメーションを作成する方法と、TalkBackや拡大文字を利用しているユーザーを支援するための工夫について解説します。すべてのコードはサンプルリポジトリで公開しており、アニメーションするビューを含む単一のアクティビティで構成されています。元となるコードはCalum Turner氏との共同執筆です。

Kotlinで実装するAndroidアニメーションのアクセシビリティ対応ガイド
完成したアプリの動作を示すGIF

Androidアクセシビリティ(a11y)とは

すべてのAndroidデバイスには「TalkBack」というスクリーンリーダーが標準で搭載されています。デバイスの設定から有効化でき、初回利用時のガイドも組み込まれています。ジェスチャー操作でページ内を移動でき、フォーカスされた要素の説明が音声で読み上げられます。この機能がないと、多くの視覚障害のあるユーザーにとってアプリは使い物にならなくなってしまいます。

特に重要なのは、適切な要素がフォーカス可能であること、各要素に説明が付与されていること、そしてビューの変更がユーザーに通知されることの3点です。

同じ設定メニューから、デフォルトの基本フォントサイズを1.0を基準とした倍率で調整することも可能です。ビューはこのフォントサイズの変更に適切に反応し、すべての要素が表示され続け、正常に機能する必要があります。

レイアウトのアクセシビリティ設定

ここではレイアウトのスタイリングの詳細には触れません。それらはこの例特有のものだからです。しかし、アクセシビリティに関わる部分は強調する価値があります。

使用するのは android:contentDescriptionandroid:importantForAccessibility の2つのプロパティです。

contentDescriptionは、要素がフォーカスを得たときに読み上げられるテキストです。フォーカスを受け取るImageViewにとってこれは必須であり、設定しないとスクリーンリーダーは役に立たない「ラベルなし(unlabelled)」を読み上げてしまいます。

要素がボタンであれば、デフォルトで「<説明> ボタン、ダブルタップして有効化」と読み上げられますが、今回のようなImageViewアイコンにはこのデフォルトがないため、アクションを手動で指定します。

android:contentDescription="タップして人物の詳細情報を切り替え"

さらに、importantForAccessibility:no を使って「+」TextViewのフォーカスを無効にしています。2つのバッジの下にあるテキストがすでに説明を提供しているため、「+」まで読み上げるとむしろ混乱を招くからです。

いずれの場合も、TalkBackをオンにした実機での手動テストこそが、視覚情報なしで文脈が意味をなすかどうかを判断する最良の指標になります。

展開アニメーションの実装

今回のアニメーションは「info」アイコンのタップで起動し、詳細セクションの展開・収納を切り替えます。

実装はすべて単一のアクティビティ内で行い、アニメーションコードに集中できるようにしました。実際のアプリでは、対象のビューはフラグメントやRecyclerView内に配置されることが多いため、より抽象化されたコード構造を採用することになるでしょう。

リスナーの設定

サンプルアクティビティの onCreate 内で、まずアイコンにクリックリスナーを設定し、切り替え対象のビューを渡します。

infoIcon.setOnClickListener { toggleCardBody(root.personEntryBody) }

あわせて、ビューが展開状態かどうかを追跡する変数をクラス内に定義し、初期状態は「閉じている」ものとします。

private var isToggled = false

展開アニメーションの切り替え処理

レイアウトでは、personEntryBody の高さを 0dp に設定しています。

これを展開状態に切り替えるには、設定すべき新しい高さ、アニメーションの所要時間、そしてアニメーションの各フレームにおける高さを把握する必要があります。

そのうえで、isToggled を反転させ、再度タップされたときに逆の動作を行うようにします。

private fun toggleCardBody(body: View) {
    body.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
    val maxHeight = body.measuredHeight + body.paddingTop + body.paddingBottom
    val startHeight = if (isToggled) maxHeight else 0
    val targetHeight = if (isToggled) 0 else maxHeight

    val expandAnimator = ValueAnimator
        .ofInt(startHeight, targetHeight)
        .setDuration(200)
    
    expandAnimator.addUpdateListener {
        val value = it.animatedValue as Int
        body.layoutParams.height = value
        body.requestLayout()
    }

    expandAnimator.doOnEnd {
        isToggled = !isToggled
    }

    expandAnimator.start()
}

ビューが最初に描画されるときの高さは0なので、レイアウトを再測定することで新しいサイズを算出する必要があります。

Androidのビューレイアウト公式ドキュメントに記載されているとおり、measure() をビューに割り当てたレイアウトパラメータと組み合わせて使えば、infoアイコンがタップされるたびに再測定が可能です。

最大高さを求める際は、上下のパディングを手動で加算する必要があります。測定結果の高さ(measuredHeight)にはパディングが含まれないためです。

isToggled の値に応じて、開始位置が0なのか展開後の最大高さなのかが判明するので、対になる目標高さも自動的に決まります。

アニメーションにはValueAnimatorを使用し、開始値から目標値へ遷移させます。所要時間はミリ秒単位で指定し、今回は200msとしました。この数値は後に行った手動テストでのUX感覚に基づくものです。

ValueAnimator
        .ofInt(startHeight, targetHeight)
        .setDuration(200)

更新リスナー(update listener)によって時間経過と高さを紐付け、各更新後に新しいレイアウトの描画を要求しながら高さを段階的に調整していきます。

    expandAnimator.addUpdateListener {
        val value = it.animatedValue as Int
        body.layoutParams.height = value
        body.requestLayout()
    }

    expandAnimator.doOnEnd {
        isToggled = !isToggled
    }

    expandAnimator.start()

Kotlinを利用しているため、build.gradleandroidx ライブラリを追加すれば、doOnEnd 拡張関数の恩恵を受けられます。これにより isToggled 変数の反転が非常に簡単になります。

最後にアニメーションを開始します。これで、アイコンに触れると伸縮するボディが完成しました!

より滑らかなアニメーションへ

このままでも技術的には動作しますが、インターポレーター(interpolator)を追加すると、動きがより自然で洗練された印象になります。

expandAnimator.interpolator = FastOutSlowInInterpolator()

アクセシビリティのさらなる向上

a11yユーザーへの配慮として、最後に2つの改善を加えます。

1つ目は、AccessibilityEvent を使ったナビゲーション支援です。

expandAnimator.doOnEnd {
    if (!isToggled)       body.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
    isToggled = !isToggled
}

これにより、アニメーションが閉じた状態から開いた状態へ移行した直後に、フォーカスが自動的にボディ内の最初の項目(この場合は説明テキスト)へジャンプします。レイアウトではinfoアイコンのアクション説明を設定済みですが、視覚的なインジケーターに頼らず次の項目へ移動できるよう、アプリ側で先回りして対応できるのです。

2つ目は、異なるフォントサイズへの対応です。measure() が返す測定高さは、端末のアクセシビリティ設定で指定されたフォントスケーリングを考慮していません。そのため、大きな倍率設定では説明テキストの下部が収まりきらず、切り取られてしまう問題が発生します。

そこで、フォントスケールをプログラムから取得し、それに基づいて高さをスケーリングします。フォントスケールは浮動小数点(float)になる可能性があり、レイアウトの高さとして直接は使えないため、整数に変換しています。

val a11yFontScale = body.context.resources.configuration.fontScale
val maxHeight = ((body.measuredHeight + body.paddingTop + body.paddingBottom) * a11yFontScale).toInt()

完成!

Kotlinで実装するAndroidアニメーションのアクセシビリティ対応ガイド
完成したアプリの動作を示すGIF

以上で最終的なアニメーションが完成しました!わずか数行のコード追加だけで、a11y対応範囲を大幅に広げ、KotlinとAndroidのバッジを表示する滑らかな展開セクションを実現できました。

お読みいただきありがとうございました。

最近執筆したその他の記事:

  • CodeceptJS E2Eテストのカスタマイズ
  • JestとEnzymeによるReactテスト入門 Part 2

参考リソース

  • Joe Birch氏による優れた記事「Exploring KTX for Android(androidx解説)」
  • Android Accessibility Tutorial: Getting Started(Ray Wenderlich)
  1. 【Android】YouTubeの曲を着信音にする方法|MP3変換から設定まで完全解説

    Androidスマホの初期着信音に飽きていませんか?多くのユーザーが、お気に入りの楽曲を着信音に設定して、自分だけのオリジナルな着信音を楽しみたいと考えています。YouTubeで聴いた曲をスマホの着信音にしたいと思ったことはありませんか? YouTubeは世界最大級のエンターテインメントプラットフォームであり、着信音に使える楽曲が数百万曲も揃っています。しかし、YouTubeでは動画から音声を直接ダウンロードすることができません。「YouTubeの曲をどうやって着信音にすればいいの?」と悩む方も多いでしょう。ご安心ください。YouTubeから楽曲をダウンロードして着信音として設定できる方法があ

  2. AndroidスマホでLaravel 8をセットアップする方法【PC不要】

    こんにちは!この記事では、スマートフォンだけを使ってLaravel 8をインストールする方法をご紹介します。このガイドを最大限に活用するには、PHPの基礎知識があると理想的ですが、なくても心配いりません。基本の部分も丁寧に解説するので、どなたでもスムーズに始められます。Laravelとは?Laravelは、表現力豊かでエレガントな構文が特徴のWebアプリケーションフレームワークです。PHPをベースに構築されているため、Laravelを使えばPHPでの開発が格段に効率化されます。認証機能をはじめ、さまざまな機能に対応したパッケージが豊富に用意されている点も魅力です。たとえば認証機能なら、自分で一