/* * 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 mChannels; private TextView mChannelNumberView; private ListView mChannelItemListView; private final ChannelNumber mTypedChannelNumber = new ChannelNumber(); private final ArrayList 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 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 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; } } }