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