1 /* 2 * Copyright (C) 2023 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; 18 19 import android.app.Activity; 20 import android.graphics.Rect; 21 import android.os.Bundle; 22 import android.util.ListItemFactory; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.view.Window; 26 27 import com.google.android.collect.Maps; 28 29 import java.util.ArrayList; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.Set; 34 35 /** 36 * Utility base class for creating various List scenarios. Configurable by the number 37 * of items, how tall each item should be (in relation to the screen height), and 38 * what item should start with selection. 39 */ 40 public abstract class ListScenario extends Activity { 41 42 private ListView mListView; 43 private TextView mHeaderTextView; 44 45 private int mNumItems; 46 protected boolean mItemsFocusable; 47 48 private int mStartingSelectionPosition; 49 private double mItemScreenSizeFactor; 50 private Map<Integer, Double> mOverrideItemScreenSizeFactors = Maps.newHashMap(); 51 52 private int mScreenHeight; 53 54 // whether to include a text view above the list 55 private boolean mIncludeHeader; 56 57 // separators 58 private Set<Integer> mUnselectableItems = new HashSet<Integer>(); 59 60 private boolean mStackFromBottom; 61 62 private int mClickedPosition = -1; 63 64 private int mLongClickedPosition = -1; 65 66 private int mConvertMisses = 0; 67 68 private int mHeaderViewCount; 69 private boolean mHeadersFocusable; 70 71 private int mFooterViewCount; 72 private LinearLayout mLinearLayout; 73 getListView()74 public ListView getListView() { 75 return mListView; 76 } 77 getScreenHeight()78 protected int getScreenHeight() { 79 return mScreenHeight; 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 */ isItemAtPositionSelectable(int position)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 = Maps.newHashMap(); 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 */ setNumItems(int numItems)117 public Params setNumItems(int numItems) { 118 mNumItems = numItems; 119 return this; 120 } 121 122 /** 123 * Set whether the items are focusable. 124 */ setItemsFocusable(boolean itemsFocusable)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 */ setStartingSelectionPosition(int startingSelectionPosition)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 */ setItemScreenSizeFactor(double itemScreenSizeFactor)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 */ setPositionScreenSizeFactorOverride( int position, double itemScreenSizeFactor)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 */ setPositionUnselectable(int position)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 */ setPositionsUnselectable(int ...positions)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 */ includeHeaderAboveList(boolean includeHeader)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 */ setStackFromBottom(boolean stackFromBottom)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 */ setMustFillScreen(boolean fillScreen)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 */ setFadingEdgeScreenSizeFactor(double fadingEdgeScreenSizeFactor)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 */ setHeaderViewCount(int headerViewCount)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 */ setHeaderFocusable(boolean headerFocusable)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 */ setFooterViewCount(int footerViewCount)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 */ setConnectAdapter(boolean connectAdapter)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 */ init(Params params)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 */ positionSelected(int positon)269 protected void positionSelected(int positon) { 270 } 271 272 /** 273 * Override this if you want to know that nothing is selected. 274 */ nothingSelected()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 */ positionClicked(int position)283 protected void positionClicked(int position) { 284 setClickedPosition(position); 285 } 286 287 /** 288 * Override this if you want to know when something has been long clicked (perhaps 289 * more importantly, that {@link android.widget.AdapterView.OnItemLongClickListener} has 290 * been triggered). 291 */ positionLongClicked(int position)292 protected void positionLongClicked(int position) { 293 setLongClickedPosition(position); 294 } 295 296 @Override onCreate(Bundle icicle)297 protected void onCreate(Bundle icicle) { 298 super.onCreate(icicle); 299 300 // for test stability, turn off title bar 301 requestWindowFeature(Window.FEATURE_NO_TITLE); 302 303 304 mScreenHeight = getWindowManager().getCurrentWindowMetrics().getBounds().height(); 305 306 final Params params = createParams(); 307 init(params); 308 309 readAndValidateParams(params); 310 311 312 mListView = createListView(); 313 mListView.setLayoutParams(new ViewGroup.LayoutParams( 314 ViewGroup.LayoutParams.MATCH_PARENT, 315 ViewGroup.LayoutParams.MATCH_PARENT)); 316 mListView.setDrawSelectorOnTop(false); 317 318 for (int i=0; i<mHeaderViewCount; i++) { 319 TextView header = mHeadersFocusable ? 320 new EditText(this) : 321 new TextView(this); 322 header.setText("Header: " + i); 323 mListView.addHeaderView(header); 324 } 325 326 for (int i=0; i<mFooterViewCount; i++) { 327 TextView header = new TextView(this); 328 header.setText("Footer: " + i); 329 mListView.addFooterView(header); 330 } 331 332 if (params.mConnectAdapter) { 333 setAdapter(mListView); 334 } 335 336 mListView.setItemsCanFocus(mItemsFocusable); 337 if (mStartingSelectionPosition >= 0) { 338 mListView.setSelection(mStartingSelectionPosition); 339 } 340 mListView.setPadding(0, 0, 0, 0); 341 mListView.setStackFromBottom(mStackFromBottom); 342 mListView.setDivider(null); 343 344 mListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 345 public void onItemSelected(AdapterView parent, View v, int position, long id) { 346 positionSelected(position); 347 } 348 349 public void onNothingSelected(AdapterView parent) { 350 nothingSelected(); 351 } 352 }); 353 354 mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 355 public void onItemClick(AdapterView parent, View v, int position, long id) { 356 positionClicked(position); 357 } 358 }); 359 360 // set the fading edge length porportionally to the screen 361 // height for test stability 362 if (params.mFadingEdgeScreenSizeFactor != null) { 363 mListView.setFadingEdgeLength( 364 (int) (params.mFadingEdgeScreenSizeFactor * mScreenHeight)); 365 } else { 366 mListView.setFadingEdgeLength((int) ((64.0 / 480) * mScreenHeight)); 367 } 368 369 if (mIncludeHeader) { 370 mLinearLayout = new LinearLayout(this); 371 372 mHeaderTextView = new TextView(this); 373 mHeaderTextView.setText("hi"); 374 mHeaderTextView.setLayoutParams(new LinearLayout.LayoutParams( 375 ViewGroup.LayoutParams.MATCH_PARENT, 376 ViewGroup.LayoutParams.WRAP_CONTENT)); 377 mLinearLayout.addView(mHeaderTextView); 378 379 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 380 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 381 ViewGroup.LayoutParams.MATCH_PARENT, 382 ViewGroup.LayoutParams.MATCH_PARENT)); 383 mListView.setLayoutParams((new LinearLayout.LayoutParams( 384 ViewGroup.LayoutParams.MATCH_PARENT, 385 0, 386 1f))); 387 388 mLinearLayout.addView(mListView); 389 setContentView(mLinearLayout); 390 } else { 391 mLinearLayout = new LinearLayout(this); 392 mLinearLayout.setOrientation(LinearLayout.VERTICAL); 393 mLinearLayout.setLayoutParams(new ViewGroup.LayoutParams( 394 ViewGroup.LayoutParams.MATCH_PARENT, 395 ViewGroup.LayoutParams.MATCH_PARENT)); 396 mListView.setLayoutParams((new LinearLayout.LayoutParams( 397 ViewGroup.LayoutParams.MATCH_PARENT, 398 0, 399 1f))); 400 mLinearLayout.addView(mListView); 401 setContentView(mLinearLayout); 402 } 403 mLinearLayout.restoreDefaultFocus(); 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 /** 508 * @return The contents of the header above the list. 509 * @throws IllegalArgumentException if there is no header. 510 */ getHeaderValue()511 public final String getHeaderValue() { 512 if (!mIncludeHeader) { 513 throw new IllegalArgumentException("no header above list"); 514 } 515 return mHeaderTextView.getText().toString(); 516 } 517 518 /** 519 * @param value What to put in the header text view 520 * @throws IllegalArgumentException if there is no header. 521 */ setHeaderValue(String value)522 protected final void setHeaderValue(String value) { 523 if (!mIncludeHeader) { 524 throw new IllegalArgumentException("no header above list"); 525 } 526 mHeaderTextView.setText(value); 527 } 528 529 /** 530 * Create a view for a list item. Override this to create a custom view beyond 531 * the simple focusable / unfocusable text view. 532 * @param position The position. 533 * @param parent The parent 534 * @param desiredHeight The height the view should be to respect the desired item 535 * to screen height ratio. 536 * @return a view for the list. 537 */ createView(int position, ViewGroup parent, int desiredHeight)538 protected View createView(int position, ViewGroup parent, int desiredHeight) { 539 return ListItemFactory.text(position, parent.getContext(), getValueAtPosition(position), 540 desiredHeight); 541 } 542 543 /** 544 * Convert a non-null view. 545 */ convertView(int position, View convertView, ViewGroup parent)546 public View convertView(int position, View convertView, ViewGroup parent) { 547 return ListItemFactory.convertText(convertView, getValueAtPosition(position), position); 548 } 549 setClickedPosition(int clickedPosition)550 public void setClickedPosition(int clickedPosition) { 551 mClickedPosition = clickedPosition; 552 } 553 getClickedPosition()554 public int getClickedPosition() { 555 return mClickedPosition; 556 } 557 setLongClickedPosition(int longClickedPosition)558 public void setLongClickedPosition(int longClickedPosition) { 559 mLongClickedPosition = longClickedPosition; 560 } 561 getLongClickedPosition()562 public int getLongClickedPosition() { 563 return mLongClickedPosition; 564 } 565 566 /** 567 * Have a child of the list view call {@link View#requestRectangleOnScreen(android.graphics.Rect)}. 568 * @param childIndex The index into the viewgroup children (i.e the children that are 569 * currently visible). 570 * @param rect The rectangle, in the child's coordinates. 571 */ requestRectangleOnScreen(int childIndex, final Rect rect)572 public void requestRectangleOnScreen(int childIndex, final Rect rect) { 573 final View child = getListView().getChildAt(childIndex); 574 575 child.post(new Runnable() { 576 public void run() { 577 child.requestRectangleOnScreen(rect); 578 } 579 }); 580 } 581 582 /** 583 * Return an item type for the specified position in the adapter. Override if your 584 * adapter creates more than one type. 585 */ getItemViewType(int position)586 public int getItemViewType(int position) { 587 return 0; 588 } 589 590 /** 591 * Return the number of types created by the adapter. Override if your 592 * adapter creates more than one type. 593 */ getViewTypeCount()594 public int getViewTypeCount() { 595 return 1; 596 } 597 598 /** 599 * @return The number of times convertView failed 600 */ getConvertMisses()601 public int getConvertMisses() { 602 return mConvertMisses; 603 } 604 605 private class MyAdapter extends BaseAdapter { 606 getCount()607 public int getCount() { 608 return mNumItems; 609 } 610 getItem(int position)611 public Object getItem(int position) { 612 return getValueAtPosition(position); 613 } 614 getItemId(int position)615 public long getItemId(int position) { 616 return position; 617 } 618 619 @Override areAllItemsEnabled()620 public boolean areAllItemsEnabled() { 621 return mUnselectableItems.isEmpty(); 622 } 623 624 @Override isEnabled(int position)625 public boolean isEnabled(int position) { 626 return isItemAtPositionSelectable(position); 627 } 628 getView(int position, View convertView, ViewGroup parent)629 public View getView(int position, View convertView, ViewGroup parent) { 630 View result = null; 631 if (position >= mNumItems || position < 0) { 632 throw new IllegalStateException("position out of range for adapter!"); 633 } 634 635 if (convertView != null) { 636 result = convertView(position, convertView, parent); 637 if (result == null) { 638 mConvertMisses++; 639 } 640 } 641 642 if (result == null) { 643 int desiredHeight = getHeightForPosition(position); 644 result = createView(position, parent, desiredHeight); 645 } 646 return result; 647 } 648 649 @Override getItemViewType(int position)650 public int getItemViewType(int position) { 651 return ListScenario.this.getItemViewType(position); 652 } 653 654 @Override getViewTypeCount()655 public int getViewTypeCount() { 656 return ListScenario.this.getViewTypeCount(); 657 } 658 659 } 660 } 661