diff --git a/android/src/main/java/com/henninghall/date_picker/LocaleUtils.java b/android/src/main/java/com/henninghall/date_picker/LocaleUtils.java index 66b3437..3545628 100644 --- a/android/src/main/java/com/henninghall/date_picker/LocaleUtils.java +++ b/android/src/main/java/com/henninghall/date_picker/LocaleUtils.java @@ -67,28 +67,33 @@ public class LocaleUtils { } public static String getLocaleStringResource(Locale requestedLocale, int resourceId, Context context) { - String result; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { // use latest api - Configuration config = new Configuration(context.getResources().getConfiguration()); - config.setLocale(requestedLocale); - result = context.createConfigurationContext(config).getText(resourceId).toString(); - } - else { // support older android versions - Resources resources = context.getResources(); - Configuration conf = resources.getConfiguration(); - Locale savedLocale = conf.locale; - conf.locale = requestedLocale; - resources.updateConfiguration(conf, null); - - // retrieve resources from desired locale - result = resources.getString(resourceId); - - // restore original locale - conf.locale = savedLocale; - resources.updateConfiguration(conf, null); - } + try { + String result; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { // use latest api + Configuration config = new Configuration(context.getResources().getConfiguration()); + config.setLocale(requestedLocale); + result = context.createConfigurationContext(config).getText(resourceId).toString(); + } + else { // support older android versions + Resources resources = context.getResources(); + Configuration conf = resources.getConfiguration(); + Locale savedLocale = conf.locale; + conf.locale = requestedLocale; + resources.updateConfiguration(conf, null); + + // retrieve resources from desired locale + result = resources.getString(resourceId); + + // restore original locale + conf.locale = savedLocale; + resources.updateConfiguration(conf, null); + } - return result; + return result; + } catch (Exception e) { + return ""; // Eat the error and return empty string when no resource was found + } } } diff --git a/android/src/main/java/com/henninghall/date_picker/PickerView.java b/android/src/main/java/com/henninghall/date_picker/PickerView.java index b2ff6da..b028cbf 100644 --- a/android/src/main/java/com/henninghall/date_picker/PickerView.java +++ b/android/src/main/java/com/henninghall/date_picker/PickerView.java @@ -20,6 +20,8 @@ import com.henninghall.date_picker.props.LocaleProp; import com.henninghall.date_picker.props.ModeProp; import com.henninghall.date_picker.props.TextColorProp; import com.henninghall.date_picker.ui.UIManager; +import com.henninghall.date_picker.ui.Accessibility; + import java.util.ArrayList; public class PickerView extends RelativeLayout { @@ -80,8 +82,8 @@ public class PickerView extends RelativeLayout { uiManager.updateDisplayValues(); } - if (didUpdate(ModeProp.name, LocaleProp.name)) { - uiManager.updateAccessibilityValues(); + if (didUpdate(LocaleProp.name)) { + Accessibility.setLocale(state.getLocale()); } uiManager.setWheelsToDate(); diff --git a/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java b/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java index 5b3ea57..7601e00 100644 --- a/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java +++ b/android/src/main/java/com/henninghall/date_picker/pickers/IosClone.java @@ -2,39 +2,52 @@ package com.henninghall.date_picker.pickers; import android.content.Context; import android.graphics.Color; +import android.os.Build; import android.util.AttributeSet; import android.view.View; import android.view.MotionEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import cn.carbswang.android.numberpickerview.library.NumberPickerView; import com.henninghall.date_picker.ui.Accessibility; + public class IosClone extends NumberPickerView implements Picker { private Picker.OnValueChangeListenerInScrolling mOnValueChangeListenerInScrolling; public IosClone(Context context) { super(context); - initSetOnValueChangeListenerInScrolling(); + init(); } public IosClone(Context context, AttributeSet attrs) { super(context, attrs); - initSetOnValueChangeListenerInScrolling(); + init(); } public IosClone(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + initAccessibility(); initSetOnValueChangeListenerInScrolling(); } + private void initAccessibility() { + Accessibility.startAccessibilityDelegate(this); + } + private void initSetOnValueChangeListenerInScrolling() { final Picker self = this; + super.setOnValueChangeListenerInScrolling(new NumberPickerView.OnValueChangeListenerInScrolling() { @Override - public void onValueChangeInScrolling(NumberPickerView picker, int oldVal, int newVal) { - Accessibility.announceNumberPickerValue(picker, newVal); + Accessibility.sendValueChangedEvent(self, newVal); if (mOnValueChangeListenerInScrolling != null) { mOnValueChangeListenerInScrolling.onValueChangeInScrolling(self, oldVal, newVal); @@ -79,9 +92,15 @@ public class IosClone extends NumberPickerView implements Picker { @Override public boolean onTouchEvent(MotionEvent event) { if(Accessibility.shouldAllowScroll(this)){ - super.onTouchEvent(event); - return true; + return super.onTouchEvent(event); } return false; } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + // Set the accessibility properties of this view + Accessibility.setRoleToSlider(this, info); + } } diff --git a/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java b/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java index 5977354..b438dcf 100644 --- a/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java +++ b/android/src/main/java/com/henninghall/date_picker/pickers/Picker.java @@ -12,6 +12,7 @@ public interface Picker { int getMaxValue(); boolean getWrapSelectorWheel(); void setDisplayedValues(String[] value); + String[] getDisplayedValues(); int getValue(); void setValue(int value); void setTextColor(String value); diff --git a/android/src/main/java/com/henninghall/date_picker/ui/Accessibility.java b/android/src/main/java/com/henninghall/date_picker/ui/Accessibility.java index 54f65c8..2b3c66e 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/Accessibility.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/Accessibility.java @@ -2,37 +2,43 @@ package com.henninghall.date_picker.ui; import android.content.Context; import android.os.Build; +import android.os.Bundle; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.accessibilityservice.AccessibilityServiceInfo; -import cn.carbswang.android.numberpickerview.library.NumberPickerView; import com.henninghall.date_picker.DatePickerPackage; -import com.henninghall.date_picker.State; import com.henninghall.date_picker.Utils; -import com.henninghall.date_picker.wheelFunctions.WheelFunction; -import com.henninghall.date_picker.wheels.Wheel; +import com.henninghall.date_picker.pickers.Picker; import java.util.Locale; import java.util.List; +import android.view.accessibility.AccessibilityNodeInfo; + public class Accessibility { + private final static AccessibilityManager systemManager = (AccessibilityManager) DatePickerPackage.context + .getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + private static Locale mLocale = Locale.getDefault(); - private final static AccessibilityManager systemManager = - (AccessibilityManager) DatePickerPackage.context - .getApplicationContext() - .getSystemService(Context.ACCESSIBILITY_SERVICE); + public static void setLocale(Locale nextLocale) { + mLocale = nextLocale; + } + + public static Locale getLocale() { + return mLocale; + } /** - When TalkBack is active, user can use one finger to explore the screen - and set focus to elements. Then user can proceed to use second finger - to scroll contents of focused element. - When there's multiple pickers next to each other horizontally, - it's easy to accidentally scroll more than one picker at a time. - Following code aims to fix this issue. - */ - public static boolean shouldAllowScroll(View view){ + * When TalkBack is active, user can use one finger to explore the screen and + * set focus to elements. Then user can proceed to use second finger to scroll + * contents of focused element. When there's multiple pickers next to each other + * horizontally, it's easy to accidentally scroll more than one picker at a + * time. Following code aims to fix this issue. + */ + public static boolean shouldAllowScroll(View view) { // If TalkBack isn't active, always proceed without suppressing touch events if (!systemManager.isTouchExplorationEnabled()) { return true; @@ -43,49 +49,45 @@ public class Accessibility { return false; } - public static class SetAccessibilityDelegate implements WheelFunction { - - private final Locale locale; - - public SetAccessibilityDelegate(Locale locale) { - this.locale = locale; - } - - @Override - public void apply(Wheel wheel) { - final View view = wheel.picker.getView(); - view.setAccessibilityDelegate( - new View.AccessibilityDelegate(){ - @Override - public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(host, event); - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { - String resourceKey = view.getTag().toString()+"_description"; - String localeTag = Utils.getLocalisedStringFromResources(locale, resourceKey); - // Screen reader reads the content description when focused on each picker wheel - view.setContentDescription(localeTag); - } + /** + * Enable capturing of accessibility events for a Picker + */ + public static void startAccessibilityDelegate(Picker picker) { + final Picker fPicker = picker; + final View view = picker.getView(); + + view.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + int currentValue = fPicker.getValue(); + + // Capture system automagic accessibility actions or custom actions created & sent manually via accessibility events + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + if (!fPicker.isSpinning()) { + // Increase value when pressing hardware volume up (or scrolling by other means) + fPicker.smoothScrollToValue(currentValue - 1); } - } - ); - } - } - - private final State state; - private final Wheels wheels; + break; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + if (!fPicker.isSpinning()) { + // Decrease value when pressing hardware volume down (or scrolling by other means) + fPicker.smoothScrollToValue(currentValue + 1); + } + break; + } - public Accessibility(State state, Wheels wheels){ - this.state = state; - this.wheels = wheels; + return super.performAccessibilityAction(host, action, args); + } + }); } - public void update(Wheel picker){ - String tagName = picker.picker.getView().getTag().toString(); - String selectedDateString = getAccessibleTextForSelectedDate(); - String descriptionPrefix = Utils.getLocalisedStringFromResources(state.getLocale(), "selected_"+tagName+"_description"); - String descriptionPostFix = Utils.getLocalisedStringFromResources(state.getLocale(), "selected_value_description"); + public static boolean isAccessibilityEnabled() { + if (systemManager == null) { + return false; + } - picker.picker.getView().setContentDescription(descriptionPrefix + ", "+ descriptionPostFix + " "+ selectedDateString); + return systemManager.isEnabled(); } /** @@ -99,33 +101,34 @@ public class Accessibility { * Get a list of accessibility services currently active */ private static boolean hasAccessibilityFeatureTypeEnabled(int type) { - - List enabledServices = - systemManager.getEnabledAccessibilityServiceList(type); + List enabledServices = systemManager.getEnabledAccessibilityServiceList(type); return enabledServices != null && enabledServices.size() > 0; } /** * Read a message out loud when spoken feedback is active + * + * After announcing has been requested from TalkBack it can't be interrupted. + * For example, when values change in rapid succession, all changes are + * announced instead of just announcing the last change. Hence it's recommended + * usually not to announce. Instead notify TalkBack about changes by sending + * accessibility events or using accessibilityLiveRegion. */ - public static void announce(String message) { - if (systemManager == null || !isSpokenFeedbackEnabled()) { + public static void announce(String message, View host) { + if (!isAccessibilityEnabled() || !isSpokenFeedbackEnabled()) { return; - } + } - AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT); - event.getText().add(message); - systemManager.sendAccessibilityEvent(event); + host.announceForAccessibility(message); } /** - * Get NumberPickerView displayValue from value. + * Get Picker displayValue from value. */ - private static String numberPickerValueToDisplayedValue(NumberPickerView numberPicker, int value) { - final String[] displayValues = numberPicker.getDisplayedValues(); - - final String displayValue = displayValues[value]; + private static String pickerValueToDisplayedValue(Picker picker, int value) { + String[] displayValues = picker.getDisplayedValues(); + String displayValue = displayValues[value]; if (displayValue != null) { return displayValue; @@ -135,31 +138,69 @@ public class Accessibility { } /** - * Read NumberPickerView displayed value. - * For TalkBack to read dates etc. correctly, make sure they are in localised format. + * Tell TalkBack a value has changed */ - public static void announceNumberPickerValue(NumberPickerView numberPicker, int newValue) { - announce(numberPickerValueToDisplayedValue(numberPicker, newValue)); + public static void sendValueChangedEvent(Picker picker, int newValue) { + AccessibilityEvent event = buildEvent(picker.getView(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + String message = pickerValueToDisplayedValue(picker, newValue); + event.getText().add(message); + + sendEvent(event); + } + + private static String getContentDescriptionLabel(String tagName) { + return Utils.getLocalisedStringFromResources(Accessibility.getLocale(), tagName + "_description"); + } + + public static String getContentDescription(Picker picker) { + String tagName = picker.getView().getTag().toString(); + int currentValue = picker.getValue(); + String currentDisplayValue = pickerValueToDisplayedValue(picker, currentValue); + String label = getContentDescriptionLabel(tagName); + + return currentDisplayValue + ", " + label; + } + + public static AccessibilityEvent buildEvent(View host, int eventType) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setClassName(host.getClass().getName()); + event.setPackageName(host.getContext().getPackageName()); + + return event; } - private String getAccessibleTextForSelectedDate() { - String accessibleText; - switch(state.getMode()) { - case date: - accessibleText = wheels.getDateString(); - break; - case time: - accessibleText = wheels.getTimeString(); - break; - default: - // default is dateTime - String timePrefix = Utils.getLocalisedStringFromResources(state.getLocale(), "time_tag"); - String hourPrefix = Utils.getLocalisedStringFromResources(state.getLocale(), "hour_tag"); - String minutesPrefix = Utils.getLocalisedStringFromResources(state.getLocale(), "minutes_tag"); - accessibleText = wheels.getAccessibleDateTimeString(timePrefix, hourPrefix, minutesPrefix); - break; + public static void sendEvent(AccessibilityEvent event) { + if (systemManager == null || !systemManager.isEnabled()) { + return; } - return accessibleText; + systemManager.sendAccessibilityEvent(event); } + /** + * Sets the view associated with given AccessibilityNodeInfo to behave like a seekBar / (slider) + * when said view receives accessibility focus + */ + public static void setRoleToSlider(Picker picker, AccessibilityNodeInfo info) { + // Sets "accessibility role" of the control - affects how the element is read by TalkBack + info.setClassName("android.widget.SeekBar"); + info.setScrollable(true); + // Set the "accessibility label" read by spoken feedback + info.setContentDescription(getContentDescription(picker)); + + // Inform that we want to send and receive scrolling-related actions + // Enables us to handle special accessibility-only scroll-events created by TalkBack + if (Build.VERSION.SDK_INT >= 21) { + AccessibilityNodeInfo.AccessibilityAction increaseValue = new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, "Increase value"); + AccessibilityNodeInfo.AccessibilityAction decreaseValue = new AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, "Decrease value"); + + info.addAction(increaseValue); + info.addAction(decreaseValue); + + } else { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + } } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java b/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java index 56f4743..6bf8342 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/UIManager.java @@ -21,13 +21,11 @@ public class UIManager { private Wheels wheels; private FadingOverlay fadingOverlay; private WheelScroller wheelScroller = new WheelScroller(); - private Accessibility accessibility; public UIManager(State state, View rootView){ this.state = state; this.rootView = rootView; wheels = new Wheels(state, rootView); - accessibility = new Accessibility(state, wheels); addOnChangeListener(); } @@ -92,14 +90,6 @@ public class UIManager { wheels.applyOnVisible(new HorizontalPadding()); } - public void updateContentDescription(Wheel picker) { - accessibility.update(picker); - } - - public void updateAccessibilityValues() { - wheels.applyOnAll(new Accessibility.SetAccessibilityDelegate(state.getLocale())); - } - public void updateLastSelectedDate(Calendar date) { state.setLastSelectedDate(date); } diff --git a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java index 4c19f0b..4cf43fd 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/WheelChangeListenerImpl.java @@ -59,8 +59,6 @@ public class WheelChangeListenerImpl implements WheelChangeListener { return; } - uiManager.updateContentDescription(picker); - String displayData = uiManager.getDisplayValueString(); uiManager.updateLastSelectedDate(selectedDate); diff --git a/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java b/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java index 62b3247..b8b5e3b 100644 --- a/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java +++ b/android/src/main/java/com/henninghall/date_picker/ui/Wheels.java @@ -150,15 +150,6 @@ public class Wheels { return getDateString(0); } - String getAccessibleDateTimeString(String timePrefix, String hourTag, String minutesTag) { - String date = getDateString(); - String hour = hourWheel.getValue(); - String minutes = minutesWheel.getValue(); - String ampm = ampmWheel.getValue(); - String time = timePrefix+ " "+ hour + hourTag + minutes + minutesTag + ampm; - return date+", "+ time; - } - String getDisplayValue() { StringBuilder sb = new StringBuilder(); for (Wheel wheel: getOrderedVisibleWheels()) { diff --git a/android/src/main/res/values-fi/strings.xml b/android/src/main/res/values-fi/strings.xml index 0c3445f..8916884 100644 --- a/android/src/main/res/values-fi/strings.xml +++ b/android/src/main/res/values-fi/strings.xml @@ -1,6 +1,6 @@ - Päivämääränvalitsimen päällekkäin + Päivämääränvalitsin Valitse Vuosi Valitse Kuukausi Valitse Päivämäärä @@ -8,14 +8,6 @@ Valitse Tunti Valitse Minuutit Valitse am/pm - Valittu Vuosi - Valittu Kuukausi - Valittu Päivämäärä - Valittu Päivä - Valittu Tunti - Valitut Minuutit - Valittu am/pm - Arvo on Kello on Tunti: Minuutit diff --git a/android/src/main/res/values-he/strings.xml b/android/src/main/res/values-he/strings.xml index 65615ec..626efeb 100644 --- a/android/src/main/res/values-he/strings.xml +++ b/android/src/main/res/values-he/strings.xml @@ -7,14 +7,6 @@ בחירת שעה בחירת דקות בחירה ב־AM/PM - השנה הנבחרת - החודש הנבחר - התאריך הנבחר - היום הנבחר - השעה הנבחרת - הדקות שנבחרו - מה נבחר מבחינת AM/PM - הערך הוא השעה היא שעה : דקות diff --git a/android/src/main/res/values-nb/strings.xml b/android/src/main/res/values-nb/strings.xml index 0e15c70..b0ac361 100644 --- a/android/src/main/res/values-nb/strings.xml +++ b/android/src/main/res/values-nb/strings.xml @@ -8,14 +8,6 @@ Velg time Velg minutter Velg am/pm - Valgt år - Valgt måned - Valgt dato - Valgt dag - Valgt time - Valgte minutter - Valgt am/pm - Verdien er Tiden er Time Minutter diff --git a/android/src/main/res/values-nn/strings.xml b/android/src/main/res/values-nn/strings.xml index da7603c..0145dbb 100644 --- a/android/src/main/res/values-nn/strings.xml +++ b/android/src/main/res/values-nn/strings.xml @@ -8,14 +8,6 @@ Vel time Vel minutt Vel am/pm - Vald år - Vald månad - Vald dato - Vald dag - Vald time - Valde minutt - Vald am/pm - Verdien er Tida er Time Minutt diff --git a/android/src/main/res/values-no/strings.xml b/android/src/main/res/values-no/strings.xml index 0e15c70..b0ac361 100644 --- a/android/src/main/res/values-no/strings.xml +++ b/android/src/main/res/values-no/strings.xml @@ -8,14 +8,6 @@ Velg time Velg minutter Velg am/pm - Valgt år - Valgt måned - Valgt dato - Valgt dag - Valgt time - Valgte minutter - Valgt am/pm - Verdien er Tiden er Time Minutter diff --git a/android/src/main/res/values-sv/strings.xml b/android/src/main/res/values-sv/strings.xml index 421755e..4f6f188 100644 --- a/android/src/main/res/values-sv/strings.xml +++ b/android/src/main/res/values-sv/strings.xml @@ -8,14 +8,6 @@ Välj en timme Välj minuter Välj am/pm - Valt år - Valt månad - Valt datum - Vald dag - Vald timme - Valda minuter - Vald am/pm - Värdet är Tiden är Timme: Minuter diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index c7d09cd..94f962a 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -7,14 +7,6 @@ Select Hour Select Minutes Select AM/PM - Selected Year - Selected Month - Selected Date - Selected Day - Selected Hour - Selected Minutes - Selected AM/PM - Value is Time is Hour : Minutes