Forum Xamarin.Forms
We are excited to announce that the Xamarin Forums are moving to the new Microsoft Q&A experience. Q&A is the home for technical questions and answers at across all products at Microsoft now including Xamarin!

We encourage you to head over to Microsoft Q&A for .NET for posting new questions and get involved today.

Xamarin Forms - Accessibility - Screen reader to read each page name/header on page load/appearing

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.

Best Answers

  • skadookkunnanskadookkunnan Member ✭✭
    Accepted Answer

    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

  • skadookkunnanskadookkunnan Member ✭✭

    @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

  • LandLuLandLu Member, Xamarin Team Xamurai

    (but not with the 'screen' in Android)

    I could hear it when using renderer but not very clearly.

    I will try and keep you posted on this thread.

    waiting for your update.

  • skadookkunnanskadookkunnan Member ✭✭
    Accepted Answer

    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

  • JonAlzaJonAlza ESMember ✭✭✭

    With this feature the accessibility of my app will be improved. Thank you both!

  • JohnHardmanJohnHardman GBUniversity admin

    @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.

  • JohnHardmanJohnHardman GBUniversity admin

    @skadookkunnan said:
    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));
            }
        }
    }
    

    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?

Sign In or Register to comment.