1 /* 2 * Copyright (C) 2023 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.car.carlauncher; 18 19 import static com.android.car.carlauncher.AppGridConstants.PageOrientation; 20 21 import android.content.Context; 22 import android.graphics.Rect; 23 import android.util.AttributeSet; 24 import android.view.View; 25 import android.view.ViewGroup; 26 27 import androidx.recyclerview.widget.RecyclerView; 28 29 import com.android.car.carlauncher.pagination.PageIndexingHelper; 30 import com.android.car.carlauncher.pagination.PageMeasurementHelper.GridDimensions; 31 import com.android.car.carlauncher.pagination.PageMeasurementHelper.PageDimensions; 32 import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateListener; 33 import com.android.car.carlauncher.recyclerview.AppGridAdapter; 34 import com.android.car.carlauncher.recyclerview.PageMarginDecoration; 35 36 /** 37 * The RecyclerView that holds all the apps as children in the main app grid. 38 */ 39 public class AppGridRecyclerView extends RecyclerView implements DimensionUpdateListener { 40 // the previous rotary focus direction 41 private int mPrevRotaryPageScrollDirection = View.FOCUS_FORWARD; 42 private final int mNumOfCols; 43 private final int mNumOfRows; 44 @PageOrientation 45 private final int mPageOrientation; 46 private AppGridAdapter mAdapter; 47 private PageMarginDecoration mPageMarginDecoration; 48 AppGridRecyclerView(Context context, AttributeSet attrs)49 public AppGridRecyclerView(Context context, AttributeSet attrs) { 50 super(context, attrs); 51 mNumOfCols = getResources().getInteger(R.integer.car_app_selector_column_number); 52 mNumOfRows = getResources().getInteger(R.integer.car_app_selector_row_number); 53 mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid) 54 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 55 } 56 57 @Override setAdapter(RecyclerView.Adapter adapter)58 public void setAdapter(RecyclerView.Adapter adapter) { 59 if (!(adapter instanceof AppGridAdapter)) { 60 throw new IllegalStateException("Expected Adapter of type AppGridAdapter"); 61 } 62 mAdapter = (AppGridAdapter) adapter; 63 super.setAdapter(mAdapter); 64 } 65 66 /** 67 * Finds the next focusable descendant given rotary input of either View.FOCUS_FORWARD or 68 * View.FOCUS_BACKWARD. 69 * 70 * This method could be called during a scroll event, or to initiate a scroll event when the 71 * intended viewHolder item is not on the screen. 72 */ 73 @Override focusSearch(View focused, int direction)74 public View focusSearch(View focused, int direction) { 75 ViewHolder viewHolder = findContainingViewHolder(focused); 76 AppGridAdapter adapter = (AppGridAdapter) getAdapter(); 77 78 if (viewHolder == null || getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 79 // user may input additional rotary rotations during a page sling, so we return the 80 // currently focused view. 81 return focused; 82 } 83 84 int currentPosition = viewHolder.getAbsoluteAdapterPosition(); 85 int nextPosition = adapter.getNextRotaryFocus(currentPosition, direction); 86 87 int blockSize = mNumOfCols * mNumOfRows; 88 if ((currentPosition / blockSize) == (nextPosition / blockSize)) { 89 // if the views are on the same page, then RecyclerView#getChildAt will be able to find 90 // the child on screen. 91 return getChildAt(nextPosition % blockSize); 92 } 93 94 // since the view is not on the screen and focusSearch cannot target a view that has not 95 // been recycled yet, we need to dispatch a scroll event and postpone focusing. 96 if (AppGridConstants.isHorizontal(mPageOrientation)) { 97 // TODO: fix rounding issue on last page with rotary 98 int pageWidth = getMeasuredWidth(); 99 int dx = (direction == View.FOCUS_FORWARD) ? pageWidth : -pageWidth; 100 smoothScrollBy(dx, 0); 101 } else { 102 int pageHeight = getMeasuredHeight(); 103 int dy = (direction == View.FOCUS_FORWARD) ? pageHeight : -pageHeight; 104 smoothScrollBy(0, dy); 105 } 106 mPrevRotaryPageScrollDirection = direction; 107 108 // the focus should remain on current focused view until maybeHandleRotaryFocus is called 109 return focused; 110 } 111 112 /** 113 * Handles the delayed rotary focus request. This method should only be called after rotary page 114 * scroll completed. 115 */ maybeHandleRotaryFocus()116 public void maybeHandleRotaryFocus() { 117 if (!isInTouchMode()) { 118 // if the recyclerview just settled, and it is using remote inputs, it must have been 119 // scrolled by focusSearch 120 if (mPrevRotaryPageScrollDirection == View.FOCUS_FORWARD) { 121 getChildAt(0).requestFocus(); 122 return; 123 } 124 getChildAt(mNumOfCols * mNumOfRows - 1).requestFocus(); 125 } 126 } 127 128 @Override onDimensionsUpdated(PageDimensions pageDimens, GridDimensions gridDimens)129 public void onDimensionsUpdated(PageDimensions pageDimens, GridDimensions gridDimens) { 130 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 131 layoutParams.width = pageDimens.recyclerViewWidthPx; 132 layoutParams.height = pageDimens.recyclerViewHeightPx; 133 134 Rect pageBounds = new Rect(); 135 getGlobalVisibleRect(pageBounds); 136 mAdapter.updateViewHolderDimensions(pageBounds, gridDimens.cellWidthPx, 137 gridDimens.cellHeightPx); 138 mAdapter.notifyDataSetChanged(); 139 140 if (mPageMarginDecoration != null) { 141 removeItemDecoration(mPageMarginDecoration); 142 } 143 mPageMarginDecoration = new PageMarginDecoration(pageDimens.marginHorizontalPx, 144 pageDimens.marginVerticalPx, new PageIndexingHelper(mNumOfCols, mNumOfRows, 145 mPageOrientation)); 146 addItemDecoration(mPageMarginDecoration); 147 } 148 } 149