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