DetailsOverviewRowPresenter.java revision d391b19d1bf663ce300b0f4550e6fbaa7e12b0d4
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.app.Activity; 17import android.content.Context; 18import android.graphics.Bitmap; 19import android.graphics.Color; 20import android.graphics.drawable.Drawable; 21import android.graphics.drawable.BitmapDrawable; 22import android.support.v17.leanback.R; 23import android.support.v7.widget.RecyclerView; 24import android.util.Log; 25import android.util.TypedValue; 26import android.view.LayoutInflater; 27import android.view.View; 28import android.view.ViewGroup; 29import android.widget.FrameLayout; 30import android.widget.ImageView; 31 32import java.util.Collection; 33 34/** 35 * A DetailsOverviewRowPresenter renders a {@link DetailsOverviewRow} to display an 36 * overview of an item. Typically this row will be the first row in a fragment 37 * such as the {@link android.support.v17.leanback.app.DetailsFragment 38 * DetailsFragment}. View created by DetailsOverviewRowPresenter is made in three parts: 39 * ImageView on the left, action list view on the bottom and a customizable detailed 40 * description view on the right. 41 * 42 * <p>The detailed description is rendered using a {@link Presenter} passed in 43 * {@link #DetailsOverviewRowPresenter(Presenter)}. User can access detailed description 44 * ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}. 45 * </p> 46 * 47 * <p> 48 * To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity, 49 * String)} during Activity's onCreate(). 50 * </p> 51 * 52 * <p> 53 * Because transition support and layout are fully controlled by DetailsOverviewRowPresenter, 54 * developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views 55 * of DetailsOverviewRowPresenter. If developer wants more customization beyond replacing 56 * detailed description , he/she should write a new presenter class for row object. 57 * </p> 58 */ 59public class DetailsOverviewRowPresenter extends RowPresenter { 60 61 private static final String TAG = "DetailsOverviewRowPresenter"; 62 private static final boolean DEBUG = false; 63 64 private static final int MORE_ACTIONS_FADE_MS = 100; 65 private static final long DEFAULT_TIMEOUT = 5000; 66 67 /** 68 * A ViewHolder for the DetailsOverviewRow. 69 */ 70 public final class ViewHolder extends RowPresenter.ViewHolder { 71 final ViewGroup mOverviewView; 72 final ImageView mImageView; 73 final ViewGroup mRightPanel; 74 final FrameLayout mDetailsDescriptionFrame; 75 final HorizontalGridView mActionsRow; 76 public final Presenter.ViewHolder mDetailsDescriptionViewHolder; 77 int mNumItems; 78 boolean mShowMoreRight; 79 boolean mShowMoreLeft; 80 final ItemBridgeAdapter mActionBridgeAdapter = new ItemBridgeAdapter(); 81 82 void bind(ObjectAdapter adapter) { 83 mActionBridgeAdapter.setAdapter(adapter); 84 mActionsRow.setAdapter(mActionBridgeAdapter); 85 mNumItems = mActionBridgeAdapter.getItemCount(); 86 87 mShowMoreRight = false; 88 mShowMoreLeft = true; 89 showMoreLeft(false); 90 } 91 92 final View.OnLayoutChangeListener mLayoutChangeListener = 93 new View.OnLayoutChangeListener() { 94 95 @Override 96 public void onLayoutChange(View v, int left, int top, int right, 97 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 98 if (DEBUG) Log.v(TAG, "onLayoutChange " + v); 99 checkFirstAndLastPosition(false); 100 } 101 }; 102 103 final OnChildSelectedListener mChildSelectedListener = new OnChildSelectedListener() { 104 @Override 105 public void onChildSelected(ViewGroup parent, View view, int position, long id) { 106 dispatchItemSelection(view); 107 } 108 }; 109 110 void dispatchItemSelection(View view) { 111 if (!isSelected()) { 112 return; 113 } 114 ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) (view != null ? 115 mActionsRow.getChildViewHolder(view) : 116 mActionsRow.findViewHolderForPosition(mActionsRow.getSelectedPosition())); 117 if (ibvh == null) { 118 if (getOnItemSelectedListener() != null) { 119 getOnItemSelectedListener().onItemSelected(null, getRow()); 120 } 121 if (getOnItemViewSelectedListener() != null) { 122 getOnItemViewSelectedListener().onItemSelected(null, null, 123 ViewHolder.this, getRow()); 124 } 125 } else { 126 if (getOnItemSelectedListener() != null) { 127 getOnItemSelectedListener().onItemSelected(ibvh.getItem(), getRow()); 128 } 129 if (getOnItemViewSelectedListener() != null) { 130 getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(), 131 ViewHolder.this, getRow()); 132 } 133 } 134 }; 135 136 final ItemBridgeAdapter.AdapterListener mAdapterListener = 137 new ItemBridgeAdapter.AdapterListener() { 138 139 @Override 140 public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) { 141 if (getOnItemViewClickedListener() != null || getOnItemClickedListener() != null 142 || mActionClickedListener != null) { 143 ibvh.getPresenter().setOnClickListener( 144 ibvh.getViewHolder(), new View.OnClickListener() { 145 @Override 146 public void onClick(View v) { 147 if (getOnItemViewClickedListener() != null) { 148 getOnItemViewClickedListener().onItemClicked(ibvh.getViewHolder(), 149 ibvh.getItem(), ViewHolder.this, getRow()); 150 } 151 if (mActionClickedListener != null) { 152 mActionClickedListener.onActionClicked((Action) ibvh.getItem()); 153 } 154 } 155 }); 156 } 157 } 158 @Override 159 public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) { 160 if (getOnItemViewClickedListener() != null || getOnItemClickedListener() != null 161 || mActionClickedListener != null) { 162 ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null); 163 } 164 } 165 @Override 166 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 167 // Remove first to ensure we don't add ourselves more than once. 168 viewHolder.itemView.removeOnLayoutChangeListener(mLayoutChangeListener); 169 viewHolder.itemView.addOnLayoutChangeListener(mLayoutChangeListener); 170 } 171 @Override 172 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 173 viewHolder.itemView.removeOnLayoutChangeListener(mLayoutChangeListener); 174 checkFirstAndLastPosition(false); 175 } 176 }; 177 178 final RecyclerView.OnScrollListener mScrollListener = 179 new RecyclerView.OnScrollListener() { 180 181 @Override 182 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 183 } 184 @Override 185 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 186 checkFirstAndLastPosition(true); 187 } 188 }; 189 190 private int getViewCenter(View view) { 191 return (view.getRight() - view.getLeft()) / 2; 192 } 193 194 private void checkFirstAndLastPosition(boolean fromScroll) { 195 RecyclerView.ViewHolder viewHolder; 196 197 viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1); 198 boolean showRight = (viewHolder == null || 199 viewHolder.itemView.getRight() > mActionsRow.getWidth()); 200 201 viewHolder = mActionsRow.findViewHolderForPosition(0); 202 boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0); 203 204 if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll + 205 " showRight " + showRight + " showLeft " + showLeft); 206 207 showMoreRight(showRight); 208 showMoreLeft(showLeft); 209 } 210 211 private void showMoreLeft(boolean show) { 212 if (show != mShowMoreLeft) { 213 mActionsRow.setFadingLeftEdge(show); 214 mShowMoreLeft = show; 215 } 216 } 217 218 private void showMoreRight(boolean show) { 219 if (show != mShowMoreRight) { 220 mActionsRow.setFadingRightEdge(show); 221 mShowMoreRight = show; 222 } 223 } 224 225 /** 226 * Constructor for the ViewHolder. 227 * 228 * @param rootView The root View that this view holder will be attached 229 * to. 230 */ 231 public ViewHolder(View rootView, Presenter detailsPresenter) { 232 super(rootView); 233 mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview); 234 mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image); 235 mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel); 236 mDetailsDescriptionFrame = 237 (FrameLayout) mRightPanel.findViewById(R.id.details_overview_description); 238 mActionsRow = 239 (HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions); 240 mActionsRow.setHasOverlappingRendering(false); 241 mActionsRow.setOnScrollListener(mScrollListener); 242 mActionsRow.setAdapter(mActionBridgeAdapter); 243 mActionsRow.setOnChildSelectedListener(mChildSelectedListener); 244 245 final int fadeLength = rootView.getResources().getDimensionPixelSize( 246 R.dimen.lb_details_overview_actions_fade_size); 247 mActionsRow.setFadingRightEdgeLength(fadeLength); 248 mActionsRow.setFadingLeftEdgeLength(fadeLength); 249 mDetailsDescriptionViewHolder = 250 detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame); 251 mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view); 252 253 mActionBridgeAdapter.setAdapterListener(mAdapterListener); 254 } 255 } 256 257 private static float sShadowZ; 258 259 private final Presenter mDetailsPresenter; 260 private final ActionPresenterSelector mActionPresenterSelector; 261 private OnActionClickedListener mActionClickedListener; 262 263 private int mBackgroundColor = Color.TRANSPARENT; 264 private boolean mBackgroundColorSet; 265 private boolean mIsStyleLarge = true; 266 267 private DetailsOverviewSharedElementHelper mSharedElementHelper; 268 269 /** 270 * Constructor for a DetailsOverviewRowPresenter. 271 * 272 * @param detailsPresenter The {@link Presenter} used to render the detailed 273 * description of the row. 274 */ 275 public DetailsOverviewRowPresenter(Presenter detailsPresenter) { 276 setHeaderPresenter(null); 277 setSelectEffectEnabled(false); 278 mDetailsPresenter = detailsPresenter; 279 mActionPresenterSelector = new ActionPresenterSelector(); 280 } 281 282 /** 283 * Sets the listener for Action click events. 284 */ 285 public void setOnActionClickedListener(OnActionClickedListener listener) { 286 mActionClickedListener = listener; 287 } 288 289 /** 290 * Gets the listener for Action click events. 291 */ 292 public OnActionClickedListener getOnActionClickedListener() { 293 return mActionClickedListener; 294 } 295 296 /** 297 * Sets the background color. If not set, a default from the theme will be used. 298 */ 299 public void setBackgroundColor(int color) { 300 mBackgroundColor = color; 301 mBackgroundColorSet = true; 302 } 303 304 /** 305 * Returns the background color. If no background color was set, transparent 306 * is returned. 307 */ 308 public int getBackgroundColor() { 309 return mBackgroundColor; 310 } 311 312 /** 313 * Sets the layout style to be large or small. This affects the height of 314 * the overview, including the text description. The default is large. 315 */ 316 public void setStyleLarge(boolean large) { 317 mIsStyleLarge = large; 318 } 319 320 /** 321 * Returns true if the layout style is large. 322 */ 323 public boolean isStyleLarge() { 324 return mIsStyleLarge; 325 } 326 327 /** 328 * Set enter transition of target activity (typically a DetailActivity) to be 329 * transiting into overview row created by this presenter. The transition will 330 * be cancelled if overview image is not loaded in the timeout period. 331 * <p> 332 * It assumes shared element passed from calling activity is an ImageView; 333 * the shared element transits to overview image on the left of detail 334 * overview row, while bounds of overview row grows and reveals text 335 * and buttons on the right. 336 * <p> 337 * The method must be invoked in target Activity's onCreate(). 338 */ 339 public final void setSharedElementEnterTransition(Activity activity, 340 String sharedElementName, long timeoutMs) { 341 if (mSharedElementHelper == null) { 342 mSharedElementHelper = new DetailsOverviewSharedElementHelper(); 343 } 344 mSharedElementHelper.setSharedElementEnterTransition(activity, sharedElementName, 345 timeoutMs); 346 } 347 348 /** 349 * Set enter transition of target activity (typically a DetailActivity) to be 350 * transiting into overview row created by this presenter. The transition will 351 * be cancelled if overview image is not loaded in a default timeout period. 352 * <p> 353 * It assumes shared element passed from calling activity is an ImageView; 354 * the shared element transits to overview image on the left of detail 355 * overview row, while bounds of overview row grows and reveals text 356 * and buttons on the right. 357 * <p> 358 * The method must be invoked in target Activity's onCreate(). 359 */ 360 public final void setSharedElementEnterTransition(Activity activity, 361 String sharedElementName) { 362 setSharedElementEnterTransition(activity, sharedElementName, DEFAULT_TIMEOUT); 363 } 364 365 private int getDefaultBackgroundColor(Context context) { 366 TypedValue outValue = new TypedValue(); 367 context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true); 368 return context.getResources().getColor(outValue.resourceId); 369 } 370 371 protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) { 372 super.onRowViewSelected(vh, selected); 373 if (selected) { 374 ((ViewHolder) vh).dispatchItemSelection(null); 375 } 376 } 377 378 @Override 379 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 380 View v = LayoutInflater.from(parent.getContext()) 381 .inflate(R.layout.lb_details_overview, parent, false); 382 ViewHolder vh = new ViewHolder(v, mDetailsPresenter); 383 384 initDetailsOverview(vh); 385 386 return vh; 387 } 388 389 private int getCardHeight(Context context) { 390 int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large : 391 R.dimen.lb_details_overview_height_small; 392 return context.getResources().getDimensionPixelSize(resId); 393 } 394 395 private void initDetailsOverview(ViewHolder vh) { 396 final View overview = vh.mOverviewView; 397 ViewGroup.LayoutParams lp = overview.getLayoutParams(); 398 lp.height = getCardHeight(overview.getContext()); 399 overview.setLayoutParams(lp); 400 401 if (sShadowZ == 0) { 402 sShadowZ = overview.getResources().getDimensionPixelSize( 403 R.dimen.lb_details_overview_z); 404 } 405 ShadowHelper.getInstance().setZ(overview, sShadowZ); 406 } 407 408 private static int getNonNegativeWidth(Drawable drawable) { 409 final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth(); 410 return (width > 0 ? width : 0); 411 } 412 413 private static int getNonNegativeHeight(Drawable drawable) { 414 final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight(); 415 return (height > 0 ? height : 0); 416 } 417 418 @Override 419 protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { 420 super.onBindRowViewHolder(holder, item); 421 422 DetailsOverviewRow row = (DetailsOverviewRow) item; 423 ViewHolder vh = (ViewHolder) holder; 424 425 ViewGroup.MarginLayoutParams layoutParams = 426 (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams(); 427 final int cardHeight = getCardHeight(vh.mImageView.getContext()); 428 final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize( 429 R.dimen.lb_details_overview_image_margin_vertical); 430 final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize( 431 R.dimen.lb_details_overview_image_margin_horizontal); 432 final int drawableWidth = getNonNegativeWidth(row.getImageDrawable()); 433 final int drawableHeight = getNonNegativeHeight(row.getImageDrawable()); 434 435 boolean scaleImage = row.isImageScaleUpAllowed(); 436 boolean useMargin = false; 437 438 if (row.getImageDrawable() != null) { 439 boolean landscape = false; 440 441 // If large style and landscape image we always use margin. 442 if (drawableWidth > drawableHeight) { 443 landscape = true; 444 if (mIsStyleLarge) { 445 useMargin = true; 446 } 447 } 448 // If long dimension bigger than the card height we scale down. 449 if ((landscape && drawableWidth > cardHeight) || 450 (!landscape && drawableHeight > cardHeight)) { 451 scaleImage = true; 452 } 453 // If we're not scaling to fit the card height then we always use margin. 454 if (!scaleImage) { 455 useMargin = true; 456 } 457 // If using margin than may need to scale down. 458 if (useMargin && !scaleImage) { 459 if (landscape && drawableWidth > cardHeight - horizontalMargin) { 460 scaleImage = true; 461 } else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) { 462 scaleImage = true; 463 } 464 } 465 } 466 467 final int bgColor = mBackgroundColorSet ? mBackgroundColor : 468 getDefaultBackgroundColor(vh.mOverviewView.getContext()); 469 470 if (useMargin) { 471 layoutParams.leftMargin = horizontalMargin; 472 layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin; 473 RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewView, bgColor); 474 vh.mRightPanel.setBackground(null); 475 vh.mImageView.setBackground(null); 476 } else { 477 layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0; 478 vh.mRightPanel.setBackgroundColor(bgColor); 479 vh.mImageView.setBackgroundColor(bgColor); 480 RoundedRectHelper.getInstance().setRoundedRectBackground(vh.mOverviewView, 481 Color.TRANSPARENT); 482 } 483 if (scaleImage) { 484 vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START); 485 vh.mImageView.setAdjustViewBounds(true); 486 vh.mImageView.setMaxWidth(cardHeight); 487 layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; 488 layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; 489 } else { 490 vh.mImageView.setScaleType(ImageView.ScaleType.CENTER); 491 vh.mImageView.setAdjustViewBounds(false); 492 layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; 493 // Limit width to the card height 494 layoutParams.width = Math.min(cardHeight, drawableWidth); 495 } 496 vh.mImageView.setLayoutParams(layoutParams); 497 vh.mImageView.setImageDrawable(row.getImageDrawable()); 498 499 mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row.getItem()); 500 501 ArrayObjectAdapter aoa = new ArrayObjectAdapter(mActionPresenterSelector); 502 aoa.addAll(0, (Collection)row.getActions()); 503 vh.bind(aoa); 504 505 if (row.getImageDrawable() != null && mSharedElementHelper != null) { 506 mSharedElementHelper.onBindToDrawable(vh); 507 } 508 } 509 510 @Override 511 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) { 512 super.onUnbindRowViewHolder(holder); 513 514 ViewHolder vh = (ViewHolder) holder; 515 if (vh.mDetailsDescriptionViewHolder != null) { 516 mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder); 517 } 518 } 519} 520