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 import static com.android.car.carlauncher.AppGridConstants.isHorizontal; 21 22 import android.content.Context; 23 import android.graphics.Rect; 24 import android.util.AttributeSet; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import androidx.annotation.VisibleForTesting; 29 import androidx.recyclerview.widget.GridLayoutManager; 30 import androidx.recyclerview.widget.RecyclerView; 31 32 import com.android.car.carlauncher.pagination.PageIndexingHelper; 33 import com.android.car.carlauncher.pagination.PageMeasurementHelper.GridDimensions; 34 import com.android.car.carlauncher.pagination.PageMeasurementHelper.PageDimensions; 35 import com.android.car.carlauncher.pagination.PaginationController.DimensionUpdateListener; 36 import com.android.car.carlauncher.recyclerview.AppGridAdapter; 37 import com.android.car.carlauncher.recyclerview.PageMarginDecoration; 38 39 /** 40 * The RecyclerView that holds all the apps as children in the main app grid. 41 */ 42 public class AppGridRecyclerView extends RecyclerView implements DimensionUpdateListener { 43 // the previous rotary focus direction 44 private int mPrevRotaryPageScrollDirection = View.FOCUS_FORWARD; 45 private int mNumOfCols; 46 private int mNumOfRows; 47 48 @PageOrientation 49 private final int mPageOrientation; 50 private AppGridAdapter mAdapter; 51 private PageMarginDecoration mPageMarginDecoration; 52 private PageIndexingHelper mPageIndexingHelper; 53 private static final String TAG = "AppGridRecyclerView"; 54 AppGridRecyclerView(Context context, AttributeSet attrs)55 public AppGridRecyclerView(Context context, AttributeSet attrs) { 56 super(context, attrs); 57 mPageOrientation = getResources().getBoolean(R.bool.use_vertical_app_grid) 58 ? PageOrientation.VERTICAL : PageOrientation.HORIZONTAL; 59 } 60 61 @Override setAdapter(RecyclerView.Adapter adapter)62 public void setAdapter(RecyclerView.Adapter adapter) { 63 if (!(adapter instanceof AppGridAdapter)) { 64 throw new IllegalStateException("Expected Adapter of type AppGridAdapter"); 65 } 66 // skip super.setAdapter() call. We create but do not attach the adapter to recyclerview 67 // until view tree layout is complete and the total size of the app grid is measurable. 68 // Check AppGridRecyclerView#onDimensionsUpdated 69 mAdapter = (AppGridAdapter) adapter; 70 } 71 72 /** 73 * Finds the next focusable descendant given rotary input of either View.FOCUS_FORWARD or 74 * View.FOCUS_BACKWARD. 75 * 76 * This method could be called during a scroll event, or to initiate a scroll event when the 77 * intended viewHolder item is not on the screen. 78 */ 79 @Override focusSearch(View focused, int direction)80 public View focusSearch(View focused, int direction) { 81 ViewHolder viewHolder = findContainingViewHolder(focused); 82 AppGridAdapter adapter = (AppGridAdapter) getAdapter(); 83 84 if (viewHolder == null || getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 85 // user may input additional rotary rotations during a page sling, so we return the 86 // currently focused view. 87 return focused; 88 } 89 90 int currentPosition = viewHolder.getAbsoluteAdapterPosition(); 91 int nextPosition = adapter.getNextRotaryFocus(currentPosition, direction); 92 93 int blockSize = mNumOfCols * mNumOfRows; 94 if ((currentPosition / blockSize) == (nextPosition / blockSize)) { 95 // if the views are on the same page, then RecyclerView#getChildAt will be able to find 96 // the child on screen. 97 return getChildAt(nextPosition % blockSize); 98 } 99 100 // since the view is not on the screen and focusSearch cannot target a view that has not 101 // been recycled yet, we need to dispatch a scroll event and postpone focusing. 102 if (AppGridConstants.isHorizontal(mPageOrientation)) { 103 // TODO: fix rounding issue on last page with rotary 104 int pageWidth = getMeasuredWidth(); 105 int dx = (direction == View.FOCUS_FORWARD) ? pageWidth : -pageWidth; 106 smoothScrollBy(dx, 0); 107 } else { 108 int pageHeight = getMeasuredHeight(); 109 int dy = (direction == View.FOCUS_FORWARD) ? pageHeight : -pageHeight; 110 smoothScrollBy(0, dy); 111 } 112 mPrevRotaryPageScrollDirection = direction; 113 114 // the focus should remain on current focused view until maybeHandleRotaryFocus is called 115 return focused; 116 } 117 118 /** 119 * Handles the delayed rotary focus request. This method should only be called after rotary page 120 * scroll completed. 121 */ maybeHandleRotaryFocus()122 public void maybeHandleRotaryFocus() { 123 if (!isInTouchMode()) { 124 // if the recyclerview just settled, and it is using remote inputs, it must have been 125 // scrolled by focusSearch 126 if (mPrevRotaryPageScrollDirection == View.FOCUS_FORWARD) { 127 getChildAt(0).requestFocus(); 128 return; 129 } 130 getChildAt(mNumOfCols * mNumOfRows - 1).requestFocus(); 131 } 132 } 133 getPageIndexingHelper()134 public PageIndexingHelper getPageIndexingHelper() { 135 return mPageIndexingHelper; 136 } 137 getNumOfRows()138 public int getNumOfRows() { 139 return mNumOfRows; 140 } 141 getNumOfCols()142 public int getNumOfCols() { 143 return mNumOfCols; 144 } 145 146 /** 147 * Forces the adapter to be attached with the specified number of rows and columns. 148 * 149 * <p>This method is intended for testing purposes only. 150 */ 151 @VisibleForTesting forceAttachAdapter(int numOfRows, int numOfCols)152 protected void forceAttachAdapter(int numOfRows, int numOfCols) { 153 mNumOfRows = numOfRows; 154 mNumOfCols = numOfCols; 155 super.setAdapter(mAdapter); 156 } 157 158 @Override onDimensionsUpdated(PageDimensions pageDimens, GridDimensions gridDimens)159 public void onDimensionsUpdated(PageDimensions pageDimens, GridDimensions gridDimens) { 160 ViewGroup.LayoutParams layoutParams = getLayoutParams(); 161 layoutParams.width = pageDimens.recyclerViewWidthPx; 162 layoutParams.height = pageDimens.recyclerViewHeightPx; 163 this.mNumOfRows = gridDimens.mNumOfRows; 164 this.mNumOfCols = gridDimens.mNumOfCols; 165 if (!(getLayoutManager() instanceof GridLayoutManager)) { 166 throw new IllegalStateException( 167 "AppGridRecyclerView can only be used with GridLayoutManager."); 168 } 169 if (isHorizontal(mPageOrientation)) { 170 ((GridLayoutManager) getLayoutManager()).setSpanCount(mNumOfRows); 171 } else { 172 ((GridLayoutManager) getLayoutManager()).setSpanCount(mNumOfCols); 173 } 174 175 Rect pageBounds = new Rect(); 176 getGlobalVisibleRect(pageBounds); 177 mAdapter.updateViewHolderDimensions(pageBounds, gridDimens.cellWidthPx, 178 gridDimens.cellHeightPx); 179 mAdapter.notifyDataSetChanged(); 180 181 if (mPageMarginDecoration != null) { 182 removeItemDecoration(mPageMarginDecoration); 183 } 184 mPageIndexingHelper = new PageIndexingHelper(mNumOfCols, mNumOfRows, mPageOrientation); 185 mPageMarginDecoration = new PageMarginDecoration(pageDimens.marginHorizontalPx, 186 pageDimens.marginVerticalPx, mPageIndexingHelper); 187 addItemDecoration(mPageMarginDecoration); 188 // Now attach adapter to the recyclerView, after dimens are updated. 189 super.setAdapter(mAdapter); 190 } 191 } 192