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 android.support.v17.leanback.widget.picker; 16 17 import android.content.Context; 18 import android.graphics.Rect; 19 import android.support.v17.leanback.R; 20 import android.support.v17.leanback.widget.OnChildViewHolderSelectedListener; 21 import android.support.v17.leanback.widget.VerticalGridView; 22 import android.support.v7.widget.RecyclerView; 23 import android.util.AttributeSet; 24 import android.util.TypedValue; 25 import android.view.KeyEvent; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 import android.view.animation.AccelerateInterpolator; 30 import android.view.animation.DecelerateInterpolator; 31 import android.view.animation.Interpolator; 32 import android.widget.FrameLayout; 33 import android.widget.TextView; 34 35 import java.util.ArrayList; 36 import 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 */ 54 public class Picker extends FrameLayout { 55 56 public interface PickerValueListener { onValueChanged(Picker picker, int column)57 public void onValueChanged(Picker picker, int column); 58 } 59 60 private ViewGroup mRootView; 61 private ViewGroup mPickerView; 62 private final List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>(); 63 private 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 */ getSeparator()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 */ setSeparator(CharSequence separator)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 */ getPickerItemLayoutId()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 */ getPickerItemTextViewId()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 */ setPickerItemTextViewId(int textViewId)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 */ Picker(Context context, AttributeSet attrs, int defStyleAttr)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 137 mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha); 138 mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha); 139 mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha); 140 mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha); 141 142 mAlphaAnimDuration = 200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration); 143 144 mDecelerateInterpolator = new DecelerateInterpolator(2.5F); 145 mAccelerateInterpolator = new AccelerateInterpolator(2.5F); 146 147 LayoutInflater inflater = LayoutInflater.from(getContext()); 148 mRootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true); 149 mPickerView = (ViewGroup) mRootView.findViewById(R.id.picker); 150 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 */ getColumnAt(int colIndex)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 */ getColumnsCount()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 */ setColumns(List<PickerColumn> columns)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 mColumnViews.add(columnView); 200 201 // add view to root 202 mPickerView.addView(columnView); 203 204 // add a separator if not the last element 205 if (i != totalCol - 1 && getSeparator() != null) { 206 TextView separator = (TextView) inflater.inflate( 207 R.layout.lb_picker_separator, mPickerView, false); 208 separator.setText(getSeparator()); 209 mPickerView.addView(separator); 210 } 211 212 columnView.setAdapter(new PickerScrollArrayAdapter(getContext(), 213 getPickerItemLayoutId(), getPickerItemTextViewId(), colIndex)); 214 columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener); 215 } 216 } 217 218 /** 219 * When column labels change or column range changes, call this function to re-populate the 220 * selection list. Note this function cannot be called from RecyclerView layout/scroll pass. 221 * @param columnIndex Index of column to update. 222 * @param column New column to update. 223 */ setColumnAt(int columnIndex, PickerColumn column)224 public void setColumnAt(int columnIndex, PickerColumn column) { 225 mColumns.set(columnIndex, column); 226 VerticalGridView columnView = mColumnViews.get(columnIndex); 227 PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter(); 228 if (adapter != null) { 229 adapter.notifyDataSetChanged(); 230 } 231 columnView.setSelectedPosition(column.getCurrentValue() - column.getMinValue()); 232 } 233 234 /** 235 * Manually set current value of a column. The function will update UI and notify listeners. 236 * @param columnIndex Index of column to update. 237 * @param value New value of the column. 238 * @param runAnimation True to scroll to the value or false otherwise. 239 */ setColumnValue(int columnIndex, int value, boolean runAnimation)240 public void setColumnValue(int columnIndex, int value, boolean runAnimation) { 241 PickerColumn column = mColumns.get(columnIndex); 242 if (column.getCurrentValue() != value) { 243 column.setCurrentValue(value); 244 notifyValueChanged(columnIndex); 245 VerticalGridView columnView = mColumnViews.get(columnIndex); 246 if (columnView != null) { 247 int position = value - mColumns.get(columnIndex).getMinValue(); 248 if (runAnimation) { 249 columnView.setSelectedPositionSmooth(position); 250 } else { 251 columnView.setSelectedPosition(position); 252 } 253 } 254 } 255 } 256 notifyValueChanged(int columnIndex)257 private void notifyValueChanged(int columnIndex) { 258 if (mListeners != null) { 259 for (int i = mListeners.size() - 1; i >= 0; i--) { 260 mListeners.get(i).onValueChanged(this, columnIndex); 261 } 262 } 263 } 264 265 /** 266 * Register a callback to be invoked when the picker's value has changed. 267 * @param listener The callback to ad 268 */ addOnValueChangedListener(PickerValueListener listener)269 public void addOnValueChangedListener(PickerValueListener listener) { 270 if (mListeners == null) { 271 mListeners = new ArrayList<Picker.PickerValueListener>(); 272 } 273 mListeners.add(listener); 274 } 275 276 /** 277 * Remove a previously installed value changed callback 278 * @param listener The callback to remove. 279 */ removeOnValueChangedListener(PickerValueListener listener)280 public void removeOnValueChangedListener(PickerValueListener listener) { 281 if (mListeners != null) { 282 mListeners.remove(listener); 283 } 284 } 285 updateColumnAlpha(int colIndex, boolean animate)286 private void updateColumnAlpha(int colIndex, boolean animate) { 287 VerticalGridView column = mColumnViews.get(colIndex); 288 289 int selected = column.getSelectedPosition(); 290 View item; 291 292 for (int i = 0; i < column.getAdapter().getItemCount(); i++) { 293 item = column.getLayoutManager().findViewByPosition(i); 294 if (item != null) { 295 setOrAnimateAlpha(item, (selected == i), colIndex, animate); 296 } 297 } 298 } 299 setOrAnimateAlpha(View view, boolean selected, int colIndex, boolean animate)300 private void setOrAnimateAlpha(View view, boolean selected, int colIndex, 301 boolean animate) { 302 boolean columnShownAsActivated = colIndex == mSelectedColumn || !hasFocus(); 303 if (selected) { 304 // set alpha for main item (selected) in the column 305 if (columnShownAsActivated) { 306 setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator); 307 } else { 308 setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, mDecelerateInterpolator); 309 } 310 } else { 311 // set alpha for remaining items in the column 312 if (columnShownAsActivated) { 313 setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator); 314 } else { 315 setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1, 316 mDecelerateInterpolator); 317 } 318 } 319 } 320 setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, Interpolator interpolator)321 private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, 322 Interpolator interpolator) { 323 view.animate().cancel(); 324 if (!animate) { 325 view.setAlpha(destAlpha); 326 } else { 327 if (startAlpha >= 0.0f) { 328 // set a start alpha 329 view.setAlpha(startAlpha); 330 } 331 view.animate().alpha(destAlpha) 332 .setDuration(mAlphaAnimDuration).setInterpolator(interpolator) 333 .start(); 334 } 335 } 336 337 /** 338 * Classes extending {@link Picker} can override this function to supply the 339 * behavior when a list has been scrolled. Subclass may call {@link #setColumnValue(int, int, 340 * boolean)} and or {@link #setColumnAt(int,PickerColumn)}. Subclass should not directly call 341 * {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify 342 * listeners. 343 * @param columnIndex index of which column was changed. 344 * @param newValue A new value desired to be set on the column. 345 */ onColumnValueChanged(int columnIndex, int newValue)346 public void onColumnValueChanged(int columnIndex, int newValue) { 347 PickerColumn column = mColumns.get(columnIndex); 348 if (column.getCurrentValue() != newValue) { 349 column.setCurrentValue(newValue); 350 notifyValueChanged(columnIndex); 351 } 352 } 353 getFloat(int resourceId)354 private float getFloat(int resourceId) { 355 TypedValue buffer = new TypedValue(); 356 getContext().getResources().getValue(resourceId, buffer, true); 357 return buffer.getFloat(); 358 } 359 360 static class ViewHolder extends RecyclerView.ViewHolder { 361 final TextView textView; 362 ViewHolder(View v, TextView textView)363 ViewHolder(View v, TextView textView) { 364 super(v); 365 this.textView = textView; 366 } 367 } 368 369 class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> { 370 371 private final int mResource; 372 private final int mColIndex; 373 private final int mTextViewResourceId; 374 private PickerColumn mData; 375 PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId, int colIndex)376 PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId, 377 int colIndex) { 378 mResource = resource; 379 mColIndex = colIndex; 380 mTextViewResourceId = textViewResourceId; 381 mData = mColumns.get(mColIndex); 382 } 383 384 @Override onCreateViewHolder(ViewGroup parent, int viewType)385 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 386 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 387 View v = inflater.inflate(mResource, parent, false); 388 TextView textView; 389 if (mTextViewResourceId != 0) { 390 textView = (TextView) v.findViewById(mTextViewResourceId); 391 } else { 392 textView = (TextView) v; 393 } 394 ViewHolder vh = new ViewHolder(v, textView); 395 return vh; 396 } 397 398 @Override onBindViewHolder(ViewHolder holder, int position)399 public void onBindViewHolder(ViewHolder holder, int position) { 400 if (holder.textView != null && mData != null) { 401 holder.textView.setText(mData.getLabelFor(mData.getMinValue() + position)); 402 } 403 setOrAnimateAlpha(holder.itemView, 404 (mColumnViews.get(mColIndex).getSelectedPosition() == position), 405 mColIndex, false); 406 } 407 408 @Override onViewAttachedToWindow(ViewHolder holder)409 public void onViewAttachedToWindow(ViewHolder holder) { 410 holder.itemView.setFocusable(isActivated()); 411 } 412 413 @Override getItemCount()414 public int getItemCount() { 415 return mData == null ? 0 : mData.getCount(); 416 } 417 } 418 419 private final OnChildViewHolderSelectedListener mColumnChangeListener = new 420 OnChildViewHolderSelectedListener() { 421 422 @Override 423 public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child, 424 int position, int subposition) { 425 PickerScrollArrayAdapter pickerScrollArrayAdapter = (PickerScrollArrayAdapter) parent 426 .getAdapter(); 427 428 int colIndex = mColumnViews.indexOf(parent); 429 updateColumnAlpha(colIndex, true); 430 if (child != null) { 431 int newValue = mColumns.get(colIndex).getMinValue() + position; 432 onColumnValueChanged(colIndex, newValue); 433 } 434 } 435 436 }; 437 438 @Override dispatchKeyEvent(android.view.KeyEvent event)439 public boolean dispatchKeyEvent(android.view.KeyEvent event) { 440 if (isActivated()) { 441 final int keyCode = event.getKeyCode(); 442 switch (keyCode) { 443 case KeyEvent.KEYCODE_DPAD_CENTER: 444 case KeyEvent.KEYCODE_ENTER: 445 if (event.getAction() == KeyEvent.ACTION_UP) { 446 performClick(); 447 } 448 break; 449 default: 450 return super.dispatchKeyEvent(event); 451 } 452 return true; 453 } 454 return super.dispatchKeyEvent(event); 455 } 456 457 @Override onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)458 protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 459 int column = getSelectedColumn(); 460 if (column < mColumnViews.size()) { 461 return mColumnViews.get(column).requestFocus(direction, previouslyFocusedRect); 462 } 463 return false; 464 } 465 466 /** 467 * Classes extending {@link Picker} can choose to override this method to 468 * supply the {@link Picker}'s column's single item height in pixels. 469 */ getPickerItemHeightPixels()470 protected int getPickerItemHeightPixels() { 471 return getContext().getResources().getDimensionPixelSize(R.dimen.picker_item_height); 472 } 473 updateColumnSize()474 private void updateColumnSize() { 475 for (int i = 0; i < getColumnsCount(); i++) { 476 updateColumnSize(mColumnViews.get(i)); 477 } 478 } 479 updateColumnSize(VerticalGridView columnView)480 private void updateColumnSize(VerticalGridView columnView) { 481 ViewGroup.LayoutParams lp = columnView.getLayoutParams(); 482 lp.height = (int) (getPickerItemHeightPixels() * (isActivated() ? 483 getActivatedVisibleItemCount() : getVisibleItemCount())); 484 columnView.setLayoutParams(lp); 485 } 486 updateItemFocusable()487 private void updateItemFocusable() { 488 final boolean activated = isActivated(); 489 for (int i = 0; i < getColumnsCount(); i++) { 490 VerticalGridView grid = mColumnViews.get(i); 491 for (int j = 0; j < grid.getChildCount(); j++) { 492 View view = grid.getChildAt(j); 493 view.setFocusable(activated); 494 } 495 } 496 } 497 /** 498 * Returns number of visible items showing in a column when it's activated. The default value 499 * is 3. 500 * @return Number of visible items showing in a column when it's activated. 501 */ getActivatedVisibleItemCount()502 public float getActivatedVisibleItemCount() { 503 return mVisibleItemsActivated; 504 } 505 506 /** 507 * Changes number of visible items showing in a column when it's activated. The default value 508 * is 3. 509 * @param visiblePickerItems Number of visible items showing in a column when it's activated. 510 */ setActivatedVisibleItemCount(float visiblePickerItems)511 public void setActivatedVisibleItemCount(float visiblePickerItems) { 512 if (visiblePickerItems <= 0) { 513 throw new IllegalArgumentException(); 514 } 515 if (mVisibleItemsActivated != visiblePickerItems) { 516 mVisibleItemsActivated = visiblePickerItems; 517 if (isActivated()) { 518 updateColumnSize(); 519 } 520 } 521 } 522 523 /** 524 * Returns number of visible items showing in a column when it's not activated. The default 525 * value is 1. 526 * @return Number of visible items showing in a column when it's not activated. 527 */ getVisibleItemCount()528 public float getVisibleItemCount() { 529 return 1; 530 } 531 532 /** 533 * Changes number of visible items showing in a column when it's not activated. The default 534 * value is 1. 535 * @param pickerItems Number of visible items showing in a column when it's not activated. 536 */ setVisibleItemCount(float pickerItems)537 public void setVisibleItemCount(float pickerItems) { 538 if (pickerItems <= 0) { 539 throw new IllegalArgumentException(); 540 } 541 if (mVisibleItems != pickerItems) { 542 mVisibleItems = pickerItems; 543 if (!isActivated()) { 544 updateColumnSize(); 545 } 546 } 547 } 548 549 @Override setActivated(boolean activated)550 public void setActivated(boolean activated) { 551 if (activated != isActivated()) { 552 super.setActivated(activated); 553 updateColumnSize(); 554 updateItemFocusable(); 555 } else { 556 super.setActivated(activated); 557 } 558 } 559 560 @Override requestChildFocus(View child, View focused)561 public void requestChildFocus(View child, View focused) { 562 super.requestChildFocus(child, focused); 563 for (int i = 0; i < mColumnViews.size(); i++) { 564 if (mColumnViews.get(i).hasFocus()) { 565 setSelectedColumn(i); 566 } 567 } 568 } 569 570 /** 571 * Change current selected column. Picker shows multiple items on selected column if Picker has 572 * focus. Picker shows multiple items on all column if Picker has no focus (e.g. a Touchscreen 573 * screen). 574 * @param columnIndex Index of column to activate. 575 */ setSelectedColumn(int columnIndex)576 public void setSelectedColumn(int columnIndex) { 577 if (mSelectedColumn != columnIndex) { 578 mSelectedColumn = columnIndex; 579 for (int i = 0; i < mColumnViews.size(); i++) { 580 updateColumnAlpha(i, true); 581 } 582 } 583 } 584 585 /** 586 * Get current activated column index. 587 * @return Current activated column index. 588 */ getSelectedColumn()589 public int getSelectedColumn() { 590 return mSelectedColumn; 591 } 592 593 } 594