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