このエントリは Xamarin Advent Calendar 2016 の18日目の記事です。
はじめに
某社が障碍者向け支援技術製品を利用してるユーザに対してWindows 10 の無償アップグレード期間を延長したり、2016年内における全ての Office 365 製品の Accessibility Standards 準拠を発表していたり、昨今 Accessibility は局地的に割とホットな話題です(?)。
ソフトウェアを作成していてまず気になる Accessibility 関連の機能としては、キーボードで操作への対応 や 音声読み上げ機能 対応だと筆者は思います。個人的な感想です。
キーボード操作への対応については、Xamarin.Forms のメインターゲットである携帯端末では少し優先度が低いかもしれませんが、音声読み上げ機能は割と重要です。
目の不自由な人でもスマートフォンを操作しますし、そういったタッチ操作主体の端末ではキーボードのようにショートカットキーを使うことができませんから。
ということで、本日は Xamarin.Forms で音声読み上げ機能への対応をカスタマイズする方法を、主に UWP 向けにご紹介します。
Xamarin.Forms での音声読み上げ機能対応
音声読み上げ機能(スクリーンリーダー)はそれぞれのプラットフォーム毎に、iOS なら VoiceOver、Android なら TalkBack、Windows なら Narrator と標準機能として搭載されており、それをカスタマイズするにはそれぞれのプラットフォームごとに独自のコードを記述する必要があります。
実際の所それぞれのプラットフォームに大差はなく、本来なら Accessibility に関する機能は Xamarin.Forms のフレームワークレベルで提供されるべきはないかとも思いますが、本記事執筆時点においては残念ながらサポートされていません。
なので、スクリーンリーダーへの対応などをカスタマイズするためには自前でいろいろごにょごにょする必要があります。
(2016/12/18追記: Xamarin で作成したアプリケーションがそのままでは Accessible でないというわけではありません。それをカスタムするためのプロパティ等が提供されていないだけです。)
何はともあれ Xamarin のドキュメントを検索してみましょう。
…
ありました。
Accessibility in Xamarin.Forms
ドンピシャです。
このドキュメントにのってるコードをコピペするだけで Android でも iOS でもしっかり動くので、もうこれでいいじゃん(いいじゃん)って感じです。記事のネタ潰れてしまった。やばい。
でもよく読んでみると、なぜか UWP の実装が省略されているではないですか。
「Xamarin.Forms 使って C# + XAML でプログラム書きたいような人ならそれくらい当然知ってるよね!」ってことなのか「UWP そんな需要ないし紹介面倒だからいいかー」ってことなのか、前者であることを祈りつつ、それでは UWP での実装を試してみましょう。
サンプルプログラム
サンプルプログラムを Github で公開しています。
Visual Studio 2015 と Xamarin 4.2.1.73 で作ってあります。
サンプルプログラムは以下のように2つのボタンが配置されていて、上の Change Color ボタンをクリックすると、アプリケーションの背景色が白→赤→青→緑→黒→(以下ループ)と切り替わります。
Change Color ボタンですが、わかりやすくするためにスクリーンリーダーに読ませる文言は「Change background color」とさせます。
(本来、スクリーンリーダーは画面に表示されている文字をそのまま読むべきで、実際の実装においては今回の記事で利用する AutomationProeprty.Name プロパティではなく AutomationProperty.HelpText プロパティを利用するほうが好ましいでしょう。)
もう一つのボタン「Button hidden from Narrator」はなにも機能のない飾りのボタンなので、スクリーンリーダーではアクセスさせないようにします。
この画面は以下のような XAML で定義されており、Change Color ボタンには AccessibilityLabel プロパティが、 Button hidden from Narrator ボタンには IsAccessibleTree プロパティが、それぞれ添付されています。
(Github上のソースコードはこちら)
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:effects="clr-namespace:XamarinFormsNarratorSample.Views.Effects;assembly=XamarinFormsNarratorSample" x:Class="XamarinFormsNarratorSample.Views.MainPage"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Button Grid.Row="0" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="White" BorderColor="Gray" Text="Change Color" Clicked="Button_Clicked" effects:AccessibilityEffect.AccessibilityLabel="Change background color" /> <Button Grid.Row="1" HorizontalOptions="Center" VerticalOptions="Center" BackgroundColor="White" BorderColor="Gray" Text="Button hidden from Narrator" Clicked="Button_Clicked" effects:AccessibilityEffect.InAccessibleTree="false" /> </Grid> </ContentPage>
実装の概要
UWP での実装を解説する前に、サンプルプログラムというか、上記 Xamarin のドキュメントを軽く解説します。
この手法では、任意のコントロールにプラットフォーム固有のビジュアルや動作などを添付できる、Effects という仕組みを利用しています。
Effects の詳しい解説はリンク先のドキュメントに任せるとして、今回のプロジェクトではこの Effect を使った AccessibilityEffect クラスを定義しています。
このクラスは AccessibilityLabel : string と InAccessibleTree : bool という二つの添付プロパティを定義しており、前者はスクリーンリーダーに読ませたい文字列を指定でき、後者は”そのコントロールがスクリーンリーダーに読み上げられるべきか、スクリーンリーダーから操作可能であるべきか”を指定できます。
また、このクラス内に入れ子で RoutingEffect クラスを継承した AddAccessibilityEffect クラスが定義されていて、上記どちらかのプロパティがあるコントロールに添付されると、そのコントロールにこの AddAccessibilityEffect が追加されるようになっています。
public class AddAccessibilityEffect : RoutingEffect { // string identifier matches [assembly] attributes in the platform-specific projects public AddAccessibilityEffect() : base("MyCompany.AddAccessibilityEffect") { } }
これは上記の Xamarin によるドキュメントからコピーしただけのコードですが、Android でのこの Effect の実装は以下のようになります。
public class AddAccessibilityEffect : PlatformEffect { protected override void OnAttached() { try { Control.ContentDescription = AccessibilityEffect.GetAccessibilityLabel(Element); Control.Focusable = AccessibilityEffect.GetInAccessibleTree(Element); } catch (Exception ex) { Console.WriteLine("Cannot set property on attached control. Error: ", ex.Message); } } protected override void OnDetached() { } protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs args) { if (args.PropertyName == AccessibilityEffect.AccessibilityLabelProperty.PropertyName) { Control.ContentDescription = AccessibilityEffect.GetAccessibilityLabel(Element); } else if (args.PropertyName == AccessibilityEffect.InAccessibleTreeProperty.PropertyName) { Control.Focusable = AccessibilityEffect.GetInAccessibleTree(Element); } } }
要点は OnAttached() メソッドの中6-7行目と、18行目から26行目までの OnElementPropertyChanged() メソッドです。
前者では、Effect がコントロールにアタッチされた時、その時点で設定されている AccessibilityLabel もしくは InAccessibleTree の添付プロパティの値を、Android のネイティブコントロールに適用します。
後者は、同プロパティの値が変更されたとき、同じように値を更新する処理です。
とても簡単ですね。
Windows(UWP) での実装
Windows 10 Universal App でも同じように UWP 用の Effect を用意して、アタッチ時と値の変更時にその値を適用させます。
Windows のスクリーンリーダーは、アプリケーション上の各UIコントロールが持っている AutomationPeer の情報に基づいて、その読み上げや操作を行います。
Windows の Accessibility について詳しくは MSDN のドキュメント Accessibility などを読むと色々と分かりますが、今回の要点は Expose basic accessibility information に書いてあります。
筆者の独断と偏見により要約すると、
- AccessibilityLabel 添付プロパティの値を AutomationProperties.Name プロパティに指定
- InAccessibleTree 添付プロパティの値をもとに AutomationProperties.AccessibilityView プロパティに値を指定
すれば良いであろうことがわかります。
上記の点をコードにすると、以下のようになります。(サンプルコードはこちら)
public class UwpAccessibilityEffect : PlatformEffect { protected override void OnAttached() { UpdateName(); UpdateAccessibilityView(); } protected override void OnDetached() { } protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) { if (args.PropertyName == AccessibilityEffect.AccessibilityLabelProperty.PropertyName) { UpdateName(); } else if (args.PropertyName == AccessibilityEffect.InAccessibleTreeProperty.PropertyName) { UpdateAccessibilityView(); } } void UpdateName() { AutomationProperties.SetName(Control, AccessibilityEffect.GetAccessibilityLabel(Element) as string ?? string.Empty); } void UpdateAccessibilityView() { AutomationProperties.SetAccessibilityView(Control, AccessibilityEffect.GetInAccessibleTree(Element) ? AccessibilityView.Control : AccessibilityView.Raw); } }
要点をハイライトしていますが、Android の場合と同じく Effect のアタッチ時と添付プロパティの値変更時、AutomationProperties.Name および AutomationProeprties.AccessibilityView の値を設定しています。
InAccessibleTree プロパティが true の時には AccessibilityView.Control を、false の時には AccessibilityView.Raw を AutomationProperties.AccessibilityView に指定することにより、後者の場合には同コントロールがナレータから無視されるようになります。
動作確認
実際に動作するかを確認してみましょう。
Accessibility 関連のデバッグには Inspect というツールが便利です。
VS2015 の開発者コマンドプロンプトで inspect と入力してエンターを押せば、簡単に起動できます。
Inspect を用いて Change Color ボタンの AutomationProperties.Name プロパティを確認してみると、以下のように「Change background color」となっています。
実際に Narrator を起動して、同ボタンを選択してもそのように読み上げます。イェイ
また、Inspect を、キーボードフォーカスを追尾するように変更し Button hidden from Narrator ボタンにキーボードフォーカスを当ててみます。するとキーボードフォーカスは同ボタンにあたるのですが、Inspect のフォーカスは Window 全体に当たっており、ボタンにはあたりません。
同じように、実際に Narrator をオンにして同ボタンをフォーカスしてみると、Narrator はボタンの代わりに Window 全体を選択することが確認できます。
タッチスクリーンでは左右スワイプによりキーボードフォーカスとは独立して Narrator のフォーカスを移動できますが、その際にも同ボタンがスキップされます。
おわりに
Xamarin.Forms では Effects を利用することで各プラットフォームごとに Accessibility 機能をカスタマイズすることができます。
今回の例はとても簡単なものであまり意義は大きくないかもしれませんが、UWP の ListViewItem のように、アイテムのToString()が規定で Narrator 読まれてしまうとんでもない仕様のコントロールがちらほらあります。そういった場面でこのように値を上書きすることにで、より User-friendly なアプリケーションの開発が可能となります。
さらに深く動作をカスタマイズするため AutomationPeer を置き換える必要があるような場合には、また少し複雑になる(CustomeRenderer が必要になる)かもしれませんが、そういった場合にも合わせて便利に利用できる方法かなと思います。
興味深い記事を有難うございます。
Xamarin.Formsレベルでサポートされていないのはとても残念に思います。
社内ではSatyaになってからAccessibilityへのコミットメントがとても強いので、近く改善されるはず…と思いますが、こちら (<a href="https://xamarin.uservoice.com/forums/144858-xamarin-platform-suggestions" rel="nofollow ugc">https://xamarin.uservoice.com/forums/144858-xamarin-platform-suggestions</a> ) などからフィードバックを送って頂けるとプライオリティが上がると思います。Xamarinで作れば自然とAccessibleになるようなプラットフォームになると嬉しいですね。
コメントをありがとうございます。
> Xamarin.Formsレベルでサポートされていないのはとても残念に思います。
一部のコントロールを除けば、ほぼなにもせずとも割と Accessible なアプリケーションにはなりますが、カスタマイズ機能が標準で提供されるといいなと思います。すこし記事に追記させていただきました。
UserVoiceの存在もすっかり忘れていましたが、確かに直接フィードバックを送ることもできるのですね!早速送ってみます!