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.widget;
15 
16 import android.view.View;
17 import android.view.ViewGroup;
18 
19 import androidx.collection.ArrayMap;
20 
21 import org.jspecify.annotations.NonNull;
22 import org.jspecify.annotations.Nullable;
23 
24 import java.util.List;
25 import java.util.Map;
26 
27 /**
28  * A Presenter is used to generate {@link View}s and bind Objects to them on
29  * demand. It is closely related to the concept of an {@link
30  * androidx.recyclerview.widget.RecyclerView.Adapter RecyclerView.Adapter}, but is
31  * not position-based.  The leanback framework implements the adapter concept using
32  * {@link ObjectAdapter} which refers to a Presenter (or {@link PresenterSelector}) instance.
33  *
34  * <p>
35  * Presenters should be stateless.  Presenters typically extend {@link ViewHolder} to store all
36  * necessary view state information, such as references to child views to be used when
37  * binding to avoid expensive calls to {@link View#findViewById(int)}.
38  * </p>
39  *
40  * <p>
41  * A trivial Presenter that takes a string and renders it into a {@link
42  * android.widget.TextView TextView}:
43  *
44  * <pre class="prettyprint">
45  * public class StringTextViewPresenter extends Presenter {
46  *     // This class does not need a custom ViewHolder, since it does not use
47  *     // a complex layout.
48  *
49  *     {@literal @}Override
50  *     public ViewHolder onCreateViewHolder(ViewGroup parent) {
51  *         return new ViewHolder(new TextView(parent.getContext()));
52  *     }
53  *
54  *     {@literal @}Override
55  *     public void onBindViewHolder(ViewHolder viewHolder, Object item) {
56  *         String str = (String) item;
57  *         TextView textView = (TextView) viewHolder.mView;
58  *
59  *         textView.setText(item);
60  *     }
61  *
62  *     {@literal @}Override
63  *     public void onUnbindViewHolder(ViewHolder viewHolder) {
64  *         // Nothing to unbind for TextView, but if this viewHolder had
65  *         // allocated bitmaps, they can be released here.
66  *     }
67  * }
68  * </pre>
69  * In addition to view creation and binding, Presenter allows dynamic interface (facet) to
70  * be added: {@link #setFacet(Class, Object)}.  Supported facets:
71  * {@link ItemAlignmentFacet} is used by {@link HorizontalGridView} and
72  * {@link VerticalGridView} to customize child alignment.
73  */
74 public abstract class Presenter implements FacetProvider {
75     /**
76      * ViewHolder can be subclassed and used to cache any view accessors needed
77      * to improve binding performance (for example, results of findViewById)
78      * without needing to subclass a View.
79      */
80     public static class ViewHolder implements FacetProvider {
81         public final View view;
82         private Map<Class<?>, Object> mFacets;
83 
ViewHolder(View view)84         public ViewHolder(View view) {
85             this.view = view;
86         }
87 
88         @Override
getFacet(Class<?> facetClass)89         public final Object getFacet(Class<?> facetClass) {
90             if (mFacets == null) {
91                 return null;
92             }
93             return mFacets.get(facetClass);
94         }
95 
96         /**
97          * Sets dynamic implemented facet in addition to basic ViewHolder functions.
98          * @param facetClass   Facet classes to query,  can be class of {@link ItemAlignmentFacet}.
99          * @param facetImpl  Facet implementation.
100          */
setFacet(Class<?> facetClass, Object facetImpl)101         public final void setFacet(Class<?> facetClass, Object facetImpl) {
102             if (mFacets == null) {
103                 mFacets = new ArrayMap<>();
104             }
105             mFacets.put(facetClass, facetImpl);
106         }
107     }
108 
109     /**
110      * Base class to perform a task on Presenter.ViewHolder.
111      */
112     public static abstract class ViewHolderTask {
113         /**
114          * Called to perform a task on view holder.
115          * @param holder The view holder to perform task.
116          */
run(Presenter.ViewHolder holder)117         public void run(Presenter.ViewHolder holder) {
118         }
119     }
120 
121     private Map<Class<?>, Object> mFacets;
122 
123     /**
124      * Creates a new {@link View}.
125      */
onCreateViewHolder(@onNull ViewGroup parent)126     public abstract @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent);
127 
128     /**
129      * Binds a {@link View} to an item.
130      */
onBindViewHolder(@onNull ViewHolder viewHolder, @Nullable Object item)131     public abstract void onBindViewHolder(@NonNull ViewHolder viewHolder, @Nullable Object item);
132 
133     /**
134      * Binds a {@link View} to an item with a list of payloads.
135      * @param viewHolder  The ViewHolder which should be updated to represent the contents of the
136      *                    item at the given position in the data set.
137      * @param item        The item which should be bound to view holder.
138      * @param payloads    A non-null list of merged payloads. Can be empty list if requires full
139      *                    update.
140      */
onBindViewHolder( @onNull ViewHolder viewHolder, @NonNull Object item, @NonNull List<Object> payloads )141     public void onBindViewHolder(
142             @NonNull ViewHolder viewHolder,
143             @NonNull Object item,
144             @NonNull List<Object> payloads
145     ) {
146         onBindViewHolder(viewHolder, item);
147     }
148 
149     /**
150      * Unbinds a {@link View} from an item. Any expensive references may be
151      * released here, and any fields that are not bound for every item should be
152      * cleared here.
153      */
onUnbindViewHolder(@onNull ViewHolder viewHolder)154     public abstract void onUnbindViewHolder(@NonNull ViewHolder viewHolder);
155 
156     /**
157      * Called when a view created by this presenter has been attached to a window.
158      *
159      * <p>This can be used as a reasonable signal that the view is about to be seen
160      * by the user. If the adapter previously freed any resources in
161      * {@link #onViewDetachedFromWindow(ViewHolder)}
162      * those resources should be restored here.</p>
163      *
164      * @param holder Holder of the view being attached
165      */
onViewAttachedToWindow(@onNull ViewHolder holder)166     public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
167     }
168 
169     /**
170      * Called when a view created by this presenter has been detached from its window.
171      *
172      * <p>Becoming detached from the window is not necessarily a permanent condition;
173      * the consumer of an presenter's views may choose to cache views offscreen while they
174      * are not visible, attaching and detaching them as appropriate.</p>
175      *
176      * Any view property animations should be cancelled here or the view may fail
177      * to be recycled.
178      *
179      * @param holder Holder of the view being detached
180      */
onViewDetachedFromWindow(@onNull ViewHolder holder)181     public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
182         // If there are view property animations running then RecyclerView won't recycle.
183         cancelAnimationsRecursive(holder.view);
184     }
185 
186     /**
187      * Utility method for removing all running animations on a view.
188      */
cancelAnimationsRecursive(View view)189     protected static void cancelAnimationsRecursive(View view) {
190         if (view != null && view.hasTransientState()) {
191             view.animate().cancel();
192             if (view instanceof ViewGroup) {
193                 final int count = ((ViewGroup) view).getChildCount();
194                 for (int i = 0; view.hasTransientState() && i < count; i++) {
195                     cancelAnimationsRecursive(((ViewGroup) view).getChildAt(i));
196                 }
197             }
198         }
199     }
200 
201     /**
202      * Called to set a click listener for the given view holder.
203      *
204      * The default implementation sets the click listener on the root view in the view holder.
205      * If the root view isn't focusable this method should be overridden to set the listener
206      * on the appropriate focusable child view(s).
207      *
208      * @param holder The view holder containing the view(s) on which the listener should be set.
209      * @param listener The click listener to be set.
210      */
setOnClickListener(ViewHolder holder, View.OnClickListener listener)211     public void setOnClickListener(ViewHolder holder, View.OnClickListener listener) {
212         holder.view.setOnClickListener(listener);
213     }
214 
215     @Override
getFacet(Class<?> facetClass)216     public final Object getFacet(Class<?> facetClass) {
217         if (mFacets == null) {
218             return null;
219         }
220         return mFacets.get(facetClass);
221     }
222 
223     /**
224      * Sets dynamic implemented facet in addition to basic Presenter functions.
225      * @param facetClass   Facet classes to query,  can be class of {@link ItemAlignmentFacet}.
226      * @param facetImpl  Facet implementation.
227      */
setFacet(Class<?> facetClass, Object facetImpl)228     public final void setFacet(Class<?> facetClass, Object facetImpl) {
229         if (mFacets == null) {
230             mFacets = new ArrayMap<>();
231         }
232         mFacets.put(facetClass, facetImpl);
233     }
234 }
235