/* * Copyright (C) 2007 The Android Open Source Project * * 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 android.util; import android.app.Activity; import android.graphics.Rect; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import com.google.android.collect.Maps; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Utility base class for creating various List scenarios. Configurable by the number * of items, how tall each item should be (in relation to the screen height), and * what item should start with selection. */ public abstract class ListScenario extends Activity { private ListView mListView; private TextView mHeaderTextView; private int mNumItems; protected boolean mItemsFocusable; private int mStartingSelectionPosition; private double mItemScreenSizeFactor; private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); private int mScreenHeight; // whether to include a text view above the list private boolean mIncludeHeader; // separators private Set mUnselectableItems = new HashSet(); private boolean mStackFromBottom; private int mClickedPosition = -1; private int mLongClickedPosition = -1; private int mConvertMisses = 0; private int mHeaderViewCount; private boolean mHeadersFocusable; private int mFooterViewCount; private LinearLayout mLinearLayout; public ListView getListView() { return mListView; } protected int getScreenHeight() { return mScreenHeight; } /** * Return whether the item at position is selectable (i.e is a separator). * (external users can access this info using the adapter) */ private boolean isItemAtPositionSelectable(int position) { return !mUnselectableItems.contains(position); } /** * Better way to pass in optional params than a honkin' paramater list :) */ public static class Params { private int mNumItems = 4; private boolean mItemsFocusable = false; private int mStartingSelectionPosition = 0; private double mItemScreenSizeFactor = 1 / 5; private Double mFadingEdgeScreenSizeFactor = null; private Map mOverrideItemScreenSizeFactors = Maps.newHashMap(); // separators private List mUnselectableItems = new ArrayList(8); // whether to include a text view above the list private boolean mIncludeHeader = false; private boolean mStackFromBottom = false; public boolean mMustFillScreen = true; private int mHeaderViewCount; private boolean mHeaderFocusable = false; private int mFooterViewCount; private boolean mConnectAdapter = true; /** * Set the number of items in the list. */ public Params setNumItems(int numItems) { mNumItems = numItems; return this; } /** * Set whether the items are focusable. */ public Params setItemsFocusable(boolean itemsFocusable) { mItemsFocusable = itemsFocusable; return this; } /** * Set the position that starts selected. * * @param startingSelectionPosition The selected position within the adapter's data set. * Pass -1 if you do not want to force a selection. * @return */ public Params setStartingSelectionPosition(int startingSelectionPosition) { mStartingSelectionPosition = startingSelectionPosition; return this; } /** * Set the factor that determines how tall each item is in relation to the * screen height. */ public Params setItemScreenSizeFactor(double itemScreenSizeFactor) { mItemScreenSizeFactor = itemScreenSizeFactor; return this; } /** * Override the item screen size factor for a particular item. Useful for * creating lists with non-uniform item height. * @param position The position in the list. * @param itemScreenSizeFactor The screen size factor to use for the height. */ public Params setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor) { mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor); return this; } /** * Set a position as unselectable (a.k.a a separator) * @param position * @return */ public Params setPositionUnselectable(int position) { mUnselectableItems.add(position); return this; } /** * Set positions as unselectable (a.k.a a separator) */ public Params setPositionsUnselectable(int ...positions) { for (int pos : positions) { setPositionUnselectable(pos); } return this; } /** * Include a header text view above the list. * @param includeHeader * @return */ public Params includeHeaderAboveList(boolean includeHeader) { mIncludeHeader = includeHeader; return this; } /** * Sets the stacking direction * @param stackFromBottom * @return */ public Params setStackFromBottom(boolean stackFromBottom) { mStackFromBottom = stackFromBottom; return this; } /** * Sets whether the sum of the height of the list items must be at least the * height of the list view. */ public Params setMustFillScreen(boolean fillScreen) { mMustFillScreen = fillScreen; return this; } /** * Set the factor for the fading edge length. */ public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) { mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor; return this; } /** * Set the number of header views to appear within the list */ public Params setHeaderViewCount(int headerViewCount) { mHeaderViewCount = headerViewCount; return this; } /** * Set whether the headers should be focusable. * @param headerFocusable Whether the headers should be focusable (i.e * created as edit texts rather than text views). */ public Params setHeaderFocusable(boolean headerFocusable) { mHeaderFocusable = headerFocusable; return this; } /** * Set the number of footer views to appear within the list */ public Params setFooterViewCount(int footerViewCount) { mFooterViewCount = footerViewCount; return this; } /** * Sets whether the {@link ListScenario} will automatically set the * adapter on the list view. If this is false, the client MUST set it * manually (this is useful when adding headers to the list view, which * must be done before the adapter is set). */ public Params setConnectAdapter(boolean connectAdapter) { mConnectAdapter = connectAdapter; return this; } } /** * How each scenario customizes its behavior. * @param params */ protected abstract void init(Params params); /** * Override this if you want to know when something has been selected (perhaps * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has * been triggered). */ protected void positionSelected(int positon) { } /** * Override this if you want to know that nothing is selected. */ protected void nothingSelected() { } /** * Override this if you want to know when something has been clicked (perhaps * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has * been triggered). */ protected void positionClicked(int position) { setClickedPosition(position); } /** * Override this if you want to know when something has been long clicked (perhaps * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has * been triggered). */ protected void positionLongClicked(int position) { setLongClickedPosition(position); } @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); // for test stability, turn off title bar requestWindowFeature(Window.FEATURE_NO_TITLE); mScreenHeight = getWindowManager().getDefaultDisplay().getHeight(); final Params params = createParams(); init(params); readAndValidateParams(params); mListView = createListView(); mListView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mListView.setDrawSelectorOnTop(false); for (int i=0; i= 0) { mListView.setSelection(mStartingSelectionPosition); } mListView.setPadding(0, 0, 0, 0); mListView.setStackFromBottom(mStackFromBottom); mListView.setDivider(null); mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView parent, View v, int position, long id) { positionSelected(position); } public void onNothingSelected(AdapterView parent) { nothingSelected(); } }); mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView parent, View v, int position, long id) { positionClicked(position); } }); // set the fading edge length porportionally to the screen // height for test stability if (params.mFadingEdgeScreenSizeFactor != null) { mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight)); } else { mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight)); } if (mIncludeHeader) { mLinearLayout = new LinearLayout(this); mHeaderTextView = new TextView(this); mHeaderTextView.setText("hi"); mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mLinearLayout.addView(mHeaderTextView); mLinearLayout.setOrientation(LinearLayout.VERTICAL); mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mListView.setLayoutParams((new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))); mLinearLayout.addView(mListView); setContentView(mLinearLayout); } else { mLinearLayout = new LinearLayout(this); mLinearLayout.setOrientation(LinearLayout.VERTICAL); mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); mListView.setLayoutParams((new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f))); mLinearLayout.addView(mListView); setContentView(mLinearLayout); } } /** * Returns the LinearLayout containing the ListView in this scenario. * * @return The LinearLayout in which the ListView is held. */ protected LinearLayout getListViewContainer() { return mLinearLayout; } /** * Attaches a long press listener. You can find out which views were clicked by calling * {@link #getLongClickedPosition()}. */ public void enableLongPress() { mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { public boolean onItemLongClick(AdapterView parent, View v, int position, long id) { positionLongClicked(position); return true; } }); } /** * @return The newly created ListView widget. */ protected ListView createListView() { return new ListView(this); } /** * @return The newly created Params object. */ protected Params createParams() { return new Params(); } /** * Sets an adapter on a ListView. * * @param listView The ListView to set the adapter on. */ protected void setAdapter(ListView listView) { listView.setAdapter(new MyAdapter()); } /** * Read in and validate all of the params passed in by the scenario. * @param params */ protected void readAndValidateParams(Params params) { if (params.mMustFillScreen ) { double totalFactor = 0.0; for (int i = 0; i < params.mNumItems; i++) { if (params.mOverrideItemScreenSizeFactors.containsKey(i)) { totalFactor += params.mOverrideItemScreenSizeFactors.get(i); } else { totalFactor += params.mItemScreenSizeFactor; } } if (totalFactor < 1.0) { throw new IllegalArgumentException("list items must combine to be at least " + "the height of the screen. this is not the case with " + params.mNumItems + " items and " + params.mItemScreenSizeFactor + " screen factor and " + "screen height of " + mScreenHeight); } } mNumItems = params.mNumItems; mItemsFocusable = params.mItemsFocusable; mStartingSelectionPosition = params.mStartingSelectionPosition; mItemScreenSizeFactor = params.mItemScreenSizeFactor; mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors); mUnselectableItems.addAll(params.mUnselectableItems); mIncludeHeader = params.mIncludeHeader; mStackFromBottom = params.mStackFromBottom; mHeaderViewCount = params.mHeaderViewCount; mHeadersFocusable = params.mHeaderFocusable; mFooterViewCount = params.mFooterViewCount; } public final String getValueAtPosition(int position) { return isItemAtPositionSelectable(position) ? "position " + position: "------- " + position; } /** * @return The height that will be set for a particular position. */ public int getHeightForPosition(int position) { int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor); if (mOverrideItemScreenSizeFactors.containsKey(position)) { desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position)); } return desiredHeight; } /** * @return The contents of the header above the list. * @throws IllegalArgumentException if there is no header. */ public final String getHeaderValue() { if (!mIncludeHeader) { throw new IllegalArgumentException("no header above list"); } return mHeaderTextView.getText().toString(); } /** * @param value What to put in the header text view * @throws IllegalArgumentException if there is no header. */ protected final void setHeaderValue(String value) { if (!mIncludeHeader) { throw new IllegalArgumentException("no header above list"); } mHeaderTextView.setText(value); } /** * Create a view for a list item. Override this to create a custom view beyond * the simple focusable / unfocusable text view. * @param position The position. * @param parent The parent * @param desiredHeight The height the view should be to respect the desired item * to screen height ratio. * @return a view for the list. */ protected View createView(int position, ViewGroup parent, int desiredHeight) { return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position), desiredHeight); } /** * Convert a non-null view. */ public View convertView(int position, View convertView, ViewGroup parent) { return ListItemFactory.convertText(convertView, getValueAtPosition(position), position); } public void setClickedPosition(int clickedPosition) { mClickedPosition = clickedPosition; } public int getClickedPosition() { return mClickedPosition; } public void setLongClickedPosition(int longClickedPosition) { mLongClickedPosition = longClickedPosition; } public int getLongClickedPosition() { return mLongClickedPosition; } /** * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}. * @param childIndex The index into the viewgroup children (i.e the children that are * currently visible). * @param rect The rectangle, in the child's coordinates. */ public void requestRectangleOnScreen(int childIndex, final Rect rect) { final View child = getListView().getChildAt(childIndex); child.post(new Runnable() { public void run() { child.requestRectangleOnScreen(rect); } }); } /** * Return an item type for the specified position in the adapter. Override if your * adapter creates more than one type. */ public int getItemViewType(int position) { return 0; } /** * Return the number of types created by the adapter. Override if your * adapter creates more than one type. */ public int getViewTypeCount() { return 1; } /** * @return The number of times convertView failed */ public int getConvertMisses() { return mConvertMisses; } private class MyAdapter extends BaseAdapter { public int getCount() { return mNumItems; } public Object getItem(int position) { return getValueAtPosition(position); } public long getItemId(int position) { return position; } @Override public boolean areAllItemsEnabled() { return mUnselectableItems.isEmpty(); } @Override public boolean isEnabled(int position) { return isItemAtPositionSelectable(position); } public View getView(int position, View convertView, ViewGroup parent) { View result = null; if (position >= mNumItems || position < 0) { throw new IllegalStateException("position out of range for adapter!"); } if (convertView != null) { result = convertView(position, convertView, parent); if (result == null) { mConvertMisses++; } } if (result == null) { int desiredHeight = getHeightForPosition(position); result = createView(position, parent, desiredHeight); } return result; } @Override public int getItemViewType(int position) { return ListScenario.this.getItemViewType(position); } @Override public int getViewTypeCount() { return ListScenario.this.getViewTypeCount(); } } }