package org.ohmage.controls; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Color; import android.util.AttributeSet; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import org.ohmage.R; import org.ohmage.logprobe.Analytics; import java.util.ArrayList; public class FilterControl extends LinearLayout { private ArrayList<Pair<String, String>> mItemList; private FilterChangeListener mFilterChangeListener; private int mSelectionIndex; private Button mCurrentBtn; private Button mPrevBtn; private Button mNextBtn; private final Activity mActivity; // stores a reference to our calling activity private AlertDialog mItemListDialog; // stores a dialog containing a list of items, updated by populate() and add() public FilterControl(Context context) { super(context); mActivity = (Activity)context; // just construct the base control initControl(context); } public FilterControl(Context context, AttributeSet attrs) { super(context, attrs); mActivity = (Activity)context; // construct the base control initControl(context); // apply the xml-specified attributes, too initStyles(attrs); } /** * Constructs the parts of the control that provide actual functionality. Declarative styling is handled by initStyles(). * @param context the context of whoever's creating this control, usually passed into the class constructor */ protected void initControl(Context context) { LayoutParams params = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); this.setLayoutParams(params); this.setOrientation(HORIZONTAL); this.setBackgroundResource(R.color.lightergray); // load up the elements of the actionbar from controls_filter.xml LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.controls_filter, this, true); // and init an empty list so we don't crash and burn when we try to read before populating mItemList = new ArrayList<Pair<String,String>>(); mSelectionIndex = 0; mPrevBtn = (Button) findViewById(R.id.controls_filter_prev); mCurrentBtn = (Button) findViewById(R.id.controls_filter_current); mNextBtn = (Button) findViewById(R.id.controls_filter_next); FilterClickHandler handler = new FilterClickHandler(); mPrevBtn.setOnClickListener(handler); mCurrentBtn.setOnClickListener(handler); mNextBtn.setOnClickListener(handler); mCurrentBtn.setSelected(true); } /** * Initializes the appearance of the control based on the attributes passed from the xml. * @param attrs the collection of attributes, usually provided in the control's constructor */ protected void initStyles(AttributeSet attrs) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ActivityBarControl); } /** * Inserts items into the filter's list from a cursor. * * @param data the cursor from which to load the data; will be closed when the method returns. * @param textColumn the column of the cursor to use as the displayed text on the filter * @param valueColumn the column of the cursor to return from the {@link populate} method */ public void populate(Cursor data, String textColumn, String valueColumn) { int textColIdx = data.getColumnIndex(textColumn); int valueColIdx = data.getColumnIndex(valueColumn); // read the cursor into our internal paging list mItemList.clear(); mSelectionIndex = 0; while (data.moveToNext()) { mItemList.add(Pair.create(data.getString(textColIdx), data.getString(valueColIdx))); } data.close(); // and do a final sync updateListDialog(); syncState(true); } /** * Replaces the items in the filter's list with an ArrayList of Pair<String,String>. * * @param itemList the list of items to populate the list */ public void populate(ArrayList<Pair<String,String>> itemList) { // just copy the arraylist into our internal list mItemList = itemList; mSelectionIndex = 0; // update the list dialog and do a final sync updateListDialog(); syncState(true); } /** * Adds an item into the existing item list before the specified index. * * @param index the index which the new item will have * @param item the item to insert */ public void add(int index, Pair<String,String> item) { mItemList.add(index, item); // and make sure we're displaying the right thing updateListDialog(); syncState(true); } /** * Adds an item to the end of the existing item list. */ public void add(Pair<String,String> item) { mItemList.add(item); updateListDialog(); syncState(true); } /** * Gets the index of the currently selected item, from 0 to list size -1. * @return a numeric index for the currently selected item */ public int getIndex() { return mSelectionIndex; } /** * Gets the index of the currently selected item, from 0 to list size -1. * @param a String to find out in the list (e.g, CampaignUrn) * @return a numeric index for the currently selected item */ public int getIndex(String text) { for(int i=0; i<mItemList.size(); i++){ if(mItemList.get(i).second.equals(text)){ return i; } } return -1; } /** * Sets the currently selected item by its index, which should be between 0 and list size - 1 inclusive. * @param index the index to set, between 0 and list size - 1 inclusive */ public void setIndex(int index) { mSelectionIndex = index; syncState(true); } /** * Returns the number of items in the list. * @return the number of items in the list */ public int size() { return mItemList.size(); } /** * Gets the displayed text for the currently selected item (i.e. the first element of the Pair) * @return the displayed text as a string */ public String getText() { return mItemList.get(mSelectionIndex).first; } /** * Gets the defined value for the currently selected item (i.e. the second element of the Pair) * @return the value as a string */ public String getValue() { if(mItemList.isEmpty()) return null; return mItemList.get(mSelectionIndex).second; } /** * Sets the currently selected item by its value, if the value exists in the collection of items * @return true if value existed in collection, false if not */ public boolean setValue(String value) { if(value != null) { for (int i = 0; i < mItemList.size(); i++) { if (value.equals(mItemList.get(i).second)) { setIndex(i); return true; } } } return false; } public void clearAll(){ mItemList.clear(); mSelectionIndex = 0; } /** * Attaches a {@link FilterChangeListener} to the filter which will be called when the user navigates between items or when the list is changed. * * @param listener an object implementing {@link FilterChangeListener} which will be called when the list index is changed. */ public void setOnChangeListener(FilterChangeListener listener) { mFilterChangeListener = listener; } /** * Exposes a callback to allow custom processing to occur when the filter is changed (either by navigation or population). */ public static interface FilterChangeListener { public void onFilterChanged(boolean selfChange, String curValue); } /** * Notifies our listener if we have one that the filter has changed */ private void notifyFilterChanged(boolean selfChange) { if (mFilterChangeListener != null) mFilterChangeListener.onFilterChanged(selfChange, mItemList.get(mSelectionIndex).second); } /** * Handles the next, previous, and current buttons in the view. * * @param v the view which generated the click; this method uses the id of the view to determine what to do */ private class FilterClickHandler implements OnClickListener { @Override public void onClick(View v) { Analytics.widget(v); switch (v.getId()) { case R.id.controls_filter_prev: if (mSelectionIndex > 0) { mSelectionIndex -= 1; syncState(false); } break; case R.id.controls_filter_current: mItemListDialog.show(); break; case R.id.controls_filter_next: if (mSelectionIndex < mItemList.size()-1) { mSelectionIndex += 1; syncState(false); } break; } } } // keeps the middle text button in sync with the current index private void syncState(boolean selfChange) { // depending on where we are in the list, dim or disable the controls mPrevBtn.setTextColor((mSelectionIndex <= 0)?Color.LTGRAY:Color.BLACK); mNextBtn.setTextColor((mSelectionIndex >= mItemList.size() - 1)?Color.LTGRAY:Color.BLACK); mCurrentBtn.setEnabled(mItemList.size() > 0); // if there's nothing in the list, display some default text and exit if (mItemList.size() <= 0) { mCurrentBtn.setText(""); return; } // check if the selection is within bounds; reset it if it's not if (mSelectionIndex < 0 || mSelectionIndex >= mItemList.size()) mSelectionIndex = 0; // grab the selection so we can populate the middle button and fire a callback Pair<String,String> curItem = mItemList.get(mSelectionIndex); mCurrentBtn.setText(curItem.first); notifyFilterChanged(selfChange); } // helper method for constructing a list dialog based on the current item list private void updateListDialog() { // build a list of items for us to choose from CharSequence[] items = new CharSequence[mItemList.size()]; int i = 0; for (Pair<String,String> pair : mItemList) items[i++] = pair.first; // and construct a dialog that displays the list AlertDialog.Builder builder = new AlertDialog.Builder(mActivity); builder.setTitle("Choose an item"); builder.setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int item) { mSelectionIndex = item; syncState(false); } }); mItemListDialog = builder.create(); } // utility method for converting dp to pixels, since the setters only take pixel values :\ private int dpToPixels(int dp) { final float scale = getResources().getDisplayMetrics().density; return (int) (dp * scale + 0.5f); } }