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 * Renders a {@link DetailsOverviewRow} to display an overview of an item. 40 * Typically this row will be the first row in a fragment 41 * such as the {@link android.support.v17.leanback.app.DetailsFragment 42 * DetailsFragment}. The View created by the 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)}. Typically this will be an instance of 48 * {@link AbstractDetailsDescriptionPresenter}. The application can access the 49 * detailed description ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}. 50 * </p> 51 * 52 * <p> 53 * To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity, 54 * String)} during Activity's onCreate(). 55 * </p> 56 * 57 * <p> 58 * Because transition support and layout are fully controlled by DetailsOverviewRowPresenter, 59 * developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views 60 * of DetailsOverviewRowPresenter. If further customization is required beyond replacing 61 * the detailed description, the application should create a new row presenter class. 62 * </p> 63 * @deprecated Use {@link FullWidthDetailsOverviewRowPresenter} 64 */ 65@Deprecated 66public class DetailsOverviewRowPresenter extends RowPresenter { 67 68 private static final String TAG = "DetailsOverviewRowPresenter"; 69 private static final boolean DEBUG = false; 70 71 private static final int MORE_ACTIONS_FADE_MS = 100; 72 private static final long DEFAULT_TIMEOUT = 5000; 73 74 class ActionsItemBridgeAdapter extends ItemBridgeAdapter { 75 DetailsOverviewRowPresenter.ViewHolder mViewHolder; 76 77 ActionsItemBridgeAdapter(DetailsOverviewRowPresenter.ViewHolder viewHolder) { 78 mViewHolder = viewHolder; 79 } 80 81 @Override 82 public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) { 83 if (mViewHolder.getOnItemViewClickedListener() != null || 84 mActionClickedListener != null) { 85 ibvh.getPresenter().setOnClickListener( 86 ibvh.getViewHolder(), new View.OnClickListener() { 87 @Override 88 public void onClick(View v) { 89 if (mViewHolder.getOnItemViewClickedListener() != null) { 90 mViewHolder.getOnItemViewClickedListener().onItemClicked( 91 ibvh.getViewHolder(), ibvh.getItem(), 92 mViewHolder, mViewHolder.getRow()); 93 } 94 if (mActionClickedListener != null) { 95 mActionClickedListener.onActionClicked((Action) ibvh.getItem()); 96 } 97 } 98 }); 99 } 100 } 101 @Override 102 public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) { 103 if (mViewHolder.getOnItemViewClickedListener() != null || 104 mActionClickedListener != null) { 105 ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null); 106 } 107 } 108 @Override 109 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 110 // Remove first to ensure we don't add ourselves more than once. 111 viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener); 112 viewHolder.itemView.addOnLayoutChangeListener(mViewHolder.mLayoutChangeListener); 113 } 114 @Override 115 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 116 viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener); 117 mViewHolder.checkFirstAndLastPosition(false); 118 } 119 } 120 121 /** 122 * A ViewHolder for the DetailsOverviewRow. 123 */ 124 public final class ViewHolder extends RowPresenter.ViewHolder { 125 final FrameLayout mOverviewFrame; 126 final ViewGroup mOverviewView; 127 final ImageView mImageView; 128 final ViewGroup mRightPanel; 129 final FrameLayout mDetailsDescriptionFrame; 130 final HorizontalGridView mActionsRow; 131 public final Presenter.ViewHolder mDetailsDescriptionViewHolder; 132 int mNumItems; 133 boolean mShowMoreRight; 134 boolean mShowMoreLeft; 135 ItemBridgeAdapter mActionBridgeAdapter; 136 final Handler mHandler = new Handler(); 137 138 final Runnable mUpdateDrawableCallback = new Runnable() { 139 @Override 140 public void run() { 141 bindImageDrawable(ViewHolder.this); 142 } 143 }; 144 145 final DetailsOverviewRow.Listener mListener = new DetailsOverviewRow.Listener() { 146 @Override 147 public void onImageDrawableChanged(DetailsOverviewRow row) { 148 mHandler.removeCallbacks(mUpdateDrawableCallback); 149 mHandler.post(mUpdateDrawableCallback); 150 } 151 152 @Override 153 public void onItemChanged(DetailsOverviewRow row) { 154 if (mDetailsDescriptionViewHolder != null) { 155 mDetailsPresenter.onUnbindViewHolder(mDetailsDescriptionViewHolder); 156 } 157 mDetailsPresenter.onBindViewHolder(mDetailsDescriptionViewHolder, row.getItem()); 158 } 159 160 @Override 161 public void onActionsAdapterChanged(DetailsOverviewRow row) { 162 bindActions(row.getActionsAdapter()); 163 } 164 }; 165 166 void bindActions(ObjectAdapter adapter) { 167 mActionBridgeAdapter.setAdapter(adapter); 168 mActionsRow.setAdapter(mActionBridgeAdapter); 169 mNumItems = mActionBridgeAdapter.getItemCount(); 170 171 mShowMoreRight = false; 172 mShowMoreLeft = true; 173 showMoreLeft(false); 174 } 175 176 final View.OnLayoutChangeListener mLayoutChangeListener = 177 new View.OnLayoutChangeListener() { 178 179 @Override 180 public void onLayoutChange(View v, int left, int top, int right, 181 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 182 if (DEBUG) Log.v(TAG, "onLayoutChange " + v); 183 checkFirstAndLastPosition(false); 184 } 185 }; 186 187 final OnChildSelectedListener mChildSelectedListener = new OnChildSelectedListener() { 188 @Override 189 public void onChildSelected(ViewGroup parent, View view, int position, long id) { 190 dispatchItemSelection(view); 191 } 192 }; 193 194 void dispatchItemSelection(View view) { 195 if (!isSelected()) { 196 return; 197 } 198 ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) (view != null ? 199 mActionsRow.getChildViewHolder(view) : 200 mActionsRow.findViewHolderForPosition(mActionsRow.getSelectedPosition())); 201 if (ibvh == null) { 202 if (getOnItemViewSelectedListener() != null) { 203 getOnItemViewSelectedListener().onItemSelected(null, null, 204 ViewHolder.this, getRow()); 205 } 206 } else { 207 if (getOnItemViewSelectedListener() != null) { 208 getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(), 209 ViewHolder.this, getRow()); 210 } 211 } 212 }; 213 214 final RecyclerView.OnScrollListener mScrollListener = 215 new RecyclerView.OnScrollListener() { 216 217 @Override 218 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 219 } 220 @Override 221 public void onScrolled(RecyclerView recyclerView, int dx, int dy) { 222 checkFirstAndLastPosition(true); 223 } 224 }; 225 226 private int getViewCenter(View view) { 227 return (view.getRight() - view.getLeft()) / 2; 228 } 229 230 private void checkFirstAndLastPosition(boolean fromScroll) { 231 RecyclerView.ViewHolder viewHolder; 232 233 viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1); 234 boolean showRight = (viewHolder == null || 235 viewHolder.itemView.getRight() > mActionsRow.getWidth()); 236 237 viewHolder = mActionsRow.findViewHolderForPosition(0); 238 boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0); 239 240 if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll + 241 " showRight " + showRight + " showLeft " + showLeft); 242 243 showMoreRight(showRight); 244 showMoreLeft(showLeft); 245 } 246 247 private void showMoreLeft(boolean show) { 248 if (show != mShowMoreLeft) { 249 mActionsRow.setFadingLeftEdge(show); 250 mShowMoreLeft = show; 251 } 252 } 253 254 private void showMoreRight(boolean show) { 255 if (show != mShowMoreRight) { 256 mActionsRow.setFadingRightEdge(show); 257 mShowMoreRight = show; 258 } 259 } 260 261 /** 262 * Constructor for the ViewHolder. 263 * 264 * @param rootView The root View that this view holder will be attached 265 * to. 266 */ 267 public ViewHolder(View rootView, Presenter detailsPresenter) { 268 super(rootView); 269 mOverviewFrame = (FrameLayout) rootView.findViewById(R.id.details_frame); 270 mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview); 271 mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image); 272 mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel); 273 mDetailsDescriptionFrame = 274 (FrameLayout) mRightPanel.findViewById(R.id.details_overview_description); 275 mActionsRow = 276 (HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions); 277 mActionsRow.setHasOverlappingRendering(false); 278 mActionsRow.setOnScrollListener(mScrollListener); 279 mActionsRow.setAdapter(mActionBridgeAdapter); 280 mActionsRow.setOnChildSelectedListener(mChildSelectedListener); 281 282 final int fadeLength = rootView.getResources().getDimensionPixelSize( 283 R.dimen.lb_details_overview_actions_fade_size); 284 mActionsRow.setFadingRightEdgeLength(fadeLength); 285 mActionsRow.setFadingLeftEdgeLength(fadeLength); 286 mDetailsDescriptionViewHolder = 287 detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame); 288 mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view); 289 } 290 } 291 292 private final Presenter mDetailsPresenter; 293 private OnActionClickedListener mActionClickedListener; 294 295 private int mBackgroundColor = Color.TRANSPARENT; 296 private boolean mBackgroundColorSet; 297 private boolean mIsStyleLarge = true; 298 299 private DetailsOverviewSharedElementHelper mSharedElementHelper; 300 301 /** 302 * Constructor for a DetailsOverviewRowPresenter. 303 * 304 * @param detailsPresenter The {@link Presenter} used to render the detailed 305 * description of the row. 306 */ 307 public DetailsOverviewRowPresenter(Presenter detailsPresenter) { 308 setHeaderPresenter(null); 309 setSelectEffectEnabled(false); 310 mDetailsPresenter = detailsPresenter; 311 } 312 313 /** 314 * Sets the listener for Action click events. 315 */ 316 public void setOnActionClickedListener(OnActionClickedListener listener) { 317 mActionClickedListener = listener; 318 } 319 320 /** 321 * Returns the listener for Action click events. 322 */ 323 public OnActionClickedListener getOnActionClickedListener() { 324 return mActionClickedListener; 325 } 326 327 /** 328 * Sets the background color. If not set, a default from the theme will be used. 329 */ 330 public void setBackgroundColor(@ColorInt int color) { 331 mBackgroundColor = color; 332 mBackgroundColorSet = true; 333 } 334 335 /** 336 * Returns the background color. If no background color was set, transparent 337 * is returned. 338 */ 339 @ColorInt 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 * Sets the enter transition of target activity to be 361 * transiting into overview row created by this presenter. The transition will 362 * be cancelled if the 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 starting edge of the detail 366 * overview row, while bounds of overview row grows and reveals text 367 * and action buttons. 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 * Sets the enter transition of target activity 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 starting edge of the detail 387 * overview row, while bounds of overview row grows and reveals text 388 * and action buttons. 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 if (context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true)) { 400 return context.getResources().getColor(outValue.resourceId); 401 } 402 return context.getResources().getColor(R.color.lb_default_brand_color); 403 } 404 405 @Override 406 protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) { 407 super.onRowViewSelected(vh, selected); 408 if (selected) { 409 ((ViewHolder) vh).dispatchItemSelection(null); 410 } 411 } 412 413 @Override 414 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 415 View v = LayoutInflater.from(parent.getContext()) 416 .inflate(R.layout.lb_details_overview, parent, false); 417 ViewHolder vh = new ViewHolder(v, mDetailsPresenter); 418 419 initDetailsOverview(vh); 420 421 return vh; 422 } 423 424 private int getCardHeight(Context context) { 425 int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large : 426 R.dimen.lb_details_overview_height_small; 427 return context.getResources().getDimensionPixelSize(resId); 428 } 429 430 private void initDetailsOverview(final ViewHolder vh) { 431 vh.mActionBridgeAdapter = new ActionsItemBridgeAdapter(vh); 432 final View overview = vh.mOverviewFrame; 433 ViewGroup.LayoutParams lp = overview.getLayoutParams(); 434 lp.height = getCardHeight(overview.getContext()); 435 overview.setLayoutParams(lp); 436 437 if (!getSelectEffectEnabled()) { 438 vh.mOverviewFrame.setForeground(null); 439 } 440 vh.mActionsRow.setOnUnhandledKeyListener(new BaseGridView.OnUnhandledKeyListener() { 441 @Override 442 public boolean onUnhandledKey(KeyEvent event) { 443 if (vh.getOnKeyListener() != null) { 444 if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) { 445 return true; 446 } 447 } 448 return false; 449 } 450 }); 451 } 452 453 private static int getNonNegativeWidth(Drawable drawable) { 454 final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth(); 455 return (width > 0 ? width : 0); 456 } 457 458 private static int getNonNegativeHeight(Drawable drawable) { 459 final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight(); 460 return (height > 0 ? height : 0); 461 } 462 463 private void bindImageDrawable(ViewHolder vh) { 464 DetailsOverviewRow row = (DetailsOverviewRow) vh.getRow(); 465 466 ViewGroup.MarginLayoutParams layoutParams = 467 (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams(); 468 final int cardHeight = getCardHeight(vh.mImageView.getContext()); 469 final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize( 470 R.dimen.lb_details_overview_image_margin_vertical); 471 final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize( 472 R.dimen.lb_details_overview_image_margin_horizontal); 473 final int drawableWidth = getNonNegativeWidth(row.getImageDrawable()); 474 final int drawableHeight = getNonNegativeHeight(row.getImageDrawable()); 475 476 boolean scaleImage = row.isImageScaleUpAllowed(); 477 boolean useMargin = false; 478 479 if (row.getImageDrawable() != null) { 480 boolean landscape = false; 481 482 // If large style and landscape image we always use margin. 483 if (drawableWidth > drawableHeight) { 484 landscape = true; 485 if (mIsStyleLarge) { 486 useMargin = true; 487 } 488 } 489 // If long dimension bigger than the card height we scale down. 490 if ((landscape && drawableWidth > cardHeight) || 491 (!landscape && drawableHeight > cardHeight)) { 492 scaleImage = true; 493 } 494 // If we're not scaling to fit the card height then we always use margin. 495 if (!scaleImage) { 496 useMargin = true; 497 } 498 // If using margin than may need to scale down. 499 if (useMargin && !scaleImage) { 500 if (landscape && drawableWidth > cardHeight - horizontalMargin) { 501 scaleImage = true; 502 } else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) { 503 scaleImage = true; 504 } 505 } 506 } 507 508 final int bgColor = mBackgroundColorSet ? mBackgroundColor : 509 getDefaultBackgroundColor(vh.mOverviewView.getContext()); 510 511 if (useMargin) { 512 layoutParams.setMarginStart(horizontalMargin); 513 layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin; 514 vh.mOverviewFrame.setBackgroundColor(bgColor); 515 vh.mRightPanel.setBackground(null); 516 vh.mImageView.setBackground(null); 517 } else { 518 layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0; 519 vh.mRightPanel.setBackgroundColor(bgColor); 520 vh.mImageView.setBackgroundColor(bgColor); 521 vh.mOverviewFrame.setBackground(null); 522 } 523 RoundedRectHelper.getInstance().setClipToRoundedOutline(vh.mOverviewFrame, true); 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