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          */
onRequestChildFocus(View oldFocus, View newFocus)104         void onRequestChildFocus(View oldFocus, View newFocus);
105     }
106 
ProgramGrid(Context context)107     public ProgramGrid(Context context) {
108         this(context, null);
109     }
110 
ProgramGrid(Context context, AttributeSet attrs)111     public ProgramGrid(Context context, AttributeSet attrs) {
112         this(context, attrs, 0);
113     }
114 
ProgramGrid(Context context, AttributeSet attrs, int defStyle)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      */
initialize(ProgramManager programManager)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      */
setChildFocusListener(ChildFocusListener childFocusListener)146     public void setChildFocusListener(ChildFocusListener childFocusListener) {
147         mChildFocusListener = childFocusListener;
148     }
149 
150     @Override
requestChildFocus(View child, View focused)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
onAttachedToWindow()159     protected void onAttachedToWindow() {
160         super.onAttachedToWindow();
161         getViewTreeObserver().addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
162         mProgramManager.addListener(mProgramManagerListener);
163     }
164 
165     @Override
onDetachedFromWindow()166     protected void onDetachedFromWindow() {
167         super.onDetachedFromWindow();
168         getViewTreeObserver().removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
169         mProgramManager.removeListener(mProgramManagerListener);
170         clearUpDownFocusState(null);
171     }
172 
173     @Override
focusSearch(View focused, int direction)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      */
resetFocusState()193     public void resetFocusState() {
194         mLastFocusedView = null;
195         clearUpDownFocusState(null);
196     }
197 
focusFind(View focused, int direction)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.
getFocusedChildIndex()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 
updateUpDownFocusState(View focused)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 
clearUpDownFocusState(View focus)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 
getRightMostFocusablePosition()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 
contains(View v)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 
onItemSelectionReset()346     public void onItemSelectionReset() {
347         getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
348     }
349 
350     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)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
onScrollChanged(int l, int t, int oldl, int oldt)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
onViewRemoved(View view)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 
getFirstVisibleChildIndex()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 
updateInputLogo()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 
findFocusables(View v, ArrayList<View> outFocusable)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