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