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.AnimatorInflater; 21 import android.animation.AnimatorListenerAdapter; 22 import android.animation.AnimatorSet; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.database.ContentObserver; 27 import android.graphics.Bitmap; 28 import android.media.tv.TvContract; 29 import android.media.tv.TvInputInfo; 30 import android.net.Uri; 31 import android.os.Handler; 32 import android.support.annotation.Nullable; 33 import android.text.Spannable; 34 import android.text.SpannableString; 35 import android.text.TextUtils; 36 import android.text.format.DateUtils; 37 import android.text.style.TextAppearanceSpan; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.TypedValue; 41 import android.view.View; 42 import android.view.ViewGroup; 43 import android.view.animation.AnimationUtils; 44 import android.view.animation.Interpolator; 45 import android.widget.FrameLayout; 46 import android.widget.ImageView; 47 import android.widget.ProgressBar; 48 import android.widget.RelativeLayout; 49 import android.widget.TextView; 50 51 import com.android.tv.MainActivity; 52 import com.android.tv.R; 53 import com.android.tv.common.recording.RecordedProgram; 54 import com.android.tv.data.Channel; 55 import com.android.tv.data.Program; 56 import com.android.tv.data.StreamInfo; 57 import com.android.tv.util.ImageCache; 58 import com.android.tv.util.ImageLoader; 59 import com.android.tv.util.ImageLoader.ImageLoaderCallback; 60 import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; 61 import com.android.tv.util.Utils; 62 63 import junit.framework.Assert; 64 65 import java.util.Objects; 66 67 /** 68 * A view to render channel banner. 69 */ 70 public class ChannelBannerView extends FrameLayout implements TvTransitionManager.TransitionLayout { 71 private static final String TAG = "ChannelBannerView"; 72 private static final boolean DEBUG = false; 73 74 /** 75 * Show all information at the channel banner. 76 */ 77 public static final int LOCK_NONE = 0; 78 79 /** 80 * Lock program details at the channel banner. 81 * This is used when a content is locked so we don't want to show program details 82 * including program description text and poster art. 83 */ 84 public static final int LOCK_PROGRAM_DETAIL = 1; 85 86 /** 87 * Lock channel information at the channel banner. 88 * This is used when a channel is locked so we only want to show input information. 89 */ 90 public static final int LOCK_CHANNEL_INFO = 2; 91 92 private static final String EMPTY_STRING = ""; 93 94 private static Program sNoProgram; 95 private static Program sLockedChannelProgram; 96 private static String sClosedCaptionMark; 97 98 private final MainActivity mMainActivity; 99 private final Resources mResources; 100 private View mChannelView; 101 102 private TextView mChannelNumberTextView; 103 private ImageView mChannelLogoImageView; 104 private TextView mProgramTextView; 105 private ImageView mTvInputLogoImageView; 106 private TextView mChannelNameTextView; 107 private TextView mProgramTimeTextView; 108 private ProgressBar mRemainingTimeView; 109 private TextView mClosedCaptionTextView; 110 private TextView mAspectRatioTextView; 111 private TextView mResolutionTextView; 112 private TextView mAudioChannelTextView; 113 private TextView mProgramDescriptionTextView; 114 private String mProgramDescriptionText; 115 private View mAnchorView; 116 private Channel mCurrentChannel; 117 private Program mLastUpdatedProgram; 118 private RecordedProgram mLastUpdatedRecordedProgram; 119 private final Handler mHandler = new Handler(); 120 121 private int mLockType; 122 123 private Animator mResizeAnimator; 124 private int mCurrentHeight; 125 private boolean mProgramInfoUpdatePendingByResizing; 126 127 private final Animator mProgramDescriptionFadeInAnimator; 128 private final Animator mProgramDescriptionFadeOutAnimator; 129 130 private final Runnable mHideRunnable = new Runnable() { 131 @Override 132 public void run() { 133 mCurrentHeight = 0; 134 mMainActivity.getOverlayManager().hideOverlays( 135 TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_DIALOG 136 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_SIDE_PANELS 137 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_PROGRAM_GUIDE 138 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_MENU 139 | TvOverlayManager.FLAG_HIDE_OVERLAYS_KEEP_FRAGMENT); 140 } 141 }; 142 private final long mShowDurationMillis; 143 private final int mChannelLogoImageViewWidth; 144 private final int mChannelLogoImageViewHeight; 145 private final int mChannelLogoImageViewMarginStart; 146 private final int mProgramDescriptionTextViewWidth; 147 private final int mChannelBannerTextColor; 148 private final int mChannelBannerDimTextColor; 149 private final int mResizeAnimDuration; 150 private final Interpolator mResizeInterpolator; 151 152 private final AnimatorListenerAdapter mResizeAnimatorListener = new AnimatorListenerAdapter() { 153 @Override 154 public void onAnimationStart(Animator animator) { 155 mProgramInfoUpdatePendingByResizing = false; 156 } 157 158 @Override 159 public void onAnimationEnd(Animator animator) { 160 mProgramDescriptionTextView.setAlpha(1f); 161 mResizeAnimator = null; 162 if (mProgramInfoUpdatePendingByResizing) { 163 mProgramInfoUpdatePendingByResizing = false; 164 updateProgramInfo(mLastUpdatedProgram); 165 } 166 } 167 }; 168 169 private final ContentObserver mProgramUpdateObserver = new ContentObserver(mHandler) { 170 @Override 171 public void onChange(boolean selfChange, Uri uri) { 172 // TODO: This {@code uri} argument may be a program which is not related to this 173 // channel. Consider adding channel id as a parameter of program URI to avoid 174 // unnecessary update. 175 mHandler.post(mProgramUpdateRunnable); 176 } 177 }; 178 179 private final Runnable mProgramUpdateRunnable = new Runnable() { 180 @Override 181 public void run() { 182 removeCallbacks(this); 183 updateViews(null); 184 } 185 }; 186 ChannelBannerView(Context context)187 public ChannelBannerView(Context context) { 188 this(context, null); 189 } 190 ChannelBannerView(Context context, AttributeSet attrs)191 public ChannelBannerView(Context context, AttributeSet attrs) { 192 this(context, attrs, 0); 193 } 194 ChannelBannerView(Context context, AttributeSet attrs, int defStyle)195 public ChannelBannerView(Context context, AttributeSet attrs, int defStyle) { 196 super(context, attrs, defStyle); 197 mResources = getResources(); 198 199 mMainActivity = (MainActivity) context; 200 201 mShowDurationMillis = mResources.getInteger( 202 R.integer.channel_banner_show_duration); 203 mChannelLogoImageViewWidth = mResources.getDimensionPixelSize( 204 R.dimen.channel_banner_channel_logo_width); 205 mChannelLogoImageViewHeight = mResources.getDimensionPixelSize( 206 R.dimen.channel_banner_channel_logo_height); 207 mChannelLogoImageViewMarginStart = mResources.getDimensionPixelSize( 208 R.dimen.channel_banner_channel_logo_margin_start); 209 mProgramDescriptionTextViewWidth = mResources.getDimensionPixelSize( 210 R.dimen.channel_banner_program_description_width); 211 mChannelBannerTextColor = Utils.getColor(mResources, R.color.channel_banner_text_color); 212 mChannelBannerDimTextColor = Utils.getColor(mResources, 213 R.color.channel_banner_dim_text_color); 214 mResizeAnimDuration = mResources.getInteger(R.integer.channel_banner_fast_anim_duration); 215 216 mResizeInterpolator = AnimationUtils.loadInterpolator(context, 217 android.R.interpolator.linear_out_slow_in); 218 219 mProgramDescriptionFadeInAnimator = AnimatorInflater.loadAnimator(mMainActivity, 220 R.animator.channel_banner_program_description_fade_in); 221 mProgramDescriptionFadeOutAnimator = AnimatorInflater.loadAnimator(mMainActivity, 222 R.animator.channel_banner_program_description_fade_out); 223 224 if (sNoProgram == null) { 225 sNoProgram = new Program.Builder() 226 .setTitle(context.getString(R.string.channel_banner_no_title)) 227 .setDescription(EMPTY_STRING) 228 .build(); 229 } 230 if (sLockedChannelProgram == null){ 231 sLockedChannelProgram = new Program.Builder() 232 .setTitle(context.getString(R.string.channel_banner_locked_channel_title)) 233 .setDescription(EMPTY_STRING) 234 .build(); 235 } 236 if (sClosedCaptionMark == null) { 237 sClosedCaptionMark = context.getString(R.string.closed_caption); 238 } 239 } 240 241 @Override onAttachedToWindow()242 protected void onAttachedToWindow() { 243 if (DEBUG) Log.d(TAG, "onAttachedToWindow"); 244 super.onAttachedToWindow(); 245 getContext().getContentResolver().registerContentObserver(TvContract.Programs.CONTENT_URI, 246 true, mProgramUpdateObserver); 247 } 248 249 @Override onDetachedFromWindow()250 protected void onDetachedFromWindow() { 251 if (DEBUG) Log.d(TAG, "onDetachedToWindow"); 252 getContext().getContentResolver().unregisterContentObserver(mProgramUpdateObserver); 253 super.onDetachedFromWindow(); 254 } 255 256 @Override onFinishInflate()257 protected void onFinishInflate() { 258 super.onFinishInflate(); 259 260 mChannelView = findViewById(R.id.channel_banner_view); 261 262 mChannelNumberTextView = (TextView) findViewById(R.id.channel_number); 263 mChannelLogoImageView = (ImageView) findViewById(R.id.channel_logo); 264 mProgramTextView = (TextView) findViewById(R.id.program_text); 265 mTvInputLogoImageView = (ImageView) findViewById(R.id.tvinput_logo); 266 mChannelNameTextView = (TextView) findViewById(R.id.channel_name); 267 mProgramTimeTextView = (TextView) findViewById(R.id.program_time_text); 268 mRemainingTimeView = (ProgressBar) findViewById(R.id.remaining_time); 269 mClosedCaptionTextView = (TextView) findViewById(R.id.closed_caption); 270 mAspectRatioTextView = (TextView) findViewById(R.id.aspect_ratio); 271 mResolutionTextView = (TextView) findViewById(R.id.resolution); 272 mAudioChannelTextView = (TextView) findViewById(R.id.audio_channel); 273 mProgramDescriptionTextView = (TextView) findViewById(R.id.program_description); 274 mAnchorView = findViewById(R.id.anchor); 275 276 mProgramDescriptionFadeInAnimator.setTarget(mProgramDescriptionTextView); 277 mProgramDescriptionFadeOutAnimator.setTarget(mProgramDescriptionTextView); 278 mProgramDescriptionFadeOutAnimator.addListener(new AnimatorListenerAdapter() { 279 @Override 280 public void onAnimationEnd(Animator animator) { 281 mProgramDescriptionTextView.setText(mProgramDescriptionText); 282 } 283 }); 284 } 285 286 @Override onEnterAction(boolean fromEmptyScene)287 public void onEnterAction(boolean fromEmptyScene) { 288 resetAnimationEffects(); 289 if (fromEmptyScene) { 290 ViewUtils.setTransitionAlpha(mChannelView, 1f); 291 } 292 scheduleHide(); 293 } 294 295 @Override onExitAction()296 public void onExitAction() { 297 mCurrentHeight = 0; 298 cancelHide(); 299 } 300 scheduleHide()301 private void scheduleHide() { 302 cancelHide(); 303 mHandler.postDelayed(mHideRunnable, mShowDurationMillis); 304 } 305 cancelHide()306 private void cancelHide() { 307 mHandler.removeCallbacks(mHideRunnable); 308 } 309 resetAnimationEffects()310 private void resetAnimationEffects() { 311 setAlpha(1f); 312 setScaleX(1f); 313 setScaleY(1f); 314 setTranslationX(0); 315 setTranslationY(0); 316 } 317 318 /** 319 * Set new lock type. 320 * 321 * @param lockType Any of LOCK_NONE, LOCK_PROGRAM_DETAIL, or LOCK_CHANNEL_INFO. 322 * @return {@code true} only if lock type is changed 323 * @throws IllegalArgumentException if lockType is invalid. 324 */ setLockType(int lockType)325 public boolean setLockType(int lockType) { 326 if (lockType != LOCK_NONE && lockType != LOCK_CHANNEL_INFO 327 && lockType != LOCK_PROGRAM_DETAIL) { 328 throw new IllegalArgumentException("No such lock type " + lockType); 329 } 330 if (mLockType != lockType) { 331 mLockType = lockType; 332 return true; 333 } 334 return false; 335 } 336 337 /** 338 * Update channel banner view. 339 * 340 * @param info A StreamInfo that includes stream information. 341 * If it's {@code null}, only program information will be updated. 342 */ updateViews(StreamInfo info)343 public void updateViews(StreamInfo info) { 344 resetAnimationEffects(); 345 Channel channel = mMainActivity.getCurrentChannel(); 346 if (!Objects.equals(mCurrentChannel, channel) && isShown()) { 347 scheduleHide(); 348 } 349 mCurrentChannel = channel; 350 mChannelView.setVisibility(VISIBLE); 351 if (info != null) { 352 // If the current channels between ChannelTuner and TvView are different, 353 // the stream information should not be seen. 354 updateStreamInfo(channel != null && channel.equals(info.getCurrentChannel()) ? info 355 : null); 356 updateChannelInfo(); 357 } 358 if (mMainActivity.isRecordingPlayback()) { 359 updateProgramInfo(mMainActivity.getPlayingRecordedProgram()); 360 } else { 361 updateProgramInfo(mMainActivity.getCurrentProgram()); 362 } 363 } 364 updateStreamInfo(StreamInfo info)365 private void updateStreamInfo(StreamInfo info) { 366 // Update stream information in a channel. 367 if (mLockType != LOCK_CHANNEL_INFO && info != null) { 368 updateText(mClosedCaptionTextView, info.hasClosedCaption() ? sClosedCaptionMark 369 : EMPTY_STRING); 370 updateText(mAspectRatioTextView, 371 Utils.getAspectRatioString(info.getVideoDisplayAspectRatio())); 372 updateText(mResolutionTextView, 373 Utils.getVideoDefinitionLevelString( 374 mMainActivity, info.getVideoDefinitionLevel())); 375 updateText(mAudioChannelTextView, 376 Utils.getAudioChannelString(mMainActivity, info.getAudioChannelCount())); 377 } else { 378 // Channel change has been requested. But, StreamInfo hasn't been updated yet. 379 mClosedCaptionTextView.setVisibility(View.GONE); 380 mAspectRatioTextView.setVisibility(View.GONE); 381 mResolutionTextView.setVisibility(View.GONE); 382 mAudioChannelTextView.setVisibility(View.GONE); 383 } 384 } 385 updateChannelInfo()386 private void updateChannelInfo() { 387 // Update static information for a channel. 388 String displayNumber = EMPTY_STRING; 389 String displayName = EMPTY_STRING; 390 if (mCurrentChannel != null) { 391 displayNumber = mCurrentChannel.getDisplayNumber(); 392 if (displayNumber == null) { 393 displayNumber = EMPTY_STRING; 394 } 395 displayName = mCurrentChannel.getDisplayName(); 396 if (displayName == null) { 397 displayName = EMPTY_STRING; 398 } 399 } 400 401 if (displayNumber.isEmpty()) { 402 mChannelNumberTextView.setVisibility(GONE); 403 } else { 404 mChannelNumberTextView.setVisibility(VISIBLE); 405 } 406 if (displayNumber.length() <= 3) { 407 updateTextView( 408 mChannelNumberTextView, 409 R.dimen.channel_banner_channel_number_large_text_size, 410 R.dimen.channel_banner_channel_number_large_margin_top); 411 } else if (displayNumber.length() <= 4) { 412 updateTextView( 413 mChannelNumberTextView, 414 R.dimen.channel_banner_channel_number_medium_text_size, 415 R.dimen.channel_banner_channel_number_medium_margin_top); 416 } else { 417 updateTextView( 418 mChannelNumberTextView, 419 R.dimen.channel_banner_channel_number_small_text_size, 420 R.dimen.channel_banner_channel_number_small_margin_top); 421 } 422 mChannelNumberTextView.setText(displayNumber); 423 mChannelNameTextView.setText(displayName); 424 TvInputInfo info = mMainActivity.getTvInputManagerHelper().getTvInputInfo( 425 getCurrentInputId()); 426 if (info == null || !ImageLoader.loadBitmap(createTvInputLogoLoaderCallback(info, this), 427 new LoadTvInputLogoTask(getContext(), ImageCache.getInstance(), info))) { 428 mTvInputLogoImageView.setVisibility(View.GONE); 429 mTvInputLogoImageView.setImageDrawable(null); 430 } 431 mChannelLogoImageView.setImageBitmap(null); 432 mChannelLogoImageView.setVisibility(View.GONE); 433 if (mCurrentChannel != null) { 434 mCurrentChannel.loadBitmap(getContext(), Channel.LOAD_IMAGE_TYPE_CHANNEL_LOGO, 435 mChannelLogoImageViewWidth, mChannelLogoImageViewHeight, 436 createChannelLogoCallback(this, mCurrentChannel)); 437 } 438 } 439 getCurrentInputId()440 private String getCurrentInputId() { 441 Channel channel = mMainActivity.getCurrentChannel(); 442 if (channel != null) { 443 return channel.getInputId(); 444 } else if (mMainActivity.isRecordingPlayback()) { 445 RecordedProgram recordedProgram = mMainActivity.getPlayingRecordedProgram(); 446 if (recordedProgram != null) { 447 return recordedProgram.getInputId(); 448 } 449 } 450 return null; 451 } 452 updateTvInputLogo(Bitmap bitmap)453 private void updateTvInputLogo(Bitmap bitmap) { 454 mTvInputLogoImageView.setVisibility(View.VISIBLE); 455 mTvInputLogoImageView.setImageBitmap(bitmap); 456 } 457 createTvInputLogoLoaderCallback( final TvInputInfo info, ChannelBannerView channelBannerView)458 private static ImageLoaderCallback<ChannelBannerView> createTvInputLogoLoaderCallback( 459 final TvInputInfo info, ChannelBannerView channelBannerView) { 460 return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) { 461 @Override 462 public void onBitmapLoaded(ChannelBannerView channelBannerView, Bitmap bitmap) { 463 if (bitmap != null && channelBannerView.mCurrentChannel != null 464 && info.getId().equals(channelBannerView.mCurrentChannel.getInputId())) { 465 channelBannerView.updateTvInputLogo(bitmap); 466 } 467 } 468 }; 469 } 470 471 private void updateText(TextView view, String text) { 472 if (TextUtils.isEmpty(text)) { 473 view.setVisibility(View.GONE); 474 } else { 475 view.setVisibility(View.VISIBLE); 476 view.setText(text); 477 } 478 } 479 480 private void updateTextView(TextView textView, int sizeRes, int marginTopRes) { 481 float textSize = mResources.getDimension(sizeRes); 482 if (textView.getTextSize() != textSize) { 483 textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); 484 } 485 updateTopMargin(textView, marginTopRes); 486 } 487 488 private void updateTopMargin(View view, int marginTopRes) { 489 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams(); 490 int topMargin = (int) mResources.getDimension(marginTopRes); 491 if (lp.topMargin != topMargin) { 492 lp.topMargin = topMargin; 493 view.setLayoutParams(lp); 494 } 495 } 496 497 private static ImageLoaderCallback<ChannelBannerView> createChannelLogoCallback( 498 ChannelBannerView channelBannerView, final Channel channel) { 499 return new ImageLoaderCallback<ChannelBannerView>(channelBannerView) { 500 @Override 501 public void onBitmapLoaded(ChannelBannerView view, @Nullable Bitmap logo) { 502 if (channel != view.mCurrentChannel) { 503 // The logo is obsolete. 504 return; 505 } 506 view.updateLogo(logo); 507 } 508 }; 509 } 510 511 private void updateLogo(@Nullable Bitmap logo) { 512 if (logo == null) { 513 // Need to update the text size of the program text view depending on the channel logo. 514 updateProgramTextView(mLastUpdatedProgram); 515 return; 516 } 517 518 mChannelLogoImageView.setImageBitmap(logo); 519 mChannelLogoImageView.setVisibility(View.VISIBLE); 520 updateProgramTextView(mLastUpdatedProgram); 521 522 if (mResizeAnimator == null) { 523 String description = mProgramDescriptionTextView.getText().toString(); 524 boolean needFadeAnimation = !description.equals(mProgramDescriptionText); 525 updateBannerHeight(needFadeAnimation); 526 } else { 527 mProgramInfoUpdatePendingByResizing = true; 528 } 529 } 530 531 private void updateProgramInfo(Program program) { 532 if (mLockType == LOCK_CHANNEL_INFO) { 533 program = sLockedChannelProgram; 534 } else if (!Program.isValid(program) || TextUtils.isEmpty(program.getTitle())) { 535 program = sNoProgram; 536 } 537 538 if (mLastUpdatedProgram == null 539 || !TextUtils.equals(program.getTitle(), mLastUpdatedProgram.getTitle()) 540 || !TextUtils.equals(program.getEpisodeDisplayTitle(getContext()), 541 mLastUpdatedProgram.getEpisodeDisplayTitle(getContext()))) { 542 updateProgramTextView(program); 543 } 544 updateProgramTimeInfo(program); 545 546 // When the program is changed, but the previous resize animation has not ended yet, 547 // cancel the animation. 548 boolean isProgramChanged = !Objects.equals(mLastUpdatedProgram, program); 549 if (mResizeAnimator != null && isProgramChanged) { 550 setLastUpdatedProgram(program); 551 mProgramInfoUpdatePendingByResizing = true; 552 mResizeAnimator.cancel(); 553 } else if (mResizeAnimator == null) { 554 if (mLockType != LOCK_NONE || TextUtils.isEmpty(program.getDescription())) { 555 mProgramDescriptionTextView.setVisibility(GONE); 556 mProgramDescriptionText = ""; 557 } else { 558 mProgramDescriptionTextView.setVisibility(VISIBLE); 559 mProgramDescriptionText = program.getDescription(); 560 } 561 String description = mProgramDescriptionTextView.getText().toString(); 562 boolean needFadeAnimation = isProgramChanged 563 || !description.equals(mProgramDescriptionText); 564 updateBannerHeight(needFadeAnimation); 565 } else { 566 mProgramInfoUpdatePendingByResizing = true; 567 } 568 setLastUpdatedProgram(program); 569 } 570 571 private void updateProgramInfo(RecordedProgram recordedProgram) { 572 if (mLockType == LOCK_CHANNEL_INFO) { 573 updateProgramInfo(sLockedChannelProgram); 574 return; 575 } else if (recordedProgram == null) { 576 updateProgramInfo(sNoProgram); 577 return; 578 } 579 580 if (mLastUpdatedRecordedProgram == null 581 || !TextUtils.equals(recordedProgram.getTitle(), 582 mLastUpdatedRecordedProgram.getTitle()) 583 || !TextUtils.equals(recordedProgram.getEpisodeDisplayTitle(getContext()), 584 mLastUpdatedRecordedProgram.getEpisodeDisplayTitle(getContext()))) { 585 updateProgramTextView(recordedProgram); 586 } 587 updateProgramTimeInfo(recordedProgram); 588 589 // When the program is changed, but the previous resize animation has not ended yet, 590 // cancel the animation. 591 boolean isProgramChanged = !Objects.equals(mLastUpdatedRecordedProgram, recordedProgram); 592 if (mResizeAnimator != null && isProgramChanged) { 593 setLastUpdatedRecordedProgram(recordedProgram); 594 mProgramInfoUpdatePendingByResizing = true; 595 mResizeAnimator.cancel(); 596 } else if (mResizeAnimator == null) { 597 if (mLockType != LOCK_NONE 598 || TextUtils.isEmpty(recordedProgram.getShortDescription())) { 599 mProgramDescriptionTextView.setVisibility(GONE); 600 mProgramDescriptionText = ""; 601 } else { 602 mProgramDescriptionTextView.setVisibility(VISIBLE); 603 mProgramDescriptionText = recordedProgram.getShortDescription(); 604 } 605 String description = mProgramDescriptionTextView.getText().toString(); 606 boolean needFadeAnimation = isProgramChanged 607 || !description.equals(mProgramDescriptionText); 608 updateBannerHeight(needFadeAnimation); 609 } else { 610 mProgramInfoUpdatePendingByResizing = true; 611 } 612 setLastUpdatedRecordedProgram(recordedProgram); 613 } 614 615 private void updateProgramTextView(Program program) { 616 if (program == null) { 617 return; 618 } 619 updateProgramTextView(program == sLockedChannelProgram, program.getTitle(), 620 program.getEpisodeTitle(), program.getEpisodeDisplayTitle(getContext())); 621 } 622 623 private void updateProgramTextView(RecordedProgram recordedProgram) { 624 if (recordedProgram == null) { 625 return; 626 } 627 updateProgramTextView(false, recordedProgram.getTitle(), recordedProgram.getEpisodeTitle(), 628 recordedProgram.getEpisodeDisplayTitle(getContext())); 629 } 630 631 private void updateProgramTextView(boolean dimText, String title, String episodeTitle, 632 String episodeDisplayTitle) { 633 mProgramTextView.setVisibility(View.VISIBLE); 634 if (dimText) { 635 mProgramTextView.setTextColor(mChannelBannerDimTextColor); 636 } else { 637 mProgramTextView.setTextColor(mChannelBannerTextColor); 638 } 639 updateTextView(mProgramTextView, 640 R.dimen.channel_banner_program_large_text_size, 641 R.dimen.channel_banner_program_large_margin_top); 642 if (TextUtils.isEmpty(episodeTitle)) { 643 mProgramTextView.setText(title); 644 } else { 645 String fullTitle = title + " " + episodeDisplayTitle; 646 647 SpannableString text = new SpannableString(fullTitle); 648 text.setSpan(new TextAppearanceSpan(getContext(), 649 R.style.text_appearance_channel_banner_episode_title), 650 fullTitle.length() - episodeDisplayTitle.length(), fullTitle.length(), 651 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 652 mProgramTextView.setText(text); 653 } 654 int width = mProgramDescriptionTextViewWidth 655 - ((mChannelLogoImageView.getVisibility() != View.VISIBLE) 656 ? 0 : mChannelLogoImageViewWidth + mChannelLogoImageViewMarginStart); 657 ViewGroup.LayoutParams lp = mProgramTextView.getLayoutParams(); 658 lp.width = width; 659 mProgramTextView.setLayoutParams(lp); 660 mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 661 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 662 663 boolean oneline = (mProgramTextView.getLineCount() == 1); 664 if (!oneline) { 665 updateTextView( 666 mProgramTextView, 667 R.dimen.channel_banner_program_medium_text_size, 668 R.dimen.channel_banner_program_medium_margin_top); 669 mProgramTextView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), 670 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); 671 oneline = (mProgramTextView.getLineCount() == 1); 672 } 673 updateTopMargin(mAnchorView, oneline 674 ? R.dimen.channel_banner_anchor_one_line_y 675 : R.dimen.channel_banner_anchor_two_line_y); 676 } 677 678 private void updateProgramTimeInfo(Program program) { 679 long startTime = program.getStartTimeUtcMillis(); 680 long endTime = program.getEndTimeUtcMillis(); 681 if (mLockType != LOCK_CHANNEL_INFO && startTime > 0 && endTime > startTime) { 682 mProgramTimeTextView.setVisibility(View.VISIBLE); 683 mRemainingTimeView.setVisibility(View.VISIBLE); 684 685 mProgramTimeTextView.setText(Utils.getDurationString( 686 getContext(), startTime, endTime, true)); 687 688 long currTime = mMainActivity.getCurrentPlayingPosition(); 689 if (currTime <= startTime) { 690 mRemainingTimeView.setProgress(0); 691 } else if (currTime >= endTime) { 692 mRemainingTimeView.setProgress(100); 693 } else { 694 mRemainingTimeView.setProgress( 695 (int) (100 * (currTime - startTime) / (endTime - startTime))); 696 } 697 } else { 698 mProgramTimeTextView.setVisibility(View.GONE); 699 mRemainingTimeView.setVisibility(View.GONE); 700 } 701 } 702 703 private void updateProgramTimeInfo(RecordedProgram recordedProgram) { 704 long durationMs = recordedProgram.getDurationMillis(); 705 if (mLockType != LOCK_CHANNEL_INFO && durationMs > 0) { 706 mProgramTimeTextView.setVisibility(View.VISIBLE); 707 mRemainingTimeView.setVisibility(View.VISIBLE); 708 709 mProgramTimeTextView.setText(DateUtils.formatElapsedTime(durationMs / 1000)); 710 711 long currTimeMs = mMainActivity.getCurrentPlayingPosition(); 712 if (currTimeMs <= 0) { 713 mRemainingTimeView.setProgress(0); 714 } else if (currTimeMs >= durationMs) { 715 mRemainingTimeView.setProgress(100); 716 } else { 717 mRemainingTimeView.setProgress((int) (100 * currTimeMs / durationMs)); 718 } 719 } else { 720 mProgramTimeTextView.setVisibility(View.GONE); 721 mRemainingTimeView.setVisibility(View.GONE); 722 } 723 } 724 725 private void setLastUpdatedProgram(Program program) { 726 mLastUpdatedProgram = program; 727 mLastUpdatedRecordedProgram = null; 728 } 729 730 private void setLastUpdatedRecordedProgram(RecordedProgram recordedProgram) { 731 mLastUpdatedProgram = null; 732 mLastUpdatedRecordedProgram = recordedProgram; 733 } 734 735 private void updateBannerHeight(boolean needFadeAnimation) { 736 Assert.assertNull(mResizeAnimator); 737 // Need to measure the layout height with the new description text. 738 CharSequence oldDescription = mProgramDescriptionTextView.getText(); 739 mProgramDescriptionTextView.setText(mProgramDescriptionText); 740 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 741 int targetHeight = getMeasuredHeight(); 742 743 if (mCurrentHeight == 0 || !isShown()) { 744 // Do not add the resize animation when the banner has not been shown before. 745 mCurrentHeight = targetHeight; 746 LayoutParams layoutParams = (LayoutParams) getLayoutParams(); 747 if (targetHeight != layoutParams.height) { 748 layoutParams.height = targetHeight; 749 setLayoutParams(layoutParams); 750 } 751 } else if (mCurrentHeight != targetHeight || needFadeAnimation) { 752 // Restore description text for fade in/out animation. 753 if (needFadeAnimation) { 754 mProgramDescriptionTextView.setText(oldDescription); 755 } 756 mResizeAnimator = createResizeAnimator(targetHeight, needFadeAnimation); 757 mResizeAnimator.start(); 758 } 759 } 760 761 private Animator createResizeAnimator(int targetHeight, boolean addFadeAnimation) { 762 final ValueAnimator heightAnimator = ValueAnimator.ofInt(mCurrentHeight, targetHeight); 763 heightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 764 @Override 765 public void onAnimationUpdate(ValueAnimator animation) { 766 int value = (Integer) animation.getAnimatedValue(); 767 LayoutParams layoutParams = (LayoutParams) ChannelBannerView.this.getLayoutParams(); 768 if (value != layoutParams.height) { 769 layoutParams.height = value; 770 ChannelBannerView.this.setLayoutParams(layoutParams); 771 } 772 mCurrentHeight = value; 773 } 774 }); 775 776 heightAnimator.setDuration(mResizeAnimDuration); 777 heightAnimator.setInterpolator(mResizeInterpolator); 778 779 if (!addFadeAnimation) { 780 heightAnimator.addListener(mResizeAnimatorListener); 781 return heightAnimator; 782 } 783 784 AnimatorSet fadeOutAndHeightAnimator = new AnimatorSet(); 785 fadeOutAndHeightAnimator.playTogether(mProgramDescriptionFadeOutAnimator, heightAnimator); 786 AnimatorSet animator = new AnimatorSet(); 787 animator.playSequentially(fadeOutAndHeightAnimator, mProgramDescriptionFadeInAnimator); 788 animator.addListener(mResizeAnimatorListener); 789 return animator; 790 } 791 } 792