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