ProgramGrid.java revision 2e1279b8bbe0603fb4399b25b73121bed5953c46
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
17package com.android.tv.guide;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Rect;
22import android.support.v17.leanback.widget.VerticalGridView;
23import android.support.v7.widget.RecyclerView.LayoutManager;
24import android.util.AttributeSet;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.ViewTreeObserver;
29
30import com.android.tv.R;
31import com.android.tv.ui.OnRepeatedKeyInterceptListener;
32
33import java.util.ArrayList;
34import java.util.concurrent.TimeUnit;
35
36/**
37 * A {@link VerticalGridView} for the program table view.
38 */
39public 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