| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.ui; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.support.annotation.Nullable; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.KeyEvent; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| import android.widget.AdapterView; |
| import android.widget.BaseAdapter; |
| import android.widget.LinearLayout; |
| import android.widget.ListView; |
| import android.widget.TextView; |
| |
| import com.android.tv.MainActivity; |
| import com.android.tv.R; |
| import com.android.tv.TvApplication; |
| import com.android.tv.analytics.DurationTimer; |
| import com.android.tv.analytics.Tracker; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.data.Channel; |
| import com.android.tv.data.ChannelNumber; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class KeypadChannelSwitchView extends LinearLayout implements |
| TvTransitionManager.TransitionLayout { |
| private static final String TAG = "KeypadChannelSwitchView"; |
| |
| private static final int MAX_CHANNEL_NUMBER_DIGIT = 4; |
| private static final int MAX_MINOR_CHANNEL_NUMBER_DIGIT = 3; |
| private static final int MAX_CHANNEL_ITEM = 8; |
| private static final String CHANNEL_DELIMITERS_REGEX = "[-\\.\\s]"; |
| public static final String SCREEN_NAME = "Channel switch"; |
| |
| private final MainActivity mMainActivity; |
| private final Tracker mTracker; |
| private final DurationTimer mViewDurationTimer = new DurationTimer(); |
| private boolean mNavigated = false; |
| @Nullable //Once mChannels is set to null it should not be used again. |
| private List<Channel> mChannels; |
| private TextView mChannelNumberView; |
| private ListView mChannelItemListView; |
| private final ChannelNumber mTypedChannelNumber = new ChannelNumber(); |
| private final ArrayList<Channel> mChannelCandidates = new ArrayList<>(); |
| protected final ChannelItemAdapter mAdapter = new ChannelItemAdapter(); |
| private final LayoutInflater mLayoutInflater; |
| private Channel mSelectedChannel; |
| |
| private final Runnable mHideRunnable = new Runnable() { |
| @Override |
| public void run() { |
| mCurrentHeight = 0; |
| if (mSelectedChannel != null) { |
| mMainActivity.tuneToChannel(mSelectedChannel); |
| mTracker.sendChannelNumberItemChosenByTimeout(); |
| } else { |
| mMainActivity.getOverlayManager().hideOverlays( |
| TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG |
| | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS |
| | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE |
| | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU |
| | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); |
| } |
| } |
| }; |
| private final long mShowDurationMillis; |
| private final long mRippleAnimDurationMillis; |
| private final int mBaseViewHeight; |
| private final int mItemHeight; |
| private final int mResizeAnimDuration; |
| private Animator mResizeAnimator; |
| private final Interpolator mResizeInterpolator; |
| // NOTE: getHeight() will be updated after layout() is called. mCurrentHeight is needed for |
| // getting the latest updated value of the view height before layout(). |
| private int mCurrentHeight; |
| |
| public KeypadChannelSwitchView(Context context) { |
| this(context, null, 0); |
| } |
| |
| public KeypadChannelSwitchView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public KeypadChannelSwitchView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| mMainActivity = (MainActivity) context; |
| mTracker = TvApplication.getSingletons(context).getTracker(); |
| Resources resources = getResources(); |
| mLayoutInflater = LayoutInflater.from(context); |
| mShowDurationMillis = resources.getInteger(R.integer.keypad_channel_switch_show_duration); |
| mRippleAnimDurationMillis = resources.getInteger( |
| R.integer.keypad_channel_switch_ripple_anim_duration); |
| mBaseViewHeight = resources.getDimensionPixelSize( |
| R.dimen.keypad_channel_switch_base_height); |
| mItemHeight = resources.getDimensionPixelSize(R.dimen.keypad_channel_switch_item_height); |
| mResizeAnimDuration = resources.getInteger(R.integer.keypad_channel_switch_anim_duration); |
| mResizeInterpolator = AnimationUtils.loadInterpolator(context, |
| android.R.interpolator.linear_out_slow_in); |
| } |
| |
| @Override |
| protected void onFinishInflate(){ |
| super.onFinishInflate(); |
| mChannelNumberView = (TextView) findViewById(R.id.channel_number); |
| mChannelItemListView = (ListView) findViewById(R.id.channel_list); |
| mChannelItemListView.setAdapter(mAdapter); |
| mChannelItemListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| if (position >= mAdapter.getCount()) { |
| // It can happen during closing. |
| return; |
| } |
| mChannelItemListView.setFocusable(false); |
| final Channel channel = ((Channel) mAdapter.getItem(position)); |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| mChannelItemListView.setFocusable(true); |
| mMainActivity.tuneToChannel(channel); |
| mTracker.sendChannelNumberItemClicked(); |
| } |
| }, mRippleAnimDurationMillis); |
| } |
| }); |
| mChannelItemListView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { |
| @Override |
| public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { |
| if (position >= mAdapter.getCount()) { |
| // It can happen during closing. |
| mSelectedChannel = null; |
| } else { |
| mSelectedChannel = (Channel) mAdapter.getItem(position); |
| } |
| if (position != 0 && !mNavigated) { |
| mNavigated = true; |
| mTracker.sendChannelInputNavigated(); |
| } |
| } |
| |
| @Override |
| public void onNothingSelected(AdapterView<?> parent) { |
| mSelectedChannel = null; |
| } |
| }); |
| } |
| |
| @Override |
| public boolean dispatchKeyEvent(KeyEvent event) { |
| scheduleHide(); |
| return super.dispatchKeyEvent(event); |
| } |
| |
| @Override |
| public boolean onKeyUp(int keyCode, KeyEvent event) { |
| SoftPreconditions.checkNotNull(mChannels, TAG, "mChannels"); |
| if (isChannelNumberKey(keyCode)) { |
| onNumberKeyUp(keyCode - KeyEvent.KEYCODE_0); |
| return true; |
| } |
| if (ChannelNumber.isChannelNumberDelimiterKey(keyCode)) { |
| onDelimiterKeyUp(); |
| return true; |
| } |
| return super.onKeyUp(keyCode, event); |
| } |
| |
| @Override |
| public void onEnterAction(boolean fromEmptyScene) { |
| reset(); |
| if (fromEmptyScene) { |
| ViewUtils.setTransitionAlpha(mChannelItemListView, 1f); |
| } |
| mNavigated = false; |
| mViewDurationTimer.start(); |
| mTracker.sendShowChannelSwitch(); |
| mTracker.sendScreenView(SCREEN_NAME); |
| updateView(); |
| scheduleHide(); |
| } |
| |
| @Override |
| public void onExitAction() { |
| mCurrentHeight = 0; |
| mTracker.sendHideChannelSwitch(mViewDurationTimer.reset()); |
| cancelHide(); |
| } |
| |
| private void scheduleHide() { |
| cancelHide(); |
| postDelayed(mHideRunnable, mShowDurationMillis); |
| } |
| |
| private void cancelHide() { |
| removeCallbacks(mHideRunnable); |
| } |
| |
| private void reset() { |
| mTypedChannelNumber.reset(); |
| mSelectedChannel = null; |
| mChannelCandidates.clear(); |
| mAdapter.notifyDataSetChanged(); |
| } |
| |
| public void setChannels(@Nullable List<Channel> channels) { |
| mChannels = channels; |
| } |
| |
| public static boolean isChannelNumberKey(int keyCode) { |
| return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9; |
| } |
| |
| public void onNumberKeyUp(int num) { |
| // Reset typed channel number in some cases. |
| if (mTypedChannelNumber.majorNumber == null) { |
| mTypedChannelNumber.reset(); |
| } else if (!mTypedChannelNumber.hasDelimiter |
| && mTypedChannelNumber.majorNumber.length() >= MAX_CHANNEL_NUMBER_DIGIT) { |
| mTypedChannelNumber.reset(); |
| } else if (mTypedChannelNumber.hasDelimiter |
| && mTypedChannelNumber.minorNumber != null |
| && mTypedChannelNumber.minorNumber.length() >= MAX_MINOR_CHANNEL_NUMBER_DIGIT) { |
| mTypedChannelNumber.reset(); |
| } |
| |
| if (!mTypedChannelNumber.hasDelimiter) { |
| mTypedChannelNumber.majorNumber += String.valueOf(num); |
| } else { |
| mTypedChannelNumber.minorNumber += String.valueOf(num); |
| } |
| mTracker.sendChannelNumberInput(); |
| updateView(); |
| } |
| |
| private void onDelimiterKeyUp() { |
| if (mTypedChannelNumber.hasDelimiter || mTypedChannelNumber.majorNumber.length() == 0) { |
| return; |
| } |
| mTypedChannelNumber.hasDelimiter = true; |
| mTracker.sendChannelNumberInput(); |
| updateView(); |
| } |
| |
| private void updateView() { |
| mChannelNumberView.setText(mTypedChannelNumber.toString() + "_"); |
| mChannelCandidates.clear(); |
| ArrayList<Channel> secondaryChannelCandidates = new ArrayList<>(); |
| for (Channel channel : mChannels) { |
| ChannelNumber chNumber = ChannelNumber.parseChannelNumber(channel.getDisplayNumber()); |
| if (chNumber == null) { |
| Log.i(TAG, "Malformed channel number (name=" + channel.getDisplayName() |
| + ", number=" + channel.getDisplayNumber() + ")"); |
| continue; |
| } |
| if (matchChannelNumber(mTypedChannelNumber, chNumber)) { |
| mChannelCandidates.add(channel); |
| } else if (!mTypedChannelNumber.hasDelimiter) { |
| // Even if a user doesn't type '-', we need to match the typed number to not only |
| // the major number but also the minor number. For example, when a user types '111' |
| // without delimiter, it should be matched to '111', '1-11' and '11-1'. |
| if (channel.getDisplayNumber().replaceAll(CHANNEL_DELIMITERS_REGEX, "") |
| .startsWith(mTypedChannelNumber.majorNumber)) { |
| secondaryChannelCandidates.add(channel); |
| } |
| } |
| } |
| mChannelCandidates.addAll(secondaryChannelCandidates); |
| mAdapter.notifyDataSetChanged(); |
| if (mAdapter.getCount() > 0) { |
| mChannelItemListView.requestFocus(); |
| mChannelItemListView.setSelection(0); |
| mSelectedChannel = mChannelCandidates.get(0); |
| } |
| |
| updateViewHeight(); |
| } |
| |
| private void updateViewHeight() { |
| int itemListHeight = mItemHeight * Math.min(MAX_CHANNEL_ITEM, mAdapter.getCount()); |
| int targetHeight = mBaseViewHeight + itemListHeight; |
| if (mResizeAnimator != null) { |
| mResizeAnimator.cancel(); |
| mResizeAnimator = null; |
| } |
| |
| if (mCurrentHeight == 0) { |
| // Do not add the resize animation when the banner has not been shown before. |
| mCurrentHeight = targetHeight; |
| setViewHeight(this, targetHeight); |
| } else if (mCurrentHeight != targetHeight){ |
| mResizeAnimator = createResizeAnimator(targetHeight); |
| mResizeAnimator.start(); |
| } |
| } |
| |
| private Animator createResizeAnimator(int targetHeight) { |
| ValueAnimator animator = ValueAnimator.ofInt(mCurrentHeight, targetHeight); |
| animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| int value = (Integer) animation.getAnimatedValue(); |
| setViewHeight(KeypadChannelSwitchView.this, value); |
| mCurrentHeight = value; |
| } |
| }); |
| animator.setDuration(mResizeAnimDuration); |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| mResizeAnimator = null; |
| } |
| }); |
| animator.setInterpolator(mResizeInterpolator); |
| return animator; |
| } |
| |
| private void setViewHeight(View view, int height) { |
| ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); |
| if (height != layoutParams.height) { |
| layoutParams.height = height; |
| view.setLayoutParams(layoutParams); |
| } |
| } |
| |
| private static boolean matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber) { |
| if (!chNumber.majorNumber.equals(typedChNumber.majorNumber)) { |
| return false; |
| } |
| if (typedChNumber.hasDelimiter) { |
| if (!chNumber.hasDelimiter) { |
| return false; |
| } |
| if (!chNumber.minorNumber.startsWith(typedChNumber.minorNumber)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| class ChannelItemAdapter extends BaseAdapter { |
| @Override |
| public int getCount() { |
| return mChannelCandidates.size(); |
| } |
| |
| @Override |
| public Object getItem(int position) { |
| return mChannelCandidates.get(position); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| @Override |
| public View getView(int position, View convertView, ViewGroup parent) { |
| final Channel channel = mChannelCandidates.get(position); |
| View v = convertView; |
| if (v == null) { |
| v = mLayoutInflater.inflate(R.layout.keypad_channel_switch_item, parent, false); |
| } |
| |
| TextView channelNumberView = (TextView) v.findViewById(R.id.number); |
| channelNumberView.setText(channel.getDisplayNumber()); |
| |
| TextView channelNameView = (TextView) v.findViewById(R.id.name); |
| channelNameView.setText(channel.getDisplayName()); |
| return v; |
| } |
| } |
| } |