summaryrefslogtreecommitdiff
path: root/samples/browseable/BasicAccessibility/src/com.example.android.basicaccessibility/DialView.java
blob: efdb44985cee3bb9e34a1f52f83d24686b084a6d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.basicaccessibility;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;

/**
 * Custom view to demonstrate accessibility.
 *
 * <p>This view does not use any framework widgets, so does not get any accessibility features
 * automatically. Instead, we use {@link android.view.accessibility.AccessibilityEvent} to provide accessibility hints to
 * the OS.
 *
 * <p>For example, if TalkBack is enabled, users will be able to receive spoken feedback as they
 * interact with this view.
 *
 * <p>More generally, this view renders a multi-position "dial" that can be used to select a value
 * between 1 and 4. Each time the dial is clicked, the next position will be selected (modulo
 * the maximum number of positions).
 */
public class DialView extends View {
    private static int SELECTION_COUNT = 4;

    private static float FONT_SIZE = 40f;
    private float mWidth;
    private float mHeight;
    private float mWidthPadded;
    private float mHeightPadded;
    private Paint mTextPaint;
    private Paint mDialPaint;
    private float mRadius;
    private int mActiveSelection;

    /**
     * Constructor that is called when inflating a view from XML. This is called
     * when a view is being constructed from an XML file, supplying attributes
     * that were specified in the XML file.
     *
     * <p>In our case, this constructor just calls init().
     *
     * @param context The Context the view is running in, through which it can
     *                access the current theme, resources, etc.
     * @param attrs   The attributes of the XML tag that is inflating the view.
     * @see #View(android.content.Context, android.util.AttributeSet, int)
     */
    public DialView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * Helper method to initialize instance variables. Called by constructor.
     */
    private void init() {
        // Paint styles used for rendering are created here, rather than at render-time. This
        // is a performance optimization, since onDraw() will get called frequently.
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setTextSize(FONT_SIZE);

        mDialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDialPaint.setColor(Color.GRAY);

        // Initialize current selection. This will store where the dial's "indicator" is pointing.
        mActiveSelection = 0;

        // Setup onClick listener for this view. Rotates between each of the different selection
        // states on each click.
        //
        // Notice that we call sendAccessibilityEvent here. Some AccessibilityEvents are generated
        // by the system. However, custom views will typically need to send events manually as the
        // user interacts with the view. The type of event sent will vary, depending on the nature
        // of the view and how the user interacts with it.
        //
        // In this case, we are sending TYPE_VIEW_SELECTED rather than TYPE_VIEW_CLICKED, because
        // clicking on this view selects a new value.
        //
        // We will give our AccessibilityEvent further information about the state of the view in
        // onPopulateAccessibilityEvent(), which will be called automatically by the system
        // for each AccessibilityEvent.
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                // Rotate selection to the next valid choice.
                mActiveSelection = (mActiveSelection + 1) % SELECTION_COUNT;
                // Send an AccessibilityEvent, since the user has interacted with the view.
                sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
                // Redraw the entire view. (Inefficient, but this is sufficient for demonstration
                // purposes.)
                invalidate();
            }
        });
    }

    /**
     * This is where a View should populate outgoing accessibility events with its text content.
     * While this method is free to modify event attributes other than text content, doing so
     * should normally be performed in
     * {@link #onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent)}.
     * <p/>
     * <p>Note that the behavior of this method will typically vary, depending on the type of
     * accessibility event is passed into it. The allowed values also very, and are documented
     * in {@link android.view.accessibility.AccessibilityEvent}.
     * <p/>
     * <p>Typically, this is where you'll describe the state of your custom view. You may also
     * want to provide custom directions when the user has focused your view.
     *
     * @param event The accessibility event which to populate.
     */
    // BEGIN_INCLUDE (on_populate_accessibility_event)
    @Override
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(event);

        // Detect what type of accessibility event is being passed in.
        int eventType = event.getEventType();

        // Common case: The user has interacted with our view in some way. State may or may not
        // have been changed. Read out the current status of the view.
        //
        // We also set some other metadata which is not used by TalkBack, but could be used by
        // other TTS engines.
        if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED ||
                eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
            event.getText().add("Mode selected: " + Integer.toString(mActiveSelection + 1) + ".");
            event.setItemCount(SELECTION_COUNT);
            event.setCurrentItemIndex(mActiveSelection);
        }

        // When a user first focuses on our view, we'll also read out some simple instructions to
        // make it clear that this is an interactive element.
        if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
            event.getText().add("Tap to change.");
        }
    }
    // END_INCLUDE (on_populate_accessibility_event)

    /**
     * This is called during layout when the size of this view has changed. If
     * you were just added to the view hierarchy, you're called with the old
     * values of 0.
     *
     * <p>This is where we determine the drawing bounds for our custom view.
     *
     * @param w    Current width of this view.
     * @param h    Current height of this view.
     * @param oldw Old width of this view.
     * @param oldh Old height of this view.
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        // Account for padding
        float xPadding = (float) (getPaddingLeft() + getPaddingRight());
        float yPadding = (float) (getPaddingTop() + getPaddingBottom());

        // Compute available width/height
        mWidth = w;
        mHeight = h;
        mWidthPadded = w - xPadding;
        mHeightPadded = h - yPadding;
        mRadius = (float) (Math.min(mWidth, mHeight) / 2 * 0.8);
    }

    /**
     * Render view content.
     *
     * <p>We render an outer grey circle to serve as our "dial", and then render a smaller black
     * circle to server as our indicator. The position for the indicator is determined based
     * on mActiveSelection.
     *
     * @param canvas the canvas on which the background will be drawn
     */
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // Draw dial
        canvas.drawCircle(mWidth / 2, mHeight / 2, (float) mRadius, mDialPaint);

        // Draw text labels
        final float labelRadius = mRadius + 10;
        for (int i = 0; i < SELECTION_COUNT; i++) {
            float[] xyData = computeXYForPosition(i, labelRadius);
            float x = xyData[0];
            float y = xyData[1];
            canvas.drawText(Integer.toString(i + 1), x, y, mTextPaint);
        }

        // Draw indicator mark
        final float markerRadius = mRadius - 35;
        float[] xyData = computeXYForPosition(mActiveSelection, markerRadius);
        float x = xyData[0];
        float y = xyData[1];
        canvas.drawCircle(x, y, 20, mTextPaint);
    }

    /**
     * Compute the X/Y-coordinates for a label or indicator, given the position number and radius
     * where the label should be drawn.
     *
     * @param pos    Zero based position index
     * @param radius Radius where label/indicator is to be drawn.
     * @return 2-element array. Element 0 is X-coordinate, element 1 is Y-coordinate.
     */
    private float[] computeXYForPosition(final int pos, final float radius) {
        float[] result = new float[2];
        Double startAngle = Math.PI * (9 / 8d);   // Angles are in radiansq
        Double angle = startAngle + (pos * (Math.PI / 4));
        result[0] = (float) (radius * Math.cos(angle)) + (mWidth / 2);
        result[1] = (float) (radius * Math.sin(angle)) + (mHeight / 2);
        return result;
    }
}