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