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.analytics.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 KeypadChannelSwitchView(Context context)103 public KeypadChannelSwitchView(Context context) { 104 this(context, null, 0); 105 } 106 KeypadChannelSwitchView(Context context, AttributeSet attrs)107 public KeypadChannelSwitchView(Context context, AttributeSet attrs) { 108 this(context, attrs, 0); 109 } 110 KeypadChannelSwitchView(Context context, AttributeSet attrs, int defStyleAttr)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 onFinishInflate()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 dispatchKeyEvent(KeyEvent event)177 public boolean dispatchKeyEvent(KeyEvent event) { 178 scheduleHide(); 179 return super.dispatchKeyEvent(event); 180 } 181 182 @Override onKeyUp(int keyCode, KeyEvent event)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 onEnterAction(boolean fromEmptyScene)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 onExitAction()211 public void onExitAction() { 212 mCurrentHeight = 0; 213 mTracker.sendHideChannelSwitch(mViewDurationTimer.reset()); 214 cancelHide(); 215 } 216 scheduleHide()217 private void scheduleHide() { 218 cancelHide(); 219 postDelayed(mHideRunnable, mShowDurationMillis); 220 } 221 cancelHide()222 private void cancelHide() { 223 removeCallbacks(mHideRunnable); 224 } 225 reset()226 private void reset() { 227 mTypedChannelNumber.reset(); 228 mSelectedChannel = null; 229 mChannelCandidates.clear(); 230 mAdapter.notifyDataSetChanged(); 231 } 232 setChannels(@ullable List<Channel> channels)233 public void setChannels(@Nullable List<Channel> channels) { 234 mChannels = channels; 235 } 236 isChannelNumberKey(int keyCode)237 public static boolean isChannelNumberKey(int keyCode) { 238 return keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9; 239 } 240 onNumberKeyUp(int num)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 onDelimiterKeyUp()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 updateView()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 updateViewHeight()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 createResizeAnimator(int targetHeight)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 setViewHeight(View view, int height)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 matchChannelNumber(ChannelNumber typedChNumber, ChannelNumber chNumber)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 getCount()370 public int getCount() { 371 return mChannelCandidates.size(); 372 } 373 374 @Override getItem(int position)375 public Object getItem(int position) { 376 return mChannelCandidates.get(position); 377 } 378 379 @Override getItemId(int position)380 public long getItemId(int position) { 381 return position; 382 } 383 384 @Override getView(int position, View convertView, ViewGroup parent)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