Picker.java revision 7041ebbfa7b73695f5c8e831fa6f14233c95d9ef
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 15package android.support.v17.leanback.widget.picker; 16 17import android.content.Context; 18import android.graphics.Rect; 19import android.support.v17.leanback.R; 20import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener; 21import android.support.v17.leanback.widget.VerticalGridView; 22import android.support.v7.widget.RecyclerView; 23import android.util.AttributeSet; 24import android.util.TypedValue; 25import android.view.KeyEvent; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.view.animation.AccelerateInterpolator; 30import android.view.animation.DecelerateInterpolator; 31import android.view.animation.Interpolator; 32import android.widget.FrameLayout; 33import android.widget.TextView; 34 35import java.util.ArrayList; 36import java.util.List; 37 38/** 39 * Picker is a widget showing multiple customized {@link PickerColumn}s. The PickerColumns are 40 * initialized in {@link #setColumns(List)}. Call {@link #setColumnAt(int, PickerColumn)} if the 41 * column value range or labels change. Call {@link #setColumnValue(int, int, boolean)} to update 42 * the current value of PickerColumn. 43 * <p> 44 * Picker has two states and will change height: 45 * <li>{@link #isActivated()} is true: Picker shows typically three items vertically (see 46 * {@link #getActivatedVisibleItemCount()}}. Columns other than {@link #getSelectedColumn()} still 47 * shows one item if the Picker is focused. On a touch screen device, the Picker will not get focus 48 * so it always show three items on all columns. On a non-touch device (a TV), the Picker will show 49 * three items only on currently activated column. If the Picker has focus, it will intercept DPAD 50 * directions and select activated column. 51 * <li>{@link #isActivated()} is false: Picker shows one item vertically (see 52 * {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks. 53 */ 54public class Picker extends FrameLayout { 55 56 public interface PickerValueListener { 57 public void onValueChanged(Picker picker, int column); 58 } 59 60 private ViewGroup mRootView; 61 private ViewGroup mPickerView; 62 final List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>(); 63 ArrayList<PickerColumn> mColumns; 64 65 private float mUnfocusedAlpha; 66 private float mFocusedAlpha; 67 private float mVisibleColumnAlpha; 68 private float mInvisibleColumnAlpha; 69 private int mAlphaAnimDuration; 70 private Interpolator mDecelerateInterpolator; 71 private Interpolator mAccelerateInterpolator; 72 private ArrayList<PickerValueListener> mListeners; 73 private float mVisibleItemsActivated = 3; 74 private float mVisibleItems = 1; 75 private int mSelectedColumn = 0; 76 77 private CharSequence mSeparator; 78 private int mPickerItemLayoutId = R.layout.lb_picker_item; 79 private int mPickerItemTextViewId = 0; 80 81 /** 82 * Gets separator string between columns. 83 */ 84 public final CharSequence getSeparator() { 85 return mSeparator; 86 } 87 88 /** 89 * Sets separator String between Picker columns. 90 * @param separator Separator String between Picker columns. 91 */ 92 public final void setSeparator(CharSequence separator) { 93 mSeparator = separator; 94 } 95 96 /** 97 * Classes extending {@link Picker} can choose to override this method to 98 * supply the {@link Picker}'s item's layout id 99 */ 100 public final int getPickerItemLayoutId() { 101 return mPickerItemLayoutId; 102 } 103 104 /** 105 * Returns the {@link Picker}'s item's {@link TextView}'s id from within the 106 * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the 107 * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link 108 * TextView}. 109 */ 110 public final int getPickerItemTextViewId() { 111 return mPickerItemTextViewId; 112 } 113 114 /** 115 * Sets the {@link Picker}'s item's {@link TextView}'s id from within the 116 * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the 117 * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link 118 * TextView}. 119 * @param textViewId View id of TextView inside a Picker item, or 0 if the Picker item is a 120 * TextView. 121 */ 122 public final void setPickerItemTextViewId(int textViewId) { 123 mPickerItemTextViewId = textViewId; 124 } 125 126 /** 127 * Creates a Picker widget. 128 * @param context 129 * @param attrs 130 * @param defStyleAttr 131 */ 132 public Picker(Context context, AttributeSet attrs, int defStyleAttr) { 133 super(context, attrs, defStyleAttr); 134 // Make it enabled and clickable to receive Click event. 135 setEnabled(true); 136 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); 137 138 mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha); 139 mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha); 140 mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha); 141 mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha); 142 143 mAlphaAnimDuration = 200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration); 144 145 mDecelerateInterpolator = new DecelerateInterpolator(2.5F); 146 mAccelerateInterpolator = new AccelerateInterpolator(2.5F); 147 148 LayoutInflater inflater = LayoutInflater.from(getContext()); 149 mRootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true); 150 mPickerView = (ViewGroup) mRootView.findViewById(R.id.picker); 151 } 152 153 /** 154 * Get nth PickerColumn. 155 * @param colIndex Index of PickerColumn. 156 * @return PickerColumn at colIndex or null if {@link #setColumns(List)} is not called yet. 157 */ 158 public PickerColumn getColumnAt(int colIndex) { 159 if (mColumns == null) { 160 return null; 161 } 162 return mColumns.get(colIndex); 163 } 164 165 /** 166 * Get number of PickerColumns. 167 * @return Number of PickerColumns or 0 if {@link #setColumns(List)} is not called yet. 168 */ 169 public int getColumnsCount() { 170 if (mColumns == null) { 171 return 0; 172 } 173 return mColumns.size(); 174 } 175 176 /** 177 * Set columns and create Views. 178 * @param columns PickerColumns to be shown in the Picker. 179 */ 180 public void setColumns(List<PickerColumn> columns) { 181 mColumnViews.clear(); 182 mPickerView.removeAllViews(); 183 mColumns = new ArrayList<PickerColumn>(columns); 184 if (mSelectedColumn > mColumns.size() - 1) { 185 mSelectedColumn = mColumns.size() - 1; 186 } 187 LayoutInflater inflater = LayoutInflater.from(getContext()); 188 int totalCol = getColumnsCount(); 189 for (int i = 0; i < totalCol; i++) { 190 final int colIndex = i; 191 final VerticalGridView columnView = (VerticalGridView) inflater.inflate( 192 R.layout.lb_picker_column, mPickerView, false); 193 // we don't want VerticalGridView to receive focus. 194 updateColumnSize(columnView); 195 // always center aligned, not aligning selected item on top/bottom edge. 196 columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 197 // Width is dynamic, so has fixed size is false. 198 columnView.setHasFixedSize(false); 199 columnView.setFocusable(isActivated()); 200 // Setting cache size to zero in order to rebind item views when picker widget becomes 201 // activated. Rebinding is necessary to update the alphas when the columns are expanded 202 // as a result of the picker getting activated, otherwise the cached views with the 203 // wrong alphas could be laid out. 204 columnView.setItemViewCacheSize(0); 205 mColumnViews.add(columnView); 206 207 // add view to root 208 mPickerView.addView(columnView); 209 210 // add a separator if not the last element 211 if (i != totalCol - 1 && getSeparator() != null) { 212 TextView separator = (TextView) inflater.inflate( 213 R.layout.lb_picker_separator, mPickerView, false); 214 separator.setText(getSeparator()); 215 mPickerView.addView(separator); 216 } 217 218 columnView.setAdapter(new PickerScrollArrayAdapter(getContext(), 219 getPickerItemLayoutId(), getPickerItemTextViewId(), colIndex)); 220 columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener); 221 } 222 } 223 224 /** 225 * When column labels change or column range changes, call this function to re-populate the 226 * selection list. Note this function cannot be called from RecyclerView layout/scroll pass. 227 * @param columnIndex Index of column to update. 228 * @param column New column to update. 229 */ 230 public void setColumnAt(int columnIndex, PickerColumn column) { 231 mColumns.set(columnIndex, column); 232 VerticalGridView columnView = mColumnViews.get(columnIndex); 233 PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter(); 234 if (adapter != null) { 235 adapter.notifyDataSetChanged(); 236 } 237 columnView.setSelectedPosition(column.getCurrentValue() - column.getMinValue()); 238 } 239 240 /** 241 * Manually set current value of a column. The function will update UI and notify listeners. 242 * @param columnIndex Index of column to update. 243 * @param value New value of the column. 244 * @param runAnimation True to scroll to the value or false otherwise. 245 */ 246 public void setColumnValue(int columnIndex, int value, boolean runAnimation) { 247 PickerColumn column = mColumns.get(columnIndex); 248 if (column.getCurrentValue() != value) { 249 column.setCurrentValue(value); 250 notifyValueChanged(columnIndex); 251 VerticalGridView columnView = mColumnViews.get(columnIndex); 252 if (columnView != null) { 253 int position = value - mColumns.get(columnIndex).getMinValue(); 254 if (runAnimation) { 255 columnView.setSelectedPositionSmooth(position); 256 } else { 257 columnView.setSelectedPosition(position); 258 } 259 } 260 } 261 } 262 263 private void notifyValueChanged(int columnIndex) { 264 if (mListeners != null) { 265 for (int i = mListeners.size() - 1; i >= 0; i--) { 266 mListeners.get(i).onValueChanged(this, columnIndex); 267 } 268 } 269 } 270 271 /** 272 * Register a callback to be invoked when the picker's value has changed. 273 * @param listener The callback to ad 274 */ 275 public void addOnValueChangedListener(PickerValueListener listener) { 276 if (mListeners == null) { 277 mListeners = new ArrayList<Picker.PickerValueListener>(); 278 } 279 mListeners.add(listener); 280 } 281 282 /** 283 * Remove a previously installed value changed callback 284 * @param listener The callback to remove. 285 */ 286 public void removeOnValueChangedListener(PickerValueListener listener) { 287 if (mListeners != null) { 288 mListeners.remove(listener); 289 } 290 } 291 292 void updateColumnAlpha(int colIndex, boolean animate) { 293 VerticalGridView column = mColumnViews.get(colIndex); 294 295 int selected = column.getSelectedPosition(); 296 View item; 297 298 for (int i = 0; i < column.getAdapter().getItemCount(); i++) { 299 item = column.getLayoutManager().findViewByPosition(i); 300 if (item != null) { 301 setOrAnimateAlpha(item, (selected == i), colIndex, animate); 302 } 303 } 304 } 305 306 void setOrAnimateAlpha(View view, boolean selected, int colIndex, 307 boolean animate) { 308 boolean columnShownAsActivated = colIndex == mSelectedColumn || !hasFocus(); 309 if (selected) { 310 // set alpha for main item (selected) in the column 311 if (columnShownAsActivated) { 312 setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator); 313 } else { 314 setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, mDecelerateInterpolator); 315 } 316 } else { 317 // set alpha for remaining items in the column 318 if (columnShownAsActivated) { 319 setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator); 320 } else { 321 setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1, 322 mDecelerateInterpolator); 323 } 324 } 325 } 326 327 private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, 328 Interpolator interpolator) { 329 view.animate().cancel(); 330 if (!animate) { 331 view.setAlpha(destAlpha); 332 } else { 333 if (startAlpha >= 0.0f) { 334 // set a start alpha 335 view.setAlpha(startAlpha); 336 } 337 view.animate().alpha(destAlpha) 338 .setDuration(mAlphaAnimDuration).setInterpolator(interpolator) 339 .start(); 340 } 341 } 342 343 /** 344 * Classes extending {@link Picker} can override this function to supply the 345 * behavior when a list has been scrolled. Subclass may call {@link #setColumnValue(int, int, 346 * boolean)} and or {@link #setColumnAt(int,PickerColumn)}. Subclass should not directly call 347 * {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify 348 * listeners. 349 * @param columnIndex index of which column was changed. 350 * @param newValue A new value desired to be set on the column. 351 */ 352 public void onColumnValueChanged(int columnIndex, int newValue) { 353 PickerColumn column = mColumns.get(columnIndex); 354 if (column.getCurrentValue() != newValue) { 355 column.setCurrentValue(newValue); 356 notifyValueChanged(columnIndex); 357 } 358 } 359 360 private float getFloat(int resourceId) { 361 TypedValue buffer = new TypedValue(); 362 getContext().getResources().getValue(resourceId, buffer, true); 363 return buffer.getFloat(); 364 } 365 366 static class ViewHolder extends RecyclerView.ViewHolder { 367 final TextView textView; 368 369 ViewHolder(View v, TextView textView) { 370 super(v); 371 this.textView = textView; 372 } 373 } 374 375 class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> { 376 377 private final int mResource; 378 private final int mColIndex; 379 private final int mTextViewResourceId; 380 private PickerColumn mData; 381 382 PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId, 383 int colIndex) { 384 mResource = resource; 385 mColIndex = colIndex; 386 mTextViewResourceId = textViewResourceId; 387 mData = mColumns.get(mColIndex); 388 } 389 390 @Override 391 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 392 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 393 View v = inflater.inflate(mResource, parent, false); 394 TextView textView; 395 if (mTextViewResourceId != 0) { 396 textView = (TextView) v.findViewById(mTextViewResourceId); 397 } else { 398 textView = (TextView) v; 399 } 400 ViewHolder vh = new ViewHolder(v, textView); 401 return vh; 402 } 403 404 @Override 405 public void onBindViewHolder(ViewHolder holder, int position) { 406 if (holder.textView != null && mData != null) { 407 holder.textView.setText(mData.getLabelFor(mData.getMinValue() + position)); 408 } 409 setOrAnimateAlpha(holder.itemView, 410 (mColumnViews.get(mColIndex).getSelectedPosition() == position), 411 mColIndex, false); 412 } 413 414 @Override 415 public void onViewAttachedToWindow(ViewHolder holder) { 416 holder.itemView.setFocusable(isActivated()); 417 } 418 419 @Override 420 public int getItemCount() { 421 return mData == null ? 0 : mData.getCount(); 422 } 423 } 424 425 private final OnChildViewHolderSelectedListener mColumnChangeListener = new 426 OnChildViewHolderSelectedListener() { 427 428 @Override 429 public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, 430 int position, int subposition) { 431 PickerScrollArrayAdapter pickerScrollArrayAdapter = (PickerScrollArrayAdapter) parent 432 .getAdapter(); 433 434 int colIndex = mColumnViews.indexOf(parent); 435 updateColumnAlpha(colIndex, true); 436 if (child != null) { 437 int newValue = mColumns.get(colIndex).getMinValue() + position; 438 onColumnValueChanged(colIndex, newValue); 439 } 440 } 441 442 }; 443 444 @Override 445 public boolean dispatchKeyEvent(android.view.KeyEvent event) { 446 if (isActivated()) { 447 final int keyCode = event.getKeyCode(); 448 switch (keyCode) { 449 case KeyEvent.KEYCODE_DPAD_CENTER: 450 case KeyEvent.KEYCODE_ENTER: 451 if (event.getAction() == KeyEvent.ACTION_UP) { 452 performClick(); 453 } 454 break; 455 default: 456 return super.dispatchKeyEvent(event); 457 } 458 return true; 459 } 460 return super.dispatchKeyEvent(event); 461 } 462 463 @Override 464 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 465 int column = getSelectedColumn(); 466 if (column < mColumnViews.size()) { 467 return mColumnViews.get(column).requestFocus(direction, previouslyFocusedRect); 468 } 469 return false; 470 } 471 472 /** 473 * Classes extending {@link Picker} can choose to override this method to 474 * supply the {@link Picker}'s column's single item height in pixels. 475 */ 476 protected int getPickerItemHeightPixels() { 477 return getContext().getResources().getDimensionPixelSize(R.dimen.picker_item_height); 478 } 479 480 private void updateColumnSize() { 481 for (int i = 0; i < getColumnsCount(); i++) { 482 updateColumnSize(mColumnViews.get(i)); 483 } 484 } 485 486 private void updateColumnSize(VerticalGridView columnView) { 487 ViewGroup.LayoutParams lp = columnView.getLayoutParams(); 488 float itemCount = isActivated() ? getActivatedVisibleItemCount() : getVisibleItemCount(); 489 lp.height = (int) (getPickerItemHeightPixels() * itemCount 490 + columnView.getVerticalSpacing() * (itemCount - 1)); 491 columnView.setLayoutParams(lp); 492 } 493 494 private void updateItemFocusable() { 495 final boolean activated = isActivated(); 496 for (int i = 0; i < getColumnsCount(); i++) { 497 VerticalGridView grid = mColumnViews.get(i); 498 for (int j = 0; j < grid.getChildCount(); j++) { 499 View view = grid.getChildAt(j); 500 view.setFocusable(activated); 501 } 502 } 503 } 504 /** 505 * Returns number of visible items showing in a column when it's activated. The default value 506 * is 3. 507 * @return Number of visible items showing in a column when it's activated. 508 */ 509 public float getActivatedVisibleItemCount() { 510 return mVisibleItemsActivated; 511 } 512 513 /** 514 * Changes number of visible items showing in a column when it's activated. The default value 515 * is 3. 516 * @param visiblePickerItems Number of visible items showing in a column when it's activated. 517 */ 518 public void setActivatedVisibleItemCount(float visiblePickerItems) { 519 if (visiblePickerItems <= 0) { 520 throw new IllegalArgumentException(); 521 } 522 if (mVisibleItemsActivated != visiblePickerItems) { 523 mVisibleItemsActivated = visiblePickerItems; 524 if (isActivated()) { 525 updateColumnSize(); 526 } 527 } 528 } 529 530 /** 531 * Returns number of visible items showing in a column when it's not activated. The default 532 * value is 1. 533 * @return Number of visible items showing in a column when it's not activated. 534 */ 535 public float getVisibleItemCount() { 536 return 1; 537 } 538 539 /** 540 * Changes number of visible items showing in a column when it's not activated. The default 541 * value is 1. 542 * @param pickerItems Number of visible items showing in a column when it's not activated. 543 */ 544 public void setVisibleItemCount(float pickerItems) { 545 if (pickerItems <= 0) { 546 throw new IllegalArgumentException(); 547 } 548 if (mVisibleItems != pickerItems) { 549 mVisibleItems = pickerItems; 550 if (!isActivated()) { 551 updateColumnSize(); 552 } 553 } 554 } 555 556 @Override 557 public void setActivated(boolean activated) { 558 if (activated == isActivated()) { 559 super.setActivated(activated); 560 return; 561 } 562 super.setActivated(activated); 563 boolean hadFocus = hasFocus(); 564 int column = getSelectedColumn(); 565 // To avoid temporary focus loss in both the following cases, we set Picker's flag to 566 // FOCUS_BEFORE_DESCENDANTS first, and then back to FOCUS_AFTER_DESCENDANTS once done with 567 // the focus logic. 568 // 1. When changing from activated to deactivated, the Picker should grab the focus 569 // back if it's focusable. However, calling requestFocus on it will transfer the focus down 570 // to its children if it's flag is FOCUS_AFTER_DESCENDANTS. 571 // 2. When changing from deactivated to activated, while setting focusable flags on each 572 // column VerticalGridView, that column will call requestFocus (regardless of which column 573 // is the selected column) since the currently focused view (Picker) has a flag of 574 // FOCUS_AFTER_DESCENDANTS. 575 setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS); 576 if (!activated && hadFocus && isFocusable()) { 577 // When picker widget that originally had focus is deactivated and it is focusable, we 578 // should not pass the focus down to the children. The Picker itself will capture focus. 579 requestFocus(); 580 } 581 582 for (int i = 0; i < getColumnsCount(); i++) { 583 mColumnViews.get(i).setFocusable(activated); 584 } 585 586 updateColumnSize(); 587 updateItemFocusable(); 588 if (activated && hadFocus && (column >= 0)) { 589 mColumnViews.get(column).requestFocus(); 590 } 591 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 592 } 593 594 @Override 595 public void requestChildFocus(View child, View focused) { 596 super.requestChildFocus(child, focused); 597 for (int i = 0; i < mColumnViews.size(); i++) { 598 if (mColumnViews.get(i).hasFocus()) { 599 setSelectedColumn(i); 600 } 601 } 602 } 603 604 /** 605 * Change current selected column. Picker shows multiple items on selected column if Picker has 606 * focus. Picker shows multiple items on all column if Picker has no focus (e.g. a Touchscreen 607 * screen). 608 * @param columnIndex Index of column to activate. 609 */ 610 public void setSelectedColumn(int columnIndex) { 611 if (mSelectedColumn != columnIndex) { 612 mSelectedColumn = columnIndex; 613 for (int i = 0; i < mColumnViews.size(); i++) { 614 updateColumnAlpha(i, true); 615 } 616 } 617 } 618 619 /** 620 * Get current activated column index. 621 * @return Current activated column index. 622 */ 623 public int getSelectedColumn() { 624 return mSelectedColumn; 625 } 626 627} 628