blob: a6ae1884825f9224b3baae4b417f144449903d46 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17package com.android.frameworktest.util;
18
19import android.app.Activity;
20import android.graphics.Rect;
21import android.os.Bundle;
22import android.view.View;
23import android.view.ViewGroup;
24import android.view.Window;
25import android.widget.AdapterView;
26import android.widget.BaseAdapter;
27import android.widget.EditText;
28import android.widget.LinearLayout;
29import android.widget.ListView;
30import android.widget.TextView;
31import com.google.android.collect.Maps;
32
33import java.util.ArrayList;
34import java.util.HashSet;
35import java.util.List;
36import java.util.Map;
37import java.util.Set;
38
39/**
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.
43 */
44public abstract class ListScenario extends Activity {
45
46 private ListView mListView;
47 private TextView mHeaderTextView;
48
49 private int mNumItems;
50 protected boolean mItemsFocusable;
51
52 private int mStartingSelectionPosition;
53 private double mItemScreenSizeFactor;
54 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
55
56 private int mScreenHeight;
57
58 // whether to include a text view above the list
59 private boolean mIncludeHeader;
60
61 // separators
62 private Set<Integer> mUnselectableItems = new HashSet<Integer>();
63
64 private boolean mStackFromBottom;
65
66 private int mClickedPosition = -1;
67
68 private int mLongClickedPosition = -1;
69
70 private int mConvertMisses = 0;
71
72 private int mHeaderViewCount;
73 private boolean mHeadersFocusable;
74
75 private int mFooterViewCount;
76 private LinearLayout mLinearLayout;
77
78 public ListView getListView() {
79 return mListView;
80 }
81
82 protected int getScreenHeight() {
83 return mScreenHeight;
84 }
85
86 /**
87 * Return whether the item at position is selectable (i.e is a separator).
88 * (external users can access this info using the adapter)
89 */
90 private boolean isItemAtPositionSelectable(int position) {
91 return !mUnselectableItems.contains(position);
92 }
93
94 /**
95 * Better way to pass in optional params than a honkin' paramater list :)
96 */
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;
103
104 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
105
106 // separators
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;
115
116 private boolean mConnectAdapter = true;
117
118 /**
119 * Set the number of items in the list.
120 */
121 public Params setNumItems(int numItems) {
122 mNumItems = numItems;
123 return this;
124 }
125
126 /**
127 * Set whether the items are focusable.
128 */
129 public Params setItemsFocusable(boolean itemsFocusable) {
130 mItemsFocusable = itemsFocusable;
131 return this;
132 }
133
134 /**
135 * Set the position that starts selected.
136 *
137 * @param startingSelectionPosition The selected position within the adapter's data set.
138 * Pass -1 if you do not want to force a selection.
139 * @return
140 */
141 public Params setStartingSelectionPosition(int startingSelectionPosition) {
142 mStartingSelectionPosition = startingSelectionPosition;
143 return this;
144 }
145
146 /**
147 * Set the factor that determines how tall each item is in relation to the
148 * screen height.
149 */
150 public Params setItemScreenSizeFactor(double itemScreenSizeFactor) {
151 mItemScreenSizeFactor = itemScreenSizeFactor;
152 return this;
153 }
154
155 /**
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.
160 */
161 public Params setPositionScreenSizeFactorOverride(
162 int position, double itemScreenSizeFactor) {
163 mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor);
164 return this;
165 }
166
167 /**
168 * Set a position as unselectable (a.k.a a separator)
169 * @param position
170 * @return
171 */
172 public Params setPositionUnselectable(int position) {
173 mUnselectableItems.add(position);
174 return this;
175 }
176
177 /**
178 * Set positions as unselectable (a.k.a a separator)
179 */
180 public Params setPositionsUnselectable(int ...positions) {
181 for (int pos : positions) {
182 setPositionUnselectable(pos);
183 }
184 return this;
185 }
186
187 /**
188 * Include a header text view above the list.
189 * @param includeHeader
190 * @return
191 */
192 public Params includeHeaderAboveList(boolean includeHeader) {
193 mIncludeHeader = includeHeader;
194 return this;
195 }
196
197 /**
198 * Sets the stacking direction
199 * @param stackFromBottom
200 * @return
201 */
202 public Params setStackFromBottom(boolean stackFromBottom) {
203 mStackFromBottom = stackFromBottom;
204 return this;
205 }
206
207 /**
208 * Sets whether the sum of the height of the list items must be at least the
209 * height of the list view.
210 */
211 public Params setMustFillScreen(boolean fillScreen) {
212 mMustFillScreen = fillScreen;
213 return this;
214 }
215
216 /**
217 * Set the factor for the fading edge length.
218 */
219 public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) {
220 mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor;
221 return this;
222 }
223
224 /**
225 * Set the number of header views to appear within the list
226 */
227 public Params setHeaderViewCount(int headerViewCount) {
228 mHeaderViewCount = headerViewCount;
229 return this;
230 }
231
232 /**
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).
236 */
237 public Params setHeaderFocusable(boolean headerFocusable) {
238 mHeaderFocusable = headerFocusable;
239 return this;
240 }
241
242 /**
243 * Set the number of footer views to appear within the list
244 */
245 public Params setFooterViewCount(int footerViewCount) {
246 mFooterViewCount = footerViewCount;
247 return this;
248 }
249
250 /**
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).
255 */
256 public Params setConnectAdapter(boolean connectAdapter) {
257 mConnectAdapter = connectAdapter;
258 return this;
259 }
260 }
261
262 /**
263 * How each scenario customizes its behavior.
264 * @param params
265 */
266 protected abstract void init(Params params);
267
268 /**
269 * Override this if you want to know when something has been selected (perhaps
270 * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has
271 * been triggered).
272 */
273 protected void positionSelected(int positon) {
274 }
275
276 /**
277 * Override this if you want to know that nothing is selected.
278 */
279 protected void nothingSelected() {
280 }
281
282 /**
283 * Override this if you want to know when something has been clicked (perhaps
284 * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has
285 * been triggered).
286 */
287 protected void positionClicked(int position) {
288 setClickedPosition(position);
289 }
290
291 /**
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
294 * been triggered).
295 */
296 protected void positionLongClicked(int position) {
297 setLongClickedPosition(position);
298 }
299
300 @Override
301 protected void onCreate(Bundle icicle) {
302 super.onCreate(icicle);
303
304 // for test stability, turn off title bar
305 requestWindowFeature(Window.FEATURE_NO_TITLE);
306
307
308 mScreenHeight = getWindowManager().getDefaultDisplay().getHeight();
309
310 final Params params = createParams();
311 init(params);
312
313 readAndValidateParams(params);
314
315
316 mListView = createListView();
317 mListView.setLayoutParams(new ViewGroup.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800318 ViewGroup.LayoutParams.MATCH_PARENT,
319 ViewGroup.LayoutParams.MATCH_PARENT));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800320 mListView.setDrawSelectorOnTop(false);
321
322 for (int i=0; i<mHeaderViewCount; i++) {
323 TextView header = mHeadersFocusable ?
324 new EditText(this) :
325 new TextView(this);
326 header.setText("Header: " + i);
327 mListView.addHeaderView(header);
328 }
329
330 for (int i=0; i<mFooterViewCount; i++) {
331 TextView header = new TextView(this);
332 header.setText("Footer: " + i);
333 mListView.addFooterView(header);
334 }
335
336 if (params.mConnectAdapter) {
337 setAdapter(mListView);
338 }
339
340 mListView.setItemsCanFocus(mItemsFocusable);
341 if (mStartingSelectionPosition >= 0) {
342 mListView.setSelection(mStartingSelectionPosition);
343 }
344 mListView.setPadding(0, 0, 0, 0);
345 mListView.setStackFromBottom(mStackFromBottom);
346 mListView.setDivider(null);
347
348 mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
349 public void onItemSelected(AdapterView parent, View v, int position, long id) {
350 positionSelected(position);
351 }
352
353 public void onNothingSelected(AdapterView parent) {
354 nothingSelected();
355 }
356 });
357
358 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
359 public void onItemClick(AdapterView parent, View v, int position, long id) {
360 positionClicked(position);
361 }
362 });
363
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));
368 } else {
369 mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
370 }
371
372 if (mIncludeHeader) {
373 mLinearLayout = new LinearLayout(this);
374
375 mHeaderTextView = new TextView(this);
376 mHeaderTextView.setText("hi");
377 mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800378 ViewGroup.LayoutParams.MATCH_PARENT,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800379 ViewGroup.LayoutParams.WRAP_CONTENT));
380 mLinearLayout.addView(mHeaderTextView);
381
382 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
383 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800384 ViewGroup.LayoutParams.MATCH_PARENT,
385 ViewGroup.LayoutParams.MATCH_PARENT));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800386 mListView.setLayoutParams((new LinearLayout.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800387 ViewGroup.LayoutParams.MATCH_PARENT,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800388 0,
389 1f)));
390
391 mLinearLayout.addView(mListView);
392 setContentView(mLinearLayout);
393 } else {
394 mLinearLayout = new LinearLayout(this);
395 mLinearLayout.setOrientation(LinearLayout.VERTICAL);
396 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800397 ViewGroup.LayoutParams.MATCH_PARENT,
398 ViewGroup.LayoutParams.MATCH_PARENT));
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800399 mListView.setLayoutParams((new LinearLayout.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800400 ViewGroup.LayoutParams.MATCH_PARENT,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 0,
402 1f)));
403 mLinearLayout.addView(mListView);
404 setContentView(mLinearLayout);
405 }
406 }
407
408 /**
409 * Returns the LinearLayout containing the ListView in this scenario.
410 *
411 * @return The LinearLayout in which the ListView is held.
412 */
413 protected LinearLayout getListViewContainer() {
414 return mLinearLayout;
415 }
416
417 /**
418 * Attaches a long press listener. You can find out which views were clicked by calling
419 * {@link #getLongClickedPosition()}.
420 */
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);
425 return true;
426 }
427 });
428 }
429
430 /**
431 * @return The newly created ListView widget.
432 */
433 protected ListView createListView() {
434 return new ListView(this);
435 }
436
437 /**
438 * @return The newly created Params object.
439 */
440 protected Params createParams() {
441 return new Params();
442 }
443
444 /**
445 * Sets an adapter on a ListView.
446 *
447 * @param listView The ListView to set the adapter on.
448 */
449 protected void setAdapter(ListView listView) {
450 listView.setAdapter(new MyAdapter());
451 }
452
453 /**
454 * Read in and validate all of the params passed in by the scenario.
455 * @param params
456 */
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);
463 } else {
464 totalFactor += params.mItemScreenSizeFactor;
465 }
466 }
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);
472 }
473 }
474
475 mNumItems = params.mNumItems;
476 mItemsFocusable = params.mItemsFocusable;
477 mStartingSelectionPosition = params.mStartingSelectionPosition;
478 mItemScreenSizeFactor = params.mItemScreenSizeFactor;
479
480 mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors);
481
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;
488 }
489
490 public final String getValueAtPosition(int position) {
491 return isItemAtPositionSelectable(position)
492 ?
493 "position " + position:
494 "------- " + position;
495 }
496
497 /**
498 * @return The height that will be set for a particular position.
499 */
500 public int getHeightForPosition(int position) {
501 int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor);
502 if (mOverrideItemScreenSizeFactors.containsKey(position)) {
503 desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position));
504 }
505 return desiredHeight;
506 }
507
508
509 /**
510 * @return The contents of the header above the list.
511 * @throws IllegalArgumentException if there is no header.
512 */
513 public final String getHeaderValue() {
514 if (!mIncludeHeader) {
515 throw new IllegalArgumentException("no header above list");
516 }
517 return mHeaderTextView.getText().toString();
518 }
519
520 /**
521 * @param value What to put in the header text view
522 * @throws IllegalArgumentException if there is no header.
523 */
524 protected final void setHeaderValue(String value) {
525 if (!mIncludeHeader) {
526 throw new IllegalArgumentException("no header above list");
527 }
528 mHeaderTextView.setText(value);
529 }
530
531 /**
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.
539 */
540 protected View createView(int position, ViewGroup parent, int desiredHeight) {
541 return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position),
542 desiredHeight);
543 }
544
545 /**
546 * Convert a non-null view.
547 */
548 public View convertView(int position, View convertView, ViewGroup parent) {
549 return ListItemFactory.convertText(convertView, getValueAtPosition(position), position);
550 }
551
552 public void setClickedPosition(int clickedPosition) {
553 mClickedPosition = clickedPosition;
554 }
555
556 public int getClickedPosition() {
557 return mClickedPosition;
558 }
559
560 public void setLongClickedPosition(int longClickedPosition) {
561 mLongClickedPosition = longClickedPosition;
562 }
563
564 public int getLongClickedPosition() {
565 return mLongClickedPosition;
566 }
567
568 /**
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.
573 */
574 public void requestRectangleOnScreen(int childIndex, final Rect rect) {
575 final View child = getListView().getChildAt(childIndex);
576
577 child.post(new Runnable() {
578 public void run() {
579 child.requestRectangleOnScreen(rect);
580 }
581 });
582 }
583
584 /**
585 * Return an item type for the specified position in the adapter. Override if your
586 * adapter creates more than one type.
587 */
588 public int getItemViewType(int position) {
589 return 0;
590 }
591
592 /**
593 * Return an the number of types created by the adapter. Override if your
594 * adapter creates more than one type.
595 */
596 public int getViewTypeCount() {
597 return 1;
598 }
599
600 /**
601 * @return The number of times convertView failed
602 */
603 public int getConvertMisses() {
604 return mConvertMisses;
605 }
606
607 private class MyAdapter extends BaseAdapter {
608
609 public int getCount() {
610 return mNumItems;
611 }
612
613 public Object getItem(int position) {
614 return getValueAtPosition(position);
615 }
616
617 public long getItemId(int position) {
618 return position;
619 }
620
621 @Override
622 public boolean areAllItemsEnabled() {
623 return mUnselectableItems.isEmpty();
624 }
625
626 @Override
627 public boolean isEnabled(int position) {
628 return isItemAtPositionSelectable(position);
629 }
630
631 public View getView(int position, View convertView, ViewGroup parent) {
632 View result = null;
633 if (position >= mNumItems || position < 0) {
634 throw new IllegalStateException("position out of range for adapter!");
635 }
636
637 if (convertView != null) {
638 result = convertView(position, convertView, parent);
639 if (result == null) {
640 mConvertMisses++;
641 }
642 }
643
644 if (result == null) {
645 int desiredHeight = getHeightForPosition(position);
646 result = createView(position, parent, desiredHeight);
647 }
648 return result;
649 }
650
651 @Override
652 public int getItemViewType(int position) {
653 return ListScenario.this.getItemViewType(position);
654 }
655
656 @Override
657 public int getViewTypeCount() {
658 return ListScenario.this.getViewTypeCount();
659 }
660
661 }
662}