8
8
import java .util .Map .Entry ;
9
9
import java .util .Set ;
10
10
11
+ import android .content .Context ;
12
+ import android .os .Build ;
13
+ import android .os .Handler ;
14
+ import android .support .v4 .view .accessibility .AccessibilityEventCompat ;
11
15
import android .text .SpannableStringBuilder ;
12
16
import android .util .Log ;
17
+ import android .view .accessibility .AccessibilityEvent ;
18
+ import android .view .accessibility .AccessibilityManager ;
13
19
import android .widget .EditText ;
14
20
import android .widget .ScrollView ;
15
21
import android .widget .TextView ;
@@ -18,12 +24,29 @@ public class ValidationManager {
18
24
private static final boolean DEBUG = true ;
19
25
private static final String TAG = "ValidationManager" ;
20
26
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
+
21
38
private LinkedHashMap <String , List <ValueValidatorInterface >> valueValidatorMap ;
22
39
private LinkedHashMap <String , List <DependencyValidatorInterface >> dependencyValidatorMap ;
23
40
24
- public ValidationManager () {
41
+ public ValidationManager (Context context ) {
42
+ mContext = context ;
25
43
valueValidatorMap = new LinkedHashMap <String , List <ValueValidatorInterface >>();
26
44
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 );
27
50
}
28
51
29
52
/*
@@ -245,12 +268,15 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
245
268
if ("EditText" .equals (aFinalValidationResult .getSource ().getClass ().getSimpleName ())) {
246
269
//autoscroll to field
247
270
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)
249
272
invalidFieldHasAlreadyBeenFocused = true ;
250
273
}
251
274
//setError
252
275
SpannableStringBuilder errorText = new SpannableStringBuilder (aFinalValidationResult .getDependencyInvalidMessage ());
253
276
((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 );
254
280
//break validationLoop;
255
281
}
256
282
}
@@ -271,6 +297,9 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
271
297
//setError
272
298
SpannableStringBuilder errorText = new SpannableStringBuilder (aFinalValidationResult .getValueInvalidMessage ());
273
299
((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 );
274
303
//return false; //break now to block a subsequent EditText or SetErrorAble-View from showing another ErrorPopup
275
304
} else if (aFinalValidationResult .getSource () instanceof SetErrorAble ) {
276
305
//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) {
287
316
//setError (for a button, this won't requestFocus() and won't show the message in a popup...it only sets the exclamation inner drawable)
288
317
SpannableStringBuilder errorText = new SpannableStringBuilder (aFinalValidationResult .getValueInvalidMessage ());
289
318
((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 );
290
322
//return false; //break now to block a subsequent ErrorText stealing focus (and thereby showing more than 1x ErrorPopup)
291
323
//manually set the rest of error
292
324
/*
@@ -313,4 +345,61 @@ public boolean validateAllAndSetError(ScrollView containerScrollView) {
313
345
return false ;
314
346
}
315
347
}
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
+ }
316
405
}
0 commit comments