2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
19 import android.app.Activity;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import android.view.Window;
25 import android.widget.AdapterView;
26 import android.widget.BaseAdapter;
27 import android.widget.EditText;
28 import android.widget.LinearLayout;
29 import android.widget.ListView;
30 import android.widget.TextView;
31 import com.google.android.collect.Maps;
33 import java.util.ArrayList;
34 import java.util.HashSet;
35 import java.util.List;
40 * Utility base class for creating various List scenarios. Configurable by the number
41 * of items, how tall each item should be (in relation to the screen height), and
42 * what item should start with selection.
44 public abstract class ListScenario extends Activity {
46 private ListView mListView;
47 private TextView mHeaderTextView;
49 private int mNumItems;
50 protected boolean mItemsFocusable;
52 private int mStartingSelectionPosition;
53 private double mItemScreenSizeFactor;
54 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
56 private int mScreenHeight;
58 // whether to include a text view above the list
59 private boolean mIncludeHeader;
62 private Set<Integer> mUnselectableItems = new HashSet<Integer>();
64 private boolean mStackFromBottom;
66 private int mClickedPosition = -1;
68 private int mLongClickedPosition = -1;
70 private int mConvertMisses = 0;
72 private int mHeaderViewCount;
73 private boolean mHeadersFocusable;
75 private int mFooterViewCount;
76 private LinearLayout mLinearLayout;
78 public ListView getListView() {
82 protected int getScreenHeight() {
87 * Return whether the item at position is selectable (i.e is a separator).
88 * (external users can access this info using the adapter)
90 private boolean isItemAtPositionSelectable(int position) {
91 return !mUnselectableItems.contains(position);
95 * Better way to pass in optional params than a honkin' paramater list :)
97 public static class Params {
98 private int mNumItems = 4;
99 private boolean mItemsFocusable = false;
100 private int mStartingSelectionPosition = 0;
101 private double mItemScreenSizeFactor = 1 / 5;
102 private Double mFadingEdgeScreenSizeFactor = null;
104 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
107 private List<Integer> mUnselectableItems = new ArrayList<Integer>(8);
108 // whether to include a text view above the list
109 private boolean mIncludeHeader = false;
110 private boolean mStackFromBottom = false;
111 public boolean mMustFillScreen = true;
112 private int mHeaderViewCount;
113 private boolean mHeaderFocusable = false;
114 private int mFooterViewCount;
116 private boolean mConnectAdapter = true;
119 * Set the number of items in the list.
121 public Params setNumItems(int numItems) {
122 mNumItems = numItems;
127 * Set whether the items are focusable.
129 public Params setItemsFocusable(boolean itemsFocusable) {
130 mItemsFocusable = itemsFocusable;
135 * Set the position that starts selected.
137 * @param startingSelectionPosition The selected position within the adapter's data set.
138 * Pass -1 if you do not want to force a selection.
141 public Params setStartingSelectionPosition(int startingSelectionPosition) {
142 mStartingSelectionPosition = startingSelectionPosition;
147 * Set the factor that determines how tall each item is in relation to the
150 public Params setItemScreenSizeFactor(double itemScreenSizeFactor) {
151 mItemScreenSizeFactor = itemScreenSizeFactor;
156 * Override the item screen size factor for a particular item. Useful for
157 * creating lists with non-uniform item height.
158 * @param position The position in the list.
159 * @param itemScreenSizeFactor The screen size factor to use for the height.
161 public Params setPositionScreenSizeFactorOverride(
162 int position, double itemScreenSizeFactor) {
163 mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor);
168 * Set a position as unselectable (a.k.a a separator)
172 public Params setPositionUnselectable(int position) {
173 mUnselectableItems.add(position);
178 * Set positions as unselectable (a.k.a a separator)
180 public Params setPositionsUnselectable(int ...positions) {
181 for (int pos : positions) {
182 setPositionUnselectable(pos);
188 * Include a header text view above the list.
189 * @param includeHeader
192 public Params includeHeaderAboveList(boolean includeHeader) {
193 mIncludeHeader = includeHeader;
198 * Sets the stacking direction
199 * @param stackFromBottom
202 public Params setStackFromBottom(boolean stackFromBottom) {
203 mStackFromBottom = stackFromBottom;
208 * Sets whether the sum of the height of the list items must be at least the
209 * height of the list view.
211 public Params setMustFillScreen(boolean fillScreen) {
212 mMustFillScreen = fillScreen;
217 * Set the factor for the fading edge length.
219 public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) {
220 mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor;
225 * Set the number of header views to appear within the list
227 public Params setHeaderViewCount(int headerViewCount) {
228 mHeaderViewCount = headerViewCount;
233 * Set whether the headers should be focusable.
234 * @param headerFocusable Whether the headers should be focusable (i.e
235 * created as edit texts rather than text views).
237 public Params setHeaderFocusable(boolean headerFocusable) {
238 mHeaderFocusable = headerFocusable;
243 * Set the number of footer views to appear within the list
245 public Params setFooterViewCount(int footerViewCount) {
246 mFooterViewCount = footerViewCount;
251 * Sets whether the {@link ListScenario} will automatically set the
252 * adapter on the list view. If this is false, the client MUST set it
253 * manually (this is useful when adding headers to the list view, which
254 * must be done before the adapter is set).
256 public Params setConnectAdapter(boolean connectAdapter) {
257 mConnectAdapter = connectAdapter;
263 * How each scenario customizes its behavior.
266 protected abstract void init(Params params);
269 * Override this if you want to know when something has been selected (perhaps
270 * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has
273 protected void positionSelected(int positon) {
277 * Override this if you want to know that nothing is selected.
279 protected void nothingSelected() {
283 * Override this if you want to know when something has been clicked (perhaps
284 * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has
287 protected void positionClicked(int position) {
288 setClickedPosition(position);
292 * Override this if you want to know when something has been long clicked (perhaps
293 * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has
296 protected void positionLongClicked(int position) {
297 setLongClickedPosition(position);
301 protected void onCreate(Bundle icicle) {
302 super.onCreate(icicle);
304 // for test stability, turn off title bar
305 requestWindowFeature(Window.FEATURE_NO_TITLE);
308 mScreenHeight = getWindowManager().getDefaultDisplay().getHeight();
310 final Params params = createParams();
313 readAndValidateParams(params);
316 mListView = createListView();
317 mListView.setLayoutParams(new ViewGroup.LayoutParams(
318 ViewGroup.LayoutParams.MATCH_PARENT,
319 ViewGroup.LayoutParams.MATCH_PARENT));
320 mListView.setDrawSelectorOnTop(false);
322 for (int i=0; i<mHeaderViewCount; i++) {
323 TextView header = mHeadersFocusable ?
326 header.setText("Header: " + i);
327 mListView.addHeaderView(header);
330 for (int i=0; i<mFooterViewCount; i++) {
331 TextView header = new TextView(this);
332 header.setText("Footer: " + i);
333 mListView.addFooterView(header);
336 if (params.mConnectAdapter) {
337 setAdapter(mListView);
340 mListView.setItemsCanFocus(mItemsFocusable);
341 if (mStartingSelectionPosition >= 0) {
342 mListView.setSelection(mStartingSelectionPosition);
344 mListView.setPadding(0, 0, 0, 0);
345 mListView.setStackFromBottom(mStackFromBottom);
346 mListView.setDivider(null);
348 mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
349 public void onItemSelected(AdapterView parent, View v, int position, long id) {
350 positionSelected(position);
353 public void onNothingSelected(AdapterView parent) {
358 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
359 public void onItemClick(AdapterView parent, View v, int position, long id) {
360 positionClicked(position);
364 // set the fading edge length porportionally to the screen
365 // height for test stability
366 if (params.mFadingEdgeScreenSizeFactor != null) {
367 mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight));
369 mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
372 if (mIncludeHeader) {
373 mLinearLayout = new LinearLayout(this);
375 mHeaderTextView = new TextView(this);
376 mHeaderTextView.setText("hi");
377 mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
378 ViewGroup.LayoutParams.MATCH_PARENT,
379 ViewGroup.LayoutParams.WRAP_CONTENT));
380 mLinearLayout.addView(mHeaderTextView);
382 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
383 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
384 ViewGroup.LayoutParams.MATCH_PARENT,
385 ViewGroup.LayoutParams.MATCH_PARENT));
386 mListView.setLayoutParams((new LinearLayout.LayoutParams(
387 ViewGroup.LayoutParams.MATCH_PARENT,
391 mLinearLayout.addView(mListView);
392 setContentView(mLinearLayout);
394 mLinearLayout = new LinearLayout(this);
395 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
396 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
397 ViewGroup.LayoutParams.MATCH_PARENT,
398 ViewGroup.LayoutParams.MATCH_PARENT));
399 mListView.setLayoutParams((new LinearLayout.LayoutParams(
400 ViewGroup.LayoutParams.MATCH_PARENT,
403 mLinearLayout.addView(mListView);
404 setContentView(mLinearLayout);
409 * Returns the LinearLayout containing the ListView in this scenario.
411 * @return The LinearLayout in which the ListView is held.
413 protected LinearLayout getListViewContainer() {
414 return mLinearLayout;
418 * Attaches a long press listener. You can find out which views were clicked by calling
419 * {@link #getLongClickedPosition()}.
421 public void enableLongPress() {
422 mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
423 public boolean onItemLongClick(AdapterView parent, View v, int position, long id) {
424 positionLongClicked(position);
431 * @return The newly created ListView widget.
433 protected ListView createListView() {
434 return new ListView(this);
438 * @return The newly created Params object.
440 protected Params createParams() {
445 * Sets an adapter on a ListView.
447 * @param listView The ListView to set the adapter on.
449 protected void setAdapter(ListView listView) {
450 listView.setAdapter(new MyAdapter());
454 * Read in and validate all of the params passed in by the scenario.
457 protected void readAndValidateParams(Params params) {
458 if (params.mMustFillScreen ) {
459 double totalFactor = 0.0;
460 for (int i = 0; i < params.mNumItems; i++) {
461 if (params.mOverrideItemScreenSizeFactors.containsKey(i)) {
462 totalFactor += params.mOverrideItemScreenSizeFactors.get(i);
464 totalFactor += params.mItemScreenSizeFactor;
467 if (totalFactor < 1.0) {
468 throw new IllegalArgumentException("list items must combine to be at least " +
469 "the height of the screen. this is not the case with " + params.mNumItems
470 + " items and " + params.mItemScreenSizeFactor + " screen factor and " +
471 "screen height of " + mScreenHeight);
475 mNumItems = params.mNumItems;
476 mItemsFocusable = params.mItemsFocusable;
477 mStartingSelectionPosition = params.mStartingSelectionPosition;
478 mItemScreenSizeFactor = params.mItemScreenSizeFactor;
480 mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors);
482 mUnselectableItems.addAll(params.mUnselectableItems);
483 mIncludeHeader = params.mIncludeHeader;
484 mStackFromBottom = params.mStackFromBottom;
485 mHeaderViewCount = params.mHeaderViewCount;
486 mHeadersFocusable = params.mHeaderFocusable;
487 mFooterViewCount = params.mFooterViewCount;
490 public final String getValueAtPosition(int position) {
491 return isItemAtPositionSelectable(position)
493 "position " + position:
494 "------- " + position;
498 * @return The height that will be set for a particular position.
500 public int getHeightForPosition(int position) {
501 int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor);
502 if (mOverrideItemScreenSizeFactors.containsKey(position)) {
503 desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position));
505 return desiredHeight;
510 * @return The contents of the header above the list.
511 * @throws IllegalArgumentException if there is no header.
513 public final String getHeaderValue() {
514 if (!mIncludeHeader) {
515 throw new IllegalArgumentException("no header above list");
517 return mHeaderTextView.getText().toString();
521 * @param value What to put in the header text view
522 * @throws IllegalArgumentException if there is no header.
524 protected final void setHeaderValue(String value) {
525 if (!mIncludeHeader) {
526 throw new IllegalArgumentException("no header above list");
528 mHeaderTextView.setText(value);
532 * Create a view for a list item. Override this to create a custom view beyond
533 * the simple focusable / unfocusable text view.
534 * @param position The position.
535 * @param parent The parent
536 * @param desiredHeight The height the view should be to respect the desired item
537 * to screen height ratio.
538 * @return a view for the list.
540 protected View createView(int position, ViewGroup parent, int desiredHeight) {
541 return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position),
546 * Convert a non-null view.
548 public View convertView(int position, View convertView, ViewGroup parent) {
549 return ListItemFactory.convertText(convertView, getValueAtPosition(position), position);
552 public void setClickedPosition(int clickedPosition) {
553 mClickedPosition = clickedPosition;
556 public int getClickedPosition() {
557 return mClickedPosition;
560 public void setLongClickedPosition(int longClickedPosition) {
561 mLongClickedPosition = longClickedPosition;
564 public int getLongClickedPosition() {
565 return mLongClickedPosition;
569 * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}.
570 * @param childIndex The index into the viewgroup children (i.e the children that are
571 * currently visible).
572 * @param rect The rectangle, in the child's coordinates.
574 public void requestRectangleOnScreen(int childIndex, final Rect rect) {
575 final View child = getListView().getChildAt(childIndex);
577 child.post(new Runnable() {
579 child.requestRectangleOnScreen(rect);
585 * Return an item type for the specified position in the adapter. Override if your
586 * adapter creates more than one type.
588 public int getItemViewType(int position) {
593 * Return an the number of types created by the adapter. Override if your
594 * adapter creates more than one type.
596 public int getViewTypeCount() {
601 * @return The number of times convertView failed
603 public int getConvertMisses() {
604 return mConvertMisses;
607 private class MyAdapter extends BaseAdapter {
609 public int getCount() {
613 public Object getItem(int position) {
614 return getValueAtPosition(position);
617 public long getItemId(int position) {
622 public boolean areAllItemsEnabled() {
623 return mUnselectableItems.isEmpty();
627 public boolean isEnabled(int position) {
628 return isItemAtPositionSelectable(position);
631 public View getView(int position, View convertView, ViewGroup parent) {
633 if (position >= mNumItems || position < 0) {
634 throw new IllegalStateException("position out of range for adapter!");
637 if (convertView != null) {
638 result = convertView(position, convertView, parent);
639 if (result == null) {
644 if (result == null) {
645 int desiredHeight = getHeightForPosition(position);
646 result = createView(position, parent, desiredHeight);
652 public int getItemViewType(int position) {
653 return ListScenario.this.getItemViewType(position);
657 public int getViewTypeCount() {
658 return ListScenario.this.getViewTypeCount();