/widgets/access-shim/src/com/googlecode/eyesfree/widget/AccessibleFrameLayout.java

http://eyes-free.googlecode.com/ · Java · 532 lines · 342 code · 95 blank · 95 comment · 56 complexity · 62910d66064555f2424d257b68e20fed MD5 · raw file

  1. /*
  2. * Copyright (C) 2011 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * 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, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.googlecode.eyesfree.widget;
  17. import android.app.Instrumentation;
  18. import android.content.ContentResolver;
  19. import android.content.Context;
  20. import android.graphics.Canvas;
  21. import android.graphics.Matrix;
  22. import android.graphics.Paint;
  23. import android.graphics.Rect;
  24. import android.os.Handler;
  25. import android.os.SystemClock;
  26. import android.os.Vibrator;
  27. import android.provider.Settings;
  28. import android.speech.tts.TextToSpeech;
  29. import android.view.GestureDetector;
  30. import android.view.KeyEvent;
  31. import android.view.MotionEvent;
  32. import android.view.MotionEvent.PointerCoords;
  33. import android.view.View;
  34. import android.view.ViewGroup;
  35. import android.view.ViewParent;
  36. import android.view.ViewTreeObserver;
  37. import android.view.accessibility.AccessibilityEvent;
  38. import android.widget.AbsListView;
  39. import android.widget.FrameLayout;
  40. /**
  41. * @author alanv@google.com (Alan Viverette)
  42. */
  43. public class AccessibleFrameLayout extends FrameLayout {
  44. private static final boolean ENABLE_VIBRATE = false;
  45. private final Handler mHandler;
  46. private final Instrumentation mInstrumentation;
  47. private final GestureDetector mDetector;
  48. private final Vibrator mVibrator;
  49. private final Paint mPaint;
  50. private final TextToSpeech mTTS;
  51. private final Rect mSelectedRect;
  52. private final boolean mCompatibilityMode;
  53. private View mSelectedView;
  54. private boolean mExplorationEnabled;
  55. private boolean mSpeechAvailable;
  56. // private static final long[] mFocusGainedFocusablePattern = new long[] {
  57. // 0, 100 };
  58. private static final long[] mFocusLostFocusablePattern = new long[] { 0, 50 };
  59. // private static final long[] mFocusGainedPattern = new long[] { 0, 50 };
  60. private static final long[] mFocusLostPattern = new long[] { 0, 15 };
  61. public AccessibleFrameLayout(Context context) {
  62. super(context);
  63. mHandler = new Handler();
  64. mInstrumentation = new Instrumentation();
  65. if (ENABLE_VIBRATE) {
  66. mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
  67. } else {
  68. mVibrator = null;
  69. }
  70. mDetector = new GestureDetector(context, gestureListener);
  71. mDetector.setIsLongpressEnabled(false);
  72. mSelectedView = null;
  73. mSelectedRect = new Rect();
  74. mPaint = new Paint();
  75. mPaint.setStyle(Paint.Style.STROKE);
  76. mPaint.setStrokeJoin(Paint.Join.ROUND);
  77. mPaint.setStrokeWidth(3);
  78. mPaint.setColor(0xFF8CD2FF);
  79. mSelectedView = null;
  80. mSpeechAvailable = false;
  81. mTTS = new TextToSpeech(context, ttsInit);
  82. updateExplorationEnabled();
  83. getViewTreeObserver().addOnGlobalFocusChangeListener(focusChangeListener);
  84. boolean compatibilityMode = true;
  85. try {
  86. Class.forName("android.view.MotionEvent.PointerCoords");
  87. compatibilityMode = false;
  88. } catch (ClassNotFoundException e) {
  89. e.printStackTrace();
  90. }
  91. mCompatibilityMode = compatibilityMode;
  92. }
  93. @Override
  94. public void getHitRect(Rect rect) {
  95. getWindowVisibleDisplayFrame(rect);
  96. }
  97. @Override
  98. public void onWindowFocusChanged(boolean hasWindowFocus) {
  99. super.onWindowFocusChanged(hasWindowFocus);
  100. if (hasWindowFocus) {
  101. updateExplorationEnabled();
  102. }
  103. if (mExplorationEnabled) {
  104. if (hasWindowFocus) {
  105. // Update focus since it may have changed.
  106. View newFocusedView = findFocus();
  107. setSelectedView(newFocusedView, false);
  108. } else {
  109. setSelectedView(null, false);
  110. }
  111. }
  112. }
  113. private void updateExplorationEnabled() {
  114. ContentResolver resolver = getContext().getContentResolver();
  115. int enabled = Settings.Secure.getInt(resolver, Settings.Secure.ACCESSIBILITY_ENABLED, -1);
  116. mExplorationEnabled = (enabled == 1);
  117. }
  118. @Override
  119. protected void onDetachedFromWindow() {
  120. super.onDetachedFromWindow();
  121. mTTS.shutdown();
  122. }
  123. @Override
  124. protected void dispatchDraw(Canvas canvas) {
  125. super.dispatchDraw(canvas);
  126. if (mSelectedView != null) {
  127. getGlobalVisibleRect(mSelectedRect);
  128. int offsetTop = mSelectedRect.top;
  129. int offsetLeft = mSelectedRect.left;
  130. mSelectedView.getGlobalVisibleRect(mSelectedRect);
  131. if (offsetTop > 0 || offsetLeft > 0) {
  132. int saveCount = canvas.save();
  133. Matrix matrix = canvas.getMatrix();
  134. matrix.postTranslate(offsetLeft, offsetTop);
  135. canvas.setMatrix(matrix);
  136. canvas.drawRect(mSelectedRect, mPaint);
  137. canvas.restoreToCount(saveCount);
  138. } else {
  139. canvas.drawRect(mSelectedRect, mPaint);
  140. }
  141. }
  142. }
  143. /**
  144. * Inserts this frame between a ViewGroup and its children by removing all
  145. * child views from the parent view, adding them to this frame, and then
  146. * adding this frame to the parent view.
  147. *
  148. * @param parent The parent view into which this frame will be inserted.
  149. */
  150. public void inject(ViewGroup parent) {
  151. int count = parent.getChildCount();
  152. while (parent.getChildCount() > 0) {
  153. View child = parent.getChildAt(0);
  154. parent.removeViewAt(0);
  155. addView(child);
  156. }
  157. parent.addView(this);
  158. }
  159. /**
  160. * Strips the last pointer from a {@link MotionEvent} and returns the
  161. * modified event. Does not modify the original event.
  162. *
  163. * @param ev The MotionEvent to modify.
  164. * @return The modified MotionEvent.
  165. */
  166. private MotionEvent stripLastPointer(MotionEvent ev) {
  167. ev.getPointerCount();
  168. int removePointer = ev.getPointerCount() - 1;
  169. int removePointerId = ev.getPointerId(removePointer);
  170. long downTime = ev.getDownTime();
  171. long eventTime = ev.getEventTime();
  172. int action = ev.getAction();
  173. int pointers = ev.getPointerCount() - 1;
  174. int[] pointerIds = new int[pointers];
  175. int metaState = ev.getMetaState();
  176. float xPrecision = ev.getXPrecision();
  177. float yPrecision = ev.getYPrecision();
  178. int deviceId = ev.getDeviceId();
  179. int edgeFlags = ev.getEdgeFlags();
  180. switch (ev.getActionMasked()) {
  181. case MotionEvent.ACTION_POINTER_DOWN:
  182. case MotionEvent.ACTION_POINTER_UP:
  183. action -= 0x100;
  184. if (pointers == 1) {
  185. action -= 0x5;
  186. }
  187. break;
  188. }
  189. MotionEvent event = null;
  190. if (mCompatibilityMode) {
  191. float x = ev.getX();
  192. float y = ev.getY();
  193. float pressure = ev.getPressure();
  194. float size = ev.getSize();
  195. event = MotionEvent.obtain(downTime, eventTime, action, pointers, x, y, pressure, size,
  196. metaState, xPrecision, yPrecision, deviceId, edgeFlags);
  197. } else {
  198. PointerCoords[] pointerCoords = new PointerCoords[pointers];
  199. int source = ev.getSource();
  200. int flags = ev.getFlags();
  201. for (int i = 0; i < pointers; i++) {
  202. pointerIds[i] = ev.getPointerId(i);
  203. pointerCoords[i] = new PointerCoords();
  204. ev.getPointerCoords(i, pointerCoords[i]);
  205. }
  206. event = MotionEvent.obtain(downTime, eventTime, action, pointers, pointerIds,
  207. pointerCoords, metaState, xPrecision, yPrecision, deviceId, edgeFlags, source,
  208. flags);
  209. }
  210. return event;
  211. }
  212. @Override
  213. public boolean dispatchTouchEvent(MotionEvent ev) {
  214. if (mExplorationEnabled) {
  215. int pointers = ev.getPointerCount();
  216. if (ev.getPointerCount() == 1) {
  217. ViewGroup target = this;
  218. if (ev.getAction() == MotionEvent.ACTION_OUTSIDE) {
  219. ev.setAction(MotionEvent.ACTION_DOWN);
  220. mDetector.onTouchEvent(ev);
  221. ev.setAction(MotionEvent.ACTION_UP);
  222. }
  223. mDetector.onTouchEvent(ev);
  224. return true;
  225. }
  226. ev = stripLastPointer(ev);
  227. }
  228. return super.dispatchTouchEvent(ev);
  229. }
  230. /**
  231. * Emulates a tap event positioned at the center of the selected view. No-op
  232. * if no view is selected.
  233. */
  234. private void tapSelectedView() {
  235. if (mSelectedView == null) {
  236. return;
  237. }
  238. final float centerX = mSelectedRect.exactCenterX();
  239. final float centerY = mSelectedRect.exactCenterY();
  240. final long currTime = SystemClock.uptimeMillis();
  241. MotionEvent down = MotionEvent.obtain(
  242. currTime, currTime, MotionEvent.ACTION_DOWN, centerX, centerY, 0);
  243. MotionEvent up = MotionEvent.obtain(
  244. currTime, currTime, MotionEvent.ACTION_UP, centerX, centerY, 0);
  245. super.dispatchTouchEvent(down);
  246. super.dispatchTouchEvent(up);
  247. }
  248. /**
  249. * Emulates a directional pad event based on the given flick direction.
  250. *
  251. * @param direction A flick direction constant.
  252. */
  253. private void changeFocus(int direction) {
  254. int keyCode = KeyEvent.KEYCODE_UNKNOWN;
  255. switch (direction) {
  256. case FlickGestureListener.FLICK_UP:
  257. keyCode = KeyEvent.KEYCODE_DPAD_UP;
  258. break;
  259. case FlickGestureListener.FLICK_RIGHT:
  260. keyCode = KeyEvent.KEYCODE_DPAD_RIGHT;
  261. break;
  262. case FlickGestureListener.FLICK_DOWN:
  263. keyCode = KeyEvent.KEYCODE_DPAD_DOWN;
  264. break;
  265. case FlickGestureListener.FLICK_LEFT:
  266. keyCode = KeyEvent.KEYCODE_DPAD_LEFT;
  267. break;
  268. default:
  269. // Invalid flick constant.
  270. return;
  271. }
  272. if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
  273. final int keyCodeFinal = keyCode;
  274. // Exit from touch mode as gracefully as possible.
  275. if (mSelectedView != null) {
  276. ViewParent parent = mSelectedView.getParent();
  277. mSelectedView.requestFocusFromTouch();
  278. // If the selected view belongs to a list, make sure it's
  279. // selected within the list (since it's not focusable).
  280. // TODO(alanv): Check whether the prior call was successful?
  281. if (parent instanceof AbsListView) {
  282. AbsListView listParent = (AbsListView) parent;
  283. int position = listParent.getPositionForView(mSelectedView);
  284. listParent.setSelection(position);
  285. }
  286. } else {
  287. requestFocusFromTouch();
  288. }
  289. // We have to send the key event on a separate thread, then return
  290. // to the main thread to synchronize the selected view with focus.
  291. new Thread() {
  292. @Override
  293. public void run() {
  294. mInstrumentation.sendKeyDownUpSync(keyCodeFinal);
  295. mHandler.post(new Runnable() {
  296. @Override
  297. public void run() {
  298. View newFocusedView = findFocus();
  299. setSelectedView(newFocusedView, false);
  300. }
  301. });
  302. }
  303. }.start();
  304. }
  305. }
  306. /**
  307. * Searches for a selectable view under the given frame-relative
  308. * coordinates.
  309. *
  310. * @param x Frame-relative X coordinate.
  311. * @param y Frame-relative Y coordinate.
  312. */
  313. private void setSelectionAtPoint(int x, int y) {
  314. View selection = SelectionFinder.getSelectionAtPoint(mSelectedView, this, x, y);
  315. setSelectedView(selection, true);
  316. }
  317. /**
  318. * Sets the selected view and optionally announces it through TalkBack.
  319. *
  320. * @param selectedView The {@link View} to set as the current selection.
  321. * @param announce Set to <code>true</code> to announce selection changes.
  322. */
  323. private void setSelectedView(View selectedView, boolean announce) {
  324. if (mSelectedView == selectedView) {
  325. return;
  326. }
  327. if (mSelectedView != null) {
  328. announceSelectionLost(mSelectedView);
  329. }
  330. if (selectedView != null) {
  331. if (selectedView instanceof AbsListView) {
  332. AbsListView absListView = (AbsListView) selectedView;
  333. View item = absListView.getSelectedView();
  334. if (item != null) {
  335. selectedView = item;
  336. } else {
  337. // We don't want to select list containers, so if there's no
  338. // selected element then we'll just select nothing.
  339. selectedView = null;
  340. }
  341. }
  342. if (selectedView != null && announce) {
  343. announceSelectionGained(selectedView);
  344. }
  345. }
  346. mSelectedView = selectedView;
  347. invalidate();
  348. }
  349. /**
  350. * Updates the selection rectangle and attempts to shift focus away from the
  351. * provided view. Clears active TTS.
  352. *
  353. * @param view
  354. */
  355. private void announceSelectionLost(View view) {
  356. // TODO(alanv): Add an additional TYPE_VIEW_HOVER_OFF event type that
  357. // fires a KickBack vibration and (probably) clears active TalkBack
  358. // utterances.
  359. if (mSpeechAvailable) {
  360. mTTS.speak("", TextToSpeech.QUEUE_FLUSH, null);
  361. }
  362. if (mVibrator != null) {
  363. if (view.isFocusable()) {
  364. mVibrator.vibrate(mFocusLostFocusablePattern, -1);
  365. } else {
  366. mVibrator.vibrate(mFocusLostPattern, -1);
  367. }
  368. }
  369. mSelectedRect.setEmpty();
  370. }
  371. /**
  372. * Updates the selection rectangle and attempts to shift focus to the
  373. * provided view. If the view is not focusable, fires an AccessibilityEvent
  374. * so that it is read aloud.
  375. *
  376. * @param view The view which has gained selection.
  377. * @return Returns the view that actually gained selection.
  378. */
  379. private View announceSelectionGained(View view) {
  380. // TODO(alanv): Add an additional TYPE_VIEW_HOVER event type with a
  381. // different KickBack response. Otherwise everything looks like a
  382. // button.
  383. if (mSpeechAvailable) {
  384. mTTS.speak("", TextToSpeech.QUEUE_FLUSH, null);
  385. }
  386. view.getGlobalVisibleRect(mSelectedRect);
  387. // If the view is focusable, request focus. This will automatically read
  388. // the the view's content description using TalkBack (if enabled).
  389. // if (view.requestFocusFromTouch()) {
  390. // return view.findFocus();
  391. // }
  392. // If the view is not focusable, force it to send an AccessibilityEvent.
  393. // TODO(alanv): This seems to retain the contentDescription from a
  394. // previous event.
  395. view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
  396. return view;
  397. }
  398. private final FlickGestureListener gestureListener = new FlickGestureListener() {
  399. @Override
  400. protected boolean onSingleTap(float x, float y, float rawX, float rawY) {
  401. setSelectionAtPoint((int) x, (int) y);
  402. return true;
  403. }
  404. @Override
  405. protected boolean onDoubleTap(float x, float y, float rawX, float rawY) {
  406. tapSelectedView();
  407. return true;
  408. }
  409. @Override
  410. protected boolean onFlick(float x, float y, float rawX, float rawY, int direction) {
  411. changeFocus(direction);
  412. return true;
  413. }
  414. @Override
  415. protected boolean onMove(float x, float y, float rawX, float rawY) {
  416. setSelectionAtPoint((int) x, (int) y);
  417. return true;
  418. }
  419. };
  420. private final TextToSpeech.OnInitListener ttsInit = new TextToSpeech.OnInitListener() {
  421. @Override
  422. public void onInit(int status) {
  423. if (status == TextToSpeech.SUCCESS) {
  424. mSpeechAvailable = true;
  425. }
  426. }
  427. };
  428. private final ViewTreeObserver.OnGlobalFocusChangeListener focusChangeListener =
  429. new ViewTreeObserver.OnGlobalFocusChangeListener() {
  430. @Override
  431. public void onGlobalFocusChanged(View oldFocus, View newFocus) {
  432. if (newFocus != null) {
  433. setSelectedView(newFocus, false);
  434. }
  435. }
  436. };
  437. }