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