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