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