Skip to content

Commit cc335f6

Browse files
author
Linden Darling
committed
Added Accessibility announcement feature for setError (when set by ValidationManager);
1 parent 29c6df9 commit cc335f6

File tree

4 files changed

+99
-5
lines changed

4 files changed

+99
-5
lines changed

android-formidable-validation/src/com/coreform/open/android/formidablevalidation/SetErrorAbleButton.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33

44
import android.content.Context;
5+
import android.graphics.Rect;
56
import android.util.AttributeSet;
67
import android.widget.Button;
78

@@ -33,5 +34,4 @@ public void setErrorPopupPadding(int left, int top, int right, int bottom) {
3334
// TODO Auto-generated method stub
3435

3536
}
36-
3737
}

android-formidable-validation/src/com/coreform/open/android/formidablevalidation/SetErrorHandler.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44
import android.content.res.TypedArray;
55
import android.graphics.Rect;
66
import android.graphics.drawable.Drawable;
7+
import android.os.Build;
8+
import android.support.v4.view.accessibility.AccessibilityEventCompat;
79
import android.text.Layout;
810
import android.text.StaticLayout;
911
import android.text.TextUtils;
1012
import android.util.AttributeSet;
1113
import android.util.Log;
1214
import android.view.LayoutInflater;
1315
import android.view.View;
16+
import android.view.View.OnFocusChangeListener;
17+
import android.view.accessibility.AccessibilityEvent;
18+
import android.view.accessibility.AccessibilityManager;
1419
import android.widget.PopupWindow;
1520
import android.widget.TextView;
1621
import android.widget.Toast;
@@ -369,7 +374,7 @@ public void setCompoundDrawables(Drawable left, Drawable top,
369374
mView.invalidate();
370375
mView.requestLayout();
371376
}
372-
377+
373378
/*
374379
* INNER CLASSES
375380
*/

android-formidable-validation/src/com/coreform/open/android/formidablevalidation/ValidationManager.java

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
import java.util.Map.Entry;
99
import java.util.Set;
1010

11+
import android.content.Context;
12+
import android.os.Build;
13+
import android.os.Handler;
14+
import android.support.v4.view.accessibility.AccessibilityEventCompat;
1115
import android.text.SpannableStringBuilder;
1216
import android.util.Log;
17+
import android.view.accessibility.AccessibilityEvent;
18+
import android.view.accessibility.AccessibilityManager;
1319
import android.widget.EditText;
1420
import android.widget.ScrollView;
1521
import android.widget.TextView;
@@ -18,12 +24,29 @@ public class ValidationManager {
1824
private static final boolean DEBUG = true;
1925
private static final String TAG = "ValidationManager";
2026

27+
private static final long ACCESSIBILITY_ANNOUNCE_DELAY = 1 * 1000;
28+
29+
private static Context mContext;
30+
private Handler mHandler;
31+
32+
/**
33+
* The accessibility manager for this context. This is used to check the
34+
* accessibility enabled state, as well as to send raw accessibility events.
35+
*/
36+
private static AccessibilityManager mAccessibilityManager;
37+
2138
private LinkedHashMap<String, List<ValueValidatorInterface>> valueValidatorMap;
2239
private LinkedHashMap<String, List<DependencyValidatorInterface>> dependencyValidatorMap;
2340

24-
public ValidationManager() {
41+
public ValidationManager(Context context) {
42+
mContext = context;
2543
valueValidatorMap = new LinkedHashMap<String, List<ValueValidatorInterface>>();
2644
dependencyValidatorMap = new LinkedHashMap<String, List<DependencyValidatorInterface>>();
45+
46+
mHandler = new Handler();
47+
48+
// Keep a handle to the accessibility manager.
49+
mAccessibilityManager = (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
2750
}
2851

2952
/*
@@ -245,12 +268,15 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
245268
if("EditText".equals(aFinalValidationResult.getSource().getClass().getSimpleName())) {
246269
//autoscroll to field
247270
if(!invalidFieldHasAlreadyBeenFocused) {
248-
((EditText) aFinalValidationResult.getSource()).requestFocus(); //problematic for onFocusChangeListener (yields 2x focused EditTexts = BAD STUFF)
271+
//((EditText) aFinalValidationResult.getSource()).requestFocus(); //problematic for onFocusChangeListener (yields 2x focused EditTexts = BAD STUFF)
249272
invalidFieldHasAlreadyBeenFocused = true;
250273
}
251274
//setError
252275
SpannableStringBuilder errorText = new SpannableStringBuilder(aFinalValidationResult.getDependencyInvalidMessage());
253276
((EditText) aFinalValidationResult.getSource()).setError(errorText);
277+
//need to delay accessibility announcement so its the last View to steal focus
278+
AnnounceForAccessibilityRunnable announceForAccessibilityRunnable = new AnnounceForAccessibilityRunnable(errorText);
279+
mHandler.postDelayed(announceForAccessibilityRunnable, ACCESSIBILITY_ANNOUNCE_DELAY);
254280
//break validationLoop;
255281
}
256282
}
@@ -271,6 +297,9 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
271297
//setError
272298
SpannableStringBuilder errorText = new SpannableStringBuilder(aFinalValidationResult.getValueInvalidMessage());
273299
((EditText) aFinalValidationResult.getSource()).setError(errorText);
300+
//need to delay accessibility announcement so its the last View to steal focus
301+
AnnounceForAccessibilityRunnable announceForAccessibilityRunnable = new AnnounceForAccessibilityRunnable(errorText);
302+
mHandler.postDelayed(announceForAccessibilityRunnable, ACCESSIBILITY_ANNOUNCE_DELAY);
274303
//return false; //break now to block a subsequent EditText or SetErrorAble-View from showing another ErrorPopup
275304
} else if(aFinalValidationResult.getSource() instanceof SetErrorAble) {
276305
//note: this is seriously custom code, specific for this use-case, not very portable to other use-cases in its current form
@@ -287,6 +316,9 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
287316
//setError (for a button, this won't requestFocus() and won't show the message in a popup...it only sets the exclamation inner drawable)
288317
SpannableStringBuilder errorText = new SpannableStringBuilder(aFinalValidationResult.getValueInvalidMessage());
289318
((SetErrorAble) aFinalValidationResult.getSource()).setError(errorText);
319+
//need to delay accessibility announcement so its the last View to steal focus
320+
AnnounceForAccessibilityRunnable announceForAccessibilityRunnable = new AnnounceForAccessibilityRunnable(errorText);
321+
mHandler.postDelayed(announceForAccessibilityRunnable, ACCESSIBILITY_ANNOUNCE_DELAY);
290322
//return false; //break now to block a subsequent ErrorText stealing focus (and thereby showing more than 1x ErrorPopup)
291323
//manually set the rest of error
292324
/*
@@ -313,4 +345,61 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
313345
return false;
314346
}
315347
}
348+
349+
/**
350+
* Generates and dispatches an SDK-specific spoken announcement.
351+
* <p>
352+
* For backwards compatibility, we're constructing an event from scratch
353+
* using the appropriate event type. If your application only targets SDK
354+
* 16+, you can just call View.announceForAccessibility(CharSequence).
355+
* </p>
356+
*
357+
* Adapted from https://http://eyes-free.googlecode.com/files/accessibility_codelab_demos_v2_src.zip
358+
*
359+
* @param text The text to announce.
360+
*/
361+
public static void announceForAccessibilityCompat(CharSequence text) {
362+
if (!mAccessibilityManager.isEnabled()) {
363+
return;
364+
}
365+
366+
// Prior to SDK 16, announcements could only be made through FOCUSED
367+
// events. Jelly Bean (SDK 16) added support for speaking text verbatim
368+
// using the ANNOUNCEMENT event type.
369+
final int eventType;
370+
if (Build.VERSION.SDK_INT < 16) {
371+
eventType = AccessibilityEvent.TYPE_VIEW_FOCUSED;
372+
} else {
373+
eventType = AccessibilityEventCompat.TYPE_ANNOUNCEMENT;
374+
}
375+
376+
// Construct an accessibility event with the minimum recommended
377+
// attributes. An event without a class name or package may be dropped.
378+
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
379+
event.getText().add(text);
380+
event.setClassName(SetErrorHandler.class.getName());
381+
event.setPackageName(mContext.getPackageName());
382+
383+
// Sends the event directly through the accessibility manager. If your
384+
// application only targets SDK 14+, you should just call
385+
// getParent().requestSendAccessibilityEvent(this, event);
386+
mAccessibilityManager.sendAccessibilityEvent(event);
387+
}
388+
389+
/*
390+
* INNER CLASSES
391+
*/
392+
393+
private static class AnnounceForAccessibilityRunnable implements Runnable {
394+
private final CharSequence mText;
395+
396+
public AnnounceForAccessibilityRunnable(CharSequence text) {
397+
mText = text;
398+
}
399+
@Override
400+
public void run() {
401+
announceForAccessibilityCompat(mText);
402+
}
403+
404+
}
316405
}

android-formidable-validation/src/com/coreform/open/android/formidablevalidation/example/AddressActivity.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void onCreate(Bundle savedInstanceState) {
7979
mCountrySpinner.setErrorPopupPadding(12, 12, 12, 12);
8080

8181
//setup validation
82-
mValidationManager = new ValidationManager();
82+
mValidationManager = new ValidationManager(this);
8383

8484
mValidationManager.add("addressLine1");
8585
mValidationManager.add("addressLine1", new RegExpressionValueValidator(mAddressLine1EditText, "^[a-zA-Z]{3}$", "please enter your address."));

0 commit comments

Comments
 (0)