• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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;
18 
19 import android.app.Activity;
20 import android.graphics.Rect;
21 import android.os.Bundle;
22 import android.util.ListItemFactory;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.view.Window;
26 
27 import com.google.android.collect.Maps;
28 
29 import java.util.ArrayList;
30 import java.util.HashSet;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Set;
34 
35 /**
36  * Utility base class for creating various List scenarios.  Configurable by the number
37  * of items, how tall each item should be (in relation to the screen height), and
38  * what item should start with selection.
39  */
40 public abstract class ListScenario extends Activity {
41 
42     private ListView mListView;
43     private TextView mHeaderTextView;
44 
45     private int mNumItems;
46     protected boolean mItemsFocusable;
47 
48     private int mStartingSelectionPosition;
49     private double mItemScreenSizeFactor;
50     private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
51 
52     private int mScreenHeight;
53 
54     // whether to include a text view above the list
55     private boolean mIncludeHeader;
56 
57     // separators
58     private Set<Integer> mUnselectableItems = new HashSet<Integer>();
59 
60     private boolean mStackFromBottom;
61 
62     private int mClickedPosition = -1;
63 
64     private int mLongClickedPosition = -1;
65 
66     private int mConvertMisses = 0;
67 
68     private int mHeaderViewCount;
69     private boolean mHeadersFocusable;
70 
71     private int mFooterViewCount;
72     private LinearLayout mLinearLayout;
73 
getListView()74     public ListView getListView() {
75         return mListView;
76     }
77 
getScreenHeight()78     protected int getScreenHeight() {
79         return mScreenHeight;
80     }
81 
82     /**
83      * Return whether the item at position is selectable (i.e is a separator).
84      * (external users can access this info using the adapter)
85      */
isItemAtPositionSelectable(int position)86     private boolean isItemAtPositionSelectable(int position) {
87         return !mUnselectableItems.contains(position);
88     }
89 
90     /**
91      * Better way to pass in optional params than a honkin' paramater list :)
92      */
93     public static class Params {
94         private int mNumItems = 4;
95         private boolean mItemsFocusable = false;
96         private int mStartingSelectionPosition = 0;
97         private double mItemScreenSizeFactor = 1 / 5;
98         private Double mFadingEdgeScreenSizeFactor = null;
99 
100         private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap();
101 
102         // separators
103         private List<Integer> mUnselectableItems = new ArrayList<Integer>(8);
104         // whether to include a text view above the list
105         private boolean mIncludeHeader = false;
106         private boolean mStackFromBottom = false;
107         public boolean mMustFillScreen = true;
108         private int mHeaderViewCount;
109         private boolean mHeaderFocusable = false;
110         private int mFooterViewCount;
111 
112         private boolean mConnectAdapter = true;
113 
114         /**
115          * Set the number of items in the list.
116          */
setNumItems(int numItems)117         public Params setNumItems(int numItems) {
118             mNumItems = numItems;
119             return this;
120         }
121 
122         /**
123          * Set whether the items are focusable.
124          */
setItemsFocusable(boolean itemsFocusable)125         public Params setItemsFocusable(boolean itemsFocusable) {
126             mItemsFocusable = itemsFocusable;
127             return this;
128         }
129 
130         /**
131          * Set the position that starts selected.
132          *
133          * @param startingSelectionPosition The selected position within the adapter's data set.
134          * Pass -1 if you do not want to force a selection.
135          * @return
136          */
setStartingSelectionPosition(int startingSelectionPosition)137         public Params setStartingSelectionPosition(int startingSelectionPosition) {
138             mStartingSelectionPosition = startingSelectionPosition;
139             return this;
140         }
141 
142         /**
143          * Set the factor that determines how tall each item is in relation to the
144          * screen height.
145          */
setItemScreenSizeFactor(double itemScreenSizeFactor)146         public Params setItemScreenSizeFactor(double itemScreenSizeFactor) {
147             mItemScreenSizeFactor = itemScreenSizeFactor;
148             return this;
149         }
150 
151         /**
152          * Override the item screen size factor for a particular item.  Useful for
153          * creating lists with non-uniform item height.
154          * @param position The position in the list.
155          * @param itemScreenSizeFactor The screen size factor to use for the height.
156          */
setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor)157         public Params setPositionScreenSizeFactorOverride(
158                 int position, double itemScreenSizeFactor) {
159             mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor);
160             return this;
161         }
162 
163         /**
164          * Set a position as unselectable (a.k.a a separator)
165          * @param position
166          * @return
167          */
setPositionUnselectable(int position)168         public Params setPositionUnselectable(int position) {
169             mUnselectableItems.add(position);
170             return this;
171         }
172 
173         /**
174          * Set positions as unselectable (a.k.a a separator)
175          */
setPositionsUnselectable(int ...positions)176         public Params setPositionsUnselectable(int ...positions) {
177             for (int pos : positions) {
178                 setPositionUnselectable(pos);
179             }
180             return this;
181         }
182 
183         /**
184          * Include a header text view above the list.
185          * @param includeHeader
186          * @return
187          */
includeHeaderAboveList(boolean includeHeader)188         public Params includeHeaderAboveList(boolean includeHeader) {
189             mIncludeHeader = includeHeader;
190             return this;
191         }
192 
193         /**
194          * Sets the stacking direction
195          * @param stackFromBottom
196          * @return
197          */
setStackFromBottom(boolean stackFromBottom)198         public Params setStackFromBottom(boolean stackFromBottom) {
199             mStackFromBottom = stackFromBottom;
200             return this;
201         }
202 
203         /**
204          * Sets whether the sum of the height of the list items must be at least the
205          * height of the list view.
206          */
setMustFillScreen(boolean fillScreen)207         public Params setMustFillScreen(boolean fillScreen) {
208             mMustFillScreen = fillScreen;
209             return this;
210         }
211 
212         /**
213          * Set the factor for the fading edge length.
214          */
setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor)215         public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) {
216             mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor;
217             return this;
218         }
219 
220         /**
221          * Set the number of header views to appear within the list
222          */
setHeaderViewCount(int headerViewCount)223         public Params setHeaderViewCount(int headerViewCount) {
224             mHeaderViewCount = headerViewCount;
225             return this;
226         }
227 
228         /**
229          * Set whether the headers should be focusable.
230          * @param headerFocusable Whether the headers should be focusable (i.e
231          *   created as edit texts rather than text views).
232          */
setHeaderFocusable(boolean headerFocusable)233         public Params setHeaderFocusable(boolean headerFocusable) {
234             mHeaderFocusable = headerFocusable;
235             return this;
236         }
237 
238         /**
239          * Set the number of footer views to appear within the list
240          */
setFooterViewCount(int footerViewCount)241         public Params setFooterViewCount(int footerViewCount) {
242             mFooterViewCount = footerViewCount;
243             return this;
244         }
245 
246         /**
247          * Sets whether the {@link ListScenario} will automatically set the
248          * adapter on the list view. If this is false, the client MUST set it
249          * manually (this is useful when adding headers to the list view, which
250          * must be done before the adapter is set).
251          */
setConnectAdapter(boolean connectAdapter)252         public Params setConnectAdapter(boolean connectAdapter) {
253             mConnectAdapter = connectAdapter;
254             return this;
255         }
256     }
257 
258     /**
259      * How each scenario customizes its behavior.
260      * @param params
261      */
init(Params params)262     protected abstract void init(Params params);
263 
264     /**
265      * Override this if you want to know when something has been selected (perhaps
266      * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has
267      * been triggered).
268      */
positionSelected(int positon)269     protected void positionSelected(int positon) {
270     }
271 
272     /**
273      * Override this if you want to know that nothing is selected.
274      */
nothingSelected()275     protected void nothingSelected() {
276     }
277 
278     /**
279      * Override this if you want to know when something has been clicked (perhaps
280      * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has
281      * been triggered).
282      */
positionClicked(int position)283     protected void positionClicked(int position) {
284         setClickedPosition(position);
285     }
286 
287     /**
288      * Override this if you want to know when something has been long clicked (perhaps
289      * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has
290      * been triggered).
291      */
positionLongClicked(int position)292     protected void positionLongClicked(int position) {
293         setLongClickedPosition(position);
294     }
295 
296     @Override
onCreate(Bundle icicle)297     protected void onCreate(Bundle icicle) {
298         super.onCreate(icicle);
299 
300         // for test stability, turn off title bar
301         requestWindowFeature(Window.FEATURE_NO_TITLE);
302 
303 
304         mScreenHeight = getWindowManager().getCurrentWindowMetrics().getBounds().height();
305 
306         final Params params = createParams();
307         init(params);
308 
309         readAndValidateParams(params);
310 
311 
312         mListView = createListView();
313         mListView.setLayoutParams(new ViewGroup.LayoutParams(
314                 ViewGroup.LayoutParams.MATCH_PARENT,
315                 ViewGroup.LayoutParams.MATCH_PARENT));
316         mListView.setDrawSelectorOnTop(false);
317 
318         for (int i=0; i<mHeaderViewCount; i++) {
319             TextView header = mHeadersFocusable ?
320                     new EditText(this) :
321                     new TextView(this);
322             header.setText("Header: " + i);
323             mListView.addHeaderView(header);
324         }
325 
326         for (int i=0; i<mFooterViewCount; i++) {
327             TextView header = new TextView(this);
328             header.setText("Footer: " + i);
329             mListView.addFooterView(header);
330         }
331 
332         if (params.mConnectAdapter) {
333             setAdapter(mListView);
334         }
335 
336         mListView.setItemsCanFocus(mItemsFocusable);
337         if (mStartingSelectionPosition >= 0) {
338             mListView.setSelection(mStartingSelectionPosition);
339         }
340         mListView.setPadding(0, 0, 0, 0);
341         mListView.setStackFromBottom(mStackFromBottom);
342         mListView.setDivider(null);
343 
344         mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
345             public void onItemSelected(AdapterView parent, View v, int position, long id) {
346                 positionSelected(position);
347             }
348 
349             public void onNothingSelected(AdapterView parent) {
350                 nothingSelected();
351             }
352         });
353 
354         mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
355             public void onItemClick(AdapterView parent, View v, int position, long id) {
356                 positionClicked(position);
357             }
358         });
359 
360         // set the fading edge length porportionally to the screen
361         // height for test stability
362         if (params.mFadingEdgeScreenSizeFactor != null) {
363             mListView.setFadingEdgeLength(
364                     (int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight));
365         } else {
366             mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
367         }
368 
369         if (mIncludeHeader) {
370             mLinearLayout = new LinearLayout(this);
371 
372             mHeaderTextView = new TextView(this);
373             mHeaderTextView.setText("hi");
374             mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
375                     ViewGroup.LayoutParams.MATCH_PARENT,
376                     ViewGroup.LayoutParams.WRAP_CONTENT));
377             mLinearLayout.addView(mHeaderTextView);
378 
379             mLinearLayout.setOrientation(LinearLayout.VERTICAL);
380             mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
381                     ViewGroup.LayoutParams.MATCH_PARENT,
382                     ViewGroup.LayoutParams.MATCH_PARENT));
383             mListView.setLayoutParams((new LinearLayout.LayoutParams(
384                     ViewGroup.LayoutParams.MATCH_PARENT,
385                     0,
386                     1f)));
387 
388             mLinearLayout.addView(mListView);
389             setContentView(mLinearLayout);
390         } else {
391             mLinearLayout = new LinearLayout(this);
392             mLinearLayout.setOrientation(LinearLayout.VERTICAL);
393             mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
394                     ViewGroup.LayoutParams.MATCH_PARENT,
395                     ViewGroup.LayoutParams.MATCH_PARENT));
396             mListView.setLayoutParams((new LinearLayout.LayoutParams(
397                     ViewGroup.LayoutParams.MATCH_PARENT,
398                     0,
399                     1f)));
400             mLinearLayout.addView(mListView);
401             setContentView(mLinearLayout);
402         }
403         mLinearLayout.restoreDefaultFocus();
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     /**
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 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