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