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