1616
1717package com .android .internal .widget .multiwaveview ;
1818
19- import java .util .ArrayList ;
20-
2119import android .animation .Animator ;
2220import android .animation .Animator .AnimatorListener ;
2321import android .animation .AnimatorListenerAdapter ;
3129import android .graphics .RectF ;
3230import android .graphics .drawable .Drawable ;
3331import android .os .Vibrator ;
32+ import android .text .TextUtils ;
3433import android .util .AttributeSet ;
3534import android .util .Log ;
3635import android .util .TypedValue ;
3736import android .view .MotionEvent ;
3837import android .view .View ;
39- import android .view .View .MeasureSpec ;
38+ import android .view .ViewConfiguration ;
39+ import android .view .accessibility .AccessibilityEvent ;
40+ import android .view .accessibility .AccessibilityManager ;
4041
4142import com .android .internal .R ;
4243
44+ import java .util .ArrayList ;
45+
4346/**
4447 * A special widget containing a center and outer ring. Moving the center ring to the outer ring
4548 * causes an event that can be caught by implementing OnTriggerListener.
@@ -82,6 +85,8 @@ public interface OnTriggerListener {
8285 private ArrayList <TargetDrawable > mChevronDrawables = new ArrayList <TargetDrawable >();
8386 private ArrayList <Tweener > mChevronAnimations = new ArrayList <Tweener >();
8487 private ArrayList <Tweener > mTargetAnimations = new ArrayList <Tweener >();
88+ private ArrayList <String > mTargetDescriptions ;
89+ private ArrayList <String > mDirectionDescriptions ;
8590 private Tweener mHandleAnimation ;
8691 private OnTriggerListener mOnTriggerListener ;
8792 private TargetDrawable mHandleDrawable ;
@@ -103,6 +108,9 @@ public interface OnTriggerListener {
103108 private boolean mDragging ;
104109 private int mNewTargetResources ;
105110
111+ private boolean mWaveHovered = false ;
112+ private long mLastHoverExitTimeMillis = 0 ;
113+
106114 private AnimatorListener mResetListener = new AnimatorListenerAdapter () {
107115 public void onAnimationEnd (Animator animator ) {
108116 switchToState (STATE_IDLE , mWaveCenterX , mWaveCenterY );
@@ -128,6 +136,8 @@ public void onAnimationEnd(Animator animator) {
128136 }
129137 };
130138 private int mTargetResourceId ;
139+ private int mTargetDescriptionsResourceId ;
140+ private int mDirectionDescriptionsResourceId ;
131141
132142 public MultiWaveView (Context context ) {
133143 this (context , null );
@@ -177,6 +187,25 @@ public MultiWaveView(Context context, AttributeSet attrs) {
177187 throw new IllegalStateException ("Must specify at least one target drawable" );
178188 }
179189
190+ // Read array of target descriptions
191+ if (a .getValue (R .styleable .MultiWaveView_targetDescriptions , outValue )) {
192+ final int resourceId = outValue .resourceId ;
193+ if (resourceId == 0 ) {
194+ throw new IllegalStateException ("Must specify target descriptions" );
195+ }
196+ setTargetDescriptionsResourceId (resourceId );
197+ }
198+
199+ // Read array of direction descriptions
200+ if (a .getValue (R .styleable .MultiWaveView_directionDescriptions , outValue )) {
201+ final int resourceId = outValue .resourceId ;
202+ if (resourceId == 0 ) {
203+ throw new IllegalStateException ("Must specify direction descriptions" );
204+ }
205+ setDirectionDescriptionsResourceId (resourceId );
206+ }
207+
208+ a .recycle ();
180209 setVibrateEnabled (mVibrationDuration > 0 );
181210 }
182211
@@ -247,6 +276,9 @@ private void switchToState(int state, float x, float y) {
247276 showTargets (true );
248277 mHandleDrawable .setState (TargetDrawable .STATE_ACTIVE );
249278 setGrabbedState (OnTriggerListener .CENTER_HANDLE );
279+ if (AccessibilityManager .getInstance (mContext ).isEnabled ()) {
280+ announceTargets ();
281+ }
250282 break ;
251283
252284 case STATE_TRACKING :
@@ -347,6 +379,13 @@ private void dispatchTriggerEvent(int whichHandle) {
347379 }
348380 }
349381
382+ private void dispatchGrabbedEvent (int whichHandler ) {
383+ vibrate ();
384+ if (mOnTriggerListener != null ) {
385+ mOnTriggerListener .onGrabbed (this , whichHandler );
386+ }
387+ }
388+
350389 private void doFinish () {
351390 final int activeTarget = mActiveTarget ;
352391 boolean targetHit = activeTarget != -1 ;
@@ -475,6 +514,7 @@ private void internalSetTargetResources(int resourceId) {
475514 Drawable drawable = array .getDrawable (i );
476515 targetDrawables .add (new TargetDrawable (res , drawable ));
477516 }
517+ array .recycle ();
478518 mTargetResourceId = resourceId ;
479519 mTargetDrawables = targetDrawables ;
480520 updateTargetPositions ();
@@ -498,6 +538,48 @@ public int getTargetResourceId() {
498538 return mTargetResourceId ;
499539 }
500540
541+ /**
542+ * Sets the resource id specifying the target descriptions for accessibility.
543+ *
544+ * @param resourceId The resource id.
545+ */
546+ public void setTargetDescriptionsResourceId (int resourceId ) {
547+ mTargetDescriptionsResourceId = resourceId ;
548+ if (mTargetDescriptions != null ) {
549+ mTargetDescriptions .clear ();
550+ }
551+ }
552+
553+ /**
554+ * Gets the resource id specifying the target descriptions for accessibility.
555+ *
556+ * @return The resource id.
557+ */
558+ public int getTargetDescriptionsResourceId () {
559+ return mTargetDescriptionsResourceId ;
560+ }
561+
562+ /**
563+ * Sets the resource id specifying the target direction descriptions for accessibility.
564+ *
565+ * @param resourceId The resource id.
566+ */
567+ public void setDirectionDescriptionsResourceId (int resourceId ) {
568+ mDirectionDescriptionsResourceId = resourceId ;
569+ if (mDirectionDescriptions != null ) {
570+ mDirectionDescriptions .clear ();
571+ }
572+ }
573+
574+ /**
575+ * Gets the resource id specifying the target direction descriptions.
576+ *
577+ * @return The resource id.
578+ */
579+ public int getDirectionDescriptionsResourceId () {
580+ return mDirectionDescriptionsResourceId ;
581+ }
582+
501583 /**
502584 * Enable or disable vibrate on touch.
503585 *
@@ -593,6 +675,43 @@ private void handleDown(MotionEvent event) {
593675 }
594676 }
595677
678+ @ Override
679+ public boolean onHoverEvent (MotionEvent event ) {
680+ if (AccessibilityManager .getInstance (mContext ).isTouchExplorationEnabled ()) {
681+ final int action = event .getAction ();
682+ switch (action ) {
683+ case MotionEvent .ACTION_HOVER_ENTER :
684+ case MotionEvent .ACTION_HOVER_MOVE :
685+ final float dx = event .getX () - mWaveCenterX ;
686+ final float dy = event .getY () - mWaveCenterY ;
687+ if (dist2 (dx ,dy ) <= square (mTapRadius )) {
688+ if (!mWaveHovered ) {
689+ mWaveHovered = true ;
690+ final long timeSinceLastHoverExitMillis =
691+ event .getEventTime () - mLastHoverExitTimeMillis ;
692+ final long recurringEventsInterval =
693+ ViewConfiguration .getSendRecurringAccessibilityEventsInterval ();
694+ if (timeSinceLastHoverExitMillis > recurringEventsInterval ) {
695+ String text =
696+ mContext .getString (R .string .content_description_sliding_handle );
697+ announceText (text );
698+ }
699+ }
700+ } else {
701+ mWaveHovered = false ;
702+ }
703+ break ;
704+ case MotionEvent .ACTION_HOVER_EXIT :
705+ mLastHoverExitTimeMillis = event .getEventTime ();
706+ mWaveHovered = false ;
707+ break ;
708+ default :
709+ mWaveHovered = false ;
710+ }
711+ }
712+ return super .onHoverEvent (event );
713+ }
714+
596715 private void handleUp (MotionEvent event ) {
597716 if (DEBUG && mDragging ) Log .v (TAG , "** Handle RELEASE" );
598717 switchToState (STATE_FINISH , event .getX (), event .getY ());
@@ -663,7 +782,11 @@ private void handleMove(MotionEvent event) {
663782 invalidateGlobalRegion (mHandleDrawable );
664783
665784 if (mActiveTarget != activeTarget && activeTarget != -1 ) {
666- vibrate ();
785+ dispatchGrabbedEvent (activeTarget );
786+ if (AccessibilityManager .getInstance (mContext ).isEnabled ()) {
787+ String targetContentDescription = getTargetDescription (activeTarget );
788+ announceText (targetContentDescription );
789+ }
667790 }
668791 mActiveTarget = activeTarget ;
669792 }
@@ -771,4 +894,62 @@ private float dist2(float dx, float dy) {
771894 return dx *dx + dy *dy ;
772895 }
773896
774- }
897+ private void announceTargets () {
898+ StringBuilder utterance = new StringBuilder ();
899+ final int targetCount = mTargetDrawables .size ();
900+ for (int i = 0 ; i < targetCount ; i ++) {
901+ String targetDescription = getTargetDescription (i );
902+ String directionDescription = getDirectionDescription (i );
903+ if (!TextUtils .isEmpty (targetDescription )
904+ && !TextUtils .isEmpty (directionDescription )) {
905+ utterance .append (targetDescription );
906+ utterance .append (" " );
907+ utterance .append (directionDescription );
908+ utterance .append ("." );
909+ }
910+ }
911+ announceText (utterance .toString ());
912+ }
913+
914+ private void announceText (String text ) {
915+ setContentDescription (text );
916+ sendAccessibilityEvent (AccessibilityEvent .TYPE_VIEW_FOCUSED );
917+ setContentDescription (null );
918+ }
919+
920+ private String getTargetDescription (int index ) {
921+ if (mTargetDescriptions == null || mTargetDescriptions .isEmpty ()) {
922+ mTargetDescriptions = loadDescriptions (mTargetDescriptionsResourceId );
923+ if (mTargetDrawables .size () != mTargetDescriptions .size ()) {
924+ Log .w (TAG , "The number of target drawables must be"
925+ + " euqal to the number of target descriptions." );
926+ return null ;
927+ }
928+ }
929+ return mTargetDescriptions .get (index );
930+ }
931+
932+ private String getDirectionDescription (int index ) {
933+ if (mDirectionDescriptions == null || mDirectionDescriptions .isEmpty ()) {
934+ mDirectionDescriptions = loadDescriptions (mDirectionDescriptionsResourceId );
935+ if (mTargetDrawables .size () != mDirectionDescriptions .size ()) {
936+ Log .w (TAG , "The number of target drawables must be"
937+ + " euqal to the number of direction descriptions." );
938+ return null ;
939+ }
940+ }
941+ return mDirectionDescriptions .get (index );
942+ }
943+
944+ private ArrayList <String > loadDescriptions (int resourceId ) {
945+ TypedArray array = getContext ().getResources ().obtainTypedArray (resourceId );
946+ final int count = array .length ();
947+ ArrayList <String > targetContentDescriptions = new ArrayList <String >(count );
948+ for (int i = 0 ; i < count ; i ++) {
949+ String contentDescription = array .getString (i );
950+ targetContentDescriptions .add (contentDescription );
951+ }
952+ array .recycle ();
953+ return targetContentDescriptions ;
954+ }
955+ }
0 commit comments