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