KeypadChannelSwitchView.java revision 4a5144ac8c51c4d89d1359e13e37fcd7f928ed9a
1/*
2 * Copyright (C) 2015 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.ui;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.support.annotation.Nullable;
25import android.util.AttributeSet;
26import android.util.Log;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.animation.AnimationUtils;
32import android.view.animation.Interpolator;
33import android.widget.AdapterView;
34import android.widget.BaseAdapter;
35import android.widget.LinearLayout;
36import android.widget.ListView;
37import android.widget.TextView;
38import com.android.tv.MainActivity;
39import com.android.tv.R;
40import com.android.tv.TvSingletons;
41import com.android.tv.analytics.Tracker;
42import com.android.tv.common.SoftPreconditions;
43import com.android.tv.common.util.DurationTimer;
44import com.android.tv.data.Channel;
45import com.android.tv.data.ChannelNumber;
46import java.util.ArrayList;
47import java.util.List;
48
49public class KeypadChannelSwitchView extends LinearLayout
50        implements TvTransitionManager.TransitionLayout {
51    private static final String TAG = "KeypadChannelSwitchView";
52
53    private static final int MAX_CHANNEL_NUMBER_DIGIT = 4;
54    private static final int MAX_MINOR_CHANNEL_NUMBER_DIGIT = 3;
55    private static final int MAX_CHANNEL_ITEM = 8;
56    private static final String CHANNEL_DELIMITERS_REGEX = "[-\\.\\s]";
57    public static final String SCREEN_NAME = "Channel switch";
58
59    private final MainActivity mMainActivity;
60    private final Tracker mTracker;
61    private final DurationTimer mViewDurationTimer = new DurationTimer();
62    private boolean mNavigated = false;
63    @Nullable // Once mChannels is set to null it should not be used again.
64    private List<Channel> mChannels;
65    private TextView mChannelNumberView;
66    private ListView mChannelItemListView;
67    private final ChannelNumber mTypedChannelNumber = new ChannelNumber();
68    private final ArrayList<Channel> mChannelCandidates = new ArrayList<>();
69    protected final ChannelItemAdapter mAdapter = new ChannelItemAdapter();
70    private final LayoutInflater mLayoutInflater;
71    private Channel mSelectedChannel;
72
73    private final Runnable mHideRunnable =
74            new Runnable() {
75                @Override
76                public void run() {
77                    mCurrentHeight = 0;
78                    if (mSelectedChannel != null) {
79                        mMainActivity.tuneToChannel(mSelectedChannel);
80                        mTracker.sendChannelNumberItemChosenByTimeout();
81                    } else {
82                        mMainActivity
83                                .getOverlayManager()
84                                .hideOverlays(
85                                        TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG
86                                                | TvOverlayManager
87                                                        .FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS
88                                                | TvOverlayManager
89                                                        .FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE
90                                                | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU
91                                                | TvOverlayManager
92                                                        .FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT);
93                    }
94                }
95            };
96    private final long mShowDurationMillis;
97    private final long mRippleAnimDurationMillis;
98    private final int mBaseViewHeight;
99    private final int mItemHeight;
100    private final int mResizeAnimDuration;
101    private Animator mResizeAnimator;
102    private final Interpolator mResizeInterpolator;
103    // NOTE: getHeight() will be updated after layout() is called. mCurrentHeight is needed for
104    // getting the latest updated value of the view height before layout().
105    private int mCurrentHeight;
106
107    public KeypadChannelSwitchView(Context context) {
108        this(context, null, 0);
109    }
110
111    public KeypadChannelSwitchView(Context context, AttributeSet attrs) {
112        this(context, attrs, 0);
113    }
114
115    public KeypadChannelSwitchView(Context context, AttributeSet attrs, int defStyleAttr) {
116        super(context, attrs, defStyleAttr);
117
118        mMainActivity = (MainActivity) context;
119        mTracker = TvSingletons.getSingletons(context).getTracker();
120        Resources resources = getResources();
121        mLayoutInflater = LayoutInflater.from(context);
122        mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration);
123        mRippleAnimDurationMillis =
124                resources.getInteger(R.integer.keypad_channel_switch_ripple_anim_duration);
125        mBaseViewHeight =
126                resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_base_height);
127        mItemHeight = resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_item_height);
128        mResizeAnimDuration = resources.getInteger(R.integer.keypad_channel_switch_anim_duration);
129        mResizeInterpolator =
130                AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
131    }
132
133    @Override
134    protected void onFinishInflate() {
135        super.onFinishInflate();
136        mChannelNumberView = (TextView) findViewById(R.id.channel_number);
137        mChannelItemListView = (ListView) findViewById(R.id.channel_list);
138        mChannelItemListView.setAdapter(mAdapter);
139        mChannelItemListView.setOnItemClickListener(
140                new AdapterView.OnItemClickListener() {
141                    @Override
142                    public void onItemClick(
143                            AdapterView<?> parent, View view, int position, long id) {
144                        if (position >= mAdapter.getCount()) {
145                            // It can happen during closing.
146                            return;
147                        }
148                        mChannelItemListView.setFocusable(false);
149                        final Channel channel = ((Channel) mAdapter.getItem(position));
150                        postDelayed(
151                                new Runnable() {
152                                    @Override
153                                    public void run() {
154                                        mChannelItemListView.setFocusable(true);
155                                        mMainActivity.tuneToChannel(channel);
156                                        mTracker.sendChannelNumberItemClicked();
157                                    }
158                                },
159                                mRippleAnimDurationMillis);
160                    }
161                });
162        mChannelItemListView.setOnItemSelectedListener(
163                new AdapterView.OnItemSelectedListener() {
164                    @Override
165                    public void onItemSelected(
166                            AdapterView<?> parent, View view, int position, long id) {
167                        if (position >= mAdapter.getCount()) {
168                            // It can happen during closing.
169                            mSelectedChannel = null;
170                        } else {
171                            mSelectedChannel = (Channel) mAdapter.getItem(position);
172                        }
173                        if (position != 0 && !mNavigated) {
174                            mNavigated = true;
175                            mTracker.sendChannelInputNavigated();
176                        }
177                    }
178
179                    @Override
180                    public void onNothingSelected(AdapterView<?> parent) {
181                        mSelectedChannel = null;
182                    }
183                });
184    }
185
186    @Override
187    public boolean dispatchKeyEvent(KeyEvent event) {
188        scheduleHide();
189        return super.dispatchKeyEvent(event);
190    }
191
192    @Override
193    public boolean onKeyUp(int keyCode, KeyEvent event) {
194        SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels");
195        if (isChannelNumberKey(keyCode)) {
196            onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0);
197            return true;
198        }
199        if (ChannelNumber.isChannelNumberDelimiterKey(keyCode)) {
200            onDelimiterKeyUp();
201            return true;
202        }
203        return super.onKeyUp(keyCode, event);
204    }
205
206    @Override
207    public void onEnterAction(boolean fromEmptyScene) {
208        reset();
209        if (fromEmptyScene) {
210            ViewUtils.setTransitionAlpha(mChannelItemListView, 1f);
211        }
212        mNavigated = false;
213        mViewDurationTimer.start();
214        mTracker.sendShowChannelSwitch();
215        mTracker.sendScreenView(SCREEN_NAME);
216        updateView();
217        scheduleHide();
218    }
219
220    @Override
221    public void onExitAction() {
222        mCurrentHeight = 0;
223        mTracker.sendHideChannelSwitch(mViewDurationTimer.reset());
224        cancelHide();
225    }
226
227    private void scheduleHide() {
228        cancelHide();
229        postDelayed(mHideRunnable, mShowDurationMillis);
230    }
231
232    private void cancelHide() {
233        removeCallbacks(mHideRunnable);
234    }
235
236    private void reset() {
237        mTypedChannelNumber.reset();
238        mSelectedChannel = null;
239        mChannelCandidates.clear();
240        mAdapter.notifyDataSetChanged();
241    }
242
243    public void setChannels(@Nullable List<Channel> channels) {
244        mChannels = channels;
245    }
246
247    public static boolean isChannelNumberKey(int keyCode) {
248        return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9;
249    }
250
251    public void onNumberKeyUp(int num) {
252        // Reset typed channel number in some cases.
253        if (mTypedChannelNumber.majorNumber == null) {
254            mTypedChannelNumber.reset();
255        } else if (!mTypedChannelNumber.hasDelimiter
256                && mTypedChannelNumber.majorNumber.length() >= MAX_CHANNEL_NUMBER_DIGIT) {
257            mTypedChannelNumber.reset();
258        } else if (mTypedChannelNumber.hasDelimiter
259                && mTypedChannelNumber.minorNumber != null
260                && mTypedChannelNumber.minorNumber.length() >= MAX_MINOR_CHANNEL_NUMBER_DIGIT) {
261            mTypedChannelNumber.reset();
262        }
263
264        if (!mTypedChannelNumber.hasDelimiter) {
265            mTypedChannelNumber.majorNumber += String.valueOf(num);
266        } else {
267            mTypedChannelNumber.minorNumber += String.valueOf(num);
268        }
269        mTracker.sendChannelNumberInput();
270        updateView();
271    }
272
273    private void onDelimiterKeyUp() {
274        if (mTypedChannelNumber.hasDelimiter || mTypedChannelNumber.majorNumber.length() == 0) {
275            return;
276        }
277        mTypedChannelNumber.hasDelimiter = true;
278        mTracker.sendChannelNumberInput();
279        updateView();
280    }
281
282    private void updateView() {
283        mChannelNumberView.setText(mTypedChannelNumber.toString() + "_");
284        mChannelCandidates.clear();
285        ArrayList<Channel> secondaryChannelCandidates = new ArrayList<>();
286        for (Channel channel : mChannels) {
287            ChannelNumber chNumber = ChannelNumber.parseChannelNumber(channel.getDisplayNumber());
288            if (chNumber == null) {
289                Log.i(
290                        TAG,
291                        "Malformed channel number (name="
292                                + channel.getDisplayName()
293                                + ", number="
294                                + channel.getDisplayNumber()
295                                + ")");
296                continue;
297            }
298            if (matchChannelNumber(mTypedChannelNumber, chNumber)) {
299                mChannelCandidates.add(channel);
300            } else if (!mTypedChannelNumber.hasDelimiter) {
301                // Even if a user doesn't type '-', we need to match the typed number to not only
302                // the major number but also the minor number. For example, when a user types '111'
303                // without delimiter, it should be matched to '111', '1-11' and '11-1'.
304                if (channel.getDisplayNumber()
305                        .replaceAll(CHANNEL_DELIMITERS_REGEX, "")
306                        .startsWith(mTypedChannelNumber.majorNumber)) {
307                    secondaryChannelCandidates.add(channel);
308                }
309            }
310        }
311        mChannelCandidates.addAll(secondaryChannelCandidates);
312        mAdapter.notifyDataSetChanged();
313        if (mAdapter.getCount() > 0) {
314            mChannelItemListView.requestFocus();
315            mChannelItemListView.setSelection(0);
316            mSelectedChannel = mChannelCandidates.get(0);
317        }
318
319        updateViewHeight();
320    }
321
322    private void updateViewHeight() {
323        int itemListHeight = mItemHeight * Math.min(MAX_CHANNEL_ITEM, mAdapter.getCount());
324        int targetHeight = mBaseViewHeight + itemListHeight;
325        if (mResizeAnimator != null) {
326            mResizeAnimator.cancel();
327            mResizeAnimator = null;
328        }
329
330        if (mCurrentHeight == 0) {
331            // Do not add the resize animation when the banner has not been shown before.
332            mCurrentHeight = targetHeight;
333            setViewHeight(this, targetHeight);
334        } else if (mCurrentHeight != targetHeight) {
335            mResizeAnimator = createResizeAnimator(targetHeight);
336            mResizeAnimator.start();
337        }
338    }
339
340    private Animator createResizeAnimator(int targetHeight) {
341        ValueAnimator animator = ValueAnimator.ofInt(mCurrentHeight, targetHeight);
342        animator.addUpdateListener(
343                new ValueAnimator.AnimatorUpdateListener() {
344                    @Override
345                    public void onAnimationUpdate(ValueAnimator animation) {
346                        int value = (Integer) animation.getAnimatedValue();
347                        setViewHeight(KeypadChannelSwitchView.this, value);
348                        mCurrentHeight = value;
349                    }
350                });
351        animator.setDuration(mResizeAnimDuration);
352        animator.addListener(
353                new AnimatorListenerAdapter() {
354                    @Override
355                    public void onAnimationEnd(Animator animator) {
356                        mResizeAnimator = null;
357                    }
358                });
359        animator.setInterpolator(mResizeInterpolator);
360        return animator;
361    }
362
363    private void setViewHeight(View view, int height) {
364        ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
365        if (height != layoutParams.height) {
366            layoutParams.height = height;
367            view.setLayoutParams(layoutParams);
368        }
369    }
370
371    private static boolean matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber) {
372        if (!chNumber.majorNumber.equals(typedChNumber.majorNumber)) {
373            return false;
374        }
375        if (typedChNumber.hasDelimiter) {
376            if (!chNumber.hasDelimiter) {
377                return false;
378            }
379            if (!chNumber.minorNumber.startsWith(typedChNumber.minorNumber)) {
380                return false;
381            }
382        }
383        return true;
384    }
385
386    class ChannelItemAdapter extends BaseAdapter {
387        @Override
388        public int getCount() {
389            return mChannelCandidates.size();
390        }
391
392        @Override
393        public Object getItem(int position) {
394            return mChannelCandidates.get(position);
395        }
396
397        @Override
398        public long getItemId(int position) {
399            return position;
400        }
401
402        @Override
403        public View getView(int position, View convertView, ViewGroup parent) {
404            final Channel channel = mChannelCandidates.get(position);
405            View v = convertView;
406            if (v == null) {
407                v = mLayoutInflater.inflate(R.layout.keypad_channel_switch_item, parent, false);
408            }
409
410            TextView channelNumberView = (TextView) v.findViewById(R.id.number);
411            channelNumberView.setText(channel.getDisplayNumber());
412
413            TextView channelNameView = (TextView) v.findViewById(R.id.name);
414            channelNameView.setText(channel.getDisplayName());
415            return v;
416        }
417    }
418}
419