aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/guide/ProgramGrid.java
blob: 58436425c909cb064eee0652394e9ebdb3f0e009 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
/*
 * 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.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.support.v17.leanback.widget.VerticalGridView;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Range;
import android.view.View;
import android.view.ViewTreeObserver;

import com.android.tv.R;
import com.android.tv.ui.OnRepeatedKeyInterceptListener;

import java.util.concurrent.TimeUnit;

/**
 * A {@link VerticalGridView} for the program table view.
 */
public class ProgramGrid extends VerticalGridView {
    private static final String TAG = "ProgramGrid";

    private static final int INVALID_INDEX = -1;
    private static final long FOCUS_AREA_RIGHT_MARGIN_MILLIS = TimeUnit.MINUTES.toMillis(15);

    private final ViewTreeObserver.OnGlobalFocusChangeListener mGlobalFocusChangeListener =
            new ViewTreeObserver.OnGlobalFocusChangeListener() {
                @Override
                public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                    if (newFocus != mNextFocusByUpDown) {
                        // If focus is changed by other buttons than UP/DOWN buttons,
                        // we clear the focus state.
                        clearUpDownFocusState(newFocus);
                    }
                    mNextFocusByUpDown = null;
                    if (GuideUtils.isDescendant(ProgramGrid.this, newFocus)) {
                        mLastFocusedView = newFocus;
                    }
                }
            };

    private final ProgramManager.Listener mProgramManagerListener =
            new ProgramManager.ListenerAdapter() {
                @Override
                public void onTimeRangeUpdated() {
                    // When time range is changed, we clear the focus state.
                    clearUpDownFocusState(null);
                }
            };

    private final ViewTreeObserver.OnPreDrawListener mPreDrawListener =
            new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    getViewTreeObserver().removeOnPreDrawListener(this);
                    updateInputLogo();
                    return true;
                }
            };

    private ProgramManager mProgramManager;
    private View mNextFocusByUpDown;

    // New focus will be overlapped with [mFocusRangeLeft, mFocusRangeRight].
    private int mFocusRangeLeft;
    private int mFocusRangeRight;

    private final int mRowHeight;
    private final int mDetailHeight;
    private final int mSelectionRow;  // Row that is focused

    private View mLastFocusedView;
    private final Rect mTempRect = new Rect();
    private int mLastUpDownDirection;

    private boolean mKeepCurrentProgramFocused;

    private ChildFocusListener mChildFocusListener;
    private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener;

    interface ChildFocusListener {
        /**
         * Is called before focus is moved. Only children to {@code ProgramGrid} will be passed.
         * See {@code ProgramGrid#setChildFocusListener(ChildFocusListener)}.
         */
        void onRequestChildFocus(View oldFocus, View newFocus);
    }

    public ProgramGrid(Context context) {
        this(context, null);
    }

    public ProgramGrid(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ProgramGrid(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        clearUpDownFocusState(null);

        // Don't cache anything that is off screen. Normally it is good to prefetch and prepopulate
        // off screen views in order to reduce jank, however the program guide is capable to scroll
        // in all four directions so not only would we prefetch views in the scrolling direction
        // but also keep views in the perpendicular direction up to date.
        // E.g. when scrolling horizontally we would have to update rows above and below the current
        // view port even though they are not visible.
        setItemViewCacheSize(0);

        Resources res = context.getResources();
        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);
        mOnRepeatedKeyInterceptListener = new OnRepeatedKeyInterceptListener(this);
        setOnKeyInterceptListener(mOnRepeatedKeyInterceptListener);
    }

    @Override
    public void requestChildFocus(View child, View focused) {
        if (mChildFocusListener != null) {
            mChildFocusListener.onRequestChildFocus(getFocusedChild(), child);
        }
        super.requestChildFocus(child, focused);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
        mProgramManager.addListener(mProgramManagerListener);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        getViewTreeObserver().removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener);
        mProgramManager.removeListener(mProgramManagerListener);
        clearUpDownFocusState(null);
    }

    @Override
    public View focusSearch(View focused, int direction) {
        mNextFocusByUpDown = null;
        if (focused == null || (focused != this && !GuideUtils.isDescendant(this, focused))) {
            return super.focusSearch(focused, direction);
        }
        if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) {
            updateUpDownFocusState(focused, direction);
            View nextFocus = focusFind(focused, direction);
            if (nextFocus != null) {
                return nextFocus;
            }
        }
        return super.focusSearch(focused, direction);
    }

    @Override
    public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        if (mLastFocusedView != null && mLastFocusedView.isShown()) {
            if (mLastFocusedView.requestFocus()) {
                return true;
            }
        }
        return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused
        // item's are at the almost end of screen, focus change to the next item doesn't work.
        // It restricts that a focus item's position cannot be too far from the desired position.
        View focusedView = findFocus();
        if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) {
            int[] location = new int[2];
            getLocationOnScreen(location);
            int[] focusedLocation = new int[2];
            focusedView.getLocationOnScreen(focusedLocation);
            int y = focusedLocation[1] - location[1];
            int minY = (mSelectionRow - 1) * mRowHeight;
            if (y < minY) scrollBy(0, y - minY);
            int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight;
            if (y > maxY) scrollBy(0, y - maxY);
        }
        updateInputLogo();
    }

    @Override
    public void onViewRemoved(View view) {
        // It is required to ensure input logo showing when the scroll is moved to most bottom.
        updateInputLogo();
    }

    /**
     * Initializes ProgramGrid. It should be called before the view is actually attached to
     * Window.
     */
    void initialize(ProgramManager programManager) {
        mProgramManager = programManager;
    }

    /**
     * Registers a listener focus events occurring on children to the {@code ProgramGrid}.
     */
    void setChildFocusListener(ChildFocusListener childFocusListener) {
        mChildFocusListener = childFocusListener;
    }

    void onItemSelectionReset() {
        getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
    }

    /**
     * Resets focus states. If the logic to keep the last focus needs to be cleared, it should
     * be called.
     */
    void resetFocusState() {
        mLastFocusedView = null;
        clearUpDownFocusState(null);
    }

    /** Returns the currently focused item's horizontal range. */
    Range<Integer> getFocusRange() {
        return new Range<>(mFocusRangeLeft, mFocusRangeRight);
    }

    /** Returns if the next focused item should be the current program if possible. */
    boolean isKeepCurrentProgramFocused() {
        return mKeepCurrentProgramFocused;
    }

    /** Returns the last up/down move direction of browsing */
    int getLastUpDownDirection() {
        return mLastUpDownDirection;
    }

    private View focusFind(View focused, int direction) {
        int focusedChildIndex = getFocusedChildIndex();
        if (focusedChildIndex == INVALID_INDEX) {
            Log.w(TAG, "No child view has focus");
            return null;
        }
        int nextChildIndex = direction == View.FOCUS_UP ? focusedChildIndex - 1
                : focusedChildIndex + 1;
        if (nextChildIndex < 0 || nextChildIndex >= getChildCount()) {
            // Wraparound if reached head or end
            if (getSelectedPosition() == 0) {
                scrollToPosition(getAdapter().getItemCount() - 1);
                return null;
            } else if (getSelectedPosition() == getAdapter().getItemCount() - 1) {
                scrollToPosition(0);
                return null;
            }
            return focused;
        }
        View nextFocusedProgram = GuideUtils.findNextFocusedProgram(getChildAt(nextChildIndex),
                mFocusRangeLeft, mFocusRangeRight, mKeepCurrentProgramFocused);
        if (nextFocusedProgram != null) {
            nextFocusedProgram.getGlobalVisibleRect(mTempRect);
            mNextFocusByUpDown = nextFocusedProgram;

        } else {
            Log.w(TAG, "focusFind doesn't find proper focusable");
        }
        return nextFocusedProgram;
    }

    // Returned value is not the position of VerticalGridView. But it's the index of ViewGroup
    // among visible children.
    private int getFocusedChildIndex() {
        for (int i = 0; i < getChildCount(); ++i) {
            if (getChildAt(i).hasFocus()) {
                return i;
            }
        }
        return INVALID_INDEX;
    }

    private void updateUpDownFocusState(View focused, int direction) {
        mLastUpDownDirection = direction;
        int rightMostFocusablePosition = getRightMostFocusablePosition();
        Rect focusedRect = mTempRect;

        // In order to avoid from focusing small width item, we clip the position with
        // mostRightFocusablePosition.
        focused.getGlobalVisibleRect(focusedRect);
        mFocusRangeLeft = Math.min(mFocusRangeLeft, rightMostFocusablePosition);
        mFocusRangeRight = Math.min(mFocusRangeRight, rightMostFocusablePosition);
        focusedRect.left = Math.min(focusedRect.left, rightMostFocusablePosition);
        focusedRect.right = Math.min(focusedRect.right, rightMostFocusablePosition);

        if (focusedRect.left > mFocusRangeRight || focusedRect.right < mFocusRangeLeft) {
            Log.w(TAG, "The current focus is out of [mFocusRangeLeft, mFocusRangeRight]");
            mFocusRangeLeft = focusedRect.left;
            mFocusRangeRight = focusedRect.right;
            return;
        }
        mFocusRangeLeft = Math.max(mFocusRangeLeft, focusedRect.left);
        mFocusRangeRight = Math.min(mFocusRangeRight, focusedRect.right);
    }

    private void clearUpDownFocusState(View focus) {
        mLastUpDownDirection = 0;
        mFocusRangeLeft = 0;
        mFocusRangeRight = getRightMostFocusablePosition();
        mNextFocusByUpDown = null;
        // If focus is not a program item, drop focus to the current program when back to the grid
        mKeepCurrentProgramFocused = !(focus instanceof ProgramItemView)
                || GuideUtils.isCurrentProgram((ProgramItemView) focus);
    }

    private int getRightMostFocusablePosition() {
        if (!getGlobalVisibleRect(mTempRect)) {
            return Integer.MAX_VALUE;
        }
        return mTempRect.right - GuideUtils.convertMillisToPixel(FOCUS_AREA_RIGHT_MARGIN_MILLIS);
    }

    private int getFirstVisibleChildIndex() {
        final LayoutManager mLayoutManager = getLayoutManager();
        int top = mLayoutManager.getPaddingTop();
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int childTop = mLayoutManager.getDecoratedTop(childView);
            int childBottom = mLayoutManager.getDecoratedBottom(childView);
            if ((childTop + childBottom) / 2 > top) {
                return i;
            }
        }
        return -1;
    }

    private void updateInputLogo() {
        int childCount = getChildCount();
        if (childCount == 0) {
            return;
        }
        int firstVisibleChildIndex = getFirstVisibleChildIndex();
        if (firstVisibleChildIndex == -1) {
            return;
        }
        View childView = getChildAt(firstVisibleChildIndex);
        int childAdapterPosition = getChildAdapterPosition(childView);
        ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
                .updateInputLogo(childAdapterPosition, true);
        for (int i = firstVisibleChildIndex + 1; i < childCount; i++) {
            childView = getChildAt(i);
            ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView))
                    .updateInputLogo(childAdapterPosition, false);
            childAdapterPosition = getChildAdapterPosition(childView);
        }
    }
}