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