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