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.guide; 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.ObjectAnimator; 24 import android.animation.PropertyValuesHolder; 25 import android.animation.ValueAnimator; 26 import android.content.SharedPreferences; 27 import android.content.res.Resources; 28 import android.graphics.Point; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.os.SystemClock; 32 import android.preference.PreferenceManager; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import android.support.v17.leanback.widget.OnChildSelectedListener; 36 import android.support.v17.leanback.widget.SearchOrbView; 37 import android.support.v17.leanback.widget.VerticalGridView; 38 import android.support.v7.widget.RecyclerView; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.View.MeasureSpec; 42 import android.view.ViewGroup; 43 import android.view.ViewGroup.LayoutParams; 44 import android.view.ViewTreeObserver; 45 46 import com.android.tv.ChannelTuner; 47 import com.android.tv.Features; 48 import com.android.tv.MainActivity; 49 import com.android.tv.R; 50 import com.android.tv.analytics.DurationTimer; 51 import com.android.tv.analytics.Tracker; 52 import com.android.tv.common.WeakHandler; 53 import com.android.tv.data.ChannelDataManager; 54 import com.android.tv.data.GenreItems; 55 import com.android.tv.data.ProgramDataManager; 56 import com.android.tv.dvr.DvrDataManager; 57 import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; 58 import com.android.tv.util.TvInputManagerHelper; 59 import com.android.tv.util.Utils; 60 61 import java.util.ArrayList; 62 import java.util.List; 63 import java.util.concurrent.TimeUnit; 64 65 /** 66 * The program guide. 67 */ 68 public class ProgramGuide implements ProgramGrid.ChildFocusListener { 69 private static final String TAG = "ProgramGuide"; 70 private static final boolean DEBUG = false; 71 72 // Whether we should show the guide partially. The first time the user enters the program guide, 73 // we show the grid partially together with the genre side panel on the left. Next time 74 // the program guide is entered, we recover the previous state (partial or full). 75 private static final String KEY_SHOW_GUIDE_PARTIAL = "show_guide_partial"; 76 private static final long TIME_INDICATOR_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1); 77 private static final long HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1); 78 private static final long HALF_HOUR_IN_MILLIS = HOUR_IN_MILLIS / 2; 79 // We keep the duration between mStartTime and the current time larger than this value. 80 // We clip out the first program entry in ProgramManager, if it does not have enough width. 81 // In order to prevent from clipping out the current program, this value need be larger than 82 // or equal to ProgramManager.FIRST_ENTRY_MIN_DURATION. 83 private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME 84 = ProgramManager.FIRST_ENTRY_MIN_DURATION; 85 86 private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000; 87 88 private static final String SCREEN_NAME = "EPG"; 89 90 private final MainActivity mActivity; 91 private final ProgramManager mProgramManager; 92 private final ChannelTuner mChannelTuner; 93 private final Tracker mTracker; 94 private final DurationTimer mVisibleDuration = new DurationTimer(); 95 private final Runnable mPreShowRunnable; 96 private final Runnable mPostHideRunnable; 97 98 private final int mWidthPerHour; 99 private final long mViewPortMillis; 100 private final int mRowHeight; 101 private final int mDetailHeight; 102 private final int mSelectionRow; // Row that is focused 103 private final int mTableFadeAnimDuration; 104 private final int mAnimationDuration; 105 private final int mDetailPadding; 106 private final SearchOrbView mSearchOrb; 107 private int mCurrentTimeIndicatorWidth; 108 109 private final View mContainer; 110 private final View mSidePanel; 111 private final VerticalGridView mSidePanelGridView; 112 private final View mTable; 113 private final TimelineRow mTimelineRow; 114 private final ProgramGrid mGrid; 115 private final TimeListAdapter mTimeListAdapter; 116 private final View mCurrentTimeIndicator; 117 118 private final Animator mShowAnimatorFull; 119 private final Animator mShowAnimatorPartial; 120 // mHideAnimatorFull and mHideAnimatorPartial are created from the same animation xmls. 121 // When we share the one animator for two different animations, the starting value 122 // is broken, even though the starting value is not defined in XML. 123 private final Animator mHideAnimatorFull; 124 private final Animator mHideAnimatorPartial; 125 private final Animator mPartialToFullAnimator; 126 private final Animator mFullToPartialAnimator; 127 private final Animator mProgramTableFadeOutAnimator; 128 private final Animator mProgramTableFadeInAnimator; 129 130 // When the program guide is popped up, we keep the previous state of the guide. 131 private boolean mShowGuidePartial; 132 private final SharedPreferences mSharedPreference; 133 private View mSelectedRow; 134 private Animator mDetailOutAnimator; 135 private Animator mDetailInAnimator; 136 137 private long mStartUtcTime; 138 private boolean mTimelineAnimation; 139 private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; 140 private boolean mIsDuringResetRowSelection; 141 private final Handler mHandler = new ProgramGuideHandler(this); 142 143 private final Runnable mHideRunnable = new Runnable() { 144 @Override 145 public void run() { 146 hide(); 147 } 148 }; 149 private final long mShowDurationMillis; 150 private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow; 151 152 private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener(); 153 154 private final Runnable mUpdateTimeIndicator = new Runnable() { 155 @Override 156 public void run() { 157 positionCurrentTimeIndicator(); 158 mHandler.postAtTime(this, 159 Utils.ceilTime(SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY)); 160 } 161 }; 162 ProgramGuide(MainActivity activity, ChannelTuner channelTuner, TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable)163 public ProgramGuide(MainActivity activity, ChannelTuner channelTuner, 164 TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, 165 ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, 166 Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable) { 167 mActivity = activity; 168 mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager, 169 programDataManager, dvrDataManager); 170 mChannelTuner = channelTuner; 171 mTracker = tracker; 172 mPreShowRunnable = preShowRunnable; 173 mPostHideRunnable = postHideRunnable; 174 175 Resources res = activity.getResources(); 176 177 mWidthPerHour = res.getDimensionPixelSize(R.dimen.program_guide_table_width_per_hour); 178 GuideUtils.setWidthPerHour(mWidthPerHour); 179 180 Point displaySize = new Point(); 181 mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize); 182 int gridWidth = displaySize.x 183 - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start) 184 - res.getDimensionPixelSize(R.dimen.program_guide_table_header_column_width); 185 mViewPortMillis = (gridWidth * HOUR_IN_MILLIS) / mWidthPerHour; 186 187 mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height); 188 mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height); 189 mSelectionRow = res.getInteger(R.integer.program_guide_selection_row); 190 mTableFadeAnimDuration = 191 res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration); 192 mShowDurationMillis = res.getInteger(R.integer.program_guide_show_duration); 193 mAnimationDuration = 194 res.getInteger(R.integer.program_guide_table_detail_toggle_anim_duration); 195 mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding); 196 197 mContainer = mActivity.findViewById(R.id.program_guide); 198 ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener 199 = new GlobalFocusChangeListener(); 200 mContainer.getViewTreeObserver().addOnGlobalFocusChangeListener(globalFocusChangeListener); 201 202 GenreListAdapter genreListAdapter = new GenreListAdapter(mActivity, mProgramManager, this); 203 mSidePanel = mContainer.findViewById(R.id.program_guide_side_panel); 204 mSidePanelGridView = (VerticalGridView) mContainer.findViewById( 205 R.id.program_guide_side_panel_grid_view); 206 mSidePanelGridView.getRecycledViewPool().setMaxRecycledViews( 207 R.layout.program_guide_side_panel_row, 208 res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row)); 209 mSidePanelGridView.setAdapter(genreListAdapter); 210 mSidePanelGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 211 mSidePanelGridView.setWindowAlignmentOffset(mActivity.getResources() 212 .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y)); 213 mSidePanelGridView.setWindowAlignmentOffsetPercent( 214 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 215 // TODO: Remove this check when we ship TV with epg search enabled. 216 if (Features.EPG_SEARCH.isEnabled(mActivity)) { 217 mSearchOrb = (SearchOrbView) mContainer.findViewById( 218 R.id.program_guide_side_panel_search_orb); 219 mSearchOrb.setVisibility(View.VISIBLE); 220 221 mSearchOrb.setOnOrbClickedListener(new View.OnClickListener() { 222 @Override 223 public void onClick(View view) { 224 hide(); 225 mActivity.showProgramGuideSearchFragment(); 226 } 227 }); 228 mSidePanelGridView.setOnChildSelectedListener( 229 new android.support.v17.leanback.widget.OnChildSelectedListener() { 230 @Override 231 public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) { 232 mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f); 233 } 234 }); 235 } else { 236 mSearchOrb = null; 237 } 238 239 mTable = mContainer.findViewById(R.id.program_guide_table); 240 241 mTimelineRow = (TimelineRow) mTable.findViewById(R.id.time_row); 242 mTimeListAdapter = new TimeListAdapter(res); 243 mTimelineRow.getRecycledViewPool().setMaxRecycledViews( 244 R.layout.program_guide_table_header_row_item, 245 res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item)); 246 mTimelineRow.setAdapter(mTimeListAdapter); 247 248 ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, 249 mProgramManager, this); 250 programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { 251 @Override 252 public void onChanged() { 253 // It is usually called when Genre is changed. 254 // Reset selection of ProgramGrid 255 resetRowSelection(); 256 updateGuidePosition(); 257 } 258 }); 259 260 mGrid = (ProgramGrid) mTable.findViewById(R.id.grid); 261 mGrid.initialize(mProgramManager); 262 mGrid.getRecycledViewPool().setMaxRecycledViews( 263 R.layout.program_guide_table_row, 264 res.getInteger(R.integer.max_recycled_view_pool_epg_table_row)); 265 mGrid.setAdapter(programTableAdapter); 266 267 mGrid.setChildFocusListener(this); 268 mGrid.setOnChildSelectedListener(new OnChildSelectedListener() { 269 @Override 270 public void onChildSelected(ViewGroup parent, View view, int position, long id) { 271 if (mIsDuringResetRowSelection) { 272 // Ignore if it's during the first resetRowSelection, because onChildSelected 273 // will be called again when rows are bound to the program table. if selectRow 274 // is called here, mSelectedRow is set and the second selectRow call doesn't 275 // work as intended. 276 mIsDuringResetRowSelection = false; 277 return; 278 } 279 selectRow(view); 280 } 281 }); 282 mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED); 283 mGrid.setWindowAlignmentOffset(mSelectionRow * mRowHeight); 284 mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 285 mGrid.setItemAlignmentOffset(0); 286 mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 287 288 RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() { 289 @Override 290 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 291 onHorizontalScrolled(dx); 292 } 293 }; 294 mTimelineRow.addOnScrollListener(onScrollListener); 295 296 mCurrentTimeIndicator = mTable.findViewById(R.id.current_time_indicator); 297 298 mShowAnimatorFull = createAnimator( 299 R.animator.program_guide_side_panel_enter_full, 300 0, 301 R.animator.program_guide_table_enter_full); 302 mShowAnimatorFull.addListener(new AnimatorListenerAdapter() { 303 @Override 304 public void onAnimationEnd(Animator animation) { 305 ((ViewGroup) mSidePanel).setDescendantFocusability( 306 ViewGroup.FOCUS_AFTER_DESCENDANTS); 307 } 308 }); 309 310 mShowAnimatorPartial = createAnimator( 311 R.animator.program_guide_side_panel_enter_partial, 312 0, 313 R.animator.program_guide_table_enter_partial); 314 mShowAnimatorPartial.addListener(new AnimatorListenerAdapter() { 315 @Override 316 public void onAnimationStart(Animator animation) { 317 mSidePanelGridView.setVisibility(View.VISIBLE); 318 mSidePanelGridView.setAlpha(1.0f); 319 } 320 }); 321 322 mHideAnimatorFull = createAnimator( 323 R.animator.program_guide_side_panel_exit, 324 0, 325 R.animator.program_guide_table_exit); 326 mHideAnimatorFull.addListener(new AnimatorListenerAdapter() { 327 @Override 328 public void onAnimationEnd(Animator animation) { 329 mContainer.setVisibility(View.GONE); 330 } 331 }); 332 mHideAnimatorPartial = createAnimator( 333 R.animator.program_guide_side_panel_exit, 334 0, 335 R.animator.program_guide_table_exit); 336 mHideAnimatorPartial.addListener(new AnimatorListenerAdapter() { 337 @Override 338 public void onAnimationEnd(Animator animation) { 339 mContainer.setVisibility(View.GONE); 340 } 341 }); 342 343 mPartialToFullAnimator = createAnimator( 344 R.animator.program_guide_side_panel_hide, 345 R.animator.program_guide_side_panel_grid_fade_out, 346 R.animator.program_guide_table_partial_to_full); 347 mFullToPartialAnimator = createAnimator( 348 R.animator.program_guide_side_panel_reveal, 349 R.animator.program_guide_side_panel_grid_fade_in, 350 R.animator.program_guide_table_full_to_partial); 351 352 mProgramTableFadeOutAnimator = AnimatorInflater.loadAnimator(mActivity, 353 R.animator.program_guide_table_fade_out); 354 mProgramTableFadeOutAnimator.setTarget(mTable); 355 mProgramTableFadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable) { 356 @Override 357 public void onAnimationEnd(Animator animation) { 358 super.onAnimationEnd(animation); 359 360 if (!isActive()) { 361 return; 362 } 363 mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); 364 resetTimelineScroll(); 365 if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { 366 mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 367 } 368 } 369 }); 370 mProgramTableFadeInAnimator = AnimatorInflater.loadAnimator(mActivity, 371 R.animator.program_guide_table_fade_in); 372 mProgramTableFadeInAnimator.setTarget(mTable); 373 mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); 374 mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity); 375 mShowGuidePartial = mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); 376 } 377 updateGuidePosition()378 private void updateGuidePosition() { 379 // Align EPG at vertical center, if EPG table height is less than the screen size. 380 Resources res = mActivity.getResources(); 381 int screenHeight = mContainer.getHeight(); 382 if (screenHeight <= 0) { 383 // mContainer is not initialized yet. 384 return; 385 } 386 int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start); 387 int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top); 388 int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom); 389 int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) 390 + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding 391 + bottomPadding; 392 if (tableHeight > screenHeight) { 393 // EPG height is longer that the screen height. 394 mTable.setPaddingRelative(startPadding, topPadding, 0, 0); 395 LayoutParams layoutParams = mTable.getLayoutParams(); 396 layoutParams.height = LayoutParams.WRAP_CONTENT; 397 mTable.setLayoutParams(layoutParams); 398 } else { 399 mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding); 400 LayoutParams layoutParams = mTable.getLayoutParams(); 401 layoutParams.height = tableHeight; 402 mTable.setLayoutParams(layoutParams); 403 } 404 } 405 406 @Override onRequestChildFocus(View oldFocus, View newFocus)407 public void onRequestChildFocus(View oldFocus, View newFocus) { 408 if (oldFocus != null && newFocus != null) { 409 int selectionRowOffset = mSelectionRow * mRowHeight; 410 if (oldFocus.getTop() < newFocus.getTop()) { 411 // Selection moves downwards 412 // Adjust scroll offset to be at the bottom of the target row and to expand up. This 413 // will set the scroll target to be one row height up from its current position. 414 mGrid.setWindowAlignmentOffset(selectionRowOffset + mRowHeight + mDetailHeight); 415 mGrid.setItemAlignmentOffsetPercent(100); 416 } else if (oldFocus.getTop() > newFocus.getTop()) { 417 // Selection moves upwards 418 // Adjust scroll offset to be at the top of the target row and to expand down. This 419 // will set the scroll target to be one row height down from its current position. 420 mGrid.setWindowAlignmentOffset(selectionRowOffset); 421 mGrid.setItemAlignmentOffsetPercent(0); 422 } 423 } 424 } 425 createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, int tableAnimResId)426 private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, 427 int tableAnimResId) { 428 List<Animator> animatorList = new ArrayList<>(); 429 430 Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId); 431 sidePanelAnimator.setTarget(mSidePanel); 432 animatorList.add(sidePanelAnimator); 433 434 if (sidePanelGridAnimResId != 0) { 435 Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity, 436 sidePanelGridAnimResId); 437 sidePanelGridAnimator.setTarget(mSidePanelGridView); 438 sidePanelGridAnimator.addListener( 439 new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView)); 440 animatorList.add(sidePanelGridAnimator); 441 } 442 Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId); 443 tableAnimator.setTarget(mTable); 444 tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); 445 animatorList.add(tableAnimator); 446 447 AnimatorSet set = new AnimatorSet(); 448 set.playTogether(animatorList); 449 return set; 450 } 451 452 /** 453 * Returns {@code true} if the program guide should process the input events. 454 */ isActive()455 public boolean isActive() { 456 return mContainer.getVisibility() == View.VISIBLE && !mHideAnimatorFull.isStarted() 457 && !mHideAnimatorPartial.isStarted(); 458 } 459 460 /** 461 * Show the program guide. This reveals the side panel, and the program guide table is shown 462 * partially. 463 * 464 * <p>Note: the animation which starts together with ProgramGuide showing animation needs to 465 * be initiated in {@code runnableAfterAnimatorReady}. If the animation starts together 466 * with show(), the animation may drop some frames. 467 */ show(final Runnable runnableAfterAnimatorReady)468 public void show(final Runnable runnableAfterAnimatorReady) { 469 if (mContainer.getVisibility() == View.VISIBLE) { 470 return; 471 } 472 mTracker.sendShowEpg(); 473 mTracker.sendScreenView(SCREEN_NAME); 474 if (mPreShowRunnable != null) { 475 mPreShowRunnable.run(); 476 } 477 mVisibleDuration.start(); 478 479 mProgramManager.programGuideVisibilityChanged(true); 480 mStartUtcTime = Utils.floorTime( 481 System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME, 482 HALF_HOUR_IN_MILLIS); 483 mProgramManager.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); 484 mProgramManager.addListener(mProgramManagerListener); 485 mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; 486 mTimeListAdapter.update(mStartUtcTime); 487 mTimelineRow.resetScroll(); 488 489 if (!mShowGuidePartial) { 490 // Avoid changing focus from the genre side panel to the grid during animation. 491 // The descendant focus is changed to FOCUS_AFTER_DESCENDANTS after the animation. 492 ((ViewGroup) mSidePanel).setDescendantFocusability( 493 ViewGroup.FOCUS_BLOCK_DESCENDANTS); 494 } 495 496 mContainer.setVisibility(View.VISIBLE); 497 positionCurrentTimeIndicator(); 498 mSidePanelGridView.setSelectedPosition(0); 499 if (DEBUG) { 500 Log.d(TAG, "show()"); 501 } 502 mOnLayoutListenerForShow = new ViewTreeObserver.OnGlobalLayoutListener() { 503 @Override 504 public void onGlobalLayout() { 505 mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); 506 mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); 507 mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 508 mTable.buildLayer(); 509 mSidePanelGridView.buildLayer(); 510 mOnLayoutListenerForShow = null; 511 mTimelineAnimation = true; 512 // Make sure that time indicator update starts after animation is finished. 513 startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); 514 if (DEBUG) { 515 mContainer.getViewTreeObserver().addOnDrawListener( 516 new ViewTreeObserver.OnDrawListener() { 517 long time = System.currentTimeMillis(); 518 int count = 0; 519 520 @Override 521 public void onDraw() { 522 long curtime = System.currentTimeMillis(); 523 Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms"); 524 time = curtime; 525 if (count > 10) { 526 mContainer.getViewTreeObserver().removeOnDrawListener(this); 527 } 528 } 529 }); 530 } 531 runnableAfterAnimatorReady.run(); 532 if (mShowGuidePartial) { 533 mShowAnimatorPartial.start(); 534 } else { 535 mShowAnimatorFull.start(); 536 } 537 updateGuidePosition(); 538 } 539 }; 540 mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); 541 scheduleHide(); 542 } 543 544 /** 545 * Hide the program guide. 546 */ hide()547 public void hide() { 548 if (!isActive()) { 549 return; 550 } 551 if (mOnLayoutListenerForShow != null) { 552 mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(mOnLayoutListenerForShow); 553 mOnLayoutListenerForShow = null; 554 } 555 mTracker.sendHideEpg(mVisibleDuration.reset()); 556 cancelHide(); 557 mProgramManager.programGuideVisibilityChanged(false); 558 mProgramManager.removeListener(mProgramManagerListener); 559 if (isFull()) { 560 mHideAnimatorFull.start(); 561 } else { 562 mHideAnimatorPartial.start(); 563 } 564 565 // Clears fade-out/in animation for genre change 566 if (mProgramTableFadeOutAnimator.isRunning()) { 567 mProgramTableFadeOutAnimator.cancel(); 568 } 569 if (mProgramTableFadeInAnimator.isRunning()) { 570 mProgramTableFadeInAnimator.cancel(); 571 } 572 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 573 mTable.setAlpha(1.0f); 574 575 mTimelineAnimation = false; 576 stopCurrentTimeIndicator(); 577 if (mPostHideRunnable != null) { 578 mPostHideRunnable.run(); 579 } 580 } 581 scheduleHide()582 public void scheduleHide() { 583 cancelHide(); 584 mHandler.postDelayed(mHideRunnable, mShowDurationMillis); 585 } 586 587 /** 588 * Returns the scroll offset of the time line row in pixels. 589 */ getTimelineRowScrollOffset()590 public int getTimelineRowScrollOffset() { 591 return mTimelineRow.getScrollOffset(); 592 } 593 594 /** 595 * Cancel hiding the program guide. 596 */ cancelHide()597 public void cancelHide() { 598 mHandler.removeCallbacks(mHideRunnable); 599 } 600 601 // Returns if program table is full screen mode. isFull()602 private boolean isFull() { 603 return mPartialToFullAnimator.isStarted() || mTable.getTranslationX() == 0; 604 } 605 startFull()606 private void startFull() { 607 if (isFull()) { 608 return; 609 } 610 mShowGuidePartial = false; 611 mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); 612 mPartialToFullAnimator.start(); 613 } 614 startPartial()615 private void startPartial() { 616 if (!isFull()) { 617 return; 618 } 619 mShowGuidePartial = true; 620 mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); 621 mFullToPartialAnimator.start(); 622 } 623 624 /** 625 * Process the {@code KEYCODE_BACK} key event. 626 */ onBackPressed()627 public void onBackPressed() { 628 hide(); 629 } 630 631 /** 632 * Gets {@link VerticalGridView} for "genre select" side panel. 633 */ getSidePanel()634 public VerticalGridView getSidePanel() { 635 return mSidePanelGridView; 636 } 637 638 /** 639 * Requests change genre to {@code genreId}. 640 */ requestGenreChange(int genreId)641 public void requestGenreChange(int genreId) { 642 if (mLastRequestedGenreId == genreId) { 643 // When Recycler.onLayout() removes its children to recycle, 644 // View tries to find next focus candidate immediately 645 // so GenreListAdapter can take focus back while it's hiding. 646 // Returns early here to prevent re-entrance. 647 return; 648 } 649 mLastRequestedGenreId = genreId; 650 if (mProgramTableFadeOutAnimator.isStarted()) { 651 // When requestGenreChange is called repeatedly in short time, we keep the fade-out 652 // state for mTableFadeAnimDuration from now. Without it, we'll see blinks. 653 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 654 mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM, 655 mTableFadeAnimDuration); 656 return; 657 } 658 if (mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) { 659 mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId); 660 mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM); 661 mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM, 662 mTableFadeAnimDuration); 663 return; 664 } 665 if (mProgramTableFadeInAnimator.isStarted()) { 666 mProgramTableFadeInAnimator.cancel(); 667 } 668 669 mProgramTableFadeOutAnimator.start(); 670 } 671 startCurrentTimeIndicator(long initialDelay)672 private void startCurrentTimeIndicator(long initialDelay) { 673 mHandler.postDelayed(mUpdateTimeIndicator, initialDelay); 674 } 675 stopCurrentTimeIndicator()676 private void stopCurrentTimeIndicator() { 677 mHandler.removeCallbacks(mUpdateTimeIndicator); 678 } 679 positionCurrentTimeIndicator()680 private void positionCurrentTimeIndicator() { 681 int offset = GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis()) 682 - mTimelineRow.getScrollOffset(); 683 if (offset < 0) { 684 mCurrentTimeIndicator.setVisibility(View.GONE); 685 } else { 686 if (mCurrentTimeIndicatorWidth == 0) { 687 mCurrentTimeIndicator.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 688 mCurrentTimeIndicatorWidth = mCurrentTimeIndicator.getMeasuredWidth(); 689 } 690 mCurrentTimeIndicator.setPaddingRelative( 691 offset - mCurrentTimeIndicatorWidth / 2, 0, 0, 0); 692 mCurrentTimeIndicator.setVisibility(View.VISIBLE); 693 } 694 } 695 resetTimelineScroll()696 private void resetTimelineScroll() { 697 if (mProgramManager.getFromUtcMillis() != mStartUtcTime) { 698 boolean timelineAnimation = mTimelineAnimation; 699 mTimelineAnimation = false; 700 // mProgramManagerListener.onTimeRangeUpdated() will be called by shiftTime(). 701 mProgramManager.shiftTime(mStartUtcTime - mProgramManager.getFromUtcMillis()); 702 mTimelineAnimation = timelineAnimation; 703 } 704 } 705 onHorizontalScrolled(int dx)706 private void onHorizontalScrolled(int dx) { 707 if (DEBUG) Log.d(TAG, "onHorizontalScrolled(dx=" + dx + ")"); 708 positionCurrentTimeIndicator(); 709 for (int i = 0, n = mGrid.getChildCount(); i < n; ++i) { 710 mGrid.getChildAt(i).findViewById(R.id.row).scrollBy(dx, 0); 711 } 712 } 713 resetRowSelection()714 private void resetRowSelection() { 715 if (mDetailOutAnimator != null) { 716 mDetailOutAnimator.end(); 717 } 718 if (mDetailInAnimator != null) { 719 mDetailInAnimator.cancel(); 720 } 721 mSelectedRow = null; 722 mIsDuringResetRowSelection = true; 723 mGrid.setSelectedPosition( 724 Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 725 0)); 726 mGrid.resetFocusState(); 727 mGrid.onItemSelectionReset(); 728 mIsDuringResetRowSelection = false; 729 } 730 selectRow(View row)731 private void selectRow(View row) { 732 if (row == null || row == mSelectedRow) { 733 return; 734 } 735 if (mSelectedRow == null 736 || mGrid.getChildAdapterPosition(mSelectedRow) == RecyclerView.NO_POSITION) { 737 if (mSelectedRow != null) { 738 View oldDetailView = mSelectedRow.findViewById(R.id.detail); 739 oldDetailView.setVisibility(View.GONE); 740 } 741 View detailView = row.findViewById(R.id.detail); 742 detailView.findViewById(R.id.detail_content_full).setAlpha(1); 743 detailView.findViewById(R.id.detail_content_full).setTranslationY(0); 744 setLayoutHeight(detailView, mDetailHeight); 745 detailView.setVisibility(View.VISIBLE); 746 747 final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row); 748 programRow.post(new Runnable() { 749 @Override 750 public void run() { 751 programRow.focusCurrentProgram(); 752 } 753 }); 754 } else { 755 animateRowChange(mSelectedRow, row); 756 } 757 mSelectedRow = row; 758 } 759 animateRowChange(View outRow, View inRow)760 private void animateRowChange(View outRow, View inRow) { 761 if (mDetailOutAnimator != null) { 762 mDetailOutAnimator.end(); 763 } 764 if (mDetailInAnimator != null) { 765 mDetailInAnimator.cancel(); 766 } 767 768 int direction = 0; 769 if (outRow != null && inRow != null) { 770 // -1 means the selection goes downwards and 1 goes upwards 771 direction = outRow.getTop() < inRow.getTop() ? -1 : 1; 772 } 773 774 View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null; 775 if (outDetail != null && outDetail.isShown()) { 776 final View outDetailContent = outDetail.findViewById(R.id.detail_content_full); 777 778 Animator fadeOutAnimator = ObjectAnimator.ofPropertyValuesHolder(outDetailContent, 779 PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f), 780 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 781 outDetailContent.getTranslationY(), direction * mDetailPadding)); 782 fadeOutAnimator.setStartDelay(0); 783 fadeOutAnimator.setDuration(mAnimationDuration); 784 fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent)); 785 786 Animator collapseAnimator = 787 createHeightAnimator(outDetail, getLayoutHeight(outDetail), 0); 788 collapseAnimator.setStartDelay(mAnimationDuration); 789 collapseAnimator.setDuration(mTableFadeAnimDuration); 790 collapseAnimator.addListener(new AnimatorListenerAdapter() { 791 @Override 792 public void onAnimationStart(Animator animator) { 793 outDetailContent.setVisibility(View.GONE); 794 } 795 796 @Override 797 public void onAnimationEnd(Animator animator) { 798 outDetailContent.setVisibility(View.VISIBLE); 799 } 800 }); 801 802 AnimatorSet outAnimator = new AnimatorSet(); 803 outAnimator.playTogether(fadeOutAnimator, collapseAnimator); 804 outAnimator.addListener(new AnimatorListenerAdapter() { 805 @Override 806 public void onAnimationEnd(Animator animator) { 807 mDetailOutAnimator = null; 808 } 809 }); 810 mDetailOutAnimator = outAnimator; 811 outAnimator.start(); 812 } 813 814 View inDetail = inRow != null ? inRow.findViewById(R.id.detail) : null; 815 if (inDetail != null) { 816 final View inDetailContent = inDetail.findViewById(R.id.detail_content_full); 817 818 Animator expandAnimator = createHeightAnimator(inDetail, 0, mDetailHeight); 819 expandAnimator.setStartDelay(mAnimationDuration); 820 expandAnimator.setDuration(mTableFadeAnimDuration); 821 expandAnimator.addListener(new AnimatorListenerAdapter() { 822 @Override 823 public void onAnimationStart(Animator animator) { 824 inDetailContent.setVisibility(View.GONE); 825 } 826 827 @Override 828 public void onAnimationEnd(Animator animator) { 829 inDetailContent.setVisibility(View.VISIBLE); 830 inDetailContent.setAlpha(0); 831 } 832 }); 833 834 Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent, 835 PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), 836 PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 837 direction * -mDetailPadding, 0f)); 838 fadeInAnimator.setStartDelay(mAnimationDuration + mTableFadeAnimDuration); 839 fadeInAnimator.setDuration(mAnimationDuration); 840 fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); 841 842 AnimatorSet inAnimator = new AnimatorSet(); 843 inAnimator.playTogether(expandAnimator, fadeInAnimator); 844 inAnimator.addListener(new AnimatorListenerAdapter() { 845 @Override 846 public void onAnimationEnd(Animator animator) { 847 mDetailInAnimator = null; 848 } 849 }); 850 mDetailInAnimator = inAnimator; 851 inAnimator.start(); 852 } 853 } 854 855 private Animator createHeightAnimator( 856 final View target, int initialHeight, int targetHeight) { 857 ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight); 858 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 859 @Override 860 public void onAnimationUpdate(ValueAnimator animation) { 861 int value = (Integer) animation.getAnimatedValue(); 862 if (value == 0) { 863 if (target.getVisibility() != View.GONE) { 864 target.setVisibility(View.GONE); 865 } 866 } else { 867 if (target.getVisibility() != View.VISIBLE) { 868 target.setVisibility(View.VISIBLE); 869 } 870 setLayoutHeight(target, value); 871 } 872 } 873 }); 874 return animator; 875 } 876 877 private int getLayoutHeight(View view) { 878 LayoutParams layoutParams = view.getLayoutParams(); 879 return layoutParams.height; 880 } 881 882 private void setLayoutHeight(View view, int height) { 883 LayoutParams layoutParams = view.getLayoutParams(); 884 if (height != layoutParams.height) { 885 layoutParams.height = height; 886 view.setLayoutParams(layoutParams); 887 } 888 } 889 890 private class GlobalFocusChangeListener implements 891 ViewTreeObserver.OnGlobalFocusChangeListener { 892 private static final int UNKNOWN = 0; 893 private static final int SIDE_PANEL = 1; 894 private static final int PROGRAM_TABLE = 2; 895 896 @Override 897 public void onGlobalFocusChanged(View oldFocus, View newFocus) { 898 if (DEBUG) Log.d(TAG, "onGlobalFocusChanged " + oldFocus + " -> " + newFocus); 899 if (!isActive()) { 900 return; 901 } 902 int fromLocation = getLocation(oldFocus); 903 int toLocation = getLocation(newFocus); 904 if (fromLocation == SIDE_PANEL && toLocation == PROGRAM_TABLE) { 905 startFull(); 906 } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) { 907 startPartial(); 908 } 909 } 910 911 private int getLocation(View view) { 912 if (view == null) { 913 return UNKNOWN; 914 } 915 for (Object obj = view; obj instanceof View; obj = ((View) obj).getParent()) { 916 if (obj == mSidePanel) { 917 return SIDE_PANEL; 918 } else if (obj == mGrid) { 919 return PROGRAM_TABLE; 920 } 921 } 922 return UNKNOWN; 923 } 924 } 925 926 private class ProgramManagerListener extends ProgramManager.ListenerAdapter { 927 @Override 928 public void onTimeRangeUpdated() { 929 int scrollOffset = (int) (mWidthPerHour * mProgramManager.getShiftedTime() 930 / HOUR_IN_MILLIS); 931 if (DEBUG) { 932 Log.d(TAG, "Horizontal scroll to " + scrollOffset + " pixels (" 933 + mProgramManager.getShiftedTime() + " millis)"); 934 } 935 mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation); 936 } 937 } 938 939 private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> { 940 public ProgramGuideHandler(ProgramGuide ref) { 941 super(ref); 942 } 943 944 @Override 945 public void handleMessage(Message msg, @NonNull ProgramGuide programGuide) { 946 if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) { 947 programGuide.mProgramTableFadeInAnimator.start(); 948 } 949 } 950 } 951 } 952