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 32 import com.google.android.collect.Maps; 33 34 import java.util.ArrayList; 35 import java.util.HashSet; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.Set; 39 40 /** 41 * Utility base class for creating various List scenarios. Configurable by the number 42 * of items, how tall each item should be (in relation to the screen height), and 43 * what item should start with selection. 44 */ 45 public abstract class ListScenario extends Activity { 46 47 private ListView mListView; 48 private TextView mHeaderTextView; 49 50 private int mNumItems; 51 protected boolean mItemsFocusable; 52 53 private int mStartingSelectionPosition; 54 private double mItemScreenSizeFactor; 55 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap(); 56 57 private int mScreenHeight; 58 59 // whether to include a text view above the list 60 private boolean mIncludeHeader; 61 62 // separators 63 private Set<Integer> mUnselectableItems = new HashSet<Integer>(); 64 65 private boolean mStackFromBottom; 66 67 private int mClickedPosition = -1; 68 69 private int mLongClickedPosition = -1; 70 71 private int mConvertMisses = 0; 72 73 private int mHeaderViewCount; 74 private boolean mHeadersFocusable; 75 76 private int mFooterViewCount; 77 private LinearLayout mLinearLayout; 78 getListView()79 public ListView getListView() { 80 return mListView; 81 } 82 getScreenHeight()83 protected int getScreenHeight() { 84 return mScreenHeight; 85 } 86 87 /** 88 * Return whether the item at position is selectable (i.e is a separator). 89 * (external users can access this info using the adapter) 90 */ isItemAtPositionSelectable(int position)91 private boolean isItemAtPositionSelectable(int position) { 92 return !mUnselectableItems.contains(position); 93 } 94 95 /** 96 * Better way to pass in optional params than a honkin' paramater list :) 97 */ 98 public static class Params { 99 private int mNumItems = 4; 100 private boolean mItemsFocusable = false; 101 private int mStartingSelectionPosition = 0; 102 private double mItemScreenSizeFactor = 1 / 5; 103 private Double mFadingEdgeScreenSizeFactor = null; 104 105 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap(); 106 107 // separators 108 private List<Integer> mUnselectableItems = new ArrayList<Integer>(8); 109 // whether to include a text view above the list 110 private boolean mIncludeHeader = false; 111 private boolean mStackFromBottom = false; 112 public boolean mMustFillScreen = true; 113 private int mHeaderViewCount; 114 private boolean mHeaderFocusable = false; 115 private int mFooterViewCount; 116 117 private boolean mConnectAdapter = true; 118 119 /** 120 * Set the number of items in the list. 121 */ setNumItems(int numItems)122 public Params setNumItems(int numItems) { 123 mNumItems = numItems; 124 return this; 125 } 126 127 /** 128 * Set whether the items are focusable. 129 */ setItemsFocusable(boolean itemsFocusable)130 public Params setItemsFocusable(boolean itemsFocusable) { 131 mItemsFocusable = itemsFocusable; 132 return this; 133 } 134 135 /** 136 * Set the position that starts selected. 137 * 138 * @param startingSelectionPosition The selected position within the adapter's data set. 139 * Pass -1 if you do not want to force a selection. 140 * @return 141 */ setStartingSelectionPosition(int startingSelectionPosition)142 public Params setStartingSelectionPosition(int startingSelectionPosition) { 143 mStartingSelectionPosition = startingSelectionPosition; 144 return this; 145 } 146 147 /** 148 * Set the factor that determines how tall each item is in relation to the 149 * screen height. 150 */ setItemScreenSizeFactor(double itemScreenSizeFactor)151 public Params setItemScreenSizeFactor(double itemScreenSizeFactor) { 152 mItemScreenSizeFactor = itemScreenSizeFactor; 153 return this; 154 } 155 156 /** 157 * Override the item screen size factor for a particular item. Useful for 158 * creating lists with non-uniform item height. 159 * @param position The position in the list. 160 * @param itemScreenSizeFactor The screen size factor to use for the height. 161 */ setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor)162 public Params setPositionScreenSizeFactorOverride( 163 int position, double itemScreenSizeFactor) { 164 mOverrideItemScreenSizeFactors.put(position, itemScreenSizeFactor); 165 return this; 166 } 167 168 /** 169 * Set a position as unselectable (a.k.a a separator) 170 * @param position 171 * @return 172 */ setPositionUnselectable(int position)173 public Params setPositionUnselectable(int position) { 174 mUnselectableItems.add(position); 175 return this; 176 } 177 178 /** 179 * Set positions as unselectable (a.k.a a separator) 180 */ setPositionsUnselectable(int ...positions)181 public Params setPositionsUnselectable(int ...positions) { 182 for (int pos : positions) { 183 setPositionUnselectable(pos); 184 } 185 return this; 186 } 187 188 /** 189 * Include a header text view above the list. 190 * @param includeHeader 191 * @return 192 */ includeHeaderAboveList(boolean includeHeader)193 public Params includeHeaderAboveList(boolean includeHeader) { 194 mIncludeHeader = includeHeader; 195 return this; 196 } 197 198 /** 199 * Sets the stacking direction 200 * @param stackFromBottom 201 * @return 202 */ setStackFromBottom(boolean stackFromBottom)203 public Params setStackFromBottom(boolean stackFromBottom) { 204 mStackFromBottom = stackFromBottom; 205 return this; 206 } 207 208 /** 209 * Sets whether the sum of the height of the list items must be at least the 210 * height of the list view. 211 */ setMustFillScreen(boolean fillScreen)212 public Params setMustFillScreen(boolean fillScreen) { 213 mMustFillScreen = fillScreen; 214 return this; 215 } 216 217 /** 218 * Set the factor for the fading edge length. 219 */ setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor)220 public Params setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor) { 221 mFadingEdgeScreenSizeFactor = fadingEdgeScreenSizeFactor; 222 return this; 223 } 224 225 /** 226 * Set the number of header views to appear within the list 227 */ setHeaderViewCount(int headerViewCount)228 public Params setHeaderViewCount(int headerViewCount) { 229 mHeaderViewCount = headerViewCount; 230 return this; 231 } 232 233 /** 234 * Set whether the headers should be focusable. 235 * @param headerFocusable Whether the headers should be focusable (i.e 236 * created as edit texts rather than text views). 237 */ setHeaderFocusable(boolean headerFocusable)238 public Params setHeaderFocusable(boolean headerFocusable) { 239 mHeaderFocusable = headerFocusable; 240 return this; 241 } 242 243 /** 244 * Set the number of footer views to appear within the list 245 */ setFooterViewCount(int footerViewCount)246 public Params setFooterViewCount(int footerViewCount) { 247 mFooterViewCount = footerViewCount; 248 return this; 249 } 250 251 /** 252 * Sets whether the {@link ListScenario} will automatically set the 253 * adapter on the list view. If this is false, the client MUST set it 254 * manually (this is useful when adding headers to the list view, which 255 * must be done before the adapter is set). 256 */ setConnectAdapter(boolean connectAdapter)257 public Params setConnectAdapter(boolean connectAdapter) { 258 mConnectAdapter = connectAdapter; 259 return this; 260 } 261 } 262 263 /** 264 * How each scenario customizes its behavior. 265 * @param params 266 */ init(Params params)267 protected abstract void init(Params params); 268 269 /** 270 * Override this if you want to know when something has been selected (perhaps 271 * more importantly, that {@link android.widget.AdapterView.OnItemSelectedListener} has 272 * been triggered). 273 */ positionSelected(int positon)274 protected void positionSelected(int positon) { 275 } 276 277 /** 278 * Override this if you want to know that nothing is selected. 279 */ nothingSelected()280 protected void nothingSelected() { 281 } 282 283 /** 284 * Override this if you want to know when something has been clicked (perhaps 285 * more importantly, that {@link android.widget.AdapterView.OnItemClickListener} has 286 * been triggered). 287 */ positionClicked(int position)288 protected void positionClicked(int position) { 289 setClickedPosition(position); 290 } 291 292 /** 293 * Override this if you want to know when something has been long clicked (perhaps 294 * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has 295 * been triggered). 296 */ positionLongClicked(int position)297 protected void positionLongClicked(int position) { 298 setLongClickedPosition(position); 299 } 300 301 @Override onCreate(Bundle icicle)302 protected void onCreate(Bundle icicle) { 303 super.onCreate(icicle); 304 305 // for test stability, turn off title bar 306 requestWindowFeature(Window.FEATURE_NO_TITLE); 307 308 309 mScreenHeight = getWindowManager().getDefaultDisplay().getHeight(); 310 311 final Params params = createParams(); 312 init(params); 313 314 readAndValidateParams(params); 315 316 317 mListView = createListView(); 318 mListView.setLayoutParams(new ViewGroup.LayoutParams( 319 ViewGroup.LayoutParams.MATCH_PARENT, 320 ViewGroup.LayoutParams.MATCH_PARENT)); 321 mListView.setDrawSelectorOnTop(false); 322 323 for (int i=0; i<mHeaderViewCount; i++) { 324 TextView header = mHeadersFocusable ? 325 new EditText(this) : 326 new TextView(this); 327 header.setText("Header: " + i); 328 mListView.addHeaderView(header); 329 } 330 331 for (int i=0; i<mFooterViewCount; i++) { 332 TextView header = new TextView(this); 333 header.setText("Footer: " + i); 334 mListView.addFooterView(header); 335 } 336 337 if (params.mConnectAdapter) { 338 setAdapter(mListView); 339 } 340 341 mListView.setItemsCanFocus(mItemsFocusable); 342 if (mStartingSelectionPosition >= 0) { 343 mListView.setSelection(mStartingSelectionPosition); 344 } 345 mListView.setPadding(0, 0, 0, 0); 346 mListView.setStackFromBottom(mStackFromBottom); 347 mListView.setDivider(null); 348 349 mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 350 public void onItemSelected(AdapterView parent, View v, int position, long id) { 351 positionSelected(position); 352 } 353 354 public void onNothingSelected(AdapterView parent) { 355 nothingSelected(); 356 } 357 }); 358 359 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 360 public void onItemClick(AdapterView parent, View v, int position, long id) { 361 positionClicked(position); 362 } 363 }); 364 365 // set the fading edge length porportionally to the screen 366 // height for test stability 367 if (params.mFadingEdgeScreenSizeFactor != null) { 368 mListView.setFadingEdgeLength( 369 (int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight)); 370 } else { 371 mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight)); 372 } 373 374 if (mIncludeHeader) { 375 mLinearLayout = new LinearLayout(this); 376 377 mHeaderTextView = new TextView(this); 378 mHeaderTextView.setText("hi"); 379 mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams( 380 ViewGroup.LayoutParams.MATCH_PARENT, 381 ViewGroup.LayoutParams.WRAP_CONTENT)); 382 mLinearLayout.addView(mHeaderTextView); 383 384 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 385 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 386 ViewGroup.LayoutParams.MATCH_PARENT, 387 ViewGroup.LayoutParams.MATCH_PARENT)); 388 mListView.setLayoutParams((new LinearLayout.LayoutParams( 389 ViewGroup.LayoutParams.MATCH_PARENT, 390 0, 391 1f))); 392 393 mLinearLayout.addView(mListView); 394 setContentView(mLinearLayout); 395 } else { 396 mLinearLayout = new LinearLayout(this); 397 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 398 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 399 ViewGroup.LayoutParams.MATCH_PARENT, 400 ViewGroup.LayoutParams.MATCH_PARENT)); 401 mListView.setLayoutParams((new LinearLayout.LayoutParams( 402 ViewGroup.LayoutParams.MATCH_PARENT, 403 0, 404 1f))); 405 mLinearLayout.addView(mListView); 406 setContentView(mLinearLayout); 407 } 408 mLinearLayout.restoreDefaultFocus(); 409 } 410 411 /** 412 * Returns the LinearLayout containing the ListView in this scenario. 413 * 414 * @return The LinearLayout in which the ListView is held. 415 */ getListViewContainer()416 protected LinearLayout getListViewContainer() { 417 return mLinearLayout; 418 } 419 420 /** 421 * Attaches a long press listener. You can find out which views were clicked by calling 422 * {@link #getLongClickedPosition()}. 423 */ enableLongPress()424 public void enableLongPress() { 425 mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 426 public boolean onItemLongClick(AdapterView parent, View v, int position, long id) { 427 positionLongClicked(position); 428 return true; 429 } 430 }); 431 } 432 433 /** 434 * @return The newly created ListView widget. 435 */ createListView()436 protected ListView createListView() { 437 return new ListView(this); 438 } 439 440 /** 441 * @return The newly created Params object. 442 */ createParams()443 protected Params createParams() { 444 return new Params(); 445 } 446 447 /** 448 * Sets an adapter on a ListView. 449 * 450 * @param listView The ListView to set the adapter on. 451 */ setAdapter(ListView listView)452 protected void setAdapter(ListView listView) { 453 listView.setAdapter(new MyAdapter()); 454 } 455 456 /** 457 * Read in and validate all of the params passed in by the scenario. 458 * @param params 459 */ readAndValidateParams(Params params)460 protected void readAndValidateParams(Params params) { 461 if (params.mMustFillScreen ) { 462 double totalFactor = 0.0; 463 for (int i = 0; i < params.mNumItems; i++) { 464 if (params.mOverrideItemScreenSizeFactors.containsKey(i)) { 465 totalFactor += params.mOverrideItemScreenSizeFactors.get(i); 466 } else { 467 totalFactor += params.mItemScreenSizeFactor; 468 } 469 } 470 if (totalFactor < 1.0) { 471 throw new IllegalArgumentException("list items must combine to be at least " + 472 "the height of the screen. this is not the case with " + params.mNumItems 473 + " items and " + params.mItemScreenSizeFactor + " screen factor and " + 474 "screen height of " + mScreenHeight); 475 } 476 } 477 478 mNumItems = params.mNumItems; 479 mItemsFocusable = params.mItemsFocusable; 480 mStartingSelectionPosition = params.mStartingSelectionPosition; 481 mItemScreenSizeFactor = params.mItemScreenSizeFactor; 482 483 mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors); 484 485 mUnselectableItems.addAll(params.mUnselectableItems); 486 mIncludeHeader = params.mIncludeHeader; 487 mStackFromBottom = params.mStackFromBottom; 488 mHeaderViewCount = params.mHeaderViewCount; 489 mHeadersFocusable = params.mHeaderFocusable; 490 mFooterViewCount = params.mFooterViewCount; 491 } 492 getValueAtPosition(int position)493 public final String getValueAtPosition(int position) { 494 return isItemAtPositionSelectable(position) 495 ? 496 "position " + position: 497 "------- " + position; 498 } 499 500 /** 501 * @return The height that will be set for a particular position. 502 */ getHeightForPosition(int position)503 public int getHeightForPosition(int position) { 504 int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor); 505 if (mOverrideItemScreenSizeFactors.containsKey(position)) { 506 desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position)); 507 } 508 return desiredHeight; 509 } 510 511 512 /** 513 * @return The contents of the header above the list. 514 * @throws IllegalArgumentException if there is no header. 515 */ getHeaderValue()516 public final String getHeaderValue() { 517 if (!mIncludeHeader) { 518 throw new IllegalArgumentException("no header above list"); 519 } 520 return mHeaderTextView.getText().toString(); 521 } 522 523 /** 524 * @param value What to put in the header text view 525 * @throws IllegalArgumentException if there is no header. 526 */ setHeaderValue(String value)527 protected final void setHeaderValue(String value) { 528 if (!mIncludeHeader) { 529 throw new IllegalArgumentException("no header above list"); 530 } 531 mHeaderTextView.setText(value); 532 } 533 534 /** 535 * Create a view for a list item. Override this to create a custom view beyond 536 * the simple focusable / unfocusable text view. 537 * @param position The position. 538 * @param parent The parent 539 * @param desiredHeight The height the view should be to respect the desired item 540 * to screen height ratio. 541 * @return a view for the list. 542 */ createView(int position, ViewGroup parent, int desiredHeight)543 protected View createView(int position, ViewGroup parent, int desiredHeight) { 544 return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position), 545 desiredHeight); 546 } 547 548 /** 549 * Convert a non-null view. 550 */ convertView(int position, View convertView, ViewGroup parent)551 public View convertView(int position, View convertView, ViewGroup parent) { 552 return ListItemFactory.convertText(convertView, getValueAtPosition(position), position); 553 } 554 setClickedPosition(int clickedPosition)555 public void setClickedPosition(int clickedPosition) { 556 mClickedPosition = clickedPosition; 557 } 558 getClickedPosition()559 public int getClickedPosition() { 560 return mClickedPosition; 561 } 562 setLongClickedPosition(int longClickedPosition)563 public void setLongClickedPosition(int longClickedPosition) { 564 mLongClickedPosition = longClickedPosition; 565 } 566 getLongClickedPosition()567 public int getLongClickedPosition() { 568 return mLongClickedPosition; 569 } 570 571 /** 572 * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}. 573 * @param childIndex The index into the viewgroup children (i.e the children that are 574 * currently visible). 575 * @param rect The rectangle, in the child's coordinates. 576 */ requestRectangleOnScreen(int childIndex, final Rect rect)577 public void requestRectangleOnScreen(int childIndex, final Rect rect) { 578 final View child = getListView().getChildAt(childIndex); 579 580 child.post(new Runnable() { 581 public void run() { 582 child.requestRectangleOnScreen(rect); 583 } 584 }); 585 } 586 587 /** 588 * Return an item type for the specified position in the adapter. Override if your 589 * adapter creates more than one type. 590 */ getItemViewType(int position)591 public int getItemViewType(int position) { 592 return 0; 593 } 594 595 /** 596 * Return the number of types created by the adapter. Override if your 597 * adapter creates more than one type. 598 */ getViewTypeCount()599 public int getViewTypeCount() { 600 return 1; 601 } 602 603 /** 604 * @return The number of times convertView failed 605 */ getConvertMisses()606 public int getConvertMisses() { 607 return mConvertMisses; 608 } 609 610 private class MyAdapter extends BaseAdapter { 611 getCount()612 public int getCount() { 613 return mNumItems; 614 } 615 getItem(int position)616 public Object getItem(int position) { 617 return getValueAtPosition(position); 618 } 619 getItemId(int position)620 public long getItemId(int position) { 621 return position; 622 } 623 624 @Override areAllItemsEnabled()625 public boolean areAllItemsEnabled() { 626 return mUnselectableItems.isEmpty(); 627 } 628 629 @Override isEnabled(int position)630 public boolean isEnabled(int position) { 631 return isItemAtPositionSelectable(position); 632 } 633 getView(int position, View convertView, ViewGroup parent)634 public View getView(int position, View convertView, ViewGroup parent) { 635 View result = null; 636 if (position >= mNumItems || position < 0) { 637 throw new IllegalStateException("position out of range for adapter!"); 638 } 639 640 if (convertView != null) { 641 result = convertView(position, convertView, parent); 642 if (result == null) { 643 mConvertMisses++; 644 } 645 } 646 647 if (result == null) { 648 int desiredHeight = getHeightForPosition(position); 649 result = createView(position, parent, desiredHeight); 650 } 651 return result; 652 } 653 654 @Override getItemViewType(int position)655 public int getItemViewType(int position) { 656 return ListScenario.this.getItemViewType(position); 657 } 658 659 @Override getViewTypeCount()660 public int getViewTypeCount() { 661 return ListScenario.this.getViewTypeCount(); 662 } 663 664 } 665 } 666