view src/uk/ac/qmul/eecs/ccmi/accessibility/LayoutSonifier.java @ 1:66b3a838feca logging tip

Added logging of user interaction
author Fiore Martin <fiore@eecs.qmul.ac.uk>
date Tue, 12 Feb 2013 15:31:48 +0000
parents e0ee6ac3a45f
children
line wrap: on
line source
/*  
 CCmI Diagram Editor for Android 
  
 Copyright (C) 2012  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.qmul.eecs.ccmi.accessibility;

import uk.ac.qmul.eecs.ccmi.utilities.ILogger;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Spinner;
import android.widget.TextView;

/**
 * The {@code LayoutSonifier} provides the sonification of a {@code View}. When a {@code View} is sonified 
 * the user can swipe on it with their finger and receive audio and haptic cues about it.
 *  
 * The {@code View} is rendered in audio by uttering (through the {@code Android} test to speech synthesizer) the content 
 * description, as per {@code getContentDescription()}. Furthermore, if the {@code View} is also a {@code TextView}
 * the text is uttered too, right after the content description. Haptic rendering is just a vibration of the device.    
 *    
 * A {@code View} that is also a {@code ViewGroup} is recursively searched in its children until the non {@code ViewGruop}
 * that is under the coordinates of the user touch is found.        
 *    
 * The way each {@code View} is sonified can be customized by subclassing this class and providing a custom 
 * implementation of {@code SonifyView()}. 
 *
 */
public class LayoutSonifier {
	private static LayoutSonifier singleton;
	/* in order not to utter the view's */
	private View lastTouchedView;
	
	/**
	 * static factory method enforces the existance of only one instance of this class at time (singleton) 
	 * 
	 * @return an instance of {@code LayoutSonifier} 
	 */
	public static LayoutSonifier getInstance(){
		if(singleton == null)
			singleton = new LayoutSonifier();
		return singleton;
	}
	
	/* privatize the constructor to enforce singleton */
	protected LayoutSonifier(){}
	
	public boolean onTouch(View view, MotionEvent evt, AccessibilityService accessibilityService) {
		if(accessibilityService == null)
			throw new IllegalArgumentException("accessibilityService cannot be null");
		switch(evt.getAction()){
		case MotionEvent.ACTION_MOVE : {
			if(evt.getPointerCount() > 1) // double finger is for scrolling 
				return false;
			View newView = findView((ViewGroup)view,evt.getX(),evt.getY(),view.getLeft(),view.getTop());
			if(newView != lastTouchedView){
				sonifyView(newView,accessibilityService);
				lastTouchedView = newView;
				return false;
			}else{
				return true;
			}
		}
		case MotionEvent.ACTION_UP : {
			lastTouchedView = null;
			return false;
		}
		default : return false;
		}
	}
	
	/**
	 * This method is called when the view under the user's finger is found. The content 
	 * description (as per {@code View.getContentDesctpion()} is uttered if not null, otherwise,
	 * if the View is a text view, the string returned by {@code TextView.getText()} is uttered.    
	 *  
	 * A vibration is triggered regardless of the type of the View.
	 * 
	 * Subclasses can overwrite this method to provide their own sonification of the view touched
	 * by the user. Unless it's an {@code AccessibleSpinner} or a  {@code AccessibleCheckbox} 
	 * is never a {@code ViewGroup} as if a {@code ViewGroup} is met the view it contains 
	 * are recursively searched for the one under the user's finger.  
	 *  
	 * @param v the view touched by the user 
	 * @param accessibilityService the accessibility service used to sonify the view 
	 */
	protected void sonifyView(View v, AccessibilityService accessibilityService){
		if(v == null)
			return;
		if(v instanceof TextView ){
			accessibilityService.vibrate();
			CharSequence contentDescription = v.getContentDescription();
			/* if the view has accessibility content description set, use it */
			if(contentDescription != null ){
				accessibilityService.speak(contentDescription.toString() + 
						((v instanceof TextView) ? ((TextView)v).getText() : "") );
				ILogger.logHover(contentDescription.toString());
			}else{
				accessibilityService.speak(((TextView)v).getText().toString());
				ILogger.logHover(((TextView)v).getText().toString());
			}
		}else if(v instanceof AccessibleCheckbox){
			AccessibleCheckbox ab = (AccessibleCheckbox)v;
			boolean isChecked = ab.getChecks()[ab.getSelectedValuePosition()];
			accessibilityService.vibrate();
			accessibilityService.speak(ab.getSelectedValue()+ (isChecked ? ", checked" : ", unchecked"));
			ILogger.logHover("check box");
		}else if (v instanceof Spinner){
			accessibilityService.vibrate();
			accessibilityService.speak(v.getContentDescription()+" "+((Spinner)v).getSelectedItem());
			ILogger.logHover("spinner");
		}
	}
	
	private static View findView(ViewGroup viewGroup, float x, float y,  float offsetX, float offsetY){
		x-= offsetX;
		y-= offsetY;
		
		/* look for other views which one is under the finger of the user */
		for(int i=0; i<viewGroup.getChildCount(); i++){
			View v = viewGroup.getChildAt(i);
			if(y > v.getTop() && y < (v.getTop()+v.getHeight()) &&  
					x > v.getLeft() && x < (v.getLeft()+v.getWidth()) ){
				if(v instanceof ViewGroup){
					if(v instanceof AccessibleSpinner || v instanceof AccessibleCheckbox)
						return v;
					return findView((ViewGroup)v,x,y,v.getLeft(),v.getTop());
				}else{
					return v;
				}
			}
		}	
		return null;
	}
}