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