Home | History | Annotate | Download | only in guide
      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.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Rect;
     22 import android.support.v17.leanback.widget.VerticalGridView;
     23 import android.util.AttributeSet;
     24 import android.util.Log;
     25 import android.util.Range;
     26 import android.view.View;
     27 import android.view.ViewTreeObserver;
     28 import com.android.tv.R;
     29 import com.android.tv.ui.OnRepeatedKeyInterceptListener;
     30 import java.util.concurrent.TimeUnit;
     31 
     32 /** A {@link VerticalGridView} for the program table view. */
     33 public class ProgramGrid extends VerticalGridView {
     34     private static final String TAG = "ProgramGrid";
     35 
     36     private static final int INVALID_INDEX = -1;
     37     private static final long FOCUS_AREA_RIGHT_MARGIN_MILLIS = TimeUnit.MINUTES.toMillis(15);
     38 
     39     private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
     40             new ViewTreeObserver.OnGlobalFocusChangeListener() {
     41                 @Override
     42                 public void onGlobalFocusChanged(View oldFocus, View newFocus) {
     43                     if (newFocus != mNextFocusByUpDown) {
     44                         // If focus is changed by other buttons than UP/DOWN buttons,
     45                         // we clear the focus state.
     46                         clearUpDownFocusState(newFocus);
     47                     }
     48                     mNextFocusByUpDown = null;
     49                     if (GuideUtils.isDescendant(ProgramGrid.this, newFocus)) {
     50                         mLastFocusedView = newFocus;
     51                     }
     52                 }
     53             };
     54 
     55     private final ProgramManager.Listener mProgramManagerListener =
     56             new ProgramManager.ListenerAdapter() {
     57                 @Override
     58                 public void onTimeRangeUpdated() {
     59                     // When time range is changed, we clear the focus state.
     60                     clearUpDownFocusState(null);
     61                 }
     62             };
     63 
     64     private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
     65             new ViewTreeObserver.OnPreDrawListener() {
     66                 @Override
     67                 public boolean onPreDraw() {
     68                     getViewTreeObserver().removeOnPreDrawListener(this);
     69                     updateInputLogo();
     70                     return true;
     71                 }
     72             };
     73 
     74     private ProgramManager mProgramManager;
     75     private View mNextFocusByUpDown;
     76 
     77     // New focus will be overlapped with [mFocusRangeLeft, mFocusRangeRight].
     78     private int mFocusRangeLeft;
     79     private int mFocusRangeRight;
     80 
     81     private final int mRowHeight;
     82     private final int mDetailHeight;
     83     private final int mSelectionRow; // Row that is focused
     84 
     85     private View mLastFocusedView;
     86     private final Rect mTempRect = new Rect();
     87     private int mLastUpDownDirection;
     88 
     89     private boolean mKeepCurrentProgramFocused;
     90 
     91     private ChildFocusListener mChildFocusListener;
     92     private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener;
     93 
     94     interface ChildFocusListener {
     95         /**
     96          * Is called before focus is moved. Only children to {@code ProgramGrid} will be passed. See
     97          * {@code ProgramGrid#setChildFocusListener(ChildFocusListener)}.
     98          */
     99         void onRequestChildFocus(View oldFocus, View newFocus);
    100     }
    101 
    102     public ProgramGrid(Context context) {
    103         this(context, null);
    104     }
    105 
    106     public ProgramGrid(Context context, AttributeSet attrs) {
    107         this(context, attrs, 0);
    108     }
    109 
    110     public ProgramGrid(Context context, AttributeSet attrs, int defStyle) {
    111         super(context, attrs, defStyle);
    112         clearUpDownFocusState(null);
    113 
    114         // Don't cache anything that is off screen. Normally it is good to prefetch and prepopulate
    115         // off screen views in order to reduce jank, however the program guide is capable to scroll
    116         // in all four directions so not only would we prefetch views in the scrolling direction
    117         // but also keep views in the perpendicular direction up to date.
    118         // E.g. when scrolling horizontally we would have to update rows above and below the current
    119         // view port even though they are not visible.
    120         setItemViewCacheSize(0);
    121 
    122         Resources res = context.getResources();
    123         mRowHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_item_row_height);
    124         mDetailHeight = res.getDimensionPixelSize(R.dimen.program_guide_table_detail_height);
    125         mSelectionRow = res.getInteger(R.integer.program_guide_selection_row);
    126         mOnRepeatedKeyInterceptListener = new OnRepeatedKeyInterceptListener(this);
    127         setOnKeyInterceptListener(mOnRepeatedKeyInterceptListener);
    128     }
    129 
    130     @Override
    131     public void requestChildFocus(View child, View focused) {
    132         if (mChildFocusListener != null) {
    133             mChildFocusListener.onRequestChildFocus(getFocusedChild(), child);
    134         }
    135         super.requestChildFocus(child, focused);
    136     }
    137 
    138     @Override
    139     protected void onAttachedToWindow() {
    140         super.onAttachedToWindow();
    141         getViewTreeObserver().addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
    142         mProgramManager.addListener(mProgramManagerListener);
    143     }
    144 
    145     @Override
    146     protected void onDetachedFromWindow() {
    147         super.onDetachedFromWindow();
    148         getViewTreeObserver().removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
    149         mProgramManager.removeListener(mProgramManagerListener);
    150         clearUpDownFocusState(null);
    151     }
    152 
    153     @Override
    154     public View focusSearch(View focused, int direction) {
    155         mNextFocusByUpDown = null;
    156         if (focused == null || (focused != this && !GuideUtils.isDescendant(this, focused))) {
    157             return super.focusSearch(focused, direction);
    158         }
    159         if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
    160             updateUpDownFocusState(focused, direction);
    161             View nextFocus = focusFind(focused, direction);
    162             if (nextFocus != null) {
    163                 return nextFocus;
    164             }
    165         }
    166         return super.focusSearch(focused, direction);
    167     }
    168 
    169     @Override
    170     public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    171         if (mLastFocusedView != null && mLastFocusedView.isShown()) {
    172             if (mLastFocusedView.requestFocus()) {
    173                 return true;
    174             }
    175         }
    176         return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
    177     }
    178 
    179     @Override
    180     protected void onScrollChanged(int l, int t, int oldl, int oldt) {
    181         // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused
    182         // item's are at the almost end of screen, focus change to the next item doesn't work.
    183         // It restricts that a focus item's position cannot be too far from the desired position.
    184         View focusedView = findFocus();
    185         if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) {
    186             int[] location = new int[2];
    187             getLocationOnScreen(location);
    188             int[] focusedLocation = new int[2];
    189             focusedView.getLocationOnScreen(focusedLocation);
    190             int y = focusedLocation[1] - location[1];
    191             int minY = (mSelectionRow - 1) * mRowHeight;
    192             if (y < minY) scrollBy(0, y - minY);
    193             int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight;
    194             if (y > maxY) scrollBy(0, y - maxY);
    195         }
    196         updateInputLogo();
    197     }
    198 
    199     @Override
    200     public void onViewRemoved(View view) {
    201         // It is required to ensure input logo showing when the scroll is moved to most bottom.
    202         updateInputLogo();
    203     }
    204 
    205     /**
    206      * Initializes ProgramGrid. It should be called before the view is actually attached to Window.
    207      */
    208     void initialize(ProgramManager programManager) {
    209         mProgramManager = programManager;
    210     }
    211 
    212     /** Registers a listener focus events occurring on children to the {@code ProgramGrid}. */
    213     void setChildFocusListener(ChildFocusListener childFocusListener) {
    214         mChildFocusListener = childFocusListener;
    215     }
    216 
    217     void onItemSelectionReset() {
    218         getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
    219     }
    220 
    221     /**
    222      * Resets focus states. If the logic to keep the last focus needs to be cleared, it should be
    223      * called.
    224      */
    225     void resetFocusState() {
    226         mLastFocusedView = null;
    227         clearUpDownFocusState(null);
    228     }
    229 
    230     /** Returns the currently focused item's horizontal range. */
    231     Range<Integer> getFocusRange() {
    232         return new Range<>(mFocusRangeLeft, mFocusRangeRight);
    233     }
    234 
    235     /** Returns if the next focused item should be the current program if possible. */
    236     boolean isKeepCurrentProgramFocused() {
    237         return mKeepCurrentProgramFocused;
    238     }
    239 
    240     /** Returns the last up/down move direction of browsing */
    241     int getLastUpDownDirection() {
    242         return mLastUpDownDirection;
    243     }
    244 
    245     private View focusFind(View focused, int direction) {
    246         int focusedChildIndex = getFocusedChildIndex();
    247         if (focusedChildIndex == INVALID_INDEX) {
    248             Log.w(TAG, "No child view has focus");
    249             return null;
    250         }
    251         int nextChildIndex =
    252                 direction == View.FOCUS_UP ? focusedChildIndex - 1 : focusedChildIndex + 1;
    253         if (nextChildIndex < 0 || nextChildIndex >= getChildCount()) {
    254             // Wraparound if reached head or end
    255             if (getSelectedPosition() == 0) {
    256                 scrollToPosition(getAdapter().getItemCount() - 1);
    257                 return null;
    258             } else if (getSelectedPosition() == getAdapter().getItemCount() - 1) {
    259                 scrollToPosition(0);
    260                 return null;
    261             }
    262             return focused;
    263         }
    264         View nextFocusedProgram =
    265                 GuideUtils.findNextFocusedProgram(
    266                         getChildAt(nextChildIndex),
    267                         mFocusRangeLeft,
    268                         mFocusRangeRight,
    269                         mKeepCurrentProgramFocused);
    270         if (nextFocusedProgram != null) {
    271             nextFocusedProgram.getGlobalVisibleRect(mTempRect);
    272             mNextFocusByUpDown = nextFocusedProgram;
    273 
    274         } else {
    275             Log.w(TAG, "focusFind doesn't find proper focusable");
    276         }
    277         return nextFocusedProgram;
    278     }
    279 
    280     // Returned value is not the position of VerticalGridView. But it's the index of ViewGroup
    281     // among visible children.
    282     private int getFocusedChildIndex() {
    283         for (int i = 0; i < getChildCount(); ++i) {
    284             if (getChildAt(i).hasFocus()) {
    285                 return i;
    286             }
    287         }
    288         return INVALID_INDEX;
    289     }
    290 
    291     private void updateUpDownFocusState(View focused, int direction) {
    292         mLastUpDownDirection = direction;
    293         int rightMostFocusablePosition = getRightMostFocusablePosition();
    294         Rect focusedRect = mTempRect;
    295 
    296         // In order to avoid from focusing small width item, we clip the position with
    297         // mostRightFocusablePosition.
    298         focused.getGlobalVisibleRect(focusedRect);
    299         mFocusRangeLeft = Math.min(mFocusRangeLeft, rightMostFocusablePosition);
    300         mFocusRangeRight = Math.min(mFocusRangeRight, rightMostFocusablePosition);
    301         focusedRect.left = Math.min(focusedRect.left, rightMostFocusablePosition);
    302         focusedRect.right = Math.min(focusedRect.right, rightMostFocusablePosition);
    303 
    304         if (focusedRect.left > mFocusRangeRight || focusedRect.right < mFocusRangeLeft) {
    305             Log.w(TAG, "The current focus is out of [mFocusRangeLeft, mFocusRangeRight]");
    306             mFocusRangeLeft = focusedRect.left;
    307             mFocusRangeRight = focusedRect.right;
    308             return;
    309         }
    310         mFocusRangeLeft = Math.max(mFocusRangeLeft, focusedRect.left);
    311         mFocusRangeRight = Math.min(mFocusRangeRight, focusedRect.right);
    312     }
    313 
    314     private void clearUpDownFocusState(View focus) {
    315         mLastUpDownDirection = 0;
    316         mFocusRangeLeft = 0;
    317         mFocusRangeRight = getRightMostFocusablePosition();
    318         mNextFocusByUpDown = null;
    319         // If focus is not a program item, drop focus to the current program when back to the grid
    320         mKeepCurrentProgramFocused =
    321                 !(focus instanceof ProgramItemView)
    322                         || GuideUtils.isCurrentProgram((ProgramItemView) focus);
    323     }
    324 
    325     private int getRightMostFocusablePosition() {
    326         if (!getGlobalVisibleRect(mTempRect)) {
    327             return Integer.MAX_VALUE;
    328         }
    329         return mTempRect.right - GuideUtils.convertMillisToPixel(FOCUS_AREA_RIGHT_MARGIN_MILLIS);
    330     }
    331 
    332     private int getFirstVisibleChildIndex() {
    333         final LayoutManager mLayoutManager = getLayoutManager();
    334         int top = mLayoutManager.getPaddingTop();
    335         int childCount = getChildCount();
    336         for (int i = 0; i < childCount; i++) {
    337             View childView = getChildAt(i);
    338             int childTop = mLayoutManager.getDecoratedTop(childView);
    339             int childBottom = mLayoutManager.getDecoratedBottom(childView);
    340             if ((childTop + childBottom) / 2 > top) {
    341                 return i;
    342             }
    343         }
    344         return -1;
    345     }
    346 
    347     private void updateInputLogo() {
    348         int childCount = getChildCount();
    349         if (childCount == 0) {
    350             return;
    351         }
    352         int firstVisibleChildIndex = getFirstVisibleChildIndex();
    353         if (firstVisibleChildIndex == -1) {
    354             return;
    355         }
    356         View childView = getChildAt(firstVisibleChildIndex);
    357         int childAdapterPosition = getChildAdapterPosition(childView);
    358         ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
    359                 .updateInputLogo(childAdapterPosition, true);
    360         for (int i = firstVisibleChildIndex + 1; i < childCount; i++) {
    361             childView = getChildAt(i);
    362             ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
    363                     .updateInputLogo(childAdapterPosition, false);
    364             childAdapterPosition = getChildAdapterPosition(childView);
    365         }
    366     }
    367 }
    368