1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.settings.widget.picker;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ObjectAnimator;
23 import android.app.Fragment;
24 import android.content.Context;
25 import android.os.Bundle;
26 import android.util.TypedValue;
27 import android.view.KeyEvent;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewGroup.LayoutParams;
32 import android.view.animation.AccelerateInterpolator;
33 import android.view.animation.DecelerateInterpolator;
34 import android.view.animation.Interpolator;
35 import android.widget.AdapterView;
36 import android.widget.AdapterView.OnItemClickListener;
37 import android.widget.TextView;
38 
39 import com.android.tv.settings.widget.ScrollAdapterView;
40 import com.android.tv.settings.widget.ScrollArrayAdapter;
41 import com.android.tv.settings.R;
42 
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.List;
46 
47 /**
48  * Picker class
49  */
50 public class Picker extends Fragment {
51 
52     /**
53      * Object listening for adapter events.
54      */
55     public interface ResultListener {
onCommitResult(List<String> result)56         void onCommitResult(List<String> result);
57     }
58 
59     private Context mContext;
60     private String mSeparator;
61     private ViewGroup mRootView;
62     private ViewGroup mPickerView;
63     private List<ScrollAdapterView> mColumnViews;
64     private ResultListener mResultListener;
65     private ChangeTextColorOnFocus mColumnChangeListener;
66     private ArrayList<PickerColumn> mColumns = new ArrayList<PickerColumn>();
67     protected PickerConstant mConstant;
68 
69     private float mUnfocusedAlpha;
70     private float mFocusedAlpha;
71     private float mVisibleColumnAlpha;
72     private float mInvisibleColumnAlpha;
73     private int mAlphaAnimDuration;
74     private Interpolator mDecelerateInterpolator;
75     private Interpolator mAccelerateInterpolator;
76     private boolean mKeyDown = false;
77     private boolean mClicked = false;
78 
79     /**
80      * selection result
81      */
82     private List<String> mResult;
83 
84     private OnItemClickListener mOnClickListener = new OnItemClickListener() {
85         @Override
86         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
87             if (mKeyDown) {
88                 mKeyDown = false;
89                 mClicked = true;
90                 updateAllColumnsForClick(true);
91             }
92         }
93     };
94 
newInstance()95     public static Picker newInstance() {
96         return new Picker();
97     }
98 
99     /**
100      * Classes extending {@link Picker} should override this method to supply
101      * the columns
102      */
getColumns()103     protected ArrayList<PickerColumn> getColumns() {
104         return null;
105     }
106 
107     /**
108      * Classes extending {@link Picker} can choose to override this method to
109      * supply the separator string
110      */
getSeparator()111     protected String getSeparator() {
112         return mSeparator;
113     }
114 
115     /**
116      * Classes extending {@link Picker} can choose to override this method to
117      * supply the {@link Picker}'s root layout id
118      */
getRootLayoutId()119     protected int getRootLayoutId() {
120         return R.layout.picker;
121     }
122 
123     /**
124      * Classes extending {@link Picker} can choose to override this method to
125      * supply the {@link Picker}'s id from within the layout provided by
126      * {@link Picker#getRootLayoutId()}
127      */
getPickerId()128     protected int getPickerId() {
129         return R.id.picker;
130     }
131 
132     /**
133      * Classes extending {@link Picker} can choose to override this method to
134      * supply the {@link Picker}'s separator's layout id
135      */
getPickerSeparatorLayoutId()136     protected int getPickerSeparatorLayoutId() {
137         return R.layout.picker_separator;
138     }
139 
140     /**
141      * Classes extending {@link Picker} can choose to override this method to
142      * supply the {@link Picker}'s item's layout id
143      */
getPickerItemLayoutId()144     protected int getPickerItemLayoutId() {
145         return R.layout.picker_item;
146     }
147 
148     /**
149      * Classes extending {@link Picker} can choose to override this method to
150      * supply the {@link Picker}'s item's {@link TextView}'s id from within the
151      * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
152      * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
153      * TextView}.
154      */
getPickerItemTextViewId()155     protected int getPickerItemTextViewId() {
156         return 0;
157     }
158 
159     /**
160      * Classes extending {@link Picker} can choose to override this method to
161      * supply the {@link Picker}'s column's height in pixels.
162      */
getPickerColumnHeightPixels()163     protected int getPickerColumnHeightPixels() {
164         return getActivity().getResources().getDimensionPixelSize(R.dimen.picker_column_height);
165     }
166 
167     @Override
onCreate(Bundle savedInstanceState)168     public void onCreate(Bundle savedInstanceState) {
169         super.onCreate(savedInstanceState);
170         mContext = getActivity();
171         mConstant = PickerConstant.getInstance(mContext.getResources());
172 
173         mFocusedAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha);
174         mUnfocusedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha);
175         mVisibleColumnAlpha = getFloat(R.dimen.picker_item_visible_column_item_alpha);
176         mInvisibleColumnAlpha = getFloat(R.dimen.picker_item_invisible_column_item_alpha);
177 
178         mColumnChangeListener = new ChangeTextColorOnFocus();
179         mAlphaAnimDuration = mContext.getResources().getInteger(
180                 R.integer.dialog_animation_duration);
181 
182         mDecelerateInterpolator = new DecelerateInterpolator(2.5F);
183         mAccelerateInterpolator = new AccelerateInterpolator(2.5F);
184     }
185 
186     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)187     public View onCreateView(LayoutInflater inflater, ViewGroup container,
188             Bundle savedInstanceState) {
189 
190         mColumns = getColumns();
191         if (mColumns == null || mColumns.size() == 0) {
192             return null;
193         }
194 
195         mRootView = (ViewGroup) inflater.inflate(getRootLayoutId(), null);
196         mPickerView = (ViewGroup) mRootView.findViewById(getPickerId());
197         mColumnViews = new ArrayList<ScrollAdapterView>();
198         mResult = new ArrayList<String>();
199 
200         int totalCol = mColumns.size();
201         for (int i = 0; i < totalCol; i++) {
202             final int colIndex = i;
203             final String[] col = mColumns.get(i).getItems();
204             mResult.add(col[0]);
205             final ScrollAdapterView columnView = (ScrollAdapterView) inflater.inflate(
206                     R.layout.picker_column, mPickerView, false);
207             LayoutParams lp = columnView.getLayoutParams();
208             lp.height = getPickerColumnHeightPixels();
209             columnView.setLayoutParams(lp);
210             mColumnViews.add(columnView);
211             columnView.setTag(Integer.valueOf(colIndex));
212 
213             // add view to root
214             mPickerView.addView(columnView);
215 
216             // add a separator if not the last element
217             if (i != totalCol - 1 && getSeparator() != null) {
218                 TextView separator = (TextView) inflater.inflate(
219                         getPickerSeparatorLayoutId(), mPickerView, false);
220                 separator.setText(getSeparator());
221                 mPickerView.addView(separator);
222             }
223         }
224         initAdapters();
225         mColumnViews.get(0).requestFocus();
226 
227         mClicked = false;
228         mKeyDown = false;
229 
230         return mRootView;
231     }
232 
initAdapters()233     private void initAdapters() {
234         final int totalCol = mColumns.size();
235         for (int i = 0; i < totalCol; i++) {
236             final int colIndex = i;
237             ScrollAdapterView columnView = mColumnViews.get(i);
238             final String[] col = mColumns.get(i).getItems();
239             setAdapter(columnView, col, colIndex);
240             columnView.setOnFocusChangeListener(mColumnChangeListener);
241             columnView.setOnItemSelectedListener(mColumnChangeListener);
242             columnView.setOnItemClickListener(mOnClickListener);
243 
244             columnView.setOnKeyListener(new View.OnKeyListener() {
245                 @Override
246                 public boolean onKey(View v, int keyCode, KeyEvent event) {
247                     switch (keyCode) {
248                         case KeyEvent.KEYCODE_DPAD_CENTER:
249                         case KeyEvent.KEYCODE_ENTER:
250                             if (event.getAction() == KeyEvent.ACTION_DOWN) {
251                                 // We are only interested in the Key DOWN event here,
252                                 // because the Key UP event will generate a click, and
253                                 // will be handled by OnItemClickListener.
254                                 if (!mKeyDown) {
255                                     mKeyDown = true;
256                                     updateAllColumnsForClick(false);
257                                 }
258                             }
259                             break;
260                     }
261                     return false;
262                 }
263             });
264         }
265     }
266 
unregisterListeners()267     private void unregisterListeners() {
268         final int totalCol = mColumns.size();
269         for (int i = 0; i < totalCol; i++) {
270             ScrollAdapterView columnView = mColumnViews.get(i);
271             columnView.setOnFocusChangeListener(null);
272             columnView.setOnItemSelectedListener(null);
273             columnView.setOnItemClickListener(null);
274             columnView.setOnKeyListener(null);
275         }
276     }
277 
setAdapter(ScrollAdapterView columnView, final String[] col, final int colIndex)278     private void setAdapter(ScrollAdapterView columnView, final String[] col, final int colIndex) {
279         List<String> arrayList = new ArrayList<String>(Arrays.asList(col));
280         PickerScrollArrayAdapter pickerScrollArrayAdapter = (getPickerItemTextViewId() == 0) ?
281                 new PickerScrollArrayAdapter(mContext, getPickerItemLayoutId(), arrayList, colIndex)
282                 : new PickerScrollArrayAdapter(mContext, getPickerItemLayoutId(),
283                         getPickerItemTextViewId(), arrayList, colIndex);
284         columnView.setAdapter(pickerScrollArrayAdapter);
285     }
286 
updateAdapter(final int index, PickerColumn pickerColumn)287     protected void updateAdapter(final int index, PickerColumn pickerColumn) {
288         ScrollAdapterView columnView = mColumnViews.get(index);
289         final String[] col = pickerColumn.getItems();
290 
291         ScrollArrayAdapter<String> adapter = (ScrollArrayAdapter<String>)(columnView.getAdapter());
292         if (adapter != null) {
293             adapter.setNotifyOnChange(false);
294             adapter.clear();
295             adapter.addAll(col);
296             adapter.notifyDataSetChanged();
297         }
298 
299         updateColumn(columnView, false, null);
300         mColumns.set(index, pickerColumn);
301     }
302 
updateSelection(int columnIndex, int selectedIndex)303     protected void updateSelection(int columnIndex, int selectedIndex) {
304         ScrollAdapterView columnView = mColumnViews.get(columnIndex);
305         if (columnView != null) {
306             columnView.setSelection(selectedIndex);
307             String text = mColumns.get(columnIndex).getItems()[selectedIndex];
308             mResult.set(columnIndex, text);
309         }
310     }
311 
setResultListener(ResultListener listener)312     public void setResultListener(ResultListener listener) {
313         mResultListener = listener;
314     }
315 
updateAllColumnsForClick(boolean keyUp)316     private void updateAllColumnsForClick(boolean keyUp) {
317         ArrayList<Animator> animList = null;
318         animList = new ArrayList<Animator>();
319         View item;
320 
321         for (int j = 0; j < mColumnViews.size(); j++) {
322             ScrollAdapterView column = mColumnViews.get(j);
323             int selected = column.getSelectedItemPosition();
324             for (int i = 0; i < column.getAdapter().getCount(); i++) {
325                 item = column.getItemView(i);
326                 if (item != null) {
327                     if (selected == i) {
328                         // set alpha for main item (selected) in the column
329                         if (keyUp) {
330                             setOrAnimateAlpha(item, true, mFocusedAlpha, mUnfocusedAlpha, animList,
331                                     mAccelerateInterpolator);
332                         } else {
333                             setOrAnimateAlpha(item, true, mUnfocusedAlpha, -1, animList,
334                                     mDecelerateInterpolator);
335                         }
336                     } else if (!keyUp) {
337                         // hide all non selected items on key down
338                         setOrAnimateAlpha(item, true, mInvisibleColumnAlpha, -1, animList,
339                                 mDecelerateInterpolator);
340                     }
341                 }
342             }
343         }
344 
345         if (animList != null && animList.size() > 0) {
346             AnimatorSet animSet = new AnimatorSet();
347             animSet.playTogether(animList);
348 
349             if (mClicked) {
350                 animSet.addListener(new AnimatorListenerAdapter() {
351                     @Override
352                     public void onAnimationEnd(Animator animation) {
353                         if (mResultListener != null) {
354                             mResultListener.onCommitResult(mResult);
355                         }
356                     }
357                 });
358             }
359             animSet.start();
360         }
361     }
362 
updateColumn(ScrollAdapterView column, boolean animateAlpha, ArrayList<Animator> animList)363     private void updateColumn(ScrollAdapterView column, boolean animateAlpha,
364             ArrayList<Animator> animList) {
365         if (column == null) {
366             return;
367         }
368 
369         int selected = column.getSelectedItemPosition();
370         View item;
371         boolean focused = column.hasFocus();
372 
373         ArrayList<Animator> localAnimList = animList;
374         if (animateAlpha && localAnimList == null) {
375             // no global animation list, create a local one for the current set
376             localAnimList = new ArrayList<Animator>();
377         }
378 
379         for (int i = 0; i < column.getAdapter().getCount(); i++) {
380             item = column.getItemView(i);
381             if (item != null) {
382                 setOrAnimateAlpha(item, (selected == i), focused, animateAlpha, localAnimList);
383             }
384         }
385         if (animateAlpha && animList == null && localAnimList != null && localAnimList.size() > 0) {
386             // No global animation list, so play these start the current set of animations now
387             AnimatorSet animSet = new AnimatorSet();
388             animSet.playTogether(localAnimList);
389             animSet.start();
390         }
391     }
392 
setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate, ArrayList<Animator> animList)393     private void setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate,
394             ArrayList<Animator> animList) {
395         if (selected) {
396             // set alpha for main item (selected) in the column
397             if ((focused && !mKeyDown) || mClicked) {
398                 setOrAnimateAlpha(view, animate, mFocusedAlpha, -1, animList,
399                         mDecelerateInterpolator);
400             } else {
401                 setOrAnimateAlpha(view, animate, mUnfocusedAlpha, -1, animList,
402                         mDecelerateInterpolator);
403             }
404         } else {
405             // set alpha for remaining items in the column
406             if (focused && !mClicked && !mKeyDown) {
407                 setOrAnimateAlpha(view, animate, mVisibleColumnAlpha, -1, animList,
408                         mDecelerateInterpolator);
409             } else {
410                 setOrAnimateAlpha(view, animate, mInvisibleColumnAlpha, -1, animList,
411                         mDecelerateInterpolator);
412             }
413         }
414     }
415 
setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha, ArrayList<Animator> animList, Interpolator interpolator)416     private void setOrAnimateAlpha(View view, boolean animate, float destAlpha, float startAlpha,
417             ArrayList<Animator> animList, Interpolator interpolator) {
418         view.clearAnimation();
419         if (!animate) {
420             view.setAlpha(destAlpha);
421         } else {
422             ObjectAnimator anim;
423             if (startAlpha >= 0.0f) {
424                 // set a start alpha
425                 anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, destAlpha);
426             } else {
427                 // no start alpha
428                 anim = ObjectAnimator.ofFloat(view, "alpha", destAlpha);
429             }
430             anim.setDuration(mAlphaAnimDuration);
431             anim.setInterpolator(interpolator);
432             if (animList != null) {
433                 animList.add(anim);
434             } else {
435                 anim.start();
436             }
437         }
438     }
439 
440     /**
441      * Classes extending {@link Picker} can override this function to supply the
442      * behavior when a list has been scrolled
443      */
onScroll(View v)444     protected void onScroll(View v) {
445     }
446 
447     @Override
onDestroyView()448     public void onDestroyView() {
449         unregisterListeners();
450         if (mColumnChangeListener != null) {
451             mColumnChangeListener.setDisabled();
452         }
453         super.onDestroyView();
454     }
455 
getFloat(int resourceId)456     private float getFloat(int resourceId) {
457         TypedValue buffer = new TypedValue();
458         mContext.getResources().getValue(resourceId, buffer, true);
459         return buffer.getFloat();
460     }
461 
462     private class PickerScrollArrayAdapter extends ScrollArrayAdapter<String> {
463 
464         private final int mColIndex;
465         private final int mTextViewResourceId;
466 
PickerScrollArrayAdapter(Context context, int resource, List<String> objects, int colIndex)467         PickerScrollArrayAdapter(Context context, int resource,
468                 List<String> objects, int colIndex) {
469             super(context, resource, objects);
470             mColIndex = colIndex;
471             mTextViewResourceId = 0;
472         }
473 
PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId, List<String> objects, int colIndex)474         PickerScrollArrayAdapter(Context context, int resource, int textViewResourceId,
475                 List<String> objects, int colIndex) {
476             super(context, resource, textViewResourceId, objects);
477             mColIndex = colIndex;
478             mTextViewResourceId = textViewResourceId;
479         }
480 
481         @Override
getView(int position, View convertView, ViewGroup parent)482         public View getView(int position, View convertView, ViewGroup parent) {
483             View view = super.getView(position, convertView, parent);
484             view.setTag(Integer.valueOf(mColIndex));
485             setOrAnimateAlpha(view,
486                     (mColumnViews.get(mColIndex).getSelectedItemPosition() == position), false,
487                     false, null);
488             return view;
489         }
490 
getTextViewFromAdapterView(View adapterView)491         TextView getTextViewFromAdapterView(View adapterView) {
492             if (mTextViewResourceId != 0) {
493                 return (TextView) adapterView.findViewById(mTextViewResourceId);
494             } else {
495                 return (TextView) adapterView;
496             }
497         }
498     }
499 
500 
501     private class ChangeTextColorOnFocus implements View.OnFocusChangeListener,
502             AdapterView.OnItemSelectedListener {
503         private boolean mDisabled;
504 
ChangeTextColorOnFocus()505         ChangeTextColorOnFocus() {
506             mDisabled = false;
507         }
508 
setDisabled()509         public void setDisabled() {
510             mDisabled = true;
511         }
512 
513         @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)514         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
515             if (mDisabled) {
516                 // If the listener has been disabled (because the view is being destroyed)
517                 // then just ignore this call.
518                 return;
519             }
520 
521             PickerScrollArrayAdapter pickerScrollArrayAdapter = (PickerScrollArrayAdapter) parent
522                     .getAdapter();
523 
524             TextView textView = pickerScrollArrayAdapter.getTextViewFromAdapterView(view);
525 
526             int colIndex = (Integer) parent.getTag();
527 
528             updateColumn((ScrollAdapterView) parent, parent.hasFocus(), null);
529 
530             mResult.set(colIndex, textView.getText().toString());
531             onScroll(textView);
532         }
533 
534         @Override
onNothingSelected(AdapterView<?> parent)535         public void onNothingSelected(AdapterView<?> parent) {
536             // N/A
537         }
538 
539         @Override
onFocusChange(View view, boolean hasFocus)540         public void onFocusChange(View view, boolean hasFocus) {
541             if (mDisabled) {
542                 // If the listener has been disabled (because the view is being destroyed)
543                 // then just ignore this call.
544                 return;
545             }
546 
547             if (view instanceof ScrollAdapterView) {
548                 updateColumn((ScrollAdapterView) view, true, null);
549             }
550         }
551     }
552 }
553