/* * Copyright (C) 2011 Virginia Tech Department of Computer Science * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sofia.app.internal; import sofia.internal.events.EventDispatcher; import sofia.internal.events.OptionalEventDispatcher; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AbsListView; import android.widget.AbsSpinner; import android.widget.AdapterView; import android.widget.EditText; import android.widget.RatingBar; import android.widget.SeekBar; import android.widget.TextView; import java.util.HashMap; //------------------------------------------------------------------------- /** * Binds events from views and widgets to Sofia-style event handling methods * in the view's context. * * @author Tony Allevato */ public class EventBinder { //~ Fields ................................................................ private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android"; private static HashMap< Class<? extends View>, Binder<? extends View>> binders; private Object receiver; //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new {@code EventBinder} that dispatches events to the * specified receiver. * * @param receiver the object that will receive the event notifications */ public EventBinder(Object receiver) { this.receiver = receiver; } //~ Methods ............................................................... // ---------------------------------------------------------- /** * Binds any Sofia event handlers that apply to the specified view. * * @param view the view to bind events to * @param attrs the attribute set that was used during inflation (may be * null if this is called outside of the inflater) */ @SuppressWarnings("unchecked") public void bindEvents(View view, AttributeSet attrs) { Class<?> viewClass = view.getClass(); Binder<? extends View> binder = null; while (binder == null && !viewClass.equals(Object.class)) { binder = binders.get(viewClass); viewClass = viewClass.getSuperclass(); } if (binder != null) { ((Binder<View>) binder).bind(receiver, view, attrs); } } // ---------------------------------------------------------- /** * Gets the string name of the specified resource ID. * * @param context the context that contains the resources * @param id the ID to look up * @return the string name of the ID, or null if it was not found */ private static String getIdName(Context context, int id) { if (id != View.NO_ID) { return context.getResources().getResourceEntryName(id); } else { return null; } } //~ Nested classes and interfaces ......................................... // ---------------------------------------------------------- /** * An interface that represents how an event should be bound to a widget of * a particular type. * * Binders are added to the binder map such that only the binder for the * most specific subclass of a widget that has a binder will be called. If * the binder wants superclass bindings to be added as well, it must * explicitly chain a call to that binder. * * @param <ViewType> the actual view type */ private static interface Binder<ViewType> { // ---------------------------------------------------------- /** * Binds a listener for the event to the specified receiver. * * @param receiver the object that will receive the event notification * @param view the view sending the event * @param attrs the attributes set in the layout XML (if any) */ public void bind(Object receiver, ViewType view, AttributeSet attrs); } // ---------------------------------------------------------- /** * Implements the following binding rule: if a view is clickable, and it * does not already have a method bound to the onClick attribute, then * connect it to the context method "${id}Clicked", where "${id}" is the * string name of the view's identifier. */ private static Binder<View> ViewBinder = new Binder<View>() { @Override public void bind(final Object receiver, View view, AttributeSet attrs) { if (!AdapterView.class.isAssignableFrom(view.getClass()) && view.isClickable() && (attrs == null || attrs.getAttributeValue(ANDROID_NS, "onClick") == null)) { final String id = getIdName(view.getContext(), view.getId()); if (id != null) { final OptionalEventDispatcher event = new OptionalEventDispatcher(id + "Clicked", 0); try { view.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { event.dispatch(receiver, v); } }); } catch (Exception e) { // Do nothing. This try/catch was placed here because // it was discovered that setOnClickListener on an // AdapterView throws an exception (Barbara Liskov // would be furious) with a message directing you to // use setOnItemClickListener instead. For // future-proofing, we want to catch any such possible // exceptions and log them so that we can place // appropriate checks at the front of this method. Log.d("EventBinder", e.getMessage()); } } } } }; // ---------------------------------------------------------- /** * Implements the following binding rule: for each AbsListView, * attach an OnItemClickListener to it that calls the context method * "${id}ItemClicked", where "${id}" is the string name of the view's * identifier. */ private static Binder<AbsListView> AbsListViewBinder = new Binder<AbsListView>() { @Override public void bind( final Object receiver, AbsListView view, AttributeSet attrs) { // Fall back to "listView" as a default handler prefix if the // list doesn't have its own ID -- this supports the // automatically created ListView on a ListScreen. String resourceId = getIdName(view.getContext(), view.getId()); final String id = (resourceId != null) ? resourceId : "listView"; final OptionalEventDispatcher event = new OptionalEventDispatcher(id + "ItemClicked", 1); view.setOnItemClickListener( new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Object item = parent.getAdapter().getItem(position); event.dispatch(receiver, item, position); } }); } }; // ---------------------------------------------------------- /** * Implements the following binding rule: for each AbsSpinner, * attach an OnItemSelectedListener to it that calls the context method * "${id}ItemSelected", where "${id}" is the string name of the view's * identifier. */ private static Binder<AbsSpinner> AbsSpinnerBinder = new Binder<AbsSpinner>() { @Override public void bind( final Object receiver, AbsSpinner view, AttributeSet attrs) { String id = getIdName(view.getContext(), view.getId()); if (id != null) { final OptionalEventDispatcher itemEvent = new OptionalEventDispatcher(id + "ItemSelected", 1); final EventDispatcher nothingEvent = new EventDispatcher(id + "NothingSelected"); view.setOnItemSelectedListener( new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { Object item = parent.getAdapter().getItem(position); itemEvent.dispatch(receiver, item, position); } @Override public void onNothingSelected(AdapterView<?> parent) { nothingEvent.dispatch(receiver); } }); } } }; // ---------------------------------------------------------- /** * Implements the following binding rule: for each EditText * view, attach an OnEditorActionListener to it that calls the context * method "${id}EditingDone", where "${id}" is the string name of the * view's identifier. */ private static Binder<EditText> EditTextBinder = new Binder<EditText>() { @Override public void bind( final Object receiver, EditText view, AttributeSet attrs) { EditText editText = (EditText) view; editText.setOnEditorActionListener( new EditorActionListener(receiver)); } }; // ---------------------------------------------------------- /** * Implements the following binding rule: for each SeekBar, * attach an OnSeekBarChangedListener to it that calls the context method * "${id}Changed", where "${id}" is the string name of the view's * identifier. */ private static Binder<SeekBar> SeekBarBinder = new Binder<SeekBar>() { @Override public void bind( final Object receiver, SeekBar view, AttributeSet attrs) { String id = getIdName(view.getContext(), view.getId()); if (id != null) { final OptionalEventDispatcher changedEvent = new OptionalEventDispatcher( id + "ProgressChanged", 0); final OptionalEventDispatcher startedEvent = new OptionalEventDispatcher( id + "TrackingStarted", 0); final OptionalEventDispatcher stoppedEvent = new OptionalEventDispatcher( id + "TrackingStopped", 0); view.setOnSeekBarChangeListener( new SeekBar.OnSeekBarChangeListener() { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { changedEvent.dispatch(receiver, seekBar, progress, fromUser); } public void onStartTrackingTouch(SeekBar seekBar) { startedEvent.dispatch(receiver, seekBar, seekBar.getProgress()); } public void onStopTrackingTouch(SeekBar seekBar) { stoppedEvent.dispatch(receiver, seekBar, seekBar.getProgress()); } }); } } }; // ---------------------------------------------------------- /** * Implements the following binding rule: for each SeekBar, * attach an OnSeekBarChangedListener to it that calls the context method * "${id}Changed", where "${id}" is the string name of the view's * identifier. */ private static Binder<RatingBar> RatingBarBinder = new Binder<RatingBar>() { @Override public void bind( final Object receiver, RatingBar view, AttributeSet attrs) { String id = getIdName(view.getContext(), view.getId()); if (id != null) { final OptionalEventDispatcher changedEvent = new OptionalEventDispatcher( id + "RatingChanged", 0); view.setOnRatingBarChangeListener( new RatingBar.OnRatingBarChangeListener() { public void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser) { changedEvent.dispatch(receiver, ratingBar, rating, fromUser); } }); } } }; // ---------------------------------------------------------- private static class EditorActionListener implements TextView.OnEditorActionListener { private Object receiver; // ---------------------------------------------------------- public EditorActionListener(Object receiver) { this.receiver = receiver; } // ---------------------------------------------------------- @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { // TODO We need to test this across multiple devices with both soft // and hard keyboards. if (actionMatchesImeOptions(v, actionId) || (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { dispatchEvent(v); if (actionId == EditorInfo.IME_ACTION_DONE) { InputMethodManager imm = (InputMethodManager) v.getContext().getSystemService( Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); } return true; } else { return false; } } // ---------------------------------------------------------- /** * By default, if the user does not explicitly set any IME options on * a text view, it sends either the Next or Done event depending on * its position in the GUI layout. We want to capture both of those in * that case. Otherwise, if the user has explicitly set a specific * action through {@link TextView#setImeOptions(int)}, then we only * want to capture that specific one to dispatch an event. * * @param v the text view * @param actionId the IME action * * @return true if the IME action matches the options of the text view */ private boolean actionMatchesImeOptions(TextView v, int actionId) { int options = v.getImeOptions(); if (options == 0) { return actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE; } else { return actionId == (options & EditorInfo.IME_MASK_ACTION); } } // ---------------------------------------------------------- /** * Dispatch the "EditingDone" event to the text view's context. * * @param v the text view */ private void dispatchEvent(TextView v) { final String id = getIdName(v.getContext(), v.getId()); if (id != null) { final OptionalEventDispatcher event = new OptionalEventDispatcher(id + "EditingDone"); event.dispatch(receiver, v); } } }; // ---------------------------------------------------------- /** * Add the binders to the static map. */ static { binders = new HashMap<Class<? extends View>, Binder<? extends View>>(); binders.put(View.class, ViewBinder); binders.put(EditText.class, EditTextBinder); binders.put(AbsListView.class, AbsListViewBinder); binders.put(AbsSpinner.class, AbsSpinnerBinder); binders.put(SeekBar.class, SeekBarBinder); binders.put(RatingBar.class, RatingBarBinder); } }