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 java.util.ArrayList; 20 import java.util.HashMap; 21 import java.util.HashSet; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.Set; 25 26 import android.app.Activity; 27 import android.graphics.Rect; 28 import android.os.Bundle; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.Window; 32 import android.widget.AdapterView; 33 import android.widget.BaseAdapter; 34 import android.widget.EditText; 35 import android.widget.LinearLayout; 36 import android.widget.ListView; 37 import android.widget.TextView; 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 getListView()78 public ListView getListView() { 79 return mListView; 80 } 81 getScreenHeight()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 */ isItemAtPositionSelectable(int position)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 = new HashMap<>(); 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 */ setNumItems(int numItems)121 public Params setNumItems(int numItems) { 122 mNumItems = numItems; 123 return this; 124 } 125 126 /** 127 * Set whether the items are focusable. 128 */ setItemsFocusable(boolean itemsFocusable)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 */ setStartingSelectionPosition(int startingSelectionPosition)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 */ setItemScreenSizeFactor(double itemScreenSizeFactor)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 */ setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor)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 */ setPositionUnselectable(int position)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 */ setPositionsUnselectable(int ...positions)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 */ includeHeaderAboveList(boolean includeHeader)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 */ setStackFromBottom(boolean stackFromBottom)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 */ setMustFillScreen(boolean fillScreen)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 */ setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor)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 */ setHeaderViewCount(int headerViewCount)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 */ setHeaderFocusable(boolean headerFocusable)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 */ setFooterViewCount(int footerViewCount)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 */ setConnectAdapter(boolean connectAdapter)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 */ init(Params params)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 */ positionSelected(int positon)273 protected void positionSelected(int positon) { 274 } 275 276 /** 277 * Override this if you want to know that nothing is selected. 278 */ nothingSelected()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 */ positionClicked(int position)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 */ positionLongClicked(int position)296 protected void positionLongClicked(int position) { 297 setLongClickedPosition(position); 298 } 299 300 @Override onCreate(Bundle icicle)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 mScreenHeight = getWindowManager().getDefaultDisplay().getHeight(); 308 309 final Params params = createParams(); 310 init(params); 311 312 readAndValidateParams(params); 313 314 mListView = createListView(); 315 mListView.setLayoutParams(new ViewGroup.LayoutParams( 316 ViewGroup.LayoutParams.MATCH_PARENT, 317 ViewGroup.LayoutParams.MATCH_PARENT)); 318 mListView.setDrawSelectorOnTop(false); 319 320 for (int i=0; i<mHeaderViewCount; i++) { 321 TextView header = mHeadersFocusable ? 322 new EditText(this) : 323 new TextView(this); 324 header.setText("Header: " + i); 325 mListView.addHeaderView(header); 326 } 327 328 for (int i=0; i<mFooterViewCount; i++) { 329 TextView header = new TextView(this); 330 header.setText("Footer: " + i); 331 mListView.addFooterView(header); 332 } 333 334 if (params.mConnectAdapter) { 335 setAdapter(mListView); 336 } 337 338 mListView.setItemsCanFocus(mItemsFocusable); 339 if (mStartingSelectionPosition >= 0) { 340 mListView.setSelection(mStartingSelectionPosition); 341 } 342 mListView.setPadding(0, 0, 0, 0); 343 mListView.setStackFromBottom(mStackFromBottom); 344 mListView.setDivider(null); 345 346 mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 347 public void onItemSelected(AdapterView parent, View v, int position, long id) { 348 positionSelected(position); 349 } 350 351 public void onNothingSelected(AdapterView parent) { 352 nothingSelected(); 353 } 354 }); 355 356 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 357 public void onItemClick(AdapterView parent, View v, int position, long id) { 358 positionClicked(position); 359 } 360 }); 361 362 // set the fading edge length porportionally to the screen 363 // height for test stability 364 if (params.mFadingEdgeScreenSizeFactor != null) { 365 mListView.setFadingEdgeLength((int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight)); 366 } else { 367 mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight)); 368 } 369 370 if (mIncludeHeader) { 371 mLinearLayout = new LinearLayout(this); 372 373 mHeaderTextView = new TextView(this); 374 mHeaderTextView.setText("hi"); 375 mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams( 376 ViewGroup.LayoutParams.MATCH_PARENT, 377 ViewGroup.LayoutParams.WRAP_CONTENT)); 378 mLinearLayout.addView(mHeaderTextView); 379 380 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 381 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 382 ViewGroup.LayoutParams.MATCH_PARENT, 383 ViewGroup.LayoutParams.MATCH_PARENT)); 384 mListView.setLayoutParams((new LinearLayout.LayoutParams( 385 ViewGroup.LayoutParams.MATCH_PARENT, 386 0, 387 1f))); 388 389 mLinearLayout.addView(mListView); 390 setContentView(mLinearLayout); 391 } else { 392 mLinearLayout = new LinearLayout(this); 393 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 394 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 395 ViewGroup.LayoutParams.MATCH_PARENT, 396 ViewGroup.LayoutParams.MATCH_PARENT)); 397 mListView.setLayoutParams((new LinearLayout.LayoutParams( 398 ViewGroup.LayoutParams.MATCH_PARENT, 399 0, 400 1f))); 401 mLinearLayout.addView(mListView); 402 setContentView(mLinearLayout); 403 } 404 } 405 406 /** 407 * Returns the LinearLayout containing the ListView in this scenario. 408 * 409 * @return The LinearLayout in which the ListView is held. 410 */ getListViewContainer()411 protected LinearLayout getListViewContainer() { 412 return mLinearLayout; 413 } 414 415 /** 416 * Attaches a long press listener. You can find out which views were clicked by calling 417 * {@link #getLongClickedPosition()}. 418 */ enableLongPress()419 public void enableLongPress() { 420 mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { 421 public boolean onItemLongClick(AdapterView parent, View v, int position, long id) { 422 positionLongClicked(position); 423 return true; 424 } 425 }); 426 } 427 428 /** 429 * @return The newly created ListView widget. 430 */ createListView()431 protected ListView createListView() { 432 return new ListView(this); 433 } 434 435 /** 436 * @return The newly created Params object. 437 */ createParams()438 protected Params createParams() { 439 return new Params(); 440 } 441 442 /** 443 * Sets an adapter on a ListView. 444 * 445 * @param listView The ListView to set the adapter on. 446 */ setAdapter(ListView listView)447 protected void setAdapter(ListView listView) { 448 listView.setAdapter(new MyAdapter()); 449 } 450 451 /** 452 * Read in and validate all of the params passed in by the scenario. 453 * @param params 454 */ readAndValidateParams(Params params)455 protected void readAndValidateParams(Params params) { 456 if (params.mMustFillScreen ) { 457 double totalFactor = 0.0; 458 for (int i = 0; i < params.mNumItems; i++) { 459 if (params.mOverrideItemScreenSizeFactors.containsKey(i)) { 460 totalFactor += params.mOverrideItemScreenSizeFactors.get(i); 461 } else { 462 totalFactor += params.mItemScreenSizeFactor; 463 } 464 } 465 if (totalFactor < 1.0) { 466 throw new IllegalArgumentException("list items must combine to be at least " + 467 "the height of the screen. this is not the case with " + params.mNumItems 468 + " items and " + params.mItemScreenSizeFactor + " screen factor and " + 469 "screen height of " + mScreenHeight); 470 } 471 } 472 473 mNumItems = params.mNumItems; 474 mItemsFocusable = params.mItemsFocusable; 475 mStartingSelectionPosition = params.mStartingSelectionPosition; 476 mItemScreenSizeFactor = params.mItemScreenSizeFactor; 477 478 mOverrideItemScreenSizeFactors.putAll(params.mOverrideItemScreenSizeFactors); 479 480 mUnselectableItems.addAll(params.mUnselectableItems); 481 mIncludeHeader = params.mIncludeHeader; 482 mStackFromBottom = params.mStackFromBottom; 483 mHeaderViewCount = params.mHeaderViewCount; 484 mHeadersFocusable = params.mHeaderFocusable; 485 mFooterViewCount = params.mFooterViewCount; 486 } 487 getValueAtPosition(int position)488 public final String getValueAtPosition(int position) { 489 return isItemAtPositionSelectable(position) 490 ? 491 "position " + position: 492 "------- " + position; 493 } 494 495 /** 496 * @return The height that will be set for a particular position. 497 */ getHeightForPosition(int position)498 public int getHeightForPosition(int position) { 499 int desiredHeight = (int) (mScreenHeight * mItemScreenSizeFactor); 500 if (mOverrideItemScreenSizeFactors.containsKey(position)) { 501 desiredHeight = (int) (mScreenHeight * mOverrideItemScreenSizeFactors.get(position)); 502 } 503 return desiredHeight; 504 } 505 506 /** 507 * @return The contents of the header above the list. 508 * @throws IllegalArgumentException if there is no header. 509 */ getHeaderValue()510 public final String getHeaderValue() { 511 if (!mIncludeHeader) { 512 throw new IllegalArgumentException("no header above list"); 513 } 514 return mHeaderTextView.getText().toString(); 515 } 516 517 /** 518 * @param value What to put in the header text view 519 * @throws IllegalArgumentException if there is no header. 520 */ setHeaderValue(String value)521 protected final void setHeaderValue(String value) { 522 if (!mIncludeHeader) { 523 throw new IllegalArgumentException("no header above list"); 524 } 525 mHeaderTextView.setText(value); 526 } 527 528 /** 529 * Create a view for a list item. Override this to create a custom view beyond 530 * the simple focusable / unfocusable text view. 531 * @param position The position. 532 * @param parent The parent 533 * @param desiredHeight The height the view should be to respect the desired item 534 * to screen height ratio. 535 * @return a view for the list. 536 */ createView(int position, ViewGroup parent, int desiredHeight)537 protected View createView(int position, ViewGroup parent, int desiredHeight) { 538 return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position), 539 desiredHeight); 540 } 541 542 /** 543 * Convert a non-null view. 544 */ convertView(int position, View convertView, ViewGroup parent)545 public View convertView(int position, View convertView, ViewGroup parent) { 546 return ListItemFactory.convertText(convertView, getValueAtPosition(position), position); 547 } 548 setClickedPosition(int clickedPosition)549 public void setClickedPosition(int clickedPosition) { 550 mClickedPosition = clickedPosition; 551 } 552 getClickedPosition()553 public int getClickedPosition() { 554 return mClickedPosition; 555 } 556 setLongClickedPosition(int longClickedPosition)557 public void setLongClickedPosition(int longClickedPosition) { 558 mLongClickedPosition = longClickedPosition; 559 } 560 getLongClickedPosition()561 public int getLongClickedPosition() { 562 return mLongClickedPosition; 563 } 564 565 /** 566 * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}. 567 * @param childIndex The index into the viewgroup children (i.e the children that are 568 * currently visible). 569 * @param rect The rectangle, in the child's coordinates. 570 */ requestRectangleOnScreen(int childIndex, final Rect rect)571 public void requestRectangleOnScreen(int childIndex, final Rect rect) { 572 final View child = getListView().getChildAt(childIndex); 573 574 child.post(new Runnable() { 575 public void run() { 576 child.requestRectangleOnScreen(rect); 577 } 578 }); 579 } 580 581 /** 582 * Return an item type for the specified position in the adapter. Override if your 583 * adapter creates more than one type. 584 */ getItemViewType(int position)585 public int getItemViewType(int position) { 586 return 0; 587 } 588 589 /** 590 * Return an the number of types created by the adapter. Override if your 591 * adapter creates more than one type. 592 */ getViewTypeCount()593 public int getViewTypeCount() { 594 return 1; 595 } 596 597 /** 598 * @return The number of times convertView failed 599 */ getConvertMisses()600 public int getConvertMisses() { 601 return mConvertMisses; 602 } 603 604 private class MyAdapter extends BaseAdapter { 605 getCount()606 public int getCount() { 607 return mNumItems; 608 } 609 getItem(int position)610 public Object getItem(int position) { 611 return getValueAtPosition(position); 612 } 613 getItemId(int position)614 public long getItemId(int position) { 615 return position; 616 } 617 618 @Override areAllItemsEnabled()619 public boolean areAllItemsEnabled() { 620 return mUnselectableItems.isEmpty(); 621 } 622 623 @Override isEnabled(int position)624 public boolean isEnabled(int position) { 625 return isItemAtPositionSelectable(position); 626 } 627 getView(int position, View convertView, ViewGroup parent)628 public View getView(int position, View convertView, ViewGroup parent) { 629 View result = null; 630 if (position >= mNumItems || position < 0) { 631 throw new IllegalStateException("position out of range for adapter!"); 632 } 633 634 if (convertView != null) { 635 result = convertView(position, convertView, parent); 636 if (result == null) { 637 mConvertMisses++; 638 } 639 } 640 641 if (result == null) { 642 int desiredHeight = getHeightForPosition(position); 643 result = createView(position, parent, desiredHeight); 644 } 645 return result; 646 } 647 648 @Override getItemViewType(int position)649 public int getItemViewType(int position) { 650 return ListScenario.this.getItemViewType(position); 651 } 652 653 @Override getViewTypeCount()654 public int getViewTypeCount() { 655 return ListScenario.this.getViewTypeCount(); 656 } 657 658 } 659 } 660