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

  • Question

  • User382997 posted

    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 { ///

    /// This renderer is invoked for all pages in the iOS project. It applies changes to the page as required by the designers. /// public class IosPageRenderer : PageRenderer { /// /// Delegate method which gets invoked on view appearing. /// /// 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 { ///

    /// 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.. /// public class AndroidPageRenderer : PageRenderer { /// /// Creates new instance of the renderer. /// /// The activity context. 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.

    Wednesday, September 16, 2020 12:06 AM

Answers

  • User369979 posted

    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");
    

    } ```

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Wednesday, September 16, 2020 7:11 AM
  • User382997 posted

    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 { ///

    /// iOS accessibility manager class. Handles the accessibility related /// operations in the iOS native. /// public class IosAccessibilityManager : IAccessibilityManager { /// /// Announces the accessibility text passed based on the VoiceOver is enabled. /// /// The text to speak/announce 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 { ///

    /// Android accessibility manager class. Handles the accessibility related /// operations in the Android native. /// public class AndroidAccessibilityManager : IAccessibilityManager { /// /// Announces the accessibility text passed based on the TalkBack or screen reader enabled. /// /// The text to speak/announce 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 { ///

    /// Base Content page for the project. /// All the content pages would be inherited from this base. /// 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

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Tuesday, September 22, 2020 5:43 AM

All replies

  • User369979 posted

    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");
    

    } ```

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Wednesday, September 16, 2020 7:11 AM
  • User382997 posted

    @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

    Wednesday, September 16, 2020 7:26 AM
  • User369979 posted

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

    Wednesday, September 16, 2020 7:46 AM
  • User382997 posted

    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 { ///

    /// iOS accessibility manager class. Handles the accessibility related /// operations in the iOS native. /// public class IosAccessibilityManager : IAccessibilityManager { /// /// Announces the accessibility text passed based on the VoiceOver is enabled. /// /// The text to speak/announce 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 { ///

    /// Android accessibility manager class. Handles the accessibility related /// operations in the Android native. /// public class AndroidAccessibilityManager : IAccessibilityManager { /// /// Announces the accessibility text passed based on the TalkBack or screen reader enabled. /// /// The text to speak/announce 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 { ///

    /// Base Content page for the project. /// All the content pages would be inherited from this base. /// 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

    • Marked as answer by Anonymous Thursday, June 3, 2021 12:00 AM
    Tuesday, September 22, 2020 5:43 AM
  • User171681 posted

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

    Tuesday, September 22, 2020 7:09 AM
  • User89714 posted

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

    Friday, October 16, 2020 4:11 PM
  • User89714 posted

    @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 { ///

    /// iOS accessibility manager class. Handles the accessibility related /// operations in the iOS native. /// public class IosAccessibilityManager : IAccessibilityManager { /// /// Announces the accessibility text passed based on the VoiceOver is enabled. /// /// The text to speak/announce 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?

    Thursday, October 29, 2020 5:44 PM