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