1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package androidx.leanback.widget.picker; 16 17 import android.annotation.SuppressLint; 18 import android.content.Context; 19 import android.content.res.TypedArray; 20 import android.graphics.Rect; 21 import android.text.TextUtils; 22 import android.util.AttributeSet; 23 import android.view.KeyEvent; 24 import android.view.LayoutInflater; 25 import android.view.View; 26 import android.view.ViewGroup; 27 import android.view.animation.DecelerateInterpolator; 28 import android.view.animation.Interpolator; 29 import android.widget.FrameLayout; 30 import android.widget.TextView; 31 32 import androidx.annotation.IdRes; 33 import androidx.annotation.LayoutRes; 34 import androidx.core.view.ViewCompat; 35 import androidx.leanback.R; 36 import androidx.leanback.widget.OnChildViewHolderSelectedListener; 37 import androidx.leanback.widget.VerticalGridView; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import org.jspecify.annotations.NonNull; 41 import org.jspecify.annotations.Nullable; 42 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.List; 46 47 /** 48 * Picker is a widget showing multiple customized {@link PickerColumn}s. The PickerColumns are 49 * initialized in {@link #setColumns(List)}. Call {@link #setColumnAt(int, PickerColumn)} if the 50 * column value range or labels change. Call {@link #setColumnValue(int, int, boolean)} to update 51 * the current value of PickerColumn. 52 * <p> 53 * Picker has two states and will change height: 54 * <ul> 55 * <li>{@link #isActivated()} is true: Picker shows typically three items vertically (see 56 * {@link #getActivatedVisibleItemCount()}}. Columns other than {@link #getSelectedColumn()} still 57 * shows one item if the Picker is focused. On a touch screen device, the Picker will not get focus 58 * so it always show three items on all columns. On a non-touch device (a TV), the Picker will show 59 * three items only on currently activated column. If the Picker has focus, it will intercept DPAD 60 * directions and select activated column.</li> 61 * <li>{@link #isActivated()} is false: Picker shows one item vertically (see 62 * {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks.</li> 63 * </ul> 64 */ 65 public class Picker extends FrameLayout { 66 67 /** 68 * Listener for {@link Picker} value changes. 69 * 70 * @see Picker#addOnValueChangedListener(PickerValueListener) 71 */ 72 public interface PickerValueListener { 73 /** 74 * Called whenever the value of the {@link Picker} changes. 75 * 76 * @param picker View whose value has changed. 77 * @param column Column whose value has changed. 78 */ onValueChanged(@onNull Picker picker, int column)79 void onValueChanged(@NonNull Picker picker, int column); 80 } 81 82 private ViewGroup mPickerView; 83 final List<VerticalGridView> mColumnViews = new ArrayList<>(); 84 ArrayList<PickerColumn> mColumns; 85 86 private float mUnfocusedAlpha; 87 private float mFocusedAlpha; 88 private float mVisibleColumnAlpha; 89 private float mInvisibleColumnAlpha; 90 private int mAlphaAnimDuration; 91 private Interpolator mDecelerateInterpolator; 92 private ArrayList<PickerValueListener> mListeners; 93 private float mVisibleItemsActivated = 3; 94 private float mVisibleItems = 1; 95 private int mSelectedColumn = 0; 96 97 private List<CharSequence> mSeparators = new ArrayList<>(); 98 private int mPickerItemLayoutId; 99 private int mPickerItemTextViewId; 100 101 /** 102 * Gets separator string between columns. 103 * 104 * @return The separator that will be populated between all the Picker columns. 105 * @deprecated Use {@link #getSeparators()} 106 */ 107 @Deprecated getSeparator()108 public final CharSequence getSeparator() { 109 return mSeparators.get(0); 110 } 111 112 /** 113 * Sets separator String between Picker columns. 114 * 115 * @param separator Separator String between Picker columns. 116 */ setSeparator(@onNull CharSequence separator)117 public final void setSeparator(@NonNull CharSequence separator) { 118 setSeparators(Arrays.asList(separator)); 119 } 120 121 /** 122 * Returns the list of separators that will be populated between the picker column fields. 123 * 124 * @return The list of separators populated between the picker column fields. 125 */ getSeparators()126 public final @NonNull List<CharSequence> getSeparators() { 127 return mSeparators; 128 } 129 130 /** 131 * Sets the list of separators that will be populated between the Picker columns. The 132 * number of the separators should be either 1 indicating the same separator used between all 133 * the columns fields (and nothing will be placed before the first and after the last column), 134 * or must be one unit larger than the number of columns passed to {@link #setColumns(List)}. 135 * In the latter case, the list of separators corresponds to the positions before the first 136 * column all the way to the position after the last column. 137 * An empty string for a given position indicates no separators needs to be placed for that 138 * position, otherwise a TextView with the given String will be created and placed there. 139 * 140 * @param separators The list of separators to be populated between the Picker columns. 141 */ setSeparators(@onNull List<CharSequence> separators)142 public final void setSeparators(@NonNull List<CharSequence> separators) { 143 mSeparators.clear(); 144 mSeparators.addAll(separators); 145 } 146 147 /** 148 * Classes extending {@link Picker} can call {@link #setPickerItemLayoutId(int)} to 149 * supply the {@link Picker}'s item's layout id 150 */ 151 @LayoutRes getPickerItemLayoutId()152 public final int getPickerItemLayoutId() { 153 return mPickerItemLayoutId; 154 } 155 156 /** 157 * Sets the layout to use for picker items. 158 * 159 * @param pickerItemLayoutId Layout resource id to use for picker items. 160 */ setPickerItemLayoutId(@ayoutRes int pickerItemLayoutId)161 public final void setPickerItemLayoutId(@LayoutRes int pickerItemLayoutId) { 162 mPickerItemLayoutId = pickerItemLayoutId; 163 } 164 165 /** 166 * Returns the {@link Picker}'s item's {@link TextView}'s id from within the 167 * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the 168 * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link 169 * TextView}. 170 */ 171 @IdRes getPickerItemTextViewId()172 public final int getPickerItemTextViewId() { 173 return mPickerItemTextViewId; 174 } 175 176 /** 177 * Sets the {@link Picker}'s item's {@link TextView}'s id from within the 178 * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the 179 * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link 180 * TextView}. 181 * 182 * @param textViewId View id of TextView inside a Picker item, or 0 if the Picker item is a 183 * TextView. 184 */ setPickerItemTextViewId(@dRes int textViewId)185 public final void setPickerItemTextViewId(@IdRes int textViewId) { 186 mPickerItemTextViewId = textViewId; 187 } 188 189 /** 190 * Creates a Picker widget. 191 */ Picker(@onNull Context context, @Nullable AttributeSet attributeSet)192 public Picker(@NonNull Context context, @Nullable AttributeSet attributeSet) { 193 this(context, attributeSet, R.attr.pickerStyle); 194 } 195 196 /** 197 * Creates a Picker widget. 198 */ 199 @SuppressLint("CustomViewStyleable") Picker(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)200 public Picker(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 201 super(context, attrs, defStyleAttr); 202 final TypedArray a = context.obtainStyledAttributes( 203 attrs, R.styleable.lbPicker, defStyleAttr, 0); 204 ViewCompat.saveAttributeDataForStyleable( 205 this, context, R.styleable.lbPicker, attrs, a, defStyleAttr, 0); 206 mPickerItemLayoutId = a.getResourceId(R.styleable.lbPicker_pickerItemLayout, 207 R.layout.lb_picker_item); 208 mPickerItemTextViewId = a.getResourceId(R.styleable.lbPicker_pickerItemTextViewId, 0); 209 a.recycle(); 210 // Make it enabled and clickable to receive Click event. 211 setEnabled(true); 212 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 213 214 mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha); 215 mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha); 216 mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha); 217 mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha); 218 219 mAlphaAnimDuration = 220 200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration); 221 222 mDecelerateInterpolator = new DecelerateInterpolator(2.5F); 223 224 LayoutInflater inflater = LayoutInflater.from(getContext()); 225 ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true); 226 mPickerView = rootView.findViewById(R.id.picker); 227 } 228 229 /** 230 * Get nth PickerColumn. 231 * 232 * @param colIndex Index of PickerColumn. 233 * @return PickerColumn at colIndex or null if {@link #setColumns(List)} is not called yet. 234 */ getColumnAt(int colIndex)235 public @Nullable PickerColumn getColumnAt(int colIndex) { 236 if (mColumns == null) { 237 return null; 238 } 239 return mColumns.get(colIndex); 240 } 241 242 /** 243 * Get number of PickerColumns. 244 * 245 * @return Number of PickerColumns or 0 if {@link #setColumns(List)} is not called yet. 246 */ getColumnsCount()247 public int getColumnsCount() { 248 if (mColumns == null) { 249 return 0; 250 } 251 return mColumns.size(); 252 } 253 254 /** 255 * Set columns and create Views. 256 * 257 * @param columns The actual focusable columns of a picker which are scrollable if the field 258 * takes more than one value (e.g. for a DatePicker, day, month, and year fields 259 * and for TimePicker, hour, minute, and am/pm fields form the columns). 260 */ setColumns(@onNull List<PickerColumn> columns)261 public void setColumns(@NonNull List<PickerColumn> columns) { 262 if (mSeparators.size() == 0) { 263 throw new IllegalStateException("Separators size is: " + mSeparators.size() 264 + ". At least one separator must be provided"); 265 } else if (mSeparators.size() == 1) { 266 CharSequence separator = mSeparators.get(0); 267 mSeparators.clear(); 268 mSeparators.add(""); 269 for (int i = 0; i < columns.size() - 1; i++) { 270 mSeparators.add(separator); 271 } 272 mSeparators.add(""); 273 } else { 274 if (mSeparators.size() != (columns.size() + 1)) { 275 throw new IllegalStateException("Separators size: " + mSeparators.size() + " must" 276 + "equal the size of columns: " + columns.size() + " + 1"); 277 } 278 } 279 280 mColumnViews.clear(); 281 mPickerView.removeAllViews(); 282 mColumns = new ArrayList<>(columns); 283 if (mSelectedColumn > mColumns.size() - 1) { 284 mSelectedColumn = mColumns.size() - 1; 285 } 286 LayoutInflater inflater = LayoutInflater.from(getContext()); 287 int totalCol = getColumnsCount(); 288 289 if (!TextUtils.isEmpty(mSeparators.get(0))) { 290 TextView separator = (TextView) inflater.inflate( 291 R.layout.lb_picker_separator, mPickerView, false); 292 separator.setText(mSeparators.get(0)); 293 mPickerView.addView(separator); 294 } 295 for (int i = 0; i < totalCol; i++) { 296 final VerticalGridView columnView = (VerticalGridView) inflater.inflate( 297 R.layout.lb_picker_column, mPickerView, false); 298 // we don't want VerticalGridView to receive focus. 299 updateColumnSize(columnView); 300 // always center aligned, not aligning selected item on top/bottom edge. 301 columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 302 // Width is dynamic, so has fixed size is false. 303 columnView.setHasFixedSize(false); 304 columnView.setFocusable(isActivated()); 305 // Setting cache size to zero in order to rebind item views when picker widget becomes 306 // activated. Rebinding is necessary to update the alphas when the columns are expanded 307 // as a result of the picker getting activated, otherwise the cached views with the 308 // wrong alphas could be laid out. 309 columnView.setItemViewCacheSize(0); 310 311 mColumnViews.add(columnView); 312 // add view to root 313 mPickerView.addView(columnView); 314 315 if (!TextUtils.isEmpty(mSeparators.get(i + 1))) { 316 // add a separator if not the last element 317 TextView separator = (TextView) inflater.inflate( 318 R.layout.lb_picker_separator, mPickerView, false); 319 separator.setText(mSeparators.get(i + 1)); 320 mPickerView.addView(separator); 321 } 322 323 columnView.setAdapter(new PickerScrollArrayAdapter( 324 getPickerItemLayoutId(), getPickerItemTextViewId(), i)); 325 columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener); 326 } 327 } 328 329 /** 330 * When column labels change or column range changes, call this function to re-populate the 331 * selection list. Note this function cannot be called from RecyclerView layout/scroll pass. 332 * 333 * @param columnIndex Index of column to update. 334 * @param column New column to update. 335 */ setColumnAt(int columnIndex, @NonNull PickerColumn column)336 public void setColumnAt(int columnIndex, @NonNull PickerColumn column) { 337 mColumns.set(columnIndex, column); 338 VerticalGridView columnView = mColumnViews.get(columnIndex); 339 PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter(); 340 if (adapter != null) { 341 adapter.notifyDataSetChanged(); 342 } 343 columnView.setSelectedPosition(column.getCurrentValue() - column.getMinValue()); 344 } 345 346 /** 347 * Manually set current value of a column. The function will update UI and notify listeners. 348 * 349 * @param columnIndex Index of column to update. 350 * @param value New value of the column. 351 * @param runAnimation True to scroll to the value or false otherwise. 352 */ setColumnValue(int columnIndex, int value, boolean runAnimation)353 public void setColumnValue(int columnIndex, int value, boolean runAnimation) { 354 PickerColumn column = mColumns.get(columnIndex); 355 if (column.getCurrentValue() != value) { 356 column.setCurrentValue(value); 357 notifyValueChanged(columnIndex); 358 VerticalGridView columnView = mColumnViews.get(columnIndex); 359 if (columnView != null) { 360 int position = value - mColumns.get(columnIndex).getMinValue(); 361 if (runAnimation) { 362 columnView.setSelectedPositionSmooth(position); 363 } else { 364 columnView.setSelectedPosition(position); 365 } 366 } 367 } 368 } 369 notifyValueChanged(int columnIndex)370 private void notifyValueChanged(int columnIndex) { 371 if (mListeners != null) { 372 for (int i = mListeners.size() - 1; i >= 0; i--) { 373 mListeners.get(i).onValueChanged(this, columnIndex); 374 } 375 } 376 } 377 378 /** 379 * Register a callback to be invoked when the picker's value has changed. 380 * 381 * @param listener The callback to ad 382 */ addOnValueChangedListener(@onNull PickerValueListener listener)383 public void addOnValueChangedListener(@NonNull PickerValueListener listener) { 384 if (mListeners == null) { 385 mListeners = new ArrayList<>(); 386 } 387 mListeners.add(listener); 388 } 389 390 /** 391 * Remove a previously installed value changed callback 392 * 393 * @param listener The callback to remove. 394 */ removeOnValueChangedListener(@onNull PickerValueListener listener)395 public void removeOnValueChangedListener(@NonNull PickerValueListener listener) { 396 if (mListeners != null) { 397 mListeners.remove(listener); 398 } 399 } 400 updateColumnAlpha(int colIndex, boolean animate)401 void updateColumnAlpha(int colIndex, boolean animate) { 402 VerticalGridView column = mColumnViews.get(colIndex); 403 404 int selected = column.getSelectedPosition(); 405 View item; 406 407 for (int i = 0; i < column.getAdapter().getItemCount(); i++) { 408 item = column.getLayoutManager().findViewByPosition(i); 409 if (item != null) { 410 setOrAnimateAlpha(item, (selected == i), colIndex, animate); 411 } 412 } 413 } 414 setOrAnimateAlpha(View view, boolean selected, int colIndex, boolean animate)415 void setOrAnimateAlpha(View view, boolean selected, int colIndex, 416 boolean animate) { 417 boolean columnShownAsActivated = colIndex == mSelectedColumn || !hasFocus(); 418 if (selected) { 419 // set alpha for main item (selected) in the column 420 if (columnShownAsActivated) { 421 setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator); 422 } else { 423 setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, mDecelerateInterpolator); 424 } 425 } else { 426 // set alpha for remaining items in the column 427 if (columnShownAsActivated) { 428 setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator); 429 } else { 430 setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1, 431 mDecelerateInterpolator); 432 } 433 } 434 } 435 setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, Interpolator interpolator)436 private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, 437 Interpolator interpolator) { 438 view.animate().cancel(); 439 if (!animate) { 440 view.setAlpha(destAlpha); 441 } else { 442 if (startAlpha >= 0.0f) { 443 // set a start alpha 444 view.setAlpha(startAlpha); 445 } 446 view.animate().alpha(destAlpha) 447 .setDuration(mAlphaAnimDuration).setInterpolator(interpolator) 448 .start(); 449 } 450 } 451 452 /** 453 * Classes extending {@link Picker} can override this function to supply the 454 * behavior when a list has been scrolled. Subclass may call {@link #setColumnValue(int, int, 455 * boolean)} and or {@link #setColumnAt(int, PickerColumn)}. Subclass should not directly call 456 * {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify 457 * listeners. 458 * 459 * @param columnIndex index of which column was changed. 460 * @param newValue A new value desired to be set on the column. 461 */ onColumnValueChanged(int columnIndex, int newValue)462 public void onColumnValueChanged(int columnIndex, int newValue) { 463 PickerColumn column = mColumns.get(columnIndex); 464 if (column.getCurrentValue() != newValue) { 465 column.setCurrentValue(newValue); 466 notifyValueChanged(columnIndex); 467 } 468 } 469 470 static class ViewHolder extends RecyclerView.ViewHolder { 471 final TextView textView; 472 ViewHolder(View v, TextView textView)473 ViewHolder(View v, TextView textView) { 474 super(v); 475 this.textView = textView; 476 } 477 } 478 479 class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> { 480 481 private final int mResource; 482 private final int mColIndex; 483 private final int mTextViewResourceId; 484 private PickerColumn mData; 485 PickerScrollArrayAdapter(int resource, int textViewResourceId, int colIndex)486 PickerScrollArrayAdapter(int resource, int textViewResourceId, 487 int colIndex) { 488 mResource = resource; 489 mColIndex = colIndex; 490 mTextViewResourceId = textViewResourceId; 491 mData = mColumns.get(mColIndex); 492 } 493 494 @Override onCreateViewHolder(ViewGroup parent, int viewType)495 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 496 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 497 View v = inflater.inflate(mResource, parent, false); 498 TextView textView; 499 if (mTextViewResourceId != 0) { 500 textView = v.findViewById(mTextViewResourceId); 501 } else { 502 textView = (TextView) v; 503 } 504 return new ViewHolder(v, textView); 505 } 506 507 @Override onBindViewHolder(ViewHolder holder, int position)508 public void onBindViewHolder(ViewHolder holder, int position) { 509 if (holder.textView != null && mData != null) { 510 holder.textView.setText(mData.getLabelFor(mData.getMinValue() + position)); 511 } 512 setOrAnimateAlpha(holder.itemView, 513 (mColumnViews.get(mColIndex).getSelectedPosition() == position), 514 mColIndex, false); 515 } 516 517 @Override onViewAttachedToWindow(ViewHolder holder)518 public void onViewAttachedToWindow(ViewHolder holder) { 519 holder.itemView.setFocusable(isActivated()); 520 } 521 522 @Override getItemCount()523 public int getItemCount() { 524 return mData == null ? 0 : mData.getCount(); 525 } 526 } 527 528 private final OnChildViewHolderSelectedListener mColumnChangeListener = new 529 OnChildViewHolderSelectedListener() { 530 531 @Override 532 public void onChildViewHolderSelected(RecyclerView parent, 533 RecyclerView.ViewHolder child, 534 int position, int subposition) { 535 536 final VerticalGridView verticalGridView = (VerticalGridView) parent; 537 int colIndex = mColumnViews.indexOf(verticalGridView); 538 updateColumnAlpha(colIndex, true); 539 if (child != null) { 540 int newValue = mColumns.get(colIndex).getMinValue() + position; 541 onColumnValueChanged(colIndex, newValue); 542 } 543 } 544 545 }; 546 547 @Override dispatchKeyEvent(android.view.KeyEvent event)548 public boolean dispatchKeyEvent(android.view.KeyEvent event) { 549 if (isActivated()) { 550 final int keyCode = event.getKeyCode(); 551 switch (keyCode) { 552 case KeyEvent.KEYCODE_DPAD_CENTER: 553 case KeyEvent.KEYCODE_ENTER: 554 if (event.getAction() == KeyEvent.ACTION_UP) { 555 performClick(); 556 } 557 break; 558 default: 559 return super.dispatchKeyEvent(event); 560 } 561 return true; 562 } 563 return super.dispatchKeyEvent(event); 564 } 565 566 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)567 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 568 int column = getSelectedColumn(); 569 if (column >= 0 && column < mColumnViews.size()) { 570 return mColumnViews.get(column).requestFocus(direction, previouslyFocusedRect); 571 } 572 return false; 573 } 574 575 /** 576 * Classes extending {@link Picker} can choose to override this method to 577 * supply the {@link Picker}'s column's single item height in pixels. 578 */ getPickerItemHeightPixels()579 protected int getPickerItemHeightPixels() { 580 return getContext().getResources().getDimensionPixelSize(R.dimen.picker_item_height); 581 } 582 updateColumnSize()583 private void updateColumnSize() { 584 for (int i = 0; i < getColumnsCount(); i++) { 585 updateColumnSize(mColumnViews.get(i)); 586 } 587 } 588 updateColumnSize(VerticalGridView columnView)589 private void updateColumnSize(VerticalGridView columnView) { 590 ViewGroup.LayoutParams lp = columnView.getLayoutParams(); 591 float itemCount = isActivated() ? getActivatedVisibleItemCount() : getVisibleItemCount(); 592 lp.height = (int) (getPickerItemHeightPixels() * itemCount 593 + columnView.getVerticalSpacing() * (itemCount - 1)); 594 columnView.setLayoutParams(lp); 595 } 596 updateItemFocusable()597 private void updateItemFocusable() { 598 final boolean activated = isActivated(); 599 for (int i = 0; i < getColumnsCount(); i++) { 600 VerticalGridView grid = mColumnViews.get(i); 601 for (int j = 0; j < grid.getChildCount(); j++) { 602 View view = grid.getChildAt(j); 603 view.setFocusable(activated); 604 } 605 } 606 } 607 608 /** 609 * Returns number of visible items showing in a column when it's activated. The default value 610 * is 3. 611 * 612 * @return Number of visible items showing in a column when it's activated. 613 */ getActivatedVisibleItemCount()614 public float getActivatedVisibleItemCount() { 615 return mVisibleItemsActivated; 616 } 617 618 /** 619 * Changes number of visible items showing in a column when it's activated. The default value 620 * is 3. 621 * 622 * @param visiblePickerItems Number of visible items showing in a column when it's activated. 623 */ setActivatedVisibleItemCount(float visiblePickerItems)624 public void setActivatedVisibleItemCount(float visiblePickerItems) { 625 if (visiblePickerItems <= 0) { 626 throw new IllegalArgumentException(); 627 } 628 if (mVisibleItemsActivated != visiblePickerItems) { 629 mVisibleItemsActivated = visiblePickerItems; 630 if (isActivated()) { 631 updateColumnSize(); 632 } 633 } 634 } 635 636 /** 637 * Returns number of visible items showing in a column when it's not activated. The default 638 * value is 1. 639 * 640 * @return Number of visible items showing in a column when it's not activated. 641 */ getVisibleItemCount()642 public float getVisibleItemCount() { 643 return 1; 644 } 645 646 /** 647 * Changes number of visible items showing in a column when it's not activated. The default 648 * value is 1. 649 * 650 * @param pickerItems Number of visible items showing in a column when it's not activated. 651 */ setVisibleItemCount(float pickerItems)652 public void setVisibleItemCount(float pickerItems) { 653 if (pickerItems <= 0) { 654 throw new IllegalArgumentException(); 655 } 656 if (mVisibleItems != pickerItems) { 657 mVisibleItems = pickerItems; 658 if (!isActivated()) { 659 updateColumnSize(); 660 } 661 } 662 } 663 664 @Override setActivated(boolean activated)665 public void setActivated(boolean activated) { 666 if (activated == isActivated()) { 667 super.setActivated(activated); 668 return; 669 } 670 super.setActivated(activated); 671 boolean hadFocus = hasFocus(); 672 int column = getSelectedColumn(); 673 // To avoid temporary focus loss in both the following cases, we set Picker's flag to 674 // FOCUS_BEFORE_DESCENDANTS first, and then back to FOCUS_AFTER_DESCENDANTS once done with 675 // the focus logic. 676 // 1. When changing from activated to deactivated, the Picker should grab the focus 677 // back if it's focusable. However, calling requestFocus on it will transfer the focus down 678 // to its children if it's flag is FOCUS_AFTER_DESCENDANTS. 679 // 2. When changing from deactivated to activated, while setting focusable flags on each 680 // column VerticalGridView, that column will call requestFocus (regardless of which column 681 // is the selected column) since the currently focused view (Picker) has a flag of 682 // FOCUS_AFTER_DESCENDANTS. 683 setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS); 684 if (!activated && hadFocus && isFocusable()) { 685 // When picker widget that originally had focus is deactivated and it is focusable, we 686 // should not pass the focus down to the children. The Picker itself will capture focus. 687 requestFocus(); 688 } 689 690 for (int i = 0; i < getColumnsCount(); i++) { 691 mColumnViews.get(i).setFocusable(activated); 692 } 693 694 updateColumnSize(); 695 updateItemFocusable(); 696 if (activated && hadFocus && (column >= 0)) { 697 mColumnViews.get(column).requestFocus(); 698 } 699 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 700 } 701 702 @Override requestChildFocus(View child, View focused)703 public void requestChildFocus(View child, View focused) { 704 super.requestChildFocus(child, focused); 705 for (int i = 0; i < mColumnViews.size(); i++) { 706 if (mColumnViews.get(i).hasFocus()) { 707 setSelectedColumn(i); 708 } 709 } 710 } 711 712 /** 713 * Change current selected column. Picker shows multiple items on selected column if Picker has 714 * focus. Picker shows multiple items on all column if Picker has no focus (e.g. a Touchscreen 715 * screen). 716 * 717 * @param columnIndex Index of column to activate. 718 */ setSelectedColumn(int columnIndex)719 public void setSelectedColumn(int columnIndex) { 720 if (mSelectedColumn != columnIndex) { 721 mSelectedColumn = columnIndex; 722 for (int i = 0; i < mColumnViews.size(); i++) { 723 updateColumnAlpha(i, true); 724 } 725 } 726 VerticalGridView columnView = mColumnViews.get(columnIndex); 727 if (hasFocus() && !columnView.hasFocus()) { 728 columnView.requestFocus(); 729 } 730 } 731 732 /** 733 * Get current activated column index. 734 * 735 * @return Current activated column index. 736 */ getSelectedColumn()737 public int getSelectedColumn() { 738 return mSelectedColumn; 739 } 740 741 } 742