aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/guide/ProgramGuide.java
diff options
context:
space:
mode:
authorNick Chalko <nchalko@google.com>2015-08-03 15:39:56 -0700
committerNick Chalko <nchalko@google.com>2015-08-03 15:53:37 -0700
commit816a4be1a0f34f6a48877c8afd3dbbca19eac435 (patch)
tree4f18dda269764494942f5313acc93db4a35d47db /src/com/android/tv/guide/ProgramGuide.java
parent6edd2b09e5d16a29c703a5fcbd2e88c5cf5e55b7 (diff)
downloadTV-816a4be1a0f34f6a48877c8afd3dbbca19eac435.tar.gz
Migrate Live Channels App Src to AOSP branch
Bug: 21625152 Change-Id: I07e2830b27440556dc757e6340b4f77d1c0cbc66
Diffstat (limited to 'src/com/android/tv/guide/ProgramGuide.java')
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java899
1 files changed, 899 insertions, 0 deletions
diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java
new file mode 100644
index 00000000..03bda694
--- /dev/null
+++ b/src/com/android/tv/guide/ProgramGuide.java
@@ -0,0 +1,899 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tv.guide;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Point;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.support.v17.leanback.widget.OnChildSelectedListener;
+import android.support.v17.leanback.widget.SearchOrbView;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+
+import com.android.tv.ChannelTuner;
+import com.android.tv.MainActivity;
+import com.android.tv.R;
+import com.android.tv.analytics.DurationTimer;
+import com.android.tv.analytics.Tracker;
+import com.android.tv.data.ChannelDataManager;
+import com.android.tv.data.GenreItems;
+import com.android.tv.data.ProgramDataManager;
+import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter;
+import com.android.tv.util.SystemProperties;
+import com.android.tv.util.TvInputManagerHelper;
+import com.android.tv.util.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The program guide.
+ */
+public class ProgramGuide implements ProgramGrid.ChildFocusListener {
+ private static final String TAG = "ProgramGuide";
+ private static final boolean DEBUG = false;
+
+ // Whether we should show the guide partially. The first time the user enters the program guide,
+ // we show the grid partially together with the genre side panel on the left. Next time
+ // the program guide is entered, we recover the previous state (partial or full).
+ private static final String KEY_SHOW_GUIDE_PARTIAL = "show_guide_partial";
+ private static final long TIME_INDICATOR_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1);
+ private static final long HOUR_IN_MILLIS = TimeUnit.HOURS.toMillis(1);
+ private static final long HALF_HOUR_IN_MILLIS = HOUR_IN_MILLIS / 2;
+ // We keep the duration between mStartTime and the current time larger than this value.
+ // We clip out the first program entry in ProgramManager, if it does not have enough width.
+ // In order to prevent from clipping out the current program, this value need be larger than
+ // or equal to ProgramManager.FIRST_ENTRY_MIN_DURATION.
+ private static final long MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME
+ = ProgramManager.FIRST_ENTRY_MIN_DURATION;
+
+ private static final int MSG_PROGRAM_TABLE_FADE_IN_ANIM = 1000;
+
+ private static final int SELECTION_ROW = 2; // Row that is focused
+ private static final String SCREEN_NAME = "EPG";
+
+ private final MainActivity mActivity;
+ private final ProgramManager mProgramManager;
+ private final ChannelTuner mChannelTuner;
+ private final Tracker mTracker;
+ private final DurationTimer mVisibleDuration = new DurationTimer();
+ private final Runnable mPreShowRunnable;
+ private final Runnable mPostHideRunnable;
+
+ private final int mWidthPerHour;
+ private final long mViewPortMillis;
+ private final int mRowHeight;
+ private final int mDetailHeight;
+ private final int mTableFadeAnimDuration;
+ private final int mAnimationDuration;
+ private final int mDetailPadding;
+ private final SearchOrbView mSearchOrb;
+ private int mCurrentTimeIndicatorWidth;
+
+ private final View mContainer;
+ private final View mSidePanel;
+ private final VerticalGridView mSidePanelGridView;
+ private final View mTable;
+ private final TimelineRow mTimelineRow;
+ private final ProgramGrid mGrid;
+ private final TimeListAdapter mTimeListAdapter;
+ private final View mCurrentTimeIndicator;
+
+ private final Animator mShowAnimatorFull;
+ private final Animator mShowAnimatorPartial;
+ // mHideAnimatorFull and mHideAnimatorPartial are created from the same animation xmls.
+ // When we share the one animator for two different animations, the starting value
+ // is broken, even though the starting value is not defined in XML.
+ private final Animator mHideAnimatorFull;
+ private final Animator mHideAnimatorPartial;
+ private final Animator mPartialToFullAnimator;
+ private final Animator mFullToPartialAnimator;
+ private final Animator mProgramTableFadeOutAnimator;
+ private final Animator mProgramTableFadeInAnimator;
+
+ // When the program guide is popped up, we keep the previous state of the guide.
+ private boolean mShowGuidePartial;
+ private final SharedPreferences mSharedPreference;
+ private View mSelectedRow;
+ private Animator mDetailOutAnimator;
+ private Animator mDetailInAnimator;
+
+ private long mStartUtcTime;
+ private boolean mTimelineAnimation;
+ private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS;
+ private boolean mIsDuringResetRowSelection;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) {
+ mProgramTableFadeInAnimator.start();
+ }
+ }
+ };
+
+ private final Runnable mHideRunnable = new Runnable() {
+ @Override
+ public void run() {
+ hide();
+ }
+ };
+ private final long mShowDurationMillis;
+
+ private final ProgramManagerListener mProgramManagerListener = new ProgramManagerListener();
+
+ private final Runnable mUpdateTimeIndicator = new Runnable() {
+ @Override
+ public void run() {
+ positionCurrentTimeIndicator();
+ mHandler.postAtTime(this,
+ Utils.ceilTime(SystemClock.uptimeMillis(), TIME_INDICATOR_UPDATE_FREQUENCY));
+ }
+ };
+
+ public ProgramGuide(MainActivity activity, ChannelTuner channelTuner,
+ TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager,
+ ProgramDataManager programDataManager, Tracker tracker, Runnable preShowRunnable,
+ Runnable postHideRunnable) {
+ mActivity = activity;
+ mProgramManager = new ProgramManager(tvInputManagerHelper,
+ channelDataManager,
+ programDataManager);
+ mChannelTuner = channelTuner;
+ mTracker = tracker;
+ mPreShowRunnable = preShowRunnable;
+ mPostHideRunnable = postHideRunnable;
+
+ Resources res = activity.getResources();
+
+ mWidthPerHour = res.getDimensionPixelSize(R.dimen.program_guide_table_width_per_hour);
+ GuideUtils.setWidthPerHour(mWidthPerHour);
+
+ Point displaySize = new Point();
+ mActivity.getWindowManager().getDefaultDisplay().getSize(displaySize);
+ int gridWidth = displaySize.x
+ - res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start)
+ - res.getDimensionPixelSize(R.dimen.program_guide_table_header_column_width);
+ mViewPortMillis = (gridWidth * HOUR_IN_MILLIS) / mWidthPerHour;
+
+ mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height);
+ mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height);
+ mTableFadeAnimDuration =
+ res.getInteger(R.integer.program_guide_table_detail_fade_anim_duration);
+ mShowDurationMillis = res.getInteger(R.integer.program_guide_show_duration);
+ mAnimationDuration =
+ res.getInteger(R.integer.program_guide_table_detail_toggle_anim_duration);
+ mDetailPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_detail_padding);
+
+ mContainer = mActivity.findViewById(R.id.program_guide);
+ ViewTreeObserver.OnGlobalFocusChangeListener globalFocusChangeListener
+ = new GlobalFocusChangeListener();
+ mContainer.getViewTreeObserver().addOnGlobalFocusChangeListener(globalFocusChangeListener);
+
+ GenreListAdapter genreListAdapter = new GenreListAdapter(mActivity, mProgramManager, this);
+ mSidePanel = mContainer.findViewById(R.id.program_guide_side_panel);
+ mSidePanelGridView = (VerticalGridView) mContainer.findViewById(
+ R.id.program_guide_side_panel_grid_view);
+ mSidePanelGridView.getRecycledViewPool().setMaxRecycledViews(
+ R.layout.program_guide_side_panel_row,
+ res.getInteger(R.integer.max_recycled_view_pool_epg_side_panel_row));
+ mSidePanelGridView.setAdapter(genreListAdapter);
+ mSidePanelGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ mSidePanelGridView.setWindowAlignmentOffset(mActivity.getResources()
+ .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y));
+ mSidePanelGridView.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ // TODO: Remove this check when we ship TV with epg search enabled.
+ if (SystemProperties.USE_EPG_SEARCH.getValue()) {
+ mSearchOrb = (SearchOrbView) mContainer.findViewById(
+ R.id.program_guide_side_panel_search_orb);
+ mSearchOrb.setVisibility(View.VISIBLE);
+
+ mSearchOrb.setOnOrbClickedListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ hide();
+ mActivity.showProgramGuideSearchFragment();
+ }
+ });
+ mSidePanelGridView.setOnChildSelectedListener(
+ new android.support.v17.leanback.widget.OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup viewGroup, View view, int i, long l) {
+ mSearchOrb.animate().alpha(i == 0 ? 1.0f : 0.0f);
+ }
+ });
+ } else {
+ mSearchOrb = null;
+ }
+
+ mTable = mContainer.findViewById(R.id.program_guide_table);
+
+ mTimelineRow = (TimelineRow) mTable.findViewById(R.id.time_row);
+ mTimeListAdapter = new TimeListAdapter(res);
+ mTimelineRow.getRecycledViewPool().setMaxRecycledViews(
+ R.layout.program_guide_table_header_row_item,
+ res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item));
+ mTimelineRow.setAdapter(mTimeListAdapter);
+
+ ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity,
+ tvInputManagerHelper, mProgramManager, this);
+ programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
+ @Override
+ public void onChanged() {
+ // It is usually called when Genre is changed.
+ // Reset selection of ProgramGrid
+ resetRowSelection();
+
+ // Align EPG at vertical center, if EPG table height is less than the screen size.
+ Resources res = mActivity.getResources();
+ int screenHeight = mContainer.getHeight();
+ int startPadding = res
+ .getDimensionPixelOffset(R.dimen.program_guide_table_margin_start);
+ int topPadding = res
+ .getDimensionPixelOffset(R.dimen.program_guide_table_margin_top);
+ int bottomPadding = res
+ .getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom);
+ int tableHeight =
+ res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height)
+ + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount()
+ + topPadding + bottomPadding;
+ if (tableHeight > screenHeight) {
+ // EPG height is longer that the screen height.
+ mTable.setPaddingRelative(startPadding, topPadding, 0, 0);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = LayoutParams.WRAP_CONTENT;
+ mTable.setLayoutParams(layoutParams);
+ } else {
+ mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding);
+ LayoutParams layoutParams = mTable.getLayoutParams();
+ layoutParams.height = tableHeight;
+ mTable.setLayoutParams(layoutParams);
+ }
+ }
+ });
+
+ mGrid = (ProgramGrid) mTable.findViewById(R.id.grid);
+ mGrid.initialize(mProgramManager);
+ mGrid.getRecycledViewPool().setMaxRecycledViews(
+ R.layout.program_guide_table_row,
+ res.getInteger(R.integer.max_recycled_view_pool_epg_table_row));
+ mGrid.setAdapter(programTableAdapter);
+
+ mGrid.setChildFocusListener(this);
+ mGrid.setOnChildSelectedListener(new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ if (mIsDuringResetRowSelection) {
+ // Ignore if it's during the first resetRowSelection, because onChildSelected
+ // will be called again when rows are bound to the program table. if selectRow
+ // is called here, mSelectedRow is set and the second selectRow call doesn't
+ // work as intended.
+ mIsDuringResetRowSelection = false;
+ return;
+ }
+ selectRow(view);
+ }
+ });
+ mGrid.setFocusScrollStrategy(ProgramGrid.FOCUS_SCROLL_ALIGNED);
+ mGrid.setWindowAlignmentOffset(SELECTION_ROW * mRowHeight);
+ mGrid.setWindowAlignmentOffsetPercent(ProgramGrid.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mGrid.setItemAlignmentOffset(0);
+ mGrid.setItemAlignmentOffsetPercent(ProgramGrid.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+
+ RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ onHorizontalScrolled(dx);
+ }
+ };
+ mTimelineRow.addOnScrollListener(onScrollListener);
+
+ mCurrentTimeIndicator = mTable.findViewById(R.id.current_time_indicator);
+
+ mShowAnimatorFull = createAnimator(
+ R.animator.program_guide_side_panel_enter_full,
+ 0,
+ R.animator.program_guide_table_enter_full);
+ mShowAnimatorFull.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ ((ViewGroup) mSidePanel).setDescendantFocusability(
+ ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ }
+ });
+
+ mShowAnimatorPartial = createAnimator(
+ R.animator.program_guide_side_panel_enter_partial,
+ 0,
+ R.animator.program_guide_table_enter_partial);
+ mShowAnimatorPartial.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mSidePanelGridView.setVisibility(View.VISIBLE);
+ mSidePanelGridView.setAlpha(1.0f);
+ }
+ });
+
+ mHideAnimatorFull = createAnimator(
+ R.animator.program_guide_side_panel_exit,
+ 0,
+ R.animator.program_guide_table_exit);
+ mHideAnimatorFull.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+ });
+ mHideAnimatorPartial = createAnimator(
+ R.animator.program_guide_side_panel_exit,
+ 0,
+ R.animator.program_guide_table_exit);
+ mHideAnimatorPartial.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mContainer.setVisibility(View.INVISIBLE);
+ }
+ });
+
+ mPartialToFullAnimator = createAnimator(
+ R.animator.program_guide_side_panel_hide,
+ R.animator.program_guide_side_panel_grid_fade_out,
+ R.animator.program_guide_table_partial_to_full);
+ mFullToPartialAnimator = createAnimator(
+ R.animator.program_guide_side_panel_reveal,
+ R.animator.program_guide_side_panel_grid_fade_in,
+ R.animator.program_guide_table_full_to_partial);
+
+ mProgramTableFadeOutAnimator = AnimatorInflater.loadAnimator(mActivity,
+ R.animator.program_guide_table_fade_out);
+ mProgramTableFadeOutAnimator.setTarget(mTable);
+ mProgramTableFadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable) {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+
+ if (!isActive()) {
+ return;
+ }
+ mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId);
+ resetTimelineScroll();
+ if (!mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) {
+ mHandler.sendEmptyMessage(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
+ }
+ }
+ });
+ mProgramTableFadeInAnimator = AnimatorInflater.loadAnimator(mActivity,
+ R.animator.program_guide_table_fade_in);
+ mProgramTableFadeInAnimator.setTarget(mTable);
+ mProgramTableFadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
+ mSharedPreference = PreferenceManager.getDefaultSharedPreferences(mActivity);
+ mShowGuidePartial = mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true);
+ }
+
+ @Override
+ public void onRequestChildFocus(View oldFocus, View newFocus) {
+ if (oldFocus != null && newFocus != null) {
+ int selectionRowOffset = SELECTION_ROW * mRowHeight;
+ if (oldFocus.getTop() < newFocus.getTop()) {
+ // Selection moves downwards
+ // Adjust scroll offset to be at the bottom of the target row and to expand up. This
+ // will set the scroll target to be one row height up from its current position.
+ mGrid.setWindowAlignmentOffset(selectionRowOffset + mRowHeight + mDetailHeight);
+ mGrid.setItemAlignmentOffsetPercent(100);
+ } else if (oldFocus.getTop() > newFocus.getTop()) {
+ // Selection moves upwards
+ // Adjust scroll offset to be at the top of the target row and to expand down. This
+ // will set the scroll target to be one row height down from its current position.
+ mGrid.setWindowAlignmentOffset(selectionRowOffset);
+ mGrid.setItemAlignmentOffsetPercent(0);
+ }
+ }
+ }
+
+ private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId,
+ int tableAnimResId) {
+ List<Animator> animatorList = new ArrayList<>();
+
+ Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId);
+ sidePanelAnimator.setTarget(mSidePanel);
+ animatorList.add(sidePanelAnimator);
+
+ if (sidePanelGridAnimResId != 0) {
+ Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity,
+ sidePanelGridAnimResId);
+ sidePanelGridAnimator.setTarget(mSidePanelGridView);
+ sidePanelGridAnimator.addListener(
+ new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView));
+ animatorList.add(sidePanelGridAnimator);
+ }
+ Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId);
+ tableAnimator.setTarget(mTable);
+ tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable));
+ animatorList.add(tableAnimator);
+
+ AnimatorSet set = new AnimatorSet();
+ set.playTogether(animatorList);
+ return set;
+ }
+
+ /**
+ * Returns {@code true} if the program guide should process the input events.
+ */
+ public boolean isActive() {
+ return mContainer.getVisibility() == View.VISIBLE && !mHideAnimatorFull.isStarted()
+ && !mHideAnimatorPartial.isStarted();
+ }
+
+ /**
+ * Show the program guide. This reveals the side panel, and the program guide table is shown
+ * partially.
+ */
+ public void show() {
+ if (mContainer.getVisibility() == View.VISIBLE) {
+ return;
+ }
+ mTracker.sendShowEpg();
+ mTracker.sendScreenView(SCREEN_NAME);
+ if (mPreShowRunnable != null) {
+ mPreShowRunnable.run();
+ }
+ mVisibleDuration.start();
+
+ mProgramManager.programGuideVisibilityChanged(true);
+ mStartUtcTime = Utils.floorTime(
+ System.currentTimeMillis() - MIN_DURATION_FROM_START_TIME_TO_CURRENT_TIME,
+ HALF_HOUR_IN_MILLIS);
+ mProgramManager.setInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis);
+ mProgramManager.addListener(mProgramManagerListener);
+ mProgramManager.buildGenreFilters();
+ mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS;
+ mTimeListAdapter.update(mStartUtcTime);
+ mTimelineRow.resetScroll();
+
+ if (!mShowGuidePartial) {
+ // Avoid changing focus from the genre side panel to the grid during animation.
+ // The descendant focus is changed to FOCUS_AFTER_DESCENDANTS after the animation.
+ ((ViewGroup) mSidePanel).setDescendantFocusability(
+ ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+
+ mContainer.setVisibility(View.VISIBLE);
+ mSidePanelGridView.setSelectedPosition(0);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // setVisibility is not immediately applied. In order to start animation after
+ // making it visible, we post mShowAnimatorXXX.start() instead of calling
+ // mShowAnimatorXXX.start() in show().
+ if (mShowGuidePartial) {
+ mShowAnimatorPartial.start();
+ } else {
+ mShowAnimatorFull.start();
+ }
+ }
+ });
+
+ mTimelineAnimation = true;
+ startCurrentTimeIndicator();
+ scheduleHide();
+ }
+
+ /**
+ * Hide the program guide.
+ */
+ public void hide() {
+ if (!isActive()) {
+ return;
+ }
+ mTracker.sendHideEpg(mVisibleDuration.reset());
+ cancelHide();
+ mProgramManager.programGuideVisibilityChanged(false);
+ mProgramManager.removeListener(mProgramManagerListener);
+ if (isFull()) {
+ mHideAnimatorFull.start();
+ } else {
+ mHideAnimatorPartial.start();
+ }
+
+ // Clears fade-out/in animation for genre change
+ if (mProgramTableFadeOutAnimator.isRunning()) {
+ mProgramTableFadeOutAnimator.cancel();
+ }
+ if (mProgramTableFadeInAnimator.isRunning()) {
+ mProgramTableFadeInAnimator.cancel();
+ }
+ mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
+ mTable.setAlpha(1.0f);
+
+ mTimelineAnimation = false;
+ stopCurrentTimeIndicator();
+ if (mPostHideRunnable != null) {
+ mPostHideRunnable.run();
+ }
+ }
+
+ public void scheduleHide() {
+ cancelHide();
+ mHandler.postDelayed(mHideRunnable, mShowDurationMillis);
+ }
+
+ /**
+ * Returns the scroll offset of the time line row in pixels.
+ */
+ public int getTimelineRowScrollOffset() {
+ return mTimelineRow.getScrollOffset();
+ }
+
+ private void cancelHide() {
+ mHandler.removeCallbacks(mHideRunnable);
+ }
+
+ // Returns if program table is full screen mode.
+ private boolean isFull() {
+ return mPartialToFullAnimator.isStarted() || mTable.getTranslationX() == 0;
+ }
+
+ private void startFull() {
+ if (isFull()) {
+ return;
+ }
+ mShowGuidePartial = false;
+ mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
+ mPartialToFullAnimator.start();
+ }
+
+ private void startPartial() {
+ if (!isFull()) {
+ return;
+ }
+ mShowGuidePartial = true;
+ mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply();
+ mFullToPartialAnimator.start();
+ }
+
+ /**
+ * Process the {@code KEYCODE_BACK} key event.
+ */
+ public void onBackPressed() {
+ hide();
+ }
+
+ /**
+ * Gets {@link VerticalGridView} for "genre select" side panel.
+ */
+ public VerticalGridView getSidePanel() {
+ return mSidePanelGridView;
+ }
+
+ /**
+ * Requests change genre to {@code genreId}.
+ */
+ public void requestGenreChange(int genreId) {
+ if (mLastRequestedGenreId == genreId) {
+ // When Recycler.onLayout() removes its children to recycle,
+ // View tries to find next focus candidate immediately
+ // so GenreListAdapter can take focus back while it's hiding.
+ // Returns early here to prevent re-entrance.
+ return;
+ }
+ mLastRequestedGenreId = genreId;
+ if (mProgramTableFadeOutAnimator.isStarted()) {
+ // When requestGenreChange is called repeatedly in short time, we keep the fade-out
+ // state for mTableFadeAnimDuration from now. Without it, we'll see blinks.
+ mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
+ mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM,
+ mTableFadeAnimDuration);
+ return;
+ }
+ if (mHandler.hasMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM)) {
+ mProgramManager.resetChannelListWithGenre(mLastRequestedGenreId);
+ mHandler.removeMessages(MSG_PROGRAM_TABLE_FADE_IN_ANIM);
+ mHandler.sendEmptyMessageDelayed(MSG_PROGRAM_TABLE_FADE_IN_ANIM,
+ mTableFadeAnimDuration);
+ return;
+ }
+ if (mProgramTableFadeInAnimator.isStarted()) {
+ mProgramTableFadeInAnimator.cancel();
+ }
+
+ mProgramTableFadeOutAnimator.start();
+ }
+
+ private void startCurrentTimeIndicator() {
+ mHandler.post(mUpdateTimeIndicator);
+ }
+
+ private void stopCurrentTimeIndicator() {
+ mHandler.removeCallbacks(mUpdateTimeIndicator);
+ }
+
+ private void positionCurrentTimeIndicator() {
+ int offset = GuideUtils.convertMillisToPixel(mStartUtcTime, System.currentTimeMillis())
+ - mTimelineRow.getScrollOffset();
+ if (offset < 0) {
+ mCurrentTimeIndicator.setVisibility(View.GONE);
+ } else {
+ if (mCurrentTimeIndicatorWidth == 0) {
+ mCurrentTimeIndicator.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mCurrentTimeIndicatorWidth = mCurrentTimeIndicator.getMeasuredWidth();
+ }
+ mCurrentTimeIndicator.setPaddingRelative(
+ offset - mCurrentTimeIndicatorWidth / 2, 0, 0, 0);
+ mCurrentTimeIndicator.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void resetTimelineScroll() {
+ if (mProgramManager.getFromUtcMillis() != mStartUtcTime) {
+ boolean timelineAnimation = mTimelineAnimation;
+ mTimelineAnimation = false;
+ // mProgramManagerListener.onTimeRangeUpdated() will be called by shiftTime().
+ mProgramManager.shiftTime(mStartUtcTime - mProgramManager.getFromUtcMillis());
+ mTimelineAnimation = timelineAnimation;
+ }
+ }
+
+ private void onHorizontalScrolled(int dx) {
+ if (DEBUG) Log.d(TAG, "onHorizontalScrolled(dx=" + dx + ")");
+ positionCurrentTimeIndicator();
+ for (int i = 0, n = mGrid.getChildCount(); i < n; ++i) {
+ mGrid.getChildAt(i).findViewById(R.id.row).scrollBy(dx, 0);
+ }
+ }
+
+ private void resetRowSelection() {
+ if (mDetailOutAnimator != null) {
+ mDetailOutAnimator.end();
+ }
+ if (mDetailInAnimator != null) {
+ mDetailInAnimator.cancel();
+ }
+ mSelectedRow = null;
+ mIsDuringResetRowSelection = true;
+ mGrid.setSelectedPosition(
+ Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()),
+ 0));
+ mGrid.resetFocusState();
+ mIsDuringResetRowSelection = false;
+ }
+
+ private void selectRow(View row) {
+ if (row == null || row == mSelectedRow) {
+ return;
+ }
+ if (mSelectedRow == null
+ || mGrid.getChildAdapterPosition(mSelectedRow) == RecyclerView.NO_POSITION) {
+ if (mSelectedRow != null) {
+ View oldDetailView = mSelectedRow.findViewById(R.id.detail);
+ oldDetailView.setVisibility(View.GONE);
+ }
+ View detailView = row.findViewById(R.id.detail);
+ detailView.findViewById(R.id.detail_content_full).setAlpha(1);
+ detailView.findViewById(R.id.detail_content_full).setTranslationY(0);
+ setLayoutHeight(detailView, mDetailHeight);
+ detailView.setVisibility(View.VISIBLE);
+
+ final ProgramRow programRow = (ProgramRow) row.findViewById(R.id.row);
+ programRow.post(new Runnable() {
+ @Override
+ public void run() {
+ programRow.focusCurrentProgram();
+ }
+ });
+ } else {
+ animateRowChange(mSelectedRow, row);
+ }
+ mSelectedRow = row;
+ }
+
+ private void animateRowChange(View outRow, View inRow) {
+ if (mDetailOutAnimator != null) {
+ mDetailOutAnimator.end();
+ }
+ if (mDetailInAnimator != null) {
+ mDetailInAnimator.cancel();
+ }
+
+ int direction = 0;
+ if (outRow != null && inRow != null) {
+ // -1 means the selection goes downwards and 1 goes upwards
+ direction = outRow.getTop() < inRow.getTop() ? -1 : 1;
+ }
+
+ View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null;
+ if (outDetail != null && outDetail.isShown()) {
+ final View outDetailContent = outDetail.findViewById(R.id.detail_content_full);
+
+ Animator fadeOutAnimator = ObjectAnimator.ofPropertyValuesHolder(outDetailContent,
+ PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+ outDetailContent.getTranslationY(), direction * mDetailPadding));
+ fadeOutAnimator.setStartDelay(0);
+ fadeOutAnimator.setDuration(mAnimationDuration);
+ fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent));
+
+ Animator collapseAnimator =
+ createHeightAnimator(outDetail, getLayoutHeight(outDetail), 0);
+ collapseAnimator.setStartDelay(mAnimationDuration);
+ collapseAnimator.setDuration(mTableFadeAnimDuration);
+ collapseAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ outDetailContent.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ outDetailContent.setVisibility(View.VISIBLE);
+ }
+ });
+
+ AnimatorSet outAnimator = new AnimatorSet();
+ outAnimator.playTogether(fadeOutAnimator, collapseAnimator);
+ outAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mDetailOutAnimator = null;
+ }
+ });
+ mDetailOutAnimator = outAnimator;
+ outAnimator.start();
+ }
+
+ View inDetail = inRow != null ? inRow.findViewById(R.id.detail) : null;
+ if (inDetail != null) {
+ final View inDetailContent = inDetail.findViewById(R.id.detail_content_full);
+
+ Animator expandAnimator = createHeightAnimator(inDetail, 0, mDetailHeight);
+ expandAnimator.setStartDelay(mAnimationDuration);
+ expandAnimator.setDuration(mTableFadeAnimDuration);
+ expandAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ inDetailContent.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ inDetailContent.setVisibility(View.VISIBLE);
+ inDetailContent.setAlpha(0);
+ }
+ });
+
+ Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent,
+ PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+ direction * -mDetailPadding, 0f));
+ fadeInAnimator.setStartDelay(mAnimationDuration + mTableFadeAnimDuration);
+ fadeInAnimator.setDuration(mAnimationDuration);
+ fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent));
+
+ AnimatorSet inAnimator = new AnimatorSet();
+ inAnimator.playTogether(expandAnimator, fadeInAnimator);
+ inAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mDetailInAnimator = null;
+ }
+ });
+ mDetailInAnimator = inAnimator;
+ inAnimator.start();
+ }
+ }
+
+ private Animator createHeightAnimator(
+ final View target, int initialHeight, int targetHeight) {
+ ValueAnimator animator = ValueAnimator.ofInt(initialHeight, targetHeight);
+ animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int value = (Integer) animation.getAnimatedValue();
+ if (value == 0) {
+ if (target.getVisibility() != View.GONE) {
+ target.setVisibility(View.GONE);
+ }
+ } else {
+ if (target.getVisibility() != View.VISIBLE) {
+ target.setVisibility(View.VISIBLE);
+ }
+ setLayoutHeight(target, value);
+ }
+ }
+ });
+ return animator;
+ }
+
+ private int getLayoutHeight(View view) {
+ LayoutParams layoutParams = view.getLayoutParams();
+ return layoutParams.height;
+ }
+
+ private void setLayoutHeight(View view, int height) {
+ LayoutParams layoutParams = view.getLayoutParams();
+ if (height != layoutParams.height) {
+ layoutParams.height = height;
+ view.setLayoutParams(layoutParams);
+ }
+ }
+
+ private class GlobalFocusChangeListener implements
+ ViewTreeObserver.OnGlobalFocusChangeListener {
+ private static final int UNKNOWN = 0;
+ private static final int SIDE_PANEL = 1;
+ private static final int PROGRAM_TABLE = 2;
+
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ if (DEBUG) Log.d(TAG, "onGlobalFocusChanged " + oldFocus + " -> " + newFocus);
+ if (!isActive()) {
+ return;
+ }
+ int fromLocation = getLocation(oldFocus);
+ int toLocation = getLocation(newFocus);
+ if (fromLocation == SIDE_PANEL && toLocation == PROGRAM_TABLE) {
+ startFull();
+ } else if (fromLocation == PROGRAM_TABLE && toLocation == SIDE_PANEL) {
+ startPartial();
+ }
+ }
+
+ private int getLocation(View view) {
+ if (view == null) {
+ return UNKNOWN;
+ }
+ for (Object obj = view; obj instanceof View; obj = ((View) obj).getParent()) {
+ if (obj == mSidePanel) {
+ return SIDE_PANEL;
+ } else if (obj == mGrid) {
+ return PROGRAM_TABLE;
+ }
+ }
+ return UNKNOWN;
+ }
+ }
+
+ private class ProgramManagerListener extends ProgramManager.ListenerAdapter {
+ @Override
+ public void onTimeRangeUpdated() {
+ int scrollOffset = (int) (mWidthPerHour * mProgramManager.getShiftedTime()
+ / HOUR_IN_MILLIS);
+ if (DEBUG) {
+ Log.d(TAG, "Horizontal scroll to " + scrollOffset + " pixels ("
+ + mProgramManager.getShiftedTime() + " millis)");
+ }
+ mTimelineRow.scrollTo(scrollOffset, mTimelineAnimation);
+ }
+ }
+}