I am currently developing a Xamarin Forms application with strict requirements on the Accessibility. We have implemented the basic Accessibility using the AutomationProperties's Name and HelpText attributes in most of our ContentPages (Page) and PopupPages. However, the current implementation does not meet the standards we look for.
For example, we want the screen name to be read (when VoiceOver or TalkBack is ON) when the user navigates to each page or say when a new page appears. There is no easy way to do it (as far as I know) from the Forms app directly, without using Custom Renderers. I was able to achieve it in iOS by defining the AutomationProperties.Name on the ContentPage level and using it in the custom PageRenderer's OnAppearing event. But, the same does not work in Android for some reason.
Following is the Custom Renderer I implemented in iOS,
[assembly: ExportRenderer(typeof(ContentPage), typeof(IosPageRenderer))] namespace Project.iOS.CustomRenderers { /// <summary> /// This renderer is invoked for all pages in the iOS project. It applies changes to the page as required by the designers. /// </summary> public class IosPageRenderer : PageRenderer { /// <summary> /// Delegate method which gets invoked on view appearing. /// </summary> /// <param name="animated"></param> public override void ViewDidAppear(bool animated) { /// To read out the current screen on loading it. var accessibilityScreenName = AutomationProperties.GetName(this.Element); if (!string.IsNullOrEmpty(accessibilityScreenName)) UIAccessibility.PostNotification(UIAccessibilityPostNotification.ScreenChanged, new NSString($"{accessibilityScreenName} screen")); base.ViewDidAppear(animated); } } }
This makes the application to announce the screen name every time a ContentPage appears.
How can I achieve the same in Android and UWP? Our main focus is iOS and Android, so if anything on Android would be so much appreciated.
I tried to implement a similar Page renderer for Android as well, but it does not work right now, Maybe my implementation is wrong, anyone have experience with this, please help.
[assembly: ExportRenderer(typeof(ContentPage), typeof(AndroidPageRenderer))] namespace Project.Droid.CustomRenderers { /// <summary> /// This renderer is invoked for all pages in the Android project. /// It applies changes to the page to make the accessibility working for screen change.. /// </summary> public class AndroidPageRenderer : PageRenderer { /// <summary> /// Creates new instance of the renderer. /// </summary> /// <param name="context">The activity context.</param> public AndroidPageRenderer(Android.Content.Context context) : base(context) { } protected override void OnAttachedToWindow() { base.OnAttachedToWindow(); var accessibilityScreenName = AutomationProperties.GetName(this.Element); if (!string.IsNullOrEmpty(accessibilityScreenName) && this.ViewGroup != null) this.ViewGroup.AnnounceForAccessibility($"{accessibilityScreenName} screen"); //SendAccessibilityEvent(Android.Views.Accessibility.EventTypes.WindowContentChanged); } } }
Anything on this would be highly appreciated.
I tried your renderer on my side. It worked.
However, it will only be triggered for the new page. When we come back to the previous page, OnAttachedToWindow
won't be called.
We could achieve this using a dependency service.
Firstly, create an interface in Forms:
public interface IAccessibilityManager { void sendAccessibility(string speakText); }
Implement it in Android:
[assembly: Dependency(typeof(AndroidAccessibility))] namespace Sample.Droid { public class AndroidAccessibility : IAccessibilityManager { public void sendAccessibility(string speakText) { AccessibilityManager manager = (AccessibilityManager)Android.App.Application.Context.GetSystemService(Context.AccessibilityService); if (manager.IsEnabled) { AccessibilityEvent e = AccessibilityEvent.Obtain(); e.EventType = EventTypes.Announcement; e.Text.Add(new Java.Lang.String(speakText)); manager.SendAccessibilityEvent(e); } } } }
Fire this dependency service in any lifecycle event as you want:
protected override void OnAppearing() { base.OnAppearing(); DependencyService.Get<IAccessibilityManager>().sendAccessibility("MainPage screen"); }
Dear @LandLu,
I have implemented the AccessibilityManager in the way you suggested and it works fine across both platforms. So I can confirm (my implementation on the AndroidPageRenderer, as well as your suggestion, works fine).
For anyone looking for similar stuff, we don't need the Custom page renderers once we implement the AccessibilityManager in the native levels.
Following is my final implementation,
Interface
namespace Sample.Services.App.Accessibility { /// <summary> /// Interface that acts as the Accessibility service handler/manager. /// </summary> public interface IAccessibilityManager { /// <summary> /// Announces/speaks the text passed in the method when the Screen reader /// or VoiceOver features are enabled. /// </summary> /// <param name="speakText">The text to speak/announce</param> void AnnounceAccessibility(string speakText); } }
iOS
namespace Sample.iOS.Platform.Accessibility { /// <summary> /// iOS accessibility manager class. Handles the accessibility related /// operations in the iOS native. /// </summary> public class IosAccessibilityManager : IAccessibilityManager { /// <summary> /// Announces the accessibility text passed based on the VoiceOver is enabled. /// </summary> /// <param name="speakText">The text to speak/announce</param> public void AnnounceAccessibility(string speakText) { if (!UIAccessibility.IsVoiceOverRunning) return; // Post notification to announce the accessibility text. UIAccessibility.PostNotification(UIAccessibilityPostNotification.Announcement, new NSString(speakText)); } } }
Android
namespace Sample.Droid.Platform.Accessibility { /// <summary> /// Android accessibility manager class. Handles the accessibility related /// operations in the Android native. /// </summary> public class AndroidAccessibilityManager : IAccessibilityManager { /// <summary> /// Announces the accessibility text passed based on the TalkBack or screen reader enabled. /// </summary> /// <param name="speakText">The text to speak/announce</param> public void AnnounceAccessibility(string speakText) { AccessibilityManager manager = (AccessibilityManager)Android.App.Application.Context.GetSystemService(Android.App.Application.AccessibilityService); if (!(manager.IsEnabled || manager.IsTouchExplorationEnabled)) return; // Sends the accessibility event to announce. AccessibilityEvent e = AccessibilityEvent.Obtain(); e.EventType = EventTypes.Announcement; e.Text.Add(new Java.Lang.String(speakText)); manager.SendAccessibilityEvent(e); } } }
Base ContentPage
namespace Sample.Views.Shared.Base { /// <summary> /// Base Content page for the project. /// All the content pages would be inherited from this base. /// </summary> public abstract class ContentPageBase : ContentPage { /* Private Fields */ private IAccessibilityManager _accessibilityManager; /// <summary> /// Gets the Accessibility manager instance. /// </summary> public IAccessibilityManager AccessibilityManager { get => _accessibilityManager; } /// <summary> /// Creates new instance of class. /// </summary> protected ContentPageBase() { _accessibilityManager = ServiceLocator.Resolve<IAccessibilityManager>(); } /// <summary> /// Creates a new instance of the class. /// </summary> /// <param name="accessibilityManager">The Accessibility manager</param> protected ContentPageBase(IAccessibilityManager accessibilityManager = null) { _accessibilityManager = accessibilityManager ?? ServiceLocator.Resolve<IAccessibilityManager>(); } /// <summary> /// Callback on the page appearing. /// </summary> protected override void OnAppearing() { base.OnAppearing(); AnnounceScreenForAccessibilityIfRequired(); } /// <summary> /// Announces the accessibility screen name for the page if enabled/required. /// </summary> public void AnnounceScreenForAccessibilityIfRequired() { if (AccessibilityManager == null) _accessibilityManager = ServiceLocator.Resolve<IAccessibilityManager>(); if (AutomationProperties.GetIsInAccessibleTree(this) != false) { var accessibilityScreenName = AutomationProperties.GetName(this); if (!string.IsNullOrEmpty(accessibilityScreenName)) AccessibilityManager.AnnounceAccessibility(string.Format(Accessibility.Generic_Screen_Title, accessibilityScreenName)); } } } }
Thank you once again @LandLu
Sincerely,
Sagar S. Kadookkunnan
Answers
I tried your renderer on my side. It worked.
However, it will only be triggered for the new page. When we come back to the previous page,
OnAttachedToWindow
won't be called.We could achieve this using a dependency service.
Firstly, create an interface in Forms:
Implement it in Android:
Fire this dependency service in any lifecycle event as you want:
@LandLu Thank you for the response and answer.
Did the android app clearly announce the page with "{given automationproperties.name} screen"? It used to announce the page (but not with the 'screen' in Android) if there is a Title property defined for the ContentPage.
And Yes, thank you for that solution on the page appearing on back navigation. I suppose I would have to create a base page and implement it there instead of adding this to all individual pages in my application.
I will try and keep you posted on this thread.
Sincerely,
Sagar S. Kadookkunnan
I could hear it when using renderer but not very clearly.
waiting for your update.
Dear @LandLu,
I have implemented the AccessibilityManager in the way you suggested and it works fine across both platforms. So I can confirm (my implementation on the AndroidPageRenderer, as well as your suggestion, works fine).
For anyone looking for similar stuff, we don't need the Custom page renderers once we implement the AccessibilityManager in the native levels.
Following is my final implementation,
Interface
iOS
Android
Base ContentPage
Thank you once again @LandLu
Sincerely,
Sagar S. Kadookkunnan
With this feature the accessibility of my app will be improved. Thank you both!
@skadookkunnan @LandLu
Have you got a UWP equivalent that you can share as well please? I'm hoping that there's an accessibility API for this, using SpeechSynthesizer directly is the fallback position if there isn't I guess.
I find that the announcement of the page name using this code is sometimes cut short on iOS, when VoiceOver decides it's time to announce the Back button (or whichever other control it decides to start with). Did you find a way to ensure the announcement of the page name is completed before VoiceOver starts on the next thing to read?