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.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 
26 import android.app.Activity;
27 import android.graphics.Rect;
28 import android.os.Bundle;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.Window;
32 import android.widget.AdapterView;
33 import android.widget.BaseAdapter;
34 import android.widget.EditText;
35 import android.widget.LinearLayout;
36 import android.widget.ListView;
37 import android.widget.TextView;
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  */
44 public 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 = new HashMap<>();
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 
getListView()78     public ListView getListView() {
79         return mListView;
80     }
81 
getScreenHeight()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      */
isItemAtPositionSelectable(int position)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 = new HashMap<>();
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          */
setNumItems(int numItems)121         public Params setNumItems(int numItems) {
122             mNumItems = numItems;
123             return this;
124         }
125 
126         /**
127          * Set whether the items are focusable.
128          */
setItemsFocusable(boolean itemsFocusable)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          */
setStartingSelectionPosition(int startingSelectionPosition)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          */
setItemScreenSizeFactor(double itemScreenSizeFactor)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          */
setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor)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          */
setPositionUnselectable(int position)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          */
setPositionsUnselectable(int ...positions)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          */
includeHeaderAboveList(boolean includeHeader)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          */
setStackFromBottom(boolean stackFromBottom)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          */
setMustFillScreen(boolean fillScreen)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          */
setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor)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          */
setHeaderViewCount(int headerViewCount)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          */
setHeaderFocusable(boolean headerFocusable)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          */
setFooterViewCount(int footerViewCount)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          */
setConnectAdapter(boolean connectAdapter)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      */
init(Params params)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      */
positionSelected(int positon)273     protected void positionSelected(int positon) {
274     }
275 
276     /**
277      * Override this if you want to know that nothing is selected.
278      */
nothingSelected()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      */
positionClicked(int position)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      */
positionLongClicked(int position)296     protected void positionLongClicked(int position) {
297         setLongClickedPosition(position);
298     }
299 
300     @Override
onCreate(Bundle icicle)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         mScreenHeight = getWindowManager().getDefaultDisplay().getHeight();
308 
309         final Params params = createParams();
310         init(params);
311 
312         readAndValidateParams(params);
313 
314         mListView = createListView();
315         mListView.setLayoutParams(new ViewGroup.LayoutParams(
316                 ViewGroup.LayoutParams.MATCH_PARENT,
317                 ViewGroup.LayoutParams.MATCH_PARENT));
318         mListView.setDrawSelectorOnTop(false);
319 
320         for (int i=0; i<mHeaderViewCount; i++) {
321             TextView header = mHeadersFocusable ?
322                     new EditText(this) :
323                     new TextView(this);
324             header.setText("Header: " + i);
325             mListView.addHeaderView(header);
326         }
327 
328         for (int i=0; i<mFooterViewCount; i++) {
329             TextView header = new TextView(this);
330             header.setText("Footer: " + i);
331             mListView.addFooterView(header);
332         }
333 
334         if (params.mConnectAdapter) {
335             setAdapter(mListView);
336         }
337 
338         mListView.setItemsCanFocus(mItemsFocusable);
339         if (mStartingSelectionPosition >= 0) {
340             mListView.setSelection(mStartingSelectionPosition);
341         }
342         mListView.setPadding(0, 0, 0, 0);
343         mListView.setStackFromBottom(mStackFromBottom);
344         mListView.setDivider(null);
345 
346         mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
347             public void onItemSelected(AdapterView parent, View v, int position, long id) {
348                 positionSelected(position);
349             }
350 
351             public void onNothingSelected(AdapterView parent) {
352                 nothingSelected();
353             }
354         });
355 
356         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
357             public void onItemClick(AdapterView parent, View v, int position, long id) {
358                 positionClicked(position);
359             }
360         });
361 
362         // set the fading edge length porportionally to the screen
363         // height for test stability
364         if (params.mFadingEdgeScreenSizeFactor != null) {
365             mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight));
366         } else {
367             mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
368         }
369 
370         if (mIncludeHeader) {
371             mLinearLayout = new LinearLayout(this);
372 
373             mHeaderTextView = new TextView(this);
374             mHeaderTextView.setText("hi");
375             mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
376                     ViewGroup.LayoutParams.MATCH_PARENT,
377                     ViewGroup.LayoutParams.WRAP_CONTENT));
378             mLinearLayout.addView(mHeaderTextView);
379 
380             mLinearLayout.setOrientation(LinearLayout.VERTICAL);
381             mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
382                     ViewGroup.LayoutParams.MATCH_PARENT,
383                     ViewGroup.LayoutParams.MATCH_PARENT));
384             mListView.setLayoutParams((new LinearLayout.LayoutParams(
385                     ViewGroup.LayoutParams.MATCH_PARENT,
386                     0,
387                     1f)));
388 
389             mLinearLayout.addView(mListView);
390             setContentView(mLinearLayout);
391         } else {
392             mLinearLayout = new LinearLayout(this);
393             mLinearLayout.setOrientation(LinearLayout.VERTICAL);
394             mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
395                     ViewGroup.LayoutParams.MATCH_PARENT,
396                     ViewGroup.LayoutParams.MATCH_PARENT));
397             mListView.setLayoutParams((new LinearLayout.LayoutParams(
398                     ViewGroup.LayoutParams.MATCH_PARENT,
399                     0,
400                     1f)));
401             mLinearLayout.addView(mListView);
402             setContentView(mLinearLayout);
403         }
404     }
405 
406     /**
407      * Returns the LinearLayout containing the ListView in this scenario.
408      *
409      * @return The LinearLayout in which the ListView is held.
410      */
getListViewContainer()411     protected LinearLayout getListViewContainer() {
412         return mLinearLayout;
413     }
414 
415     /**
416      * Attaches a long press listener. You can find out which views were clicked by calling
417      * {@link #getLongClickedPosition()}.
418      */
enableLongPress()419     public void enableLongPress() {
420         mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
421             public boolean onItemLongClick(AdapterView parent, View v, int position, long id) {
422                 positionLongClicked(position);
423                 return true;
424             }
425         });
426     }
427 
428     /**
429      * @return The newly created ListView widget.
430      */
createListView()431     protected ListView createListView() {
432         return new ListView(this);
433     }
434 
435     /**
436      * @return The newly created Params object.
437      */
createParams()438     protected Params createParams() {
439         return new Params();
440     }
441 
442     /**
443      * Sets an adapter on a ListView.
444      *
445      * @param listView The ListView to set the adapter on.
446      */
setAdapter(ListView listView)447     protected void setAdapter(ListView listView) {
448         listView.setAdapter(new MyAdapter());
449     }
450 
451     /**
452      * Read in and validate all of the params passed in by the scenario.
453      * @param params
454      */
readAndValidateParams(Params params)455     protected void readAndValidateParams(Params params) {
456         if (params.mMustFillScreen ) {
457             double totalFactor = 0.0;
458             for (int i = 0; i < params.mNumItems; i++) {
459                 if (params.mOverrideItemScreenSizeFactors.containsKey(i)) {
460                     totalFactor += params.mOverrideItemScreenSizeFactors.get(i);
461                 } else {
462                     totalFactor += params.mItemScreenSizeFactor;
463                 }
464             }
465             if (totalFactor < 1.0) {
466                 throw new IllegalArgumentException("list items must combine to be at least " +
467                         "the height of the screen.  this is not the case with " + params.mNumItems
468                         + " items and " + params.mItemScreenSizeFactor + " screen factor and " +
469                         "screen height of " + mScreenHeight);
470             }
471         }
472 
473         mNumItems = params.mNumItems;
474         mItemsFocusable = params.mItemsFocusable;
475         mStartingSelectionPosition = params.mStartingSelectionPosition;
476         mItemScreenSizeFactor = params.mItemScreenSizeFactor;
477 
478         mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors);
479 
480         mUnselectableItems.addAll(params.mUnselectableItems);
481         mIncludeHeader = params.mIncludeHeader;
482         mStackFromBottom = params.mStackFromBottom;
483         mHeaderViewCount = params.mHeaderViewCount;
484         mHeadersFocusable = params.mHeaderFocusable;
485         mFooterViewCount = params.mFooterViewCount;
486     }
487 
getValueAtPosition(int position)488     public final String getValueAtPosition(int position) {
489         return isItemAtPositionSelectable(position)
490                 ?
491                 "position " + position:
492                 "------- " + position;
493     }
494 
495     /**
496      * @return The height that will be set for a particular position.
497      */
getHeightForPosition(int position)498     public int getHeightForPosition(int position) {
499         int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor);
500         if (mOverrideItemScreenSizeFactors.containsKey(position)) {
501             desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position));
502         }
503         return desiredHeight;
504     }
505 
506     /**
507      * @return The contents of the header above the list.
508      * @throws IllegalArgumentException if there is no header.
509      */
getHeaderValue()510     public final String getHeaderValue() {
511         if (!mIncludeHeader) {
512             throw new IllegalArgumentException("no header above list");
513         }
514         return mHeaderTextView.getText().toString();
515     }
516 
517     /**
518      * @param value What to put in the header text view
519      * @throws IllegalArgumentException if there is no header.
520      */
setHeaderValue(String value)521     protected final void setHeaderValue(String value) {
522         if (!mIncludeHeader) {
523             throw new IllegalArgumentException("no header above list");
524         }
525         mHeaderTextView.setText(value);
526     }
527 
528     /**
529      * Create a view for a list item.  Override this to create a custom view beyond
530      * the simple focusable / unfocusable text view.
531      * @param position The position.
532      * @param parent The parent
533      * @param desiredHeight The height the view should be to respect the desired item
534      *   to screen height ratio.
535      * @return a view for the list.
536      */
createView(int position, ViewGroup parent, int desiredHeight)537     protected View createView(int position, ViewGroup parent, int desiredHeight) {
538         return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position),
539                 desiredHeight);
540     }
541 
542     /**
543      * Convert a non-null view.
544      */
convertView(int position, View convertView, ViewGroup parent)545     public View convertView(int position, View convertView, ViewGroup parent) {
546         return ListItemFactory.convertText(convertView, getValueAtPosition(position), position);
547     }
548 
setClickedPosition(int clickedPosition)549     public void setClickedPosition(int clickedPosition) {
550         mClickedPosition = clickedPosition;
551     }
552 
getClickedPosition()553     public int getClickedPosition() {
554         return mClickedPosition;
555     }
556 
setLongClickedPosition(int longClickedPosition)557     public void setLongClickedPosition(int longClickedPosition) {
558         mLongClickedPosition = longClickedPosition;
559     }
560 
getLongClickedPosition()561     public int getLongClickedPosition() {
562         return mLongClickedPosition;
563     }
564 
565     /**
566      * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}.
567      * @param childIndex The index into the viewgroup children (i.e the children that are
568      *   currently visible).
569      * @param rect The rectangle, in the child's coordinates.
570      */
requestRectangleOnScreen(int childIndex, final Rect rect)571     public void requestRectangleOnScreen(int childIndex, final Rect rect) {
572         final View child = getListView().getChildAt(childIndex);
573 
574         child.post(new Runnable() {
575             public void run() {
576                 child.requestRectangleOnScreen(rect);
577             }
578         });
579     }
580 
581     /**
582      * Return an item type for the specified position in the adapter. Override if your
583      * adapter creates more than one type.
584      */
getItemViewType(int position)585     public int getItemViewType(int position) {
586         return 0;
587     }
588 
589     /**
590      * Return an the number of types created by the adapter. Override if your
591      * adapter creates more than one type.
592      */
getViewTypeCount()593     public int getViewTypeCount() {
594         return 1;
595     }
596 
597     /**
598      * @return The number of times convertView failed
599      */
getConvertMisses()600     public int getConvertMisses() {
601         return mConvertMisses;
602     }
603 
604     private class MyAdapter extends BaseAdapter {
605 
getCount()606         public int getCount() {
607             return mNumItems;
608         }
609 
getItem(int position)610         public Object getItem(int position) {
611             return getValueAtPosition(position);
612         }
613 
getItemId(int position)614         public long getItemId(int position) {
615             return position;
616         }
617 
618         @Override
areAllItemsEnabled()619         public boolean areAllItemsEnabled() {
620             return mUnselectableItems.isEmpty();
621         }
622 
623         @Override
isEnabled(int position)624         public boolean isEnabled(int position) {
625             return isItemAtPositionSelectable(position);
626         }
627 
getView(int position, View convertView, ViewGroup parent)628         public View getView(int position, View convertView, ViewGroup parent) {
629             View result = null;
630             if (position >= mNumItems || position < 0) {
631                 throw new IllegalStateException("position out of range for adapter!");
632             }
633 
634             if (convertView != null) {
635                 result = convertView(position, convertView, parent);
636                 if (result == null) {
637                     mConvertMisses++;
638                 }
639             }
640 
641             if (result == null) {
642                 int desiredHeight = getHeightForPosition(position);
643                 result = createView(position, parent, desiredHeight);
644             }
645             return result;
646         }
647 
648         @Override
getItemViewType(int position)649         public int getItemViewType(int position) {
650             return ListScenario.this.getItemViewType(position);
651         }
652 
653         @Override
getViewTypeCount()654         public int getViewTypeCount() {
655             return ListScenario.this.getViewTypeCount();
656         }
657 
658     }
659 }
660