1/*
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 android.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(
318                ViewGroup.LayoutParams.MATCH_PARENT,
319                ViewGroup.LayoutParams.MATCH_PARENT));
320        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(
378                    ViewGroup.LayoutParams.MATCH_PARENT,
379                    ViewGroup.LayoutParams.WRAP_CONTENT));
380            mLinearLayout.addView(mHeaderTextView);
381
382            mLinearLayout.setOrientation(LinearLayout.VERTICAL);
383            mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
384                    ViewGroup.LayoutParams.MATCH_PARENT,
385                    ViewGroup.LayoutParams.MATCH_PARENT));
386            mListView.setLayoutParams((new LinearLayout.LayoutParams(
387                    ViewGroup.LayoutParams.MATCH_PARENT,
388                    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(
397                    ViewGroup.LayoutParams.MATCH_PARENT,
398                    ViewGroup.LayoutParams.MATCH_PARENT));
399            mListView.setLayoutParams((new LinearLayout.LayoutParams(
400                    ViewGroup.LayoutParams.MATCH_PARENT,
401                    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 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}
663