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.supportv4.widget; 18 19 import android.annotation.TargetApi; 20 import android.app.Activity; 21 import android.content.Context; 22 import android.graphics.Canvas; 23 import android.graphics.Color; 24 import android.graphics.Paint; 25 import android.graphics.Paint.Align; 26 import android.graphics.Paint.Style; 27 import android.graphics.Rect; 28 import android.graphics.RectF; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.support.v4.view.ViewCompat; 32 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; 33 import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; 34 import android.support.v4.widget.ExploreByTouchHelper; 35 import android.util.AttributeSet; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.accessibility.AccessibilityEvent; 39 import com.example.android.supportv4.R; 40 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * This example shows how to use the {@link ExploreByTouchHelper} class in the 46 * Android support library to add accessibility support to a custom view that 47 * represents multiple logical items. 48 * <p> 49 * The {@link ExploreByTouchHelper} class wraps 50 * {@link AccessibilityNodeProviderCompat} and simplifies exposing information 51 * about a custom view's logical structure to accessibility services. 52 * <p> 53 * The custom view in this example is responsible for: 54 * <ul> 55 * <li>Creating a helper class that extends {@link ExploreByTouchHelper} 56 * <li>Setting the helper as the accessibility delegate using 57 * {@link ViewCompat#setAccessibilityDelegate} 58 * <li>Dispatching hover events to the helper in {@link View#dispatchHoverEvent} 59 * </ul> 60 * <p> 61 * The helper class implementation in this example is responsible for: 62 * <ul> 63 * <li>Mapping hover event coordinates to logical items 64 * <li>Exposing information about logical items to accessibility services 65 * <li>Handling accessibility actions 66 * <ul> 67 */ 68 public class ExploreByTouchHelperActivity extends Activity { 69 @Override 70 protected void onCreate(Bundle savedInstanceState) { 71 super.onCreate(savedInstanceState); 72 73 setContentView(R.layout.explore_by_touch_helper); 74 75 final CustomView customView = (CustomView) findViewById(R.id.custom_view); 76 77 // Adds an item at the top-left quarter of the custom view. 78 customView.addItem(getString(R.string.sample_item_a), 0, 0, 0.5f, 0.5f); 79 80 // Adds an item at the bottom-right quarter of the custom view. 81 customView.addItem(getString(R.string.sample_item_b), 0.5f, 0.5f, 1, 1); 82 } 83 84 /** 85 * Simple custom view that draws rectangular items to the screen. Each item 86 * has a checked state that may be toggled by tapping on the item. 87 */ 88 public static class CustomView extends View { 89 private static final int NO_ITEM = -1; 90 91 private final Paint mPaint = new Paint(); 92 private final Rect mTempBounds = new Rect(); 93 private final List<CustomItem> mItems = new ArrayList<CustomItem>(); 94 private CustomViewTouchHelper mTouchHelper; 95 96 public CustomView(Context context, AttributeSet attrs) { 97 super(context, attrs); 98 99 // Set up accessibility helper class. 100 mTouchHelper = new CustomViewTouchHelper(this); 101 ViewCompat.setAccessibilityDelegate(this, mTouchHelper); 102 } 103 104 @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 105 @Override 106 public boolean dispatchHoverEvent(MotionEvent event) { 107 // Always attempt to dispatch hover events to accessibility first. 108 if (mTouchHelper.dispatchHoverEvent(event)) { 109 return true; 110 } 111 112 return super.dispatchHoverEvent(event); 113 } 114 115 @Override 116 public boolean onTouchEvent(MotionEvent event) { 117 switch (event.getAction()) { 118 case MotionEvent.ACTION_DOWN: 119 return true; 120 case MotionEvent.ACTION_UP: 121 final int itemIndex = getItemIndexUnder(event.getX(), event.getY()); 122 if (itemIndex >= 0) { 123 onItemClicked(itemIndex); 124 } 125 return true; 126 } 127 128 return super.onTouchEvent(event); 129 } 130 131 /** 132 * Adds an item to the custom view. The item is positioned relative to 133 * the custom view bounds and its descriptions is drawn at its center. 134 * 135 * @param description The item's description. 136 * @param top Top coordinate as a fraction of the parent height, range 137 * is [0,1]. 138 * @param left Left coordinate as a fraction of the parent width, range 139 * is [0,1]. 140 * @param bottom Bottom coordinate as a fraction of the parent height, 141 * range is [0,1]. 142 * @param right Right coordinate as a fraction of the parent width, 143 * range is [0,1]. 144 */ 145 public void addItem(String description, float top, float left, float bottom, float right) { 146 final CustomItem item = new CustomItem(); 147 item.bounds = new RectF(top, left, bottom, right); 148 item.description = description; 149 item.checked = false; 150 mItems.add(item); 151 } 152 153 @Override 154 protected void onDraw(Canvas canvas) { 155 super.onDraw(canvas); 156 157 final Paint paint = mPaint; 158 final Rect bounds = mTempBounds; 159 final int height = getHeight(); 160 final int width = getWidth(); 161 162 for (CustomItem item : mItems) { 163 paint.setColor(item.checked ? Color.RED : Color.BLUE); 164 paint.setStyle(Style.FILL); 165 scaleRectF(item.bounds, bounds, width, height); 166 canvas.drawRect(bounds, paint); 167 paint.setColor(Color.WHITE); 168 paint.setTextAlign(Align.CENTER); 169 canvas.drawText(item.description, bounds.centerX(), bounds.centerY(), paint); 170 } 171 } 172 173 protected boolean onItemClicked(int index) { 174 final CustomItem item = getItem(index); 175 if (item == null) { 176 return false; 177 } 178 179 item.checked = !item.checked; 180 invalidate(); 181 182 // Since the item's checked state is exposed to accessibility 183 // services through its AccessibilityNodeInfo, we need to invalidate 184 // the item's virtual view. At some point in the future, the 185 // framework will obtain an updated version of the virtual view. 186 mTouchHelper.invalidateVirtualView(index); 187 188 // We also need to let the framework know what type of event 189 // happened. Accessibility services may use this event to provide 190 // appropriate feedback to the user. 191 mTouchHelper.sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED); 192 193 return true; 194 } 195 196 protected int getItemIndexUnder(float x, float y) { 197 final float scaledX = (x / getWidth()); 198 final float scaledY = (y / getHeight()); 199 final int n = mItems.size(); 200 201 for (int i = 0; i < n; i++) { 202 final CustomItem item = mItems.get(i); 203 if (item.bounds.contains(scaledX, scaledY)) { 204 return i; 205 } 206 } 207 208 return NO_ITEM; 209 } 210 211 protected CustomItem getItem(int index) { 212 if ((index < 0) || (index >= mItems.size())) { 213 return null; 214 } 215 216 return mItems.get(index); 217 } 218 219 protected static void scaleRectF(RectF in, Rect out, int width, int height) { 220 out.top = (int) (in.top * height); 221 out.bottom = (int) (in.bottom * height); 222 out.left = (int) (in.left * width); 223 out.right = (int) (in.right * width); 224 } 225 226 private class CustomViewTouchHelper extends ExploreByTouchHelper { 227 private final Rect mTempRect = new Rect(); 228 229 public CustomViewTouchHelper(View forView) { 230 super(forView); 231 } 232 233 @Override 234 protected int getVirtualViewAt(float x, float y) { 235 // We also perform hit detection in onTouchEvent(), and we can 236 // reuse that logic here. This will ensure consistency whether 237 // accessibility is on or off. 238 final int index = getItemIndexUnder(x, y); 239 if (index == NO_ITEM) { 240 return ExploreByTouchHelper.INVALID_ID; 241 } 242 243 return index; 244 } 245 246 @Override 247 protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { 248 // Since every item should be visible, and since we're mapping 249 // directly from item index to virtual view id, we can just add 250 // every available index in the item list. 251 final int n = mItems.size(); 252 for (int i = 0; i < n; i++) { 253 virtualViewIds.add(i); 254 } 255 } 256 257 @Override 258 protected void onPopulateEventForVirtualView( 259 int virtualViewId, AccessibilityEvent event) { 260 final CustomItem item = getItem(virtualViewId); 261 if (item == null) { 262 throw new IllegalArgumentException("Invalid virtual view id"); 263 } 264 265 // The event must be populated with text, either using 266 // getText().add() or setContentDescription(). Since the item's 267 // description is displayed visually, we'll add it to the event 268 // text. If it was only used for accessibility, we would use 269 // setContentDescription(). 270 event.getText().add(item.description); 271 } 272 273 @Override 274 protected void onPopulateNodeForVirtualView( 275 int virtualViewId, AccessibilityNodeInfoCompat node) { 276 final CustomItem item = getItem(virtualViewId); 277 if (item == null) { 278 throw new IllegalArgumentException("Invalid virtual view id"); 279 } 280 281 // Node and event text and content descriptions are usually 282 // identical, so we'll use the exact same string as before. 283 node.setText(item.description); 284 285 // Reported bounds should be consistent with those used to draw 286 // the item in onDraw(). They should also be consistent with the 287 // hit detection performed in getVirtualViewAt() and 288 // onTouchEvent(). 289 final Rect bounds = mTempRect; 290 final int height = getHeight(); 291 final int width = getWidth(); 292 scaleRectF(item.bounds, bounds, width, height); 293 node.setBoundsInParent(bounds); 294 295 // Since the user can tap an item, add the CLICK action. We'll 296 // need to handle this later in onPerformActionForVirtualView. 297 node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); 298 299 // This item has a checked state. 300 node.setCheckable(true); 301 node.setChecked(item.checked); 302 } 303 304 @Override 305 protected boolean onPerformActionForVirtualView( 306 int virtualViewId, int action, Bundle arguments) { 307 switch (action) { 308 case AccessibilityNodeInfoCompat.ACTION_CLICK: 309 // Click handling should be consistent with 310 // onTouchEvent(). This ensures that the view works the 311 // same whether accessibility is turned on or off. 312 return onItemClicked(virtualViewId); 313 } 314 315 return false; 316 } 317 318 } 319 320 public static class CustomItem { 321 private String description; 322 private RectF bounds; 323 private boolean checked; 324 } 325 } 326 } 327