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