1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 package androidx.leanback.app; 15 16 import android.os.Bundle; 17 import android.view.LayoutInflater; 18 import android.view.View; 19 import android.view.ViewGroup; 20 21 import androidx.fragment.app.Fragment; 22 import androidx.leanback.widget.ItemBridgeAdapter; 23 import androidx.leanback.widget.ListRow; 24 import androidx.leanback.widget.ObjectAdapter; 25 import androidx.leanback.widget.OnChildViewHolderSelectedListener; 26 import androidx.leanback.widget.PresenterSelector; 27 import androidx.leanback.widget.Row; 28 import androidx.leanback.widget.VerticalGridView; 29 import androidx.recyclerview.widget.RecyclerView; 30 31 import org.jspecify.annotations.NonNull; 32 import org.jspecify.annotations.Nullable; 33 34 /** 35 * An internal base class for a fragment containing a list of rows. 36 */ 37 abstract class BaseRowSupportFragment extends Fragment { 38 private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition"; 39 private ObjectAdapter mAdapter; 40 VerticalGridView mVerticalGridView; 41 private PresenterSelector mPresenterSelector; 42 final ItemBridgeAdapter mBridgeAdapter = new ItemBridgeAdapter(); 43 int mSelectedPosition = -1; 44 private boolean mPendingTransitionPrepare; 45 LateSelectionObserver mLateSelectionObserver = new LateSelectionObserver(); 46 getLayoutResourceId()47 abstract int getLayoutResourceId(); 48 49 private final OnChildViewHolderSelectedListener mRowSelectedListener = 50 new OnChildViewHolderSelectedListener() { 51 @Override 52 public void onChildViewHolderSelected(RecyclerView parent, 53 RecyclerView.ViewHolder view, int position, int subposition) { 54 if (!mLateSelectionObserver.mIsLateSelection) { 55 mSelectedPosition = position; 56 onRowSelected(parent, view, position, subposition); 57 } 58 } 59 }; 60 onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view, int position, int subposition)61 void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder view, 62 int position, int subposition) { 63 } 64 65 @Override onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)66 public @Nullable View onCreateView(@NonNull LayoutInflater inflater, 67 @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 68 View view = inflater.inflate(getLayoutResourceId(), container, false); 69 mVerticalGridView = findGridViewFromRoot(view); 70 if (mPendingTransitionPrepare) { 71 mPendingTransitionPrepare = false; 72 onTransitionPrepare(); 73 } 74 return view; 75 } 76 findGridViewFromRoot(View view)77 VerticalGridView findGridViewFromRoot(View view) { 78 return (VerticalGridView) view; 79 } 80 81 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)82 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 83 if (savedInstanceState != null) { 84 mSelectedPosition = savedInstanceState.getInt(CURRENT_SELECTED_POSITION, -1); 85 } 86 setAdapterAndSelection(); 87 mVerticalGridView.setOnChildViewHolderSelectedListener(mRowSelectedListener); 88 } 89 90 /** 91 * This class waits for the adapter to be updated before setting the selected 92 * row. 93 */ 94 final class LateSelectionObserver extends RecyclerView.AdapterDataObserver { 95 boolean mIsLateSelection = false; 96 LateSelectionObserver()97 LateSelectionObserver() { 98 } 99 100 @Override onChanged()101 public void onChanged() { 102 performLateSelection(); 103 } 104 105 @Override onItemRangeInserted(int positionStart, int itemCount)106 public void onItemRangeInserted(int positionStart, int itemCount) { 107 performLateSelection(); 108 } 109 startLateSelection()110 void startLateSelection() { 111 mIsLateSelection = true; 112 mBridgeAdapter.registerAdapterDataObserver(this); 113 } 114 performLateSelection()115 void performLateSelection() { 116 clear(); 117 if (mVerticalGridView != null) { 118 mVerticalGridView.setSelectedPosition(mSelectedPosition); 119 } 120 } 121 clear()122 void clear() { 123 if (mIsLateSelection) { 124 mIsLateSelection = false; 125 mBridgeAdapter.unregisterAdapterDataObserver(this); 126 } 127 } 128 } 129 setAdapterAndSelection()130 void setAdapterAndSelection() { 131 if (mAdapter == null) { 132 // delay until ItemBridgeAdapter has wrappedAdapter. Once we assign ItemBridgeAdapter 133 // to RecyclerView, it will not be allowed to change "hasStableId" to true. 134 return; 135 } 136 if (mVerticalGridView.getAdapter() != mBridgeAdapter) { 137 // avoid extra layout if ItemBridgeAdapter was already set. 138 mVerticalGridView.setAdapter(mBridgeAdapter); 139 } 140 // We don't set the selected position unless we've data in the adapter. 141 boolean lateSelection = mBridgeAdapter.getItemCount() == 0 && mSelectedPosition >= 0; 142 if (lateSelection) { 143 mLateSelectionObserver.startLateSelection(); 144 } else if (mSelectedPosition >= 0) { 145 mVerticalGridView.setSelectedPosition(mSelectedPosition); 146 } 147 } 148 149 @Override onDestroyView()150 public void onDestroyView() { 151 super.onDestroyView(); 152 mLateSelectionObserver.clear(); 153 if (mVerticalGridView != null) { 154 // unregister VerticalGridView from the mBridgeAdapter's observer list. 155 mVerticalGridView.swapAdapter(null, true); 156 mVerticalGridView = null; 157 } 158 } 159 160 @Override onSaveInstanceState(Bundle outState)161 public void onSaveInstanceState(Bundle outState) { 162 super.onSaveInstanceState(outState); 163 outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition); 164 } 165 166 /** 167 * Set the presenter selector used to create and bind views. 168 */ setPresenterSelector(PresenterSelector presenterSelector)169 public final void setPresenterSelector(PresenterSelector presenterSelector) { 170 if (mPresenterSelector != presenterSelector) { 171 mPresenterSelector = presenterSelector; 172 updateAdapter(); 173 } 174 } 175 176 /** 177 * Get the presenter selector used to create and bind views. 178 */ getPresenterSelector()179 public final PresenterSelector getPresenterSelector() { 180 return mPresenterSelector; 181 } 182 183 /** 184 * Sets the adapter that represents a list of rows. 185 * @param rowsAdapter Adapter that represents list of rows. 186 */ setAdapter(ObjectAdapter rowsAdapter)187 public final void setAdapter(ObjectAdapter rowsAdapter) { 188 if (mAdapter != rowsAdapter) { 189 mAdapter = rowsAdapter; 190 updateAdapter(); 191 } 192 } 193 194 /** 195 * Returns the Adapter that represents list of rows. 196 * @return Adapter that represents list of rows. 197 */ getAdapter()198 public final ObjectAdapter getAdapter() { 199 return mAdapter; 200 } 201 202 /** 203 * Returns the RecyclerView.Adapter that wraps {@link #getAdapter()}. 204 * @return The RecyclerView.Adapter that wraps {@link #getAdapter()}. 205 */ getBridgeAdapter()206 public final ItemBridgeAdapter getBridgeAdapter() { 207 return mBridgeAdapter; 208 } 209 210 /** 211 * Sets the selected row position with smooth animation. 212 */ setSelectedPosition(int position)213 public void setSelectedPosition(int position) { 214 setSelectedPosition(position, true); 215 } 216 217 /** 218 * Gets position of currently selected row. 219 * @return Position of currently selected row. 220 */ getSelectedPosition()221 public int getSelectedPosition() { 222 return mSelectedPosition; 223 } 224 225 /** 226 * Sets the selected row position. 227 */ setSelectedPosition(int position, boolean smooth)228 public void setSelectedPosition(int position, boolean smooth) { 229 if (mSelectedPosition == position) { 230 return; 231 } 232 mSelectedPosition = position; 233 if (mVerticalGridView != null) { 234 if (mLateSelectionObserver.mIsLateSelection) { 235 return; 236 } 237 if (smooth) { 238 mVerticalGridView.setSelectedPositionSmooth(position); 239 } else { 240 mVerticalGridView.setSelectedPosition(position); 241 } 242 } 243 } 244 getVerticalGridView()245 public final VerticalGridView getVerticalGridView() { 246 return mVerticalGridView; 247 } 248 updateAdapter()249 void updateAdapter() { 250 mBridgeAdapter.setAdapter(mAdapter); 251 mBridgeAdapter.setPresenter(mPresenterSelector); 252 253 if (mVerticalGridView != null) { 254 setAdapterAndSelection(); 255 } 256 } 257 getItem(Row row, int position)258 Object getItem(Row row, int position) { 259 if (row instanceof ListRow) { 260 return ((ListRow) row).getAdapter().get(position); 261 } else { 262 return null; 263 } 264 } 265 onTransitionPrepare()266 public boolean onTransitionPrepare() { 267 if (mVerticalGridView != null) { 268 mVerticalGridView.setAnimateChildLayout(false); 269 mVerticalGridView.setScrollEnabled(false); 270 return true; 271 } 272 mPendingTransitionPrepare = true; 273 return false; 274 } 275 onTransitionStart()276 public void onTransitionStart() { 277 if (mVerticalGridView != null) { 278 mVerticalGridView.setPruneChild(false); 279 mVerticalGridView.setLayoutFrozen(true); 280 mVerticalGridView.setFocusSearchDisabled(true); 281 } 282 } 283 onTransitionEnd()284 public void onTransitionEnd() { 285 // be careful that fragment might be destroyed before header transition ends. 286 if (mVerticalGridView != null) { 287 mVerticalGridView.setLayoutFrozen(false); 288 mVerticalGridView.setAnimateChildLayout(true); 289 mVerticalGridView.setPruneChild(true); 290 mVerticalGridView.setFocusSearchDisabled(false); 291 mVerticalGridView.setScrollEnabled(true); 292 } 293 } 294 setAlignment(int windowAlignOffsetTop)295 public void setAlignment(int windowAlignOffsetTop) { 296 if (mVerticalGridView != null) { 297 // align the top edge of item 298 mVerticalGridView.setItemAlignmentOffset(0); 299 mVerticalGridView.setItemAlignmentOffsetPercent( 300 VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 301 302 // align to a fixed position from top 303 mVerticalGridView.setWindowAlignmentOffset(windowAlignOffsetTop); 304 mVerticalGridView.setWindowAlignmentOffsetPercent( 305 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 306 mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 307 } 308 } 309 } 310