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