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.recents.view; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.util.AttributeSet; 22 import android.view.View; 23 import android.view.WindowMetrics; 24 25 import androidx.annotation.NonNull; 26 import androidx.annotation.Nullable; 27 import androidx.annotation.Px; 28 import androidx.recyclerview.widget.RecyclerView; 29 30 import com.android.car.carlauncher.R; 31 import com.android.car.carlauncher.recents.RecentTasksViewModel; 32 import com.android.car.carlauncher.recents.RecentsUtils; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import org.jetbrains.annotations.NotNull; 36 37 /** 38 * RecyclerView that centers the first and last elements of the Recent task list by adding 39 * appropriate padding. 40 */ 41 public class RecentsRecyclerView extends RecyclerView { 42 private final int mFirstItemWidth; 43 private final int mColSpacing; 44 private final int mItemWidth; 45 private RecentTasksViewModel mRecentTasksViewModel; 46 private WindowMetrics mWindowMetrics; 47 RecentsRecyclerView(@onNull Context context)48 public RecentsRecyclerView(@NonNull Context context) { 49 this(context, /* attrs= */ null); 50 } 51 RecentsRecyclerView(@onNull Context context, @Nullable AttributeSet attrs)52 public RecentsRecyclerView(@NonNull Context context, 53 @Nullable AttributeSet attrs) { 54 this(context, attrs, androidx.recyclerview.R.attr.recyclerViewStyle); 55 } 56 57 RecentsRecyclerView(@onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)58 public RecentsRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, 59 int defStyleAttr) { 60 super(context, attrs, defStyleAttr); 61 mRecentTasksViewModel = RecentTasksViewModel.getInstance(); 62 if (context instanceof Activity) { 63 // needed for testing when using a mock context 64 mWindowMetrics = ((Activity) context).getWindowManager().getCurrentWindowMetrics(); 65 } 66 mFirstItemWidth = getResources().getDimensionPixelSize(R.dimen.recent_task_width_first); 67 mItemWidth = getResources().getDimensionPixelSize(R.dimen.recent_task_width); 68 mColSpacing = getResources().getDimensionPixelSize(R.dimen.recent_task_col_space); 69 } 70 71 @VisibleForTesting RecentsRecyclerView(@onNull Context context, RecentTasksViewModel recentTasksViewModel, WindowMetrics windowMetrics)72 public RecentsRecyclerView(@NonNull Context context, RecentTasksViewModel recentTasksViewModel, 73 WindowMetrics windowMetrics) { 74 this(context); 75 mRecentTasksViewModel = recentTasksViewModel; 76 mWindowMetrics = windowMetrics; 77 } 78 79 /** 80 * Handles {@link View.FOCUS_FORWARD} and {@link View.FOCUS_BACKWARD} events. 81 * For the 2 elements, A and B, the focus should go forward from A's dismiss button to 82 * A's thumbnail to B's dismiss button to B's thumbnail and backward in the inverted order. 83 */ 84 @Override focusSearch(View focused, int direction)85 public View focusSearch(View focused, int direction) { 86 if (direction != View.FOCUS_FORWARD && direction != View.FOCUS_BACKWARD) { 87 return super.focusSearch(focused, direction); 88 } 89 boolean shouldGoToNextView = shouldGoToNextView(direction); 90 ViewHolder focusedViewHolder = findContainingViewHolder(focused); 91 if (focusedViewHolder == null) { 92 return null; 93 } 94 if (focusedViewHolder instanceof BaseTaskViewHolder) { 95 View taskDismissButton = focusedViewHolder.itemView.findViewById( 96 R.id.task_dismiss_button); 97 View taskThumbnail = focusedViewHolder.itemView.findViewById(R.id.task_thumbnail); 98 if (focused == taskDismissButton && shouldGoToNextView) { 99 return taskThumbnail; 100 } 101 if (focused == taskThumbnail && !shouldGoToNextView) { 102 return taskDismissButton; 103 } 104 } 105 int position = focusedViewHolder.getAbsoluteAdapterPosition(); 106 if (position == NO_POSITION) { 107 return null; 108 } 109 110 return getNextFocusView(direction, 111 shouldGoToNextView ? ++position : --position, /* tryScrolling= */ true); 112 } 113 114 /** 115 * Should be called to find the view in the next viewHolder to take focus. 116 * 117 * @param nextPosition next view holder position to be focused. 118 * @param tryScrolling should try to scroll to find the next view holder. 119 * This would only happen if view holder at {@code nextPosition} is null. 120 * @return the next view to be focused. 121 */ 122 @Nullable getNextFocusView(int direction, int nextPosition, boolean tryScrolling)123 private View getNextFocusView(int direction, int nextPosition, boolean tryScrolling) { 124 ViewHolder nextFocusViewHolder = findViewHolderForAdapterPosition(nextPosition); 125 126 if (nextFocusViewHolder == null) { 127 if (tryScrolling) { 128 this.smoothScrollBy(direction == View.FOCUS_FORWARD ? mItemWidth : -mItemWidth, 0); 129 this.addOnScrollListener(new OnScrollListener() { 130 @Override 131 public void onScrollStateChanged(@NotNull RecyclerView recyclerView, 132 int newState) { 133 super.onScrollStateChanged(recyclerView, newState); 134 if (newState == SCROLL_STATE_IDLE) { 135 RecentsRecyclerView.this.removeOnScrollListener(this); 136 View nextFocusedView = getNextFocusView(direction, nextPosition, 137 /* tryScrolling= */ false); 138 if (nextFocusedView != null) { 139 nextFocusedView.requestFocus(); 140 } 141 } 142 } 143 }); 144 } 145 return null; 146 } 147 if (nextFocusViewHolder instanceof BaseTaskViewHolder) { 148 return shouldGoToNextView(direction) 149 ? nextFocusViewHolder.itemView.findViewById(R.id.task_dismiss_button) 150 : nextFocusViewHolder.itemView.findViewById(R.id.task_thumbnail); 151 } 152 if (nextFocusViewHolder instanceof ClearAllViewHolder) { 153 return nextFocusViewHolder.itemView.findViewById(R.id.recents_clear_all_button); 154 } 155 return nextFocusViewHolder.itemView; 156 157 } 158 159 /** 160 * @return {@code true} if the {@code direction} is meant to move the focus to next 161 * view {@code false} for previous view. 162 */ shouldGoToNextView(int direction)163 private boolean shouldGoToNextView(int direction) { 164 return (direction == View.FOCUS_FORWARD) 165 == !RecentsUtils.areItemsRightToLeft(this); 166 } 167 168 /** 169 * Resets the RecyclerView's start and end padding based on the Task list size, 170 * recent task view width and window width where Recents activity is drawn. 171 */ resetPadding()172 public void resetPadding() { 173 if (mRecentTasksViewModel.getRecentTasksSize() == 0) { 174 setPadding(/* firstItemPadding= */ 0, /* lastItemPadding= */ 0); 175 return; 176 } 177 setPadding(/* firstItemPadding= */ calculateFirstItemPadding( 178 mWindowMetrics.getBounds().width()), 179 /* lastItemPadding= */ calculateLastItemPadding()); 180 } 181 182 @Px 183 @VisibleForTesting calculateFirstItemPadding(@x int windowWidth)184 int calculateFirstItemPadding(@Px int windowWidth) { 185 // This assumes that RecyclerView's width is same as the windowWidth. This is to add padding 186 // before RecyclerView or its children is drawn. 187 return Math.max(0, (windowWidth - (mFirstItemWidth + mColSpacing)) / 2); 188 } 189 190 @Px 191 @VisibleForTesting calculateLastItemPadding()192 int calculateLastItemPadding() { 193 // no-op 194 return 0; 195 } 196 197 /** 198 * @param firstItemPadding padding set to recyclerView to fit the first item. 199 * @param lastItemPadding padding set to recyclerView to fit the last item. 200 */ setPadding(@x int firstItemPadding, @Px int lastItemPadding)201 private void setPadding(@Px int firstItemPadding, @Px int lastItemPadding) { 202 boolean shouldBeReversed = RecentsUtils.areItemsRightToLeft(this); 203 setPaddingRelative( 204 /* start= */ shouldBeReversed ? lastItemPadding : firstItemPadding, 205 getPaddingTop(), 206 /* end= */ shouldBeReversed ? firstItemPadding : lastItemPadding, 207 getPaddingBottom()); 208 } 209 } 210