Picker.java revision 71d30e1cf5514761ba8ad4bd3c8c70540d60dbd3
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.LayoutInflater;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.animation.AccelerateInterpolator;
28import android.view.animation.DecelerateInterpolator;
29import android.view.animation.Interpolator;
30import android.widget.FrameLayout;
31import android.widget.LinearLayout;
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(ArrayList)}.  You could only set columns once and not able to
40 * add or remove Column later.  Call {@link #updateAdapter(int)} if the column value range or labels
41 * change.  Call {@link #updateValue(int, int, boolean)} to update the current value of
42 * PickerColumn.
43 */
44public class Picker extends FrameLayout {
45
46    public interface PickerValueListener {
47        public void onValueChanged(Picker picker, int column);
48    }
49
50    private String mSeparator;
51    private ViewGroup mRootView;
52    private ChildFocusAwareLinearLayout mPickerView;
53    private List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>();
54    private ArrayList<PickerColumn> mColumns;
55
56    private float mUnfocusedAlpha;
57    private float mFocusedAlpha;
58    private float mVisibleColumnAlpha;
59    private float mInvisibleColumnAlpha;
60    private int mAlphaAnimDuration;
61    private Interpolator mDecelerateInterpolator;
62    private Interpolator mAccelerateInterpolator;
63    private ArrayList<PickerValueListener> mListeners;
64
65    /**
66     * Classes extending {@link Picker} can choose to override this method to
67     * supply the separator string
68     */
69    protected String getSeparator() {
70        return mSeparator;
71    }
72
73    /**
74     * Classes extending {@link Picker} can choose to override this method to
75     * supply the {@link Picker}'s root layout id
76     */
77    protected int getRootLayoutId() {
78        return R.layout.lb_picker;
79    }
80
81    /**
82     * Classes extending {@link Picker} can choose to override this method to
83     * supply the {@link Picker}'s id from within the layout provided by
84     * {@link Picker#getRootLayoutId()}
85     */
86    protected int getPickerId() {
87        return R.id.picker;
88    }
89
90    /**
91     * Classes extending {@link Picker} can choose to override this method to
92     * supply the {@link Picker}'s separator's layout id
93     */
94    protected int getPickerSeparatorLayoutId() {
95        return R.layout.lb_picker_separator;
96    }
97
98    /**
99     * Classes extending {@link Picker} can choose to override this method to
100     * supply the {@link Picker}'s item's layout id
101     */
102    protected int getPickerItemLayoutId() {
103        return R.layout.lb_picker_item;
104    }
105
106    /**
107     * Classes extending {@link Picker} can choose to override this method to
108     * supply the {@link Picker}'s item's {@link TextView}'s id from within the
109     * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
110     * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
111     * TextView}.
112     */
113    protected int getPickerItemTextViewId() {
114        return 0;
115    }
116
117    /**
118     * Classes extending {@link Picker} can choose to override this method to
119     * supply the {@link Picker}'s column's height in pixels.
120     */
121    protected int getPickerColumnHeightPixels() {
122        return getContext().getResources().getDimensionPixelSize(R.dimen.picker_column_height);
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
134        mFocusedAlpha = 1f; //getFloat(R.dimen.list_item_selected_title_text_alpha);
135        mUnfocusedAlpha = 1f; //getFloat(R.dimen.list_item_unselected_text_alpha);
136        mVisibleColumnAlpha = 0.5f; //getFloat(R.dimen.picker_item_visible_column_item_alpha);
137        mInvisibleColumnAlpha = 0f; //getFloat(R.dimen.picker_item_invisible_column_item_alpha);
138
139        mAlphaAnimDuration = 200; // mContext.getResources().getInteger(R.integer.dialog_animation_duration);
140
141        mDecelerateInterpolator = new DecelerateInterpolator(2.5F);
142        mAccelerateInterpolator = new AccelerateInterpolator(2.5F);
143
144        LayoutInflater inflater = LayoutInflater.from(getContext());
145        mRootView = (ViewGroup) inflater.inflate(getRootLayoutId(), this, true);
146        mPickerView = (ChildFocusAwareLinearLayout) mRootView.findViewById(getPickerId());
147
148        mPickerView.setOnChildFocusListener(mColumnGainFocusListener);
149
150    }
151
152    /**
153     * Get nth PickerColumn.
154     * @param colIndex  Index of PickerColumn.
155     * @return PickerColumn at colIndex or null if {@link #setColumns(ArrayList)} is not called yet.
156     */
157    public PickerColumn getColumnAt(int colIndex) {
158        if (mColumns == null) {
159            return null;
160        }
161        return mColumns.get(colIndex);
162    }
163
164    /**
165     * Get number of PickerColumns.
166     * @return Number of PickerColumns or 0 if {@link #setColumns(ArrayList)} is not called yet.
167     */
168    public int getColumnsCount() {
169        if (mColumns == null) {
170            return 0;
171        }
172        return mColumns.size();
173    }
174
175    /**
176     * Set columns and create Views.  The method is only allowed to be called once.
177     * @param columns PickerColumns to be shown in the Picker.
178     */
179    public void setColumns(ArrayList<PickerColumn> columns) {
180        if (mColumns != null) {
181            throw new IllegalStateException("columns can only be initialized once");
182        }
183        mColumns = columns;
184        LayoutInflater inflater = LayoutInflater.from(getContext());
185        int totalCol = getColumnsCount();
186        for (int i = 0; i < totalCol; i++) {
187            final int colIndex = i;
188            final VerticalGridView columnView = (VerticalGridView) inflater.inflate(
189                    R.layout.lb_picker_column, mPickerView, false);
190            ViewGroup.LayoutParams lp = columnView.getLayoutParams();
191            lp.height = getPickerColumnHeightPixels();
192            columnView.setLayoutParams(lp);
193            columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
194            columnView.setHasFixedSize(false);
195            mColumnViews.add(columnView);
196
197            // add view to root
198            mPickerView.addView(columnView);
199
200            // add a separator if not the last element
201            if (i != totalCol - 1 && getSeparator() != null) {
202                TextView separator = (TextView) inflater.inflate(
203                        getPickerSeparatorLayoutId(), mPickerView, false);
204                separator.setText(getSeparator());
205                mPickerView.addView(separator);
206            }
207
208            columnView.setAdapter(new PickerScrollArrayAdapter(getContext(),
209                    getPickerItemLayoutId(), getPickerItemTextViewId(), colIndex));
210            columnView.setOnChildViewHolderSelectedListener(mColumnChangeListener);
211        }
212    }
213
214    /**
215     * When column labels change or column range changes, call this function to re-populate the
216     * selection list.
217     * @param columnIndex Index of column to update.
218     */
219    public void updateAdapter(int columnIndex) {
220        VerticalGridView columnView = mColumnViews.get(columnIndex);
221        PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter();
222        if (adapter != null) {
223            adapter.notifyDataSetChanged();
224        }
225    }
226
227    /**
228     * Manually set current value of a column.  The function will update UI and notify listeners.
229     * @param columnIndex Index of column to update.
230     * @param value New value of the column.
231     * @param runAnimation True to scroll to the value or false otherwise.
232     */
233    public void updateValue(int columnIndex, int value, boolean runAnimation) {
234        if (mColumns.get(columnIndex).setCurrentValue(value)) {
235            notifyValueChanged(columnIndex);
236            VerticalGridView columnView = mColumnViews.get(columnIndex);
237            if (columnView != null) {
238                int position = value - mColumns.get(columnIndex).getMinValue();
239                if (runAnimation) {
240                    columnView.setSelectedPositionSmooth(position);
241                } else {
242                    columnView.setSelectedPosition(position);
243                }
244            }
245        }
246    }
247
248    private void notifyValueChanged(int columnIndex) {
249        if (mListeners != null) {
250            for (int i = mListeners.size() - 1; i >= 0; i--) {
251                mListeners.get(i).onValueChanged(this, columnIndex);
252            }
253        }
254    }
255
256    public void addPickerValueListener(PickerValueListener listener) {
257        if (mListeners == null) {
258            mListeners = new ArrayList<Picker.PickerValueListener>();
259        }
260        mListeners.add(listener);
261    }
262
263    public void removePickerValueListener(PickerValueListener listener) {
264        if (mListeners != null) {
265            mListeners.remove(listener);
266        }
267    }
268
269    private void updateColumnAlpha(VerticalGridView column, boolean animateAlpha) {
270        if (column == null) {
271            return;
272        }
273
274        int selected = column.getSelectedPosition();
275        View item;
276        boolean focused = column.hasFocus();
277
278        for (int i = 0; i < column.getAdapter().getItemCount(); i++) {
279            item = column.getLayoutManager().findViewByPosition(i);
280            if (item != null) {
281                setOrAnimateAlpha(item, (selected == i), focused, animateAlpha);
282            }
283        }
284    }
285
286    private void setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate) {
287        if (selected) {
288            // set alpha for main item (selected) in the column
289            if (focused) {
290                setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, mDecelerateInterpolator);
291            } else {
292                setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1,  mDecelerateInterpolator);
293            }
294        } else {
295            // set alpha for remaining items in the column
296            if (focused) {
297                setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, mDecelerateInterpolator);
298            } else {
299                setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1,
300                        mDecelerateInterpolator);
301            }
302        }
303    }
304
305    private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha,
306            Interpolator interpolator) {
307        view.animate().cancel();
308        if (!animate) {
309            view.setAlpha(destAlpha);
310        } else {
311            if (startAlpha >= 0.0f) {
312                // set a start alpha
313                view.setAlpha(startAlpha);
314            }
315            view.animate().alpha(destAlpha)
316                    .setDuration(mAlphaAnimDuration).setInterpolator(interpolator)
317                    .start();
318        }
319    }
320
321    /**
322     * Classes extending {@link Picker} can override this function to supply the
323     * behavior when a list has been scrolled.  Subclass may call {@link #updateValue(int, int,
324     * boolean)} and or {@link #updateAdapter(int)}.  Subclass should not directly call
325     * {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify
326     * listeners.
327     * @param columnIndex index of which column was changed.
328     * @param newValue A new value desired to be set on the column.
329     */
330    public void onColumnValueChange(int columnIndex, int newValue) {
331        if (mColumns.get(columnIndex).setCurrentValue(newValue)) {
332            notifyValueChanged(columnIndex);
333        }
334    }
335
336    private float getFloat(int resourceId) {
337        TypedValue buffer = new TypedValue();
338        getContext().getResources().getValue(resourceId, buffer, true);
339        return buffer.getFloat();
340    }
341
342    static class ViewHolder extends RecyclerView.ViewHolder {
343        final TextView textView;
344
345        ViewHolder(View v, TextView textView) {
346            super(v);
347            this.textView = textView;
348        }
349    }
350
351    class PickerScrollArrayAdapter extends RecyclerView.Adapter<ViewHolder> {
352
353        private final int mResource;
354        private final int mColIndex;
355        private final int mTextViewResourceId;
356        private PickerColumn mData;
357
358        PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId,
359                int colIndex) {
360            mResource = resource;
361            mColIndex = colIndex;
362            mTextViewResourceId = textViewResourceId;
363            mData = mColumns.get(mColIndex);
364        }
365
366        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
367            LayoutInflater inflater = LayoutInflater.from(parent.getContext());
368            View v = inflater.inflate(mResource, parent, false);
369            TextView textView;
370            if (mTextViewResourceId != 0) {
371                textView = (TextView) v.findViewById(mTextViewResourceId);
372            } else {
373                textView = (TextView) v;
374            }
375            ViewHolder vh = new ViewHolder(v, textView);
376            return vh;
377        }
378
379        public void onBindViewHolder(ViewHolder holder, int position) {
380            if (holder.textView != null && mData != null) {
381                holder.textView.setText(mData.getValueLabelAt(mData.getMinValue() + position));
382            }
383            setOrAnimateAlpha(holder.itemView,
384                    (mColumnViews.get(mColIndex).getSelectedPosition() == position),
385                    mColumnViews.get(mColIndex).hasFocus(), false);
386        }
387
388        public void setData(PickerColumn data) {
389            mData = data;
390            notifyDataSetChanged();
391        }
392
393        public int getItemCount() {
394            return mData == null ? 0 : mData.getItemsCount();
395        }
396    }
397
398    /**
399     * Interface for managing child focus in a ChildFocusAwareLinearLayout.
400     */
401    interface OnChildFocusListener {
402        public boolean onPreRequestChildFocus(View child, View focused);
403        public void onRequestChildFocus(View child, View focused);
404    }
405
406    static class ChildFocusAwareLinearLayout extends LinearLayout {
407
408
409        private OnChildFocusListener mOnChildFocusListener;
410
411        public void setOnChildFocusListener(OnChildFocusListener listener) {
412            mOnChildFocusListener = listener;
413        }
414
415        public OnChildFocusListener getOnChildFocusListener() {
416            return mOnChildFocusListener;
417        }
418
419        public ChildFocusAwareLinearLayout(Context context, AttributeSet attrs) {
420            super(context, attrs);
421        }
422
423        @Override
424        public void requestChildFocus(View child, View focused) {
425            boolean preReturnedTrue = false;
426            if (mOnChildFocusListener != null) {
427                preReturnedTrue = mOnChildFocusListener.onPreRequestChildFocus(child, focused);
428            }
429            super.requestChildFocus(child, focused);
430            if (preReturnedTrue && mOnChildFocusListener != null) {
431                mOnChildFocusListener.onRequestChildFocus(child, focused);
432            }
433        }
434    }
435
436    private final OnChildViewHolderSelectedListener mColumnChangeListener = new
437            OnChildViewHolderSelectedListener() {
438
439        @Override
440        public void onChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child,
441                int position, int subposition) {
442            PickerScrollArrayAdapter pickerScrollArrayAdapter = (PickerScrollArrayAdapter) parent
443                    .getAdapter();
444
445            int colIndex = mColumnViews.indexOf(parent);
446            updateColumnAlpha((VerticalGridView) parent, parent.hasFocus());
447            if (child != null) {
448                int newValue = mColumns.get(colIndex).getMinValue() + position;
449                onColumnValueChange(colIndex, newValue);
450            }
451        }
452
453    };
454
455    private final OnChildFocusListener mColumnGainFocusListener = new OnChildFocusListener() {
456        @Override
457        public boolean onPreRequestChildFocus(View child, View focused) {
458            return true;
459        }
460
461        @Override
462        public void onRequestChildFocus(View child, View focused) {
463            for (int i = 0; i < mColumnViews.size(); i++) {
464                VerticalGridView column = mColumnViews.get(i);
465                updateColumnAlpha(column, column.hasFocus());
466            }
467        }
468    };
469
470}
471