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