Home | History | Annotate | Download | only in ui
      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 
     17 package com.android.tv.ui;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.support.annotation.Nullable;
     25 import android.util.AttributeSet;
     26 import android.util.Log;
     27 import android.view.KeyEvent;
     28 import android.view.LayoutInflater;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.animation.AnimationUtils;
     32 import android.view.animation.Interpolator;
     33 import android.widget.AdapterView;
     34 import android.widget.BaseAdapter;
     35 import android.widget.LinearLayout;
     36 import android.widget.ListView;
     37 import android.widget.TextView;
     38 import com.android.tv.MainActivity;
     39 import com.android.tv.R;
     40 import com.android.tv.TvSingletons;
     41 import com.android.tv.analytics.Tracker;
     42 import com.android.tv.common.SoftPreconditions;
     43 import com.android.tv.common.util.DurationTimer;
     44 import com.android.tv.data.ChannelNumber;
     45 import com.android.tv.data.api.Channel;
     46 import java.util.ArrayList;
     47 import java.util.List;
     48 
     49 public 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