Home | History | Annotate | Download | only in util
      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 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 
     32 import java.util.ArrayList;
     33 import java.util.HashMap;
     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 = 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 
     78     public ListView getListView() {
     79         return mListView;
     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      */
     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 = new HashMap<>();
    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          */
    117         public Params setNumItems(int numItems) {
    118             mNumItems = numItems;
    119             return this;
    120         }
    121 
    122         /**
    123          * Set whether the items are focusable.
    124          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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      */
    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      */
    269     protected void positionSelected(int positon) {
    270     }
    271 
    272     /**
    273      * Override this if you want to know that nothing is selected.
    274      */
    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      */
    283     protected void positionClicked(int position) {
    284         setClickedPosition(position);
    285     }
    286 
    287     @Override
    288     protected void onCreate(Bundle icicle) {
    289         super.onCreate(icicle);
    290 
    291         // for test stability, turn off title bar
    292         requestWindowFeature(Window.FEATURE_NO_TITLE);
    293 
    294         mScreenHeight = getWindowManager().getDefaultDisplay().getHeight();
    295 
    296         final Params params = createParams();
    297         init(params);
    298 
    299         readAndValidateParams(params);
    300 
    301         mListView = createListView();
    302         mListView.setLayoutParams(new ViewGroup.LayoutParams(
    303                 ViewGroup.LayoutParams.MATCH_PARENT,
    304                 ViewGroup.LayoutParams.MATCH_PARENT));
    305         mListView.setDrawSelectorOnTop(false);
    306 
    307         for (int i=0; i<mHeaderViewCount; i++) {
    308             TextView header = mHeadersFocusable ?
    309                     new EditText(this) :
    310                     new TextView(this);
    311             header.setText("Header: " + i);
    312             mListView.addHeaderView(header);
    313         }
    314 
    315         for (int i=0; i<mFooterViewCount; i++) {
    316             TextView header = new TextView(this);
    317             header.setText("Footer: " + i);
    318             mListView.addFooterView(header);
    319         }
    320 
    321         if (params.mConnectAdapter) {
    322             setAdapter(mListView);
    323         }
    324 
    325         mListView.setItemsCanFocus(mItemsFocusable);
    326         if (mStartingSelectionPosition >= 0) {
    327             mListView.setSelection(mStartingSelectionPosition);
    328         }
    329         mListView.setPadding(0, 0, 0, 0);
    330         mListView.setStackFromBottom(mStackFromBottom);
    331         mListView.setDivider(null);
    332 
    333         mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    334             public void onItemSelected(AdapterView parent, View v, int position, long id) {
    335                 positionSelected(position);
    336             }
    337 
    338             public void onNothingSelected(AdapterView parent) {
    339                 nothingSelected();
    340             }
    341         });
    342 
    343         if (shouldRegisterItemClickListener()) {
    344             mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    345                 public void onItemClick(AdapterView parent, View v, int position, long id) {
    346                     positionClicked(position);
    347                 }
    348             });
    349         }
    350 
    351         // set the fading edge length porportionally to the screen
    352         // height for test stability
    353         if (params.mFadingEdgeScreenSizeFactor != null) {
    354             mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight));
    355         } else {
    356             mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight));
    357         }
    358 
    359         if (mIncludeHeader) {
    360             mLinearLayout = new LinearLayout(this);
    361 
    362             mHeaderTextView = new TextView(this);
    363             mHeaderTextView.setText("hi");
    364             mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams(
    365                     ViewGroup.LayoutParams.MATCH_PARENT,
    366                     ViewGroup.LayoutParams.WRAP_CONTENT));
    367             mLinearLayout.addView(mHeaderTextView);
    368 
    369             mLinearLayout.setOrientation(LinearLayout.VERTICAL);
    370             mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams(
    371                     ViewGroup.LayoutParams.MATCH_PARENT,
    372                     ViewGroup.LayoutParams.MATCH_PARENT));
    373             mListView.setLayoutParams((new LinearLayout.LayoutParams(
    374                     ViewGroup.LayoutParams.MATCH_PARENT,
    375                     0,
    376                     1f)));
    377 
    378             mLinearLayout.addView(mListView);
    379             setContentView(mLinearLayout);
    380         } else {
    381             mLinearLayout = new LinearLayout(this);
    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             mLinearLayout.addView(mListView);
    391             setContentView(mLinearLayout);
    392         }
    393         mLinearLayout.restoreDefaultFocus();
    394     }
    395 
    396     /**
    397      * Override to return false if you don't want the activity to register a default item click
    398      * listener that redirects clicks to {@link #positionClicked(int)}.
    399      */
    400     protected boolean shouldRegisterItemClickListener() {
    401         return true;
    402     }
    403 
    404     /**
    405      * @return The newly created ListView widget.
    406      */
    407     protected ListView createListView() {
    408         return new ListView(this);
    409     }
    410 
    411     /**
    412      * @return The newly created Params object.
    413      */
    414     protected Params createParams() {
    415         return new Params();
    416     }
    417 
    418     /**
    419      * Sets an adapter on a ListView.
    420      *
    421      * @param listView The ListView to set the adapter on.
    422      */
    423     protected void setAdapter(ListView listView) {
    424         listView.setAdapter(new MyAdapter());
    425     }
    426 
    427     /**
    428      * Read in and validate all of the params passed in by the scenario.
    429      * @param params
    430      */
    431     protected void readAndValidateParams(Params params) {
    432         if (params.mMustFillScreen ) {
    433             double totalFactor = 0.0;
    434             for (int i = 0; i < params.mNumItems; i++) {
    435                 if (params.mOverrideItemScreenSizeFactors.containsKey(i)) {
    436                     totalFactor += params.mOverrideItemScreenSizeFactors.get(i);
    437                 } else {
    438                     totalFactor += params.mItemScreenSizeFactor;
    439                 }
    440             }
    441             if (totalFactor < 1.0) {
    442                 throw new IllegalArgumentException("list items must combine to be at least " +
    443                         "the height of the screen.  this is not the case with " + params.mNumItems
    444                         + " items and " + params.mItemScreenSizeFactor + " screen factor and " +
    445                         "screen height of " + mScreenHeight);
    446             }
    447         }
    448 
    449         mNumItems = params.mNumItems;
    450         mItemsFocusable = params.mItemsFocusable;
    451         mStartingSelectionPosition = params.mStartingSelectionPosition;
    452         mItemScreenSizeFactor = params.mItemScreenSizeFactor;
    453 
    454         mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors);
    455 
    456         mUnselectableItems.addAll(params.mUnselectableItems);
    457         mIncludeHeader = params.mIncludeHeader;
    458         mStackFromBottom = params.mStackFromBottom;
    459         mHeaderViewCount = params.mHeaderViewCount;
    460         mHeadersFocusable = params.mHeaderFocusable;
    461         mFooterViewCount = params.mFooterViewCount;
    462     }
    463 
    464     public final String getValueAtPosition(int position) {
    465         return isItemAtPositionSelectable(position)
    466                 ?
    467                 "position " + position:
    468                 "------- " + position;
    469     }
    470 
    471     /**
    472      * @return The height that will be set for a particular position.
    473      */
    474     public int getHeightForPosition(int position) {
    475         int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor);
    476         if (mOverrideItemScreenSizeFactors.containsKey(position)) {
    477             desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position));
    478         }
    479         return desiredHeight;
    480     }
    481 
    482     /**
    483      * @return The contents of the header above the list.
    484      * @throws IllegalArgumentException if there is no header.
    485      */
    486     public final String getHeaderValue() {
    487         if (!mIncludeHeader) {
    488             throw new IllegalArgumentException("no header above list");
    489         }
    490         return mHeaderTextView.getText().toString();
    491     }
    492 
    493     /**
    494      * @param value What to put in the header text view
    495      * @throws IllegalArgumentException if there is no header.
    496      */
    497     protected final void setHeaderValue(String value) {
    498         if (!mIncludeHeader) {
    499             throw new IllegalArgumentException("no header above list");
    500         }
    501         mHeaderTextView.setText(value);
    502     }
    503 
    504     /**
    505      * Create a view for a list item.  Override this to create a custom view beyond
    506      * the simple focusable / unfocusable text view.
    507      * @param position The position.
    508      * @param parent The parent
    509      * @param desiredHeight The height the view should be to respect the desired item
    510      *   to screen height ratio.
    511      * @return a view for the list.
    512      */
    513     protected View createView(int position, ViewGroup parent, int desiredHeight) {
    514         return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position),
    515                 desiredHeight);
    516     }
    517 
    518     /**
    519      * Convert a non-null view.
    520      */
    521     public View convertView(int position, View convertView, ViewGroup parent) {
    522         return ListItemFactory.convertText(convertView, getValueAtPosition(position), position);
    523     }
    524 
    525     public void setClickedPosition(int clickedPosition) {
    526         mClickedPosition = clickedPosition;
    527     }
    528 
    529     public int getClickedPosition() {
    530         return mClickedPosition;
    531     }
    532 
    533     public void setLongClickedPosition(int longClickedPosition) {
    534         mLongClickedPosition = longClickedPosition;
    535     }
    536 
    537     public int getLongClickedPosition() {
    538         return mLongClickedPosition;
    539     }
    540 
    541     /**
    542      * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}.
    543      * @param childIndex The index into the viewgroup children (i.e the children that are
    544      *   currently visible).
    545      * @param rect The rectangle, in the child's coordinates.
    546      */
    547     public void requestRectangleOnScreen(int childIndex, final Rect rect) {
    548         final View child = getListView().getChildAt(childIndex);
    549 
    550         child.post(new Runnable() {
    551             public void run() {
    552                 child.requestRectangleOnScreen(rect);
    553             }
    554         });
    555     }
    556 
    557     /**
    558      * Return an item type for the specified position in the adapter. Override if your
    559      * adapter creates more than one type.
    560      */
    561     public int getItemViewType(int position) {
    562         return 0;
    563     }
    564 
    565     /**
    566      * Return an the number of types created by the adapter. Override if your
    567      * adapter creates more than one type.
    568      */
    569     public int getViewTypeCount() {
    570         return 1;
    571     }
    572 
    573     /**
    574      * @return The number of times convertView failed
    575      */
    576     public int getConvertMisses() {
    577         return mConvertMisses;
    578     }
    579 
    580     private class MyAdapter extends BaseAdapter {
    581 
    582         public int getCount() {
    583             return mNumItems;
    584         }
    585 
    586         public Object getItem(int position) {
    587             return getValueAtPosition(position);
    588         }
    589 
    590         public long getItemId(int position) {
    591             return position;
    592         }
    593 
    594         @Override
    595         public boolean areAllItemsEnabled() {
    596             return mUnselectableItems.isEmpty();
    597         }
    598 
    599         @Override
    600         public boolean isEnabled(int position) {
    601             return isItemAtPositionSelectable(position);
    602         }
    603 
    604         public View getView(int position, View convertView, ViewGroup parent) {
    605             View result = null;
    606             if (position >= mNumItems || position < 0) {
    607                 throw new IllegalStateException("position out of range for adapter!");
    608             }
    609 
    610             if (convertView != null) {
    611                 result = convertView(position, convertView, parent);
    612                 if (result == null) {
    613                     mConvertMisses++;
    614                 }
    615             }
    616 
    617             if (result == null) {
    618                 int desiredHeight = getHeightForPosition(position);
    619                 result = createView(position, parent, desiredHeight);
    620             }
    621             return result;
    622         }
    623 
    624         @Override
    625         public int getItemViewType(int position) {
    626             return ListScenario.this.getItemViewType(position);
    627         }
    628 
    629         @Override
    630         public int getViewTypeCount() {
    631             return ListScenario.this.getViewTypeCount();
    632         }
    633 
    634     }
    635 }
    636