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