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