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