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.content.Context;
17 import android.graphics.Bitmap;
18 import android.graphics.drawable.BitmapDrawable;
19 import android.graphics.drawable.Drawable;
20 
21 import java.lang.ref.WeakReference;
22 import java.util.ArrayList;
23 import java.util.List;
24 
25 /**
26  * An overview {@link Row} for a details fragment. This row consists of an image, a
27  * description view, and optionally a series of {@link Action}s that can be taken for
28  * the item.
29  *
30  * <h3>Actions</h3>
31  * Application uses {@link #setActionsAdapter(ObjectAdapter)} to set actions on the overview
32  * row.  {@link SparseArrayObjectAdapter} is recommended for easily updating actions while
33  * maintaining the order.  The application can add or remove actions on the UI thread after the
34  * row is bound to a view.
35  *
36  * <h3>Updating main item</h3>
37  * After the row is bound to a view, the application may call {@link #setItem(Object)}
38  * on UI thread and the view will be updated.
39  *
40  * <h3>Updating image</h3>
41  * After the row is bound to view, the application may change the image by calling {@link
42  * #setImageBitmap(Context, Bitmap)} or {@link #setImageDrawable(Drawable)} on the UI thread,
43  * and the view will be updated.
44  */
45 public class DetailsOverviewRow extends Row {
46 
47     /**
48      * Listener for changes of DetailsOverviewRow.
49      */
50     public static class Listener {
51 
52         /**
53          * Called when DetailsOverviewRow has changed image drawable.
54          */
onImageDrawableChanged(DetailsOverviewRow row)55         public void onImageDrawableChanged(DetailsOverviewRow row) {
56         }
57 
58         /**
59          * Called when DetailsOverviewRow has changed main item.
60          */
onItemChanged(DetailsOverviewRow row)61         public void onItemChanged(DetailsOverviewRow row) {
62         }
63 
64         /**
65          * Called when DetailsOverviewRow has changed actions adapter.
66          */
onActionsAdapterChanged(DetailsOverviewRow row)67         public void onActionsAdapterChanged(DetailsOverviewRow row) {
68         }
69     }
70 
71     private Object mItem;
72     private Drawable mImageDrawable;
73     private boolean mImageScaleUpAllowed = true;
74     private ArrayList<WeakReference<Listener>> mListeners;
75     private PresenterSelector mDefaultActionPresenter = new ActionPresenterSelector();
76     private ObjectAdapter mActionsAdapter = new ArrayObjectAdapter(mDefaultActionPresenter);
77 
78     /**
79      * Constructor for a DetailsOverviewRow.
80      *
81      * @param item The main item for the details page.
82      */
DetailsOverviewRow(Object item)83     public DetailsOverviewRow(Object item) {
84         super(null);
85         mItem = item;
86         verify();
87     }
88 
89     /**
90      * Adds listener for the details page.
91      */
addListener(Listener listener)92     final void addListener(Listener listener) {
93         if (mListeners == null) {
94             mListeners = new ArrayList<WeakReference<Listener>>();
95         } else {
96             for (int i = 0; i < mListeners.size();) {
97                 Listener l = mListeners.get(i).get();
98                 if (l == null) {
99                     mListeners.remove(i);
100                 } else {
101                     if (l == listener) {
102                         return;
103                     }
104                     i++;
105                 }
106             }
107         }
108         mListeners.add(new WeakReference<Listener>(listener));
109     }
110 
111     /**
112      * Removes listener of the details page.
113      */
removeListener(Listener listener)114     final void removeListener(Listener listener) {
115         if (mListeners != null) {
116             for (int i = 0; i < mListeners.size();) {
117                 Listener l = mListeners.get(i).get();
118                 if (l == null) {
119                     mListeners.remove(i);
120                 } else {
121                     if (l == listener) {
122                         mListeners.remove(i);
123                         return;
124                     }
125                     i++;
126                 }
127             }
128         }
129     }
130 
131     /**
132      * Notifies listeners for main item change on UI thread.
133      */
notifyItemChanged()134     final void notifyItemChanged() {
135         if (mListeners != null) {
136             for (int i = 0; i < mListeners.size();) {
137                 Listener l = mListeners.get(i).get();
138                 if (l == null) {
139                     mListeners.remove(i);
140                 } else {
141                     l.onItemChanged(this);
142                     i++;
143                 }
144             }
145         }
146     }
147 
148     /**
149      * Notifies listeners for image related change on UI thread.
150      */
notifyImageDrawableChanged()151     final void notifyImageDrawableChanged() {
152         if (mListeners != null) {
153             for (int i = 0; i < mListeners.size();) {
154                 Listener l = mListeners.get(i).get();
155                 if (l == null) {
156                     mListeners.remove(i);
157                 } else {
158                     l.onImageDrawableChanged(this);
159                     i++;
160                 }
161             }
162         }
163     }
164 
165     /**
166      * Notifies listeners for actions adapter changed on UI thread.
167      */
notifyActionsAdapterChanged()168     final void notifyActionsAdapterChanged() {
169         if (mListeners != null) {
170             for (int i = 0; i < mListeners.size();) {
171                 Listener l = mListeners.get(i).get();
172                 if (l == null) {
173                     mListeners.remove(i);
174                 } else {
175                     l.onActionsAdapterChanged(this);
176                     i++;
177                 }
178             }
179         }
180     }
181 
182     /**
183      * Returns the main item for the details page.
184      */
getItem()185     public final Object getItem() {
186         return mItem;
187     }
188 
189     /**
190      * Sets the main item for the details page.  Must be called on UI thread after
191      * row is bound to view.
192      */
setItem(Object item)193     public final void setItem(Object item) {
194         if (item != mItem) {
195             mItem = item;
196             notifyItemChanged();
197         }
198     }
199 
200     /**
201      * Sets a drawable as the image of this details overview.  Must be called on UI thread
202      * after row is bound to view.
203      *
204      * @param drawable The drawable to set.
205      */
setImageDrawable(Drawable drawable)206     public final void setImageDrawable(Drawable drawable) {
207         if (mImageDrawable != drawable) {
208             mImageDrawable = drawable;
209             notifyImageDrawableChanged();
210         }
211     }
212 
213     /**
214      * Sets a Bitmap as the image of this details overview.  Must be called on UI thread
215      * after row is bound to view.
216      *
217      * @param context The context to retrieve display metrics from.
218      * @param bm The bitmap to set.
219      */
setImageBitmap(Context context, Bitmap bm)220     public final void setImageBitmap(Context context, Bitmap bm) {
221         mImageDrawable = new BitmapDrawable(context.getResources(), bm);
222         notifyImageDrawableChanged();
223     }
224 
225     /**
226      * Returns the image drawable of this details overview.
227      *
228      * @return The overview's image drawable, or null if no drawable has been
229      *         assigned.
230      */
getImageDrawable()231     public final Drawable getImageDrawable() {
232         return mImageDrawable;
233     }
234 
235     /**
236      * Allows or disallows scaling up of images.
237      * Images will always be scaled down if necessary.  Must be called on UI thread
238      * after row is bound to view.
239      */
setImageScaleUpAllowed(boolean allowed)240     public void setImageScaleUpAllowed(boolean allowed) {
241         if (allowed != mImageScaleUpAllowed) {
242             mImageScaleUpAllowed = allowed;
243             notifyImageDrawableChanged();
244         }
245     }
246 
247     /**
248      * Returns true if the image may be scaled up; false otherwise.
249      */
isImageScaleUpAllowed()250     public boolean isImageScaleUpAllowed() {
251         return mImageScaleUpAllowed;
252     }
253 
254     /**
255      * Returns the actions adapter.  Throws ClassCastException if the current
256      * actions adapter is not an instance of {@link ArrayObjectAdapter}.
257      */
getArrayObjectAdapter()258     private ArrayObjectAdapter getArrayObjectAdapter() {
259         return (ArrayObjectAdapter) mActionsAdapter;
260     }
261 
262     /**
263      * Adds an Action to the overview. It will throw ClassCastException if the current actions
264      * adapter is not an instance of {@link ArrayObjectAdapter}. Must be called on the UI thread.
265      *
266      * @param action The Action to add.
267      * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
268      */
269     @Deprecated
addAction(Action action)270     public final void addAction(Action action) {
271         getArrayObjectAdapter().add(action);
272     }
273 
274     /**
275      * Adds an Action to the overview at the specified position. It will throw ClassCastException if
276      * current actions adapter is not an instance of f{@link ArrayObjectAdapter}. Must be called
277      * on the UI thread.
278      *
279      * @param pos The position to insert the Action.
280      * @param action The Action to add.
281      * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
282      */
283     @Deprecated
addAction(int pos, Action action)284     public final void addAction(int pos, Action action) {
285         getArrayObjectAdapter().add(pos, action);
286     }
287 
288     /**
289      * Removes the given Action from the overview. It will throw ClassCastException if current
290      * actions adapter is not {@link ArrayObjectAdapter}. Must be called on UI thread.
291      *
292      * @param action The Action to remove.
293      * @return true if the overview contained the specified Action.
294      * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
295      */
296     @Deprecated
removeAction(Action action)297     public final boolean removeAction(Action action) {
298         return getArrayObjectAdapter().remove(action);
299     }
300 
301     /**
302      * Returns a read-only view of the list of Actions of this details overview. It will throw
303      * ClassCastException if current actions adapter is not {@link ArrayObjectAdapter}. Must be
304      * called on UI thread.
305      *
306      * @return An unmodifiable view of the list of Actions.
307      * @deprecated Use {@link #setActionsAdapter(ObjectAdapter)} and {@link #getActionsAdapter()}
308      */
309     @Deprecated
getActions()310     public final List<Action> getActions() {
311         return getArrayObjectAdapter().unmodifiableList();
312     }
313 
314     /**
315      * Returns the {@link ObjectAdapter} for actions.
316      */
getActionsAdapter()317     public final ObjectAdapter getActionsAdapter() {
318         return mActionsAdapter;
319     }
320 
321     /**
322      * Sets the {@link ObjectAdapter} for actions.  A default {@link PresenterSelector} will be
323      * attached to the adapter if it doesn't have one.
324      *
325      * @param adapter  Adapter for actions.
326      */
setActionsAdapter(ObjectAdapter adapter)327     public final void setActionsAdapter(ObjectAdapter adapter) {
328         if (adapter != mActionsAdapter) {
329             mActionsAdapter = adapter;
330             if (mActionsAdapter.getPresenterSelector() == null) {
331                 mActionsAdapter.setPresenterSelector(mDefaultActionPresenter);
332             }
333             notifyActionsAdapterChanged();
334         }
335     }
336 
337     /**
338      * Returns the Action associated with the given keycode, or null if no associated action exists.
339      */
getActionForKeyCode(int keyCode)340     public Action getActionForKeyCode(int keyCode) {
341         ObjectAdapter adapter = getActionsAdapter();
342         if (adapter != null) {
343             for (int i = 0; i < adapter.size(); i++) {
344                 Action action = (Action) adapter.get(i);
345                 if (action.respondsToKeyCode(keyCode)) {
346                     return action;
347                 }
348             }
349         }
350         return null;
351     }
352 
verify()353     private void verify() {
354         if (mItem == null) {
355             throw new IllegalArgumentException("Object cannot be null");
356         }
357     }
358 }
359