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 seperator Separator String between Picker columns.
91      */
setSeparator(CharSequence seperator)92     public final void setSeparator(CharSequence seperator) {
93         mSeparator = seperator;
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 dont 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