Home | History | Annotate | Download | only in util
      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 
     17 package android.util;
     18 
     19 import android.app.Activity;
     20 import android.graphics.Rect;
     21 import android.os.Bundle;
     22 import android.view.View;
     23 import android.view.ViewGroup;
     24 import android.view.Window;
     25 import android.widget.AdapterView;
     26 import android.widget.BaseAdapter;
     27 import android.widget.EditText;
     28 import android.widget.LinearLayout;
     29 import android.widget.ListView;
     30 import android.widget.TextView;
     31 import com.google.android.collect.Maps;
     32 
     33 import java.util.ArrayList;
     34 import java.util.HashSet;
     35 import java.util.List;
     36 import java.util.Map;
     37 import 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  */
     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 = 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