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