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