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