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
17package com.android.tv.settings.widget.picker;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.annotation.DimenRes;
24import android.app.Fragment;
25import android.content.Context;
26import android.os.Bundle;
27import android.support.v17.leanback.widget.OnChildSelectedListener;
28import android.support.v17.leanback.widget.VerticalGridView;
29import android.support.v7.widget.RecyclerView;
30import android.util.AttributeSet;
31import android.util.TypedValue;
32import android.view.KeyEvent;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.ViewGroup.LayoutParams;
37import android.view.animation.AccelerateInterpolator;
38import android.view.animation.DecelerateInterpolator;
39import android.view.animation.Interpolator;
40import android.widget.LinearLayout;
41import android.widget.TextView;
42
43import com.android.tv.settings.R;
44
45import java.util.ArrayList;
46import java.util.Arrays;
47import java.util.List;
48
49/**
50 * Picker class
51 */
52public abstract class Picker extends Fragment {
53
54    /**
55     * Object listening for adapter events.
56     */
57    public interface ResultListener {
58        void onCommitResult(List<String> result);
59    }
60
61    private Context mContext;
62    private List<VerticalGridView> mColumnViews;
63    private ResultListener mResultListener;
64    private ArrayList<PickerColumn> mColumns = new ArrayList<>();
65
66    private float mUnfocusedAlpha;
67    private float mFocusedAlpha;
68    private float mVisibleColumnAlpha;
69    private float mInvisibleColumnAlpha;
70    private int mAlphaAnimDuration;
71    private Interpolator mDecelerateInterpolator;
72    private Interpolator mAccelerateInterpolator;
73    private boolean mKeyDown = false;
74    private boolean mClicked = false;
75
76    /**
77     * selection result
78     */
79    private List<String> mResult;
80
81    /**
82     * Classes extending {@link Picker} should override this method to supply
83     * the columns
84     */
85    protected abstract ArrayList<PickerColumn> getColumns();
86
87    /**
88     * Classes extending {@link Picker} can choose to override this method to
89     * supply the separator string
90     */
91    protected abstract String getSeparator();
92
93    @Override
94    public void onCreate(Bundle savedInstanceState) {
95        super.onCreate(savedInstanceState);
96        mContext = getActivity();
97
98        mFocusedAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha);
99        mUnfocusedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha);
100        mVisibleColumnAlpha = getFloat(R.dimen.picker_item_visible_column_item_alpha);
101        mInvisibleColumnAlpha = getFloat(R.dimen.picker_item_invisible_column_item_alpha);
102
103        mAlphaAnimDuration = mContext.getResources().getInteger(
104                R.integer.dialog_animation_duration);
105
106        mDecelerateInterpolator = new DecelerateInterpolator(2.5F);
107        mAccelerateInterpolator = new AccelerateInterpolator(2.5F);
108    }
109
110    @Override
111    public View onCreateView(LayoutInflater inflater, ViewGroup container,
112            Bundle savedInstanceState) {
113
114        mColumns = getColumns();
115        if (mColumns == null || mColumns.size() == 0) {
116            return null;
117        }
118
119        final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.picker, container, false);
120        final PickerLayout pickerView = (PickerLayout) rootView.findViewById(R.id.picker);
121        pickerView.setChildFocusListener(this);
122        mColumnViews = new ArrayList<>();
123        mResult = new ArrayList<>();
124
125        int totalCol = mColumns.size();
126        for (int i = 0; i < totalCol; i++) {
127            final String[] col = mColumns.get(i).getItems();
128            mResult.add(col[0]);
129            final VerticalGridView columnView = (VerticalGridView) inflater.inflate(
130                    R.layout.picker_column, pickerView, false);
131            columnView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
132            mColumnViews.add(columnView);
133            columnView.setTag(i);
134
135            // add view to root
136            pickerView.addView(columnView);
137
138            // add a separator if not the last element
139            if (i != totalCol - 1 && getSeparator() != null) {
140                final TextView separator =
141                        (TextView) inflater.inflate(R.layout.picker_separator, pickerView, false);
142                separator.setText(getSeparator());
143                pickerView.addView(separator);
144            }
145        }
146        initAdapters();
147        mColumnViews.get(0).requestFocus();
148
149        mClicked = false;
150        mKeyDown = false;
151
152        return rootView;
153    }
154
155    private void initAdapters() {
156        final int totalCol = mColumns.size();
157        for (int i = 0; i < totalCol; i++) {
158            VerticalGridView gridView = mColumnViews.get(i);
159            gridView.setAdapter(new Adapter(i, Arrays.asList(mColumns.get(i).getItems())));
160            gridView.setOnKeyInterceptListener(new VerticalGridView.OnKeyInterceptListener() {
161                @Override
162                public boolean onInterceptKeyEvent(KeyEvent event) {
163                    switch (event.getKeyCode()) {
164                        case KeyEvent.KEYCODE_DPAD_CENTER:
165                        case KeyEvent.KEYCODE_ENTER:
166                            if (event.getAction() == KeyEvent.ACTION_DOWN) {
167                                // We are only interested in the Key DOWN event here,
168                                // because the Key UP event will generate a click, and
169                                // will be handled by OnItemClickListener.
170                                if (!mKeyDown) {
171                                    mKeyDown = true;
172                                    updateAllColumnsForClick(false);
173                                }
174                            }
175                            break;
176                    }
177                    return false;
178                }
179            });
180        }
181    }
182
183    protected void updateAdapter(int index, PickerColumn pickerColumn) {
184        final VerticalGridView gridView = mColumnViews.get(index);
185        final Adapter adapter = (Adapter) gridView.getAdapter();
186
187        mColumns.set(index, pickerColumn);
188        adapter.setItems(Arrays.asList(pickerColumn.getItems()));
189
190        gridView.post(new Runnable() {
191            @Override
192            public void run() {
193                updateColumn(gridView, false, null);
194            }
195        });
196    }
197
198    protected void updateSelection(int columnIndex, int selectedIndex) {
199        VerticalGridView columnView = mColumnViews.get(columnIndex);
200        if (columnView != null) {
201            columnView.setSelectedPosition(selectedIndex);
202            String text = mColumns.get(columnIndex).getItems()[selectedIndex];
203            mResult.set(columnIndex, text);
204        }
205    }
206
207    public void setResultListener(ResultListener listener) {
208        mResultListener = listener;
209    }
210
211    private void updateAllColumnsForClick(boolean keyUp) {
212        final ArrayList<Animator> animList = new ArrayList<>();
213
214        for (final VerticalGridView column : mColumnViews) {
215            final int selected = column.getSelectedPosition();
216
217            final RecyclerView.LayoutManager manager = column.getLayoutManager();
218            final int size = manager.getChildCount();
219
220            for (int i = 0; i < size; i++) {
221                final View item = manager.getChildAt(i);
222                if (item != null) {
223                    if (selected == i) {
224                        // set alpha for main item (selected) in the column
225                        if (keyUp) {
226                            setOrAnimateAlphaInternal(item, true, mFocusedAlpha, mUnfocusedAlpha,
227                                    animList, mAccelerateInterpolator);
228                        } else {
229                            setOrAnimateAlphaInternal(item, true, mUnfocusedAlpha, -1, animList,
230                                    mDecelerateInterpolator);
231                        }
232                    } else if (!keyUp) {
233                        // hide all non selected items on key down
234                        setOrAnimateAlphaInternal(item, true, mInvisibleColumnAlpha, -1, animList,
235                                mDecelerateInterpolator);
236                    }
237                }
238            }
239        }
240
241        if (!animList.isEmpty()) {
242            AnimatorSet animSet = new AnimatorSet();
243            animSet.playTogether(animList);
244
245            if (mClicked) {
246                animSet.addListener(new AnimatorListenerAdapter() {
247                    @Override
248                    public void onAnimationEnd(Animator animation) {
249                        if (mResultListener != null) {
250                            mResultListener.onCommitResult(mResult);
251                        }
252                    }
253                });
254            }
255            animSet.start();
256        }
257    }
258
259    public void childFocusChanged() {
260        final ArrayList<Animator> animList = new ArrayList<>();
261
262        for (final VerticalGridView column : mColumnViews) {
263            updateColumn(column, column.hasFocus(), animList);
264        }
265
266        if (!animList.isEmpty()) {
267            AnimatorSet animSet = new AnimatorSet();
268            animSet.playTogether(animList);
269            animSet.start();
270        }
271    }
272
273    private void updateColumn(VerticalGridView column, boolean animateAlpha,
274            ArrayList<Animator> animList) {
275        if (column == null) {
276            return;
277        }
278
279        final int selected = column.getSelectedPosition();
280        final boolean focused = column.hasFocus();
281
282        ArrayList<Animator> localAnimList = animList;
283        if (animateAlpha && localAnimList == null) {
284            // no global animation list, create a local one for the current set
285            localAnimList = new ArrayList<>();
286        }
287
288        // Iterate through the visible views
289        final RecyclerView.LayoutManager manager = column.getLayoutManager();
290        final int size = manager.getChildCount();
291
292        for (int i = 0; i < size; i++) {
293            final View item = manager.getChildAt(i);
294            if (item != null) {
295                setOrAnimateAlpha(item, (selected == column.getChildAdapterPosition(item)), focused,
296                        animateAlpha, localAnimList);
297            }
298        }
299        if (animateAlpha && animList == null && !localAnimList.isEmpty()) {
300            // No global animation list, so play these start the current set of animations now
301            AnimatorSet animSet = new AnimatorSet();
302            animSet.playTogether(localAnimList);
303            animSet.start();
304        }
305    }
306
307    private void setOrAnimateAlpha(View view, boolean selected, boolean focused, boolean animate,
308            ArrayList<Animator> animList) {
309        if (selected) {
310            // set alpha for main item (selected) in the column
311            if ((focused && !mKeyDown) || mClicked) {
312                setOrAnimateAlphaInternal(view, animate, mFocusedAlpha, -1, animList,
313                        mDecelerateInterpolator);
314            } else {
315                setOrAnimateAlphaInternal(view, animate, mUnfocusedAlpha, -1, animList,
316                        mDecelerateInterpolator);
317            }
318        } else {
319            // set alpha for remaining items in the column
320            if (focused && !mClicked && !mKeyDown) {
321                setOrAnimateAlphaInternal(view, animate, mVisibleColumnAlpha, -1, animList,
322                        mDecelerateInterpolator);
323            } else {
324                setOrAnimateAlphaInternal(view, animate, mInvisibleColumnAlpha, -1, animList,
325                        mDecelerateInterpolator);
326            }
327        }
328    }
329
330    private void setOrAnimateAlphaInternal(View view, boolean animate, float destAlpha,
331            float startAlpha, ArrayList<Animator> animList, Interpolator interpolator) {
332        view.clearAnimation();
333        if (!animate) {
334            view.setAlpha(destAlpha);
335        } else {
336            ObjectAnimator anim;
337            if (startAlpha >= 0.0f) {
338                // set a start alpha
339                anim = ObjectAnimator.ofFloat(view, "alpha", startAlpha, destAlpha);
340            } else {
341                // no start alpha
342                anim = ObjectAnimator.ofFloat(view, "alpha", destAlpha);
343            }
344            anim.setDuration(mAlphaAnimDuration);
345            anim.setInterpolator(interpolator);
346            if (animList != null) {
347                animList.add(anim);
348            } else {
349                anim.start();
350            }
351        }
352    }
353
354    /**
355     * Classes extending {@link Picker} can override this function to supply the
356     * behavior when a list has been scrolled
357     */
358    protected void onScroll(int column, View v, int position) {}
359
360    private float getFloat(@DimenRes int resourceId) {
361        TypedValue buffer = new TypedValue();
362        mContext.getResources().getValue(resourceId, buffer, true);
363        return buffer.getFloat();
364    }
365
366    private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
367        private final TextView mTextView;
368
369        public ViewHolder(View itemView) {
370            super(itemView);
371            mTextView = (TextView) itemView.findViewById(R.id.list_item);
372            itemView.setOnClickListener(this);
373        }
374
375        public TextView getTextView() {
376            return mTextView;
377        }
378
379        @Override
380        public void onClick(View v) {
381            if (mKeyDown) {
382                mKeyDown = false;
383                mClicked = true;
384                updateAllColumnsForClick(true);
385            }
386        }
387    }
388
389    private class Adapter extends RecyclerView.Adapter<ViewHolder>
390            implements OnChildSelectedListener {
391
392        private final int mColumnId;
393
394        private List<String> mItems;
395        private VerticalGridView mGridView;
396
397        public Adapter(int columnId, List<String> items) {
398            mColumnId = columnId;
399            mItems = items;
400            setHasStableIds(true);
401        }
402
403        @Override
404        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
405            final View view = getLayoutInflater(null).inflate(R.layout.picker_item, parent, false);
406            return new ViewHolder(view);
407        }
408
409        @Override
410        public void onBindViewHolder(ViewHolder holder, int position) {
411            final TextView textView = holder.getTextView();
412            textView.setText(mItems.get(position));
413            setOrAnimateAlpha(textView, mGridView.getSelectedPosition() == position,
414                    mGridView.hasFocus(), false, null);
415        }
416
417        @Override
418        public int getItemCount() {
419            return mItems.size();
420        }
421
422        @Override
423        public long getItemId(int position) {
424            return position;
425        }
426
427        @Override
428        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
429            mGridView = (VerticalGridView) recyclerView;
430            mGridView.setOnChildSelectedListener(this);
431        }
432
433        @Override
434        public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
435            mGridView = null;
436        }
437
438        @Override
439        public void onChildSelected(ViewGroup parent, View view, int position, long id) {
440            if (mGridView == null) {
441                return;
442            }
443            final ViewHolder vh = (ViewHolder) mGridView.getChildViewHolder(view);
444            final TextView textView = vh.getTextView();
445
446            updateColumn(mGridView, mGridView.hasFocus(), null);
447            mResult.set(mColumnId, textView.getText().toString());
448            onScroll(mColumnId, textView, position);
449        }
450
451        public void setItems(List<String> items) {
452            final List<String> oldItems = mItems;
453            mItems = items;
454            if (oldItems.size() < items.size()) {
455                notifyItemRangeInserted(oldItems.size(), oldItems.size() - items.size());
456            } else if (items.size() < oldItems.size()) {
457                notifyItemRangeRemoved(items.size(), items.size() - oldItems.size());
458            }
459        }
460    }
461
462    public static class PickerLayout extends LinearLayout {
463
464        private Picker mChildFocusListener;
465
466        public PickerLayout(Context context) {
467            super(context);
468        }
469
470        public PickerLayout(Context context, AttributeSet attrs) {
471            super(context, attrs);
472        }
473
474        public PickerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
475            super(context, attrs, defStyleAttr);
476        }
477
478        public PickerLayout(Context context, AttributeSet attrs, int defStyleAttr,
479                int defStyleRes) {
480            super(context, attrs, defStyleAttr, defStyleRes);
481        }
482
483        @Override
484        public void requestChildFocus(View child, View focused) {
485            super.requestChildFocus(child, focused);
486
487            mChildFocusListener.childFocusChanged();
488        }
489
490        public void setChildFocusListener(Picker childFocusListener) {
491            mChildFocusListener = childFocusListener;
492        }
493    }
494}
495