BasicAccessibility / src / com.example.android.basicaccessibility /

DialView.java

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
     */
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
     */
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
     */
135
    @Override
136
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
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
    }
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
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
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
     */
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
}