fiore@0
|
1 /*
|
fiore@0
|
2 CCmI Diagram Editor for Android
|
fiore@0
|
3
|
fiore@0
|
4 Copyright (C) 2012 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
|
fiore@0
|
5
|
fiore@0
|
6 This program is free software: you can redistribute it and/or modify
|
fiore@0
|
7 it under the terms of the GNU General Public License as published by
|
fiore@0
|
8 the Free Software Foundation, either version 3 of the License, or
|
fiore@0
|
9 (at your option) any later version.
|
fiore@0
|
10
|
fiore@0
|
11 This program is distributed in the hope that it will be useful,
|
fiore@0
|
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
|
fiore@0
|
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
fiore@0
|
14 GNU General Public License for more details.
|
fiore@0
|
15
|
fiore@0
|
16 You should have received a copy of the GNU General Public License
|
fiore@0
|
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
|
fiore@0
|
18 */
|
fiore@0
|
19 package uk.ac.qmul.eecs.ccmi.accessibility;
|
fiore@0
|
20
|
fiore@1
|
21 import uk.ac.qmul.eecs.ccmi.utilities.ILogger;
|
fiore@0
|
22 import android.view.MotionEvent;
|
fiore@0
|
23 import android.view.View;
|
fiore@0
|
24 import android.view.ViewGroup;
|
fiore@0
|
25 import android.widget.Spinner;
|
fiore@0
|
26 import android.widget.TextView;
|
fiore@0
|
27
|
fiore@0
|
28 /**
|
fiore@0
|
29 * The {@code LayoutSonifier} provides the sonification of a {@code View}. When a {@code View} is sonified
|
fiore@0
|
30 * the user can swipe on it with their finger and receive audio and haptic cues about it.
|
fiore@0
|
31 *
|
fiore@0
|
32 * The {@code View} is rendered in audio by uttering (through the {@code Android} test to speech synthesizer) the content
|
fiore@0
|
33 * description, as per {@code getContentDescription()}. Furthermore, if the {@code View} is also a {@code TextView}
|
fiore@0
|
34 * the text is uttered too, right after the content description. Haptic rendering is just a vibration of the device.
|
fiore@0
|
35 *
|
fiore@0
|
36 * A {@code View} that is also a {@code ViewGroup} is recursively searched in its children until the non {@code ViewGruop}
|
fiore@0
|
37 * that is under the coordinates of the user touch is found.
|
fiore@0
|
38 *
|
fiore@0
|
39 * The way each {@code View} is sonified can be customized by subclassing this class and providing a custom
|
fiore@0
|
40 * implementation of {@code SonifyView()}.
|
fiore@0
|
41 *
|
fiore@0
|
42 */
|
fiore@0
|
43 public class LayoutSonifier {
|
fiore@0
|
44 private static LayoutSonifier singleton;
|
fiore@0
|
45 /* in order not to utter the view's */
|
fiore@0
|
46 private View lastTouchedView;
|
fiore@0
|
47
|
fiore@0
|
48 /**
|
fiore@0
|
49 * static factory method enforces the existance of only one instance of this class at time (singleton)
|
fiore@0
|
50 *
|
fiore@0
|
51 * @return an instance of {@code LayoutSonifier}
|
fiore@0
|
52 */
|
fiore@0
|
53 public static LayoutSonifier getInstance(){
|
fiore@0
|
54 if(singleton == null)
|
fiore@0
|
55 singleton = new LayoutSonifier();
|
fiore@0
|
56 return singleton;
|
fiore@0
|
57 }
|
fiore@0
|
58
|
fiore@0
|
59 /* privatize the constructor to enforce singleton */
|
fiore@0
|
60 protected LayoutSonifier(){}
|
fiore@0
|
61
|
fiore@0
|
62 public boolean onTouch(View view, MotionEvent evt, AccessibilityService accessibilityService) {
|
fiore@0
|
63 if(accessibilityService == null)
|
fiore@0
|
64 throw new IllegalArgumentException("accessibilityService cannot be null");
|
fiore@0
|
65 switch(evt.getAction()){
|
fiore@0
|
66 case MotionEvent.ACTION_MOVE : {
|
fiore@0
|
67 if(evt.getPointerCount() > 1) // double finger is for scrolling
|
fiore@0
|
68 return false;
|
fiore@0
|
69 View newView = findView((ViewGroup)view,evt.getX(),evt.getY(),view.getLeft(),view.getTop());
|
fiore@0
|
70 if(newView != lastTouchedView){
|
fiore@0
|
71 sonifyView(newView,accessibilityService);
|
fiore@0
|
72 lastTouchedView = newView;
|
fiore@0
|
73 return false;
|
fiore@0
|
74 }else{
|
fiore@0
|
75 return true;
|
fiore@0
|
76 }
|
fiore@0
|
77 }
|
fiore@0
|
78 case MotionEvent.ACTION_UP : {
|
fiore@0
|
79 lastTouchedView = null;
|
fiore@0
|
80 return false;
|
fiore@0
|
81 }
|
fiore@0
|
82 default : return false;
|
fiore@0
|
83 }
|
fiore@0
|
84 }
|
fiore@0
|
85
|
fiore@0
|
86 /**
|
fiore@0
|
87 * This method is called when the view under the user's finger is found. The content
|
fiore@0
|
88 * description (as per {@code View.getContentDesctpion()} is uttered if not null, otherwise,
|
fiore@0
|
89 * if the View is a text view, the string returned by {@code TextView.getText()} is uttered.
|
fiore@0
|
90 *
|
fiore@0
|
91 * A vibration is triggered regardless of the type of the View.
|
fiore@0
|
92 *
|
fiore@0
|
93 * Subclasses can overwrite this method to provide their own sonification of the view touched
|
fiore@0
|
94 * by the user. Unless it's an {@code AccessibleSpinner} or a {@code AccessibleCheckbox}
|
fiore@0
|
95 * is never a {@code ViewGroup} as if a {@code ViewGroup} is met the view it contains
|
fiore@0
|
96 * are recursively searched for the one under the user's finger.
|
fiore@0
|
97 *
|
fiore@0
|
98 * @param v the view touched by the user
|
fiore@0
|
99 * @param accessibilityService the accessibility service used to sonify the view
|
fiore@0
|
100 */
|
fiore@0
|
101 protected void sonifyView(View v, AccessibilityService accessibilityService){
|
fiore@0
|
102 if(v == null)
|
fiore@0
|
103 return;
|
fiore@0
|
104 if(v instanceof TextView ){
|
fiore@0
|
105 accessibilityService.vibrate();
|
fiore@0
|
106 CharSequence contentDescription = v.getContentDescription();
|
fiore@0
|
107 /* if the view has accessibility content description set, use it */
|
fiore@1
|
108 if(contentDescription != null ){
|
fiore@0
|
109 accessibilityService.speak(contentDescription.toString() +
|
fiore@0
|
110 ((v instanceof TextView) ? ((TextView)v).getText() : "") );
|
fiore@1
|
111 ILogger.logHover(contentDescription.toString());
|
fiore@1
|
112 }else{
|
fiore@0
|
113 accessibilityService.speak(((TextView)v).getText().toString());
|
fiore@1
|
114 ILogger.logHover(((TextView)v).getText().toString());
|
fiore@1
|
115 }
|
fiore@0
|
116 }else if(v instanceof AccessibleCheckbox){
|
fiore@0
|
117 AccessibleCheckbox ab = (AccessibleCheckbox)v;
|
fiore@0
|
118 boolean isChecked = ab.getChecks()[ab.getSelectedValuePosition()];
|
fiore@0
|
119 accessibilityService.vibrate();
|
fiore@0
|
120 accessibilityService.speak(ab.getSelectedValue()+ (isChecked ? ", checked" : ", unchecked"));
|
fiore@1
|
121 ILogger.logHover("check box");
|
fiore@0
|
122 }else if (v instanceof Spinner){
|
fiore@0
|
123 accessibilityService.vibrate();
|
fiore@0
|
124 accessibilityService.speak(v.getContentDescription()+" "+((Spinner)v).getSelectedItem());
|
fiore@1
|
125 ILogger.logHover("spinner");
|
fiore@0
|
126 }
|
fiore@0
|
127 }
|
fiore@0
|
128
|
fiore@0
|
129 private static View findView(ViewGroup viewGroup, float x, float y, float offsetX, float offsetY){
|
fiore@0
|
130 x-= offsetX;
|
fiore@0
|
131 y-= offsetY;
|
fiore@0
|
132
|
fiore@0
|
133 /* look for other views which one is under the finger of the user */
|
fiore@0
|
134 for(int i=0; i<viewGroup.getChildCount(); i++){
|
fiore@0
|
135 View v = viewGroup.getChildAt(i);
|
fiore@0
|
136 if(y > v.getTop() && y < (v.getTop()+v.getHeight()) &&
|
fiore@0
|
137 x > v.getLeft() && x < (v.getLeft()+v.getWidth()) ){
|
fiore@0
|
138 if(v instanceof ViewGroup){
|
fiore@0
|
139 if(v instanceof AccessibleSpinner || v instanceof AccessibleCheckbox)
|
fiore@0
|
140 return v;
|
fiore@0
|
141 return findView((ViewGroup)v,x,y,v.getLeft(),v.getTop());
|
fiore@0
|
142 }else{
|
fiore@0
|
143 return v;
|
fiore@0
|
144 }
|
fiore@0
|
145 }
|
fiore@0
|
146 }
|
fiore@0
|
147 return null;
|
fiore@0
|
148 }
|
fiore@0
|
149 }
|
fiore@0
|
150
|