| /* |
| * 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.content.Context; |
| 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.annotation.NonNull; |
| import android.support.annotation.Nullable; |
| 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 android.view.accessibility.AccessibilityManager; |
| |
| import com.android.tv.ChannelTuner; |
| import com.android.tv.Features; |
| import com.android.tv.MainActivity; |
| import com.android.tv.R; |
| import com.android.tv.util.DurationTimer; |
| import com.android.tv.analytics.Tracker; |
| import com.android.tv.common.WeakHandler; |
| import com.android.tv.data.ChannelDataManager; |
| import com.android.tv.data.GenreItems; |
| import com.android.tv.data.ProgramDataManager; |
| import com.android.tv.dvr.DvrDataManager; |
| import com.android.tv.dvr.DvrScheduleManager; |
| import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; |
| import com.android.tv.ui.ViewUtils; |
| 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 String SCREEN_NAME = "EPG"; |
| |
| private final MainActivity mActivity; |
| private final ProgramManager mProgramManager; |
| private final AccessibilityManager mAccessibilityManager; |
| 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 mSelectionRow; // Row that is focused |
| 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 ProgramGuideHandler(this); |
| |
| private final Runnable mHideRunnable = new Runnable() { |
| @Override |
| public void run() { |
| hide(); |
| } |
| }; |
| private final long mShowDurationMillis; |
| private ViewTreeObserver.OnGlobalLayoutListener mOnLayoutListenerForShow; |
| |
| 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, @Nullable DvrDataManager dvrDataManager, |
| @Nullable DvrScheduleManager dvrScheduleManager, Tracker tracker, |
| Runnable preShowRunnable, Runnable postHideRunnable) { |
| mActivity = activity; |
| mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager, |
| programDataManager, dvrDataManager, dvrScheduleManager); |
| 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); |
| mSelectionRow = res.getInteger(R.integer.program_guide_selection_row); |
| 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 (Features.EPG_SEARCH.isEnabled(mActivity)) { |
| 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, |
| mProgramManager, this); |
| programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { |
| @Override |
| public void onChanged() { |
| // It is usually called when Genre is changed. |
| // Reset selection of ProgramGrid |
| resetRowSelection(); |
| updateGuidePosition(); |
| } |
| }); |
| |
| 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(mSelectionRow * 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.GONE); |
| } |
| }); |
| 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.GONE); |
| } |
| }); |
| |
| 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); |
| mAccessibilityManager = |
| (AccessibilityManager) mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE); |
| mShowGuidePartial = mAccessibilityManager.isEnabled() |
| || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); |
| } |
| |
| private void updateGuidePosition() { |
| // Align EPG at vertical center, if EPG table height is less than the screen size. |
| Resources res = mActivity.getResources(); |
| int screenHeight = mContainer.getHeight(); |
| if (screenHeight <= 0) { |
| // mContainer is not initialized yet. |
| return; |
| } |
| 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); |
| } |
| } |
| |
| @Override |
| public void onRequestChildFocus(View oldFocus, View newFocus) { |
| if (oldFocus != null && newFocus != null) { |
| int selectionRowOffset = mSelectionRow * 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. |
| * |
| * <p>Note: the animation which starts together with ProgramGuide showing animation needs to |
| * be initiated in {@code runnableAfterAnimatorReady}. If the animation starts together |
| * with show(), the animation may drop some frames. |
| */ |
| public void show(final Runnable runnableAfterAnimatorReady) { |
| 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.updateInitialTimeRange(mStartUtcTime, mStartUtcTime + mViewPortMillis); |
| mProgramManager.addListener(mProgramManagerListener); |
| 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); |
| positionCurrentTimeIndicator(); |
| mSidePanelGridView.setSelectedPosition(0); |
| if (DEBUG) { |
| Log.d(TAG, "show()"); |
| } |
| mOnLayoutListenerForShow = new ViewTreeObserver.OnGlobalLayoutListener() { |
| @Override |
| public void onGlobalLayout() { |
| mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
| mTable.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
| mSidePanelGridView.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
| mTable.buildLayer(); |
| mSidePanelGridView.buildLayer(); |
| mOnLayoutListenerForShow = null; |
| mTimelineAnimation = true; |
| // Make sure that time indicator update starts after animation is finished. |
| startCurrentTimeIndicator(TIME_INDICATOR_UPDATE_FREQUENCY); |
| if (DEBUG) { |
| mContainer.getViewTreeObserver().addOnDrawListener( |
| new ViewTreeObserver.OnDrawListener() { |
| long time = System.currentTimeMillis(); |
| int count = 0; |
| |
| @Override |
| public void onDraw() { |
| long curtime = System.currentTimeMillis(); |
| Log.d(TAG, "onDraw " + count++ + " " + (curtime - time) + "ms"); |
| time = curtime; |
| if (count > 10) { |
| mContainer.getViewTreeObserver().removeOnDrawListener(this); |
| } |
| } |
| }); |
| } |
| runnableAfterAnimatorReady.run(); |
| if (mShowGuidePartial) { |
| mShowAnimatorPartial.start(); |
| } else { |
| mShowAnimatorFull.start(); |
| } |
| updateGuidePosition(); |
| } |
| }; |
| mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); |
| scheduleHide(); |
| } |
| |
| /** |
| * Hide the program guide. |
| */ |
| public void hide() { |
| if (!isActive()) { |
| return; |
| } |
| if (mOnLayoutListenerForShow != null) { |
| mContainer.getViewTreeObserver().removeOnGlobalLayoutListener(mOnLayoutListenerForShow); |
| mOnLayoutListenerForShow = null; |
| } |
| 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(); |
| } |
| |
| /** |
| * Cancel hiding the program guide. |
| */ |
| public 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() || mAccessibilityManager.isEnabled()) { |
| // If accessibility service is enabled, focus cannot be moved to side panel due to it's |
| // hidden. Therefore, we don't hide side panel when accessibility service is enabled. |
| 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(long initialDelay) { |
| mHandler.postDelayed(mUpdateTimeIndicator, initialDelay); |
| } |
| |
| 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(); |
| mGrid.onItemSelectionReset(); |
| 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); |
| ViewUtils.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 = ViewUtils |
| .createHeightAnimator(outDetail, ViewUtils.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 = ViewUtils.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.setDuration(mAnimationDuration); |
| fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); |
| |
| AnimatorSet inAnimator = new AnimatorSet(); |
| inAnimator.playSequentially(expandAnimator, fadeInAnimator); |
| inAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animator) { |
| mDetailInAnimator = null; |
| } |
| }); |
| mDetailInAnimator = inAnimator; |
| inAnimator.start(); |
| } |
| } |
| |
| 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); |
| } |
| } |
| |
| private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> { |
| public ProgramGuideHandler(ProgramGuide ref) { |
| super(ref); |
| } |
| |
| @Override |
| public void handleMessage(Message msg, @NonNull ProgramGuide programGuide) { |
| if (msg.what == MSG_PROGRAM_TABLE_FADE_IN_ANIM) { |
| programGuide.mProgramTableFadeInAnimator.start(); |
| } |
| } |
| } |
| } |