1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.basicaccessibility;
18 
19 import android.annotation.TargetApi;
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Paint;
24 import android.os.Build;
25 import android.util.AttributeSet;
26 import android.view.View;
27 import android.view.accessibility.AccessibilityEvent;
28 
29 /**
30  * Custom view to demonstrate accessibility.
31  *
32  * <p>This view does not use any framework widgets, so does not get any accessibility features
33  * automatically. Instead, we use {@link android.view.accessibility.AccessibilityEvent} to provide accessibility hints to
34  * the OS.
35  *
36  * <p>For example, if TalkBack is enabled, users will be able to receive spoken feedback as they
37  * interact with this view.
38  *
39  * <p>More generally, this view renders a multi-position "dial" that can be used to select a value
40  * between 1 and 4. Each time the dial is clicked, the next position will be selected (modulo
41  * the maximum number of positions).
42  */
43 public class DialView extends View {
44     private static int SELECTION_COUNT = 4;
45 
46     private static float FONT_SIZE = 40f;
47     private float mWidth;
48     private float mHeight;
49     private float mWidthPadded;
50     private float mHeightPadded;
51     private Paint mTextPaint;
52     private Paint mDialPaint;
53     private float mRadius;
54     private int mActiveSelection;
55 
56     /**
57      * Constructor that is called when inflating a view from XML. This is called
58      * when a view is being constructed from an XML file, supplying attributes
59      * that were specified in the XML file.
60      *
61      * <p>In our case, this constructor just calls init().
62      *
63      * @param context The Context the view is running in, through which it can
64      *                access the current theme, resources, etc.
65      * @param attrs   The attributes of the XML tag that is inflating the view.
66      * @see #View(android.content.Context, android.util.AttributeSet, int)
67      */
DialView(Context context, AttributeSet attrs)68     public DialView(Context context, AttributeSet attrs) {
69         super(context, attrs);
70         init();
71     }
72 
73     /**
74      * Helper method to initialize instance variables. Called by constructor.
75      */
init()76     private void init() {
77         // Paint styles used for rendering are created here, rather than at render-time. This
78         // is a performance optimization, since onDraw() will get called frequently.
79         mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
80         mTextPaint.setColor(Color.BLACK);
81         mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
82         mTextPaint.setTextAlign(Paint.Align.CENTER);
83         mTextPaint.setTextSize(FONT_SIZE);
84 
85         mDialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
86         mDialPaint.setColor(Color.GRAY);
87 
88         // Initialize current selection. This will store where the dial's "indicator" is pointing.
89         mActiveSelection = 0;
90 
91         // Setup onClick listener for this view. Rotates between each of the different selection
92         // states on each click.
93         //
94         // Notice that we call sendAccessibilityEvent here. Some AccessibilityEvents are generated
95         // by the system. However, custom views will typically need to send events manually as the
96         // user interacts with the view. The type of event sent will vary, depending on the nature
97         // of the view and how the user interacts with it.
98         //
99         // In this case, we are sending TYPE_VIEW_SELECTED rather than TYPE_VIEW_CLICKED, because
100         // clicking on this view selects a new value.
101         //
102         // We will give our AccessibilityEvent further information about the state of the view in
103         // onPopulateAccessibilityEvent(), which will be called automatically by the system
104         // for each AccessibilityEvent.
105         setOnClickListener(new OnClickListener() {
106             @Override
107             public void onClick(View v) {
108                 // Rotate selection to the next valid choice.
109                 mActiveSelection = (mActiveSelection + 1) % SELECTION_COUNT;
110                 // Send an AccessibilityEvent, since the user has interacted with the view.
111                 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
112                 // Redraw the entire view. (Inefficient, but this is sufficient for demonstration
113                 // purposes.)
114                 invalidate();
115             }
116         });
117     }
118 
119     /**
120      * This is where a View should populate outgoing accessibility events with its text content.
121      * While this method is free to modify event attributes other than text content, doing so
122      * should normally be performed in
123      * {@link #onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)}.
124      * <p/>
125      * <p>Note that the behavior of this method will typically vary, depending on the type of
126      * accessibility event is passed into it. The allowed values also very, and are documented
127      * in {@link android.view.accessibility.AccessibilityEvent}.
128      * <p/>
129      * <p>Typically, this is where you'll describe the state of your custom view. You may also
130      * want to provide custom directions when the user has focused your view.
131      *
132      * @param event The accessibility event which to populate.
133      */
134     // BEGIN_INCLUDE (on_populate_accessibility_event)
135     @Override
136     @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
onPopulateAccessibilityEvent(AccessibilityEvent event)137     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
138         super.onPopulateAccessibilityEvent(event);
139 
140         // Detect what type of accessibility event is being passed in.
141         int eventType = event.getEventType();
142 
143         // Common case: The user has interacted with our view in some way. State may or may not
144         // have been changed. Read out the current status of the view.
145         //
146         // We also set some other metadata which is not used by TalkBack, but could be used by
147         // other TTS engines.
148         if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED ||
149                 eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
150             event.getText().add("Mode selected: " + Integer.toString(mActiveSelection + 1) + ".");
151             event.setItemCount(SELECTION_COUNT);
152             event.setCurrentItemIndex(mActiveSelection);
153         }
154 
155         // When a user first focuses on our view, we'll also read out some simple instructions to
156         // make it clear that this is an interactive element.
157         if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
158             event.getText().add("Tap to change.");
159         }
160     }
161     // END_INCLUDE (on_populate_accessibility_event)
162 
163     /**
164      * This is called during layout when the size of this view has changed. If
165      * you were just added to the view hierarchy, you're called with the old
166      * values of 0.
167      *
168      * <p>This is where we determine the drawing bounds for our custom view.
169      *
170      * @param w    Current width of this view.
171      * @param h    Current height of this view.
172      * @param oldw Old width of this view.
173      * @param oldh Old height of this view.
174      */
175     @Override
onSizeChanged(int w, int h, int oldw, int oldh)176     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
177         // Account for padding
178         float xPadding = (float) (getPaddingLeft() + getPaddingRight());
179         float yPadding = (float) (getPaddingTop() + getPaddingBottom());
180 
181         // Compute available width/height
182         mWidth = w;
183         mHeight = h;
184         mWidthPadded = w - xPadding;
185         mHeightPadded = h - yPadding;
186         mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);
187     }
188 
189     /**
190      * Render view content.
191      *
192      * <p>We render an outer grey circle to serve as our "dial", and then render a smaller black
193      * circle to server as our indicator. The position for the indicator is determined based
194      * on mActiveSelection.
195      *
196      * @param canvas the canvas on which the background will be drawn
197      */
198     @Override
onDraw(Canvas canvas)199     protected void onDraw(Canvas canvas) {
200         super.onDraw(canvas);
201         // Draw dial
202         canvas.drawCircle(mWidth / 2, mHeight / 2, (float) mRadius, mDialPaint);
203 
204         // Draw text labels
205         final float labelRadius = mRadius + 10;
206         for (int i = 0; i < SELECTION_COUNT; i++) {
207             float[] xyData = computeXYForPosition(i, labelRadius);
208             float x = xyData[0];
209             float y = xyData[1];
210             canvas.drawText(Integer.toString(i + 1), x, y, mTextPaint);
211         }
212 
213         // Draw indicator mark
214         final float markerRadius = mRadius - 35;
215         float[] xyData = computeXYForPosition(mActiveSelection, markerRadius);
216         float x = xyData[0];
217         float y = xyData[1];
218         canvas.drawCircle(x, y, 20, mTextPaint);
219     }
220 
221     /**
222      * Compute the X/Y-coordinates for a label or indicator, given the position number and radius
223      * where the label should be drawn.
224      *
225      * @param pos    Zero based position index
226      * @param radius Radius where label/indicator is to be drawn.
227      * @return 2-element array. Element 0 is X-coordinate, element 1 is Y-coordinate.
228      */
computeXYForPosition(final int pos, final float radius)229     private float[] computeXYForPosition(final int pos, final float radius) {
230         float[] result = new float[2];
231         Double startAngle = Math.PI * (9 / 8d);   // Angles are in radiansq
232         Double angle = startAngle + (pos * (Math.PI / 4));
233         result[0] = (float) (radius * Math.cos(angle)) + (mWidth / 2);
234         result[1] = (float) (radius * Math.sin(angle)) + (mHeight / 2);
235         return result;
236     }
237 }
238