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 */ 14 package androidx.leanback.widget; 15 16 import android.animation.ObjectAnimator; 17 import android.content.Context; 18 import android.content.res.TypedArray; 19 import android.graphics.drawable.Drawable; 20 import android.util.AttributeSet; 21 import android.view.ContextThemeWrapper; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.widget.ImageView; 26 import android.widget.ImageView.ScaleType; 27 import android.widget.RelativeLayout; 28 import android.widget.TextView; 29 30 import androidx.annotation.ColorInt; 31 import androidx.leanback.R; 32 33 /** 34 * A subclass of {@link BaseCardView} with an {@link ImageView} as its main region. The 35 * {@link ImageCardView} is highly customizable and can be used for various use-cases by adjusting 36 * the ImageViewCard's type to any combination of Title, Content, Badge or ImageOnly. 37 * <p> 38 * <h3>Styling</h3> There are two different ways to style the ImageCardView. <br> 39 * No matter what way you use, all your styles applied to an ImageCardView have to extend the style 40 * {@link R.style#Widget_Leanback_ImageCardViewStyle}. 41 * <p> 42 * <u>Example:</u><br> 43 * 44 * <pre> 45 * {@code 46 * <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> 47 <item name="cardBackground">#F0F</item> 48 <item name="lbImageCardViewType">Title|Content</item> 49 </style> 50 <style name="CustomImageCardTheme" parent="Theme.Leanback"> 51 <item name="imageCardViewStyle">@style/CustomImageCardViewStyle</item> 52 <item name="imageCardViewInfoAreaStyle">@style/ImageCardViewColoredInfoArea</item> 53 <item name="imageCardViewTitleStyle">@style/ImageCardViewColoredTitle</item> 54 </style>} 55 * </pre> 56 * <p> 57 * The first possibility is to set custom Styles in the Leanback Theme's attributes 58 * <code>imageCardViewStyle</code>, <code>imageCardViewTitleStyle</code> etc. The styles set here, 59 * is the default style for all ImageCardViews. 60 * <p> 61 * The second possibility allows you to style a particular ImageCardView. This is useful if you 62 * want to create multiple types of cards. E.g. you might want to display a card with only a title 63 * and another one with title and content. Thus you need to define two different 64 * <code>ImageCardViewStyles</code> and two different themes and apply them to the ImageCardViews. 65 * You can do this by using a the {@link #ImageCardView(Context)} constructor and passing a 66 * ContextThemeWrapper with the custom ImageCardView theme id. 67 * <p> 68 * <u>Example (using constructor):</u><br> 69 * 70 * <pre> 71 * {@code 72 * new ImageCardView(new ContextThemeWrapper(context, R.style.CustomImageCardTheme)); 73 * } 74 * </pre> 75 * 76 * <p> 77 * You can style all ImageCardView's components such as the title, content, badge, infoArea and the 78 * image itself by extending the corresponding style and overriding the specific attribute in your 79 * custom ImageCardView theme. 80 * 81 * <h3>Components</h3> The ImageCardView contains three components which can be combined in any 82 * combination: 83 * <ul> 84 * <li>Title: The card's title</li> 85 * <li>Content: A short description</li> 86 * <li>Badge: An icon which can be displayed on the right or left side of the card.</li> 87 * </ul> 88 * In order to choose the components you want to use in your ImageCardView, you have to specify them 89 * in the <code>lbImageCardViewType</code> attribute of your custom <code>ImageCardViewStyle</code>. 90 * You can combine the following values: 91 * <code>Title, Content, IconOnRight, IconOnLeft, ImageOnly</code>. 92 * <p> 93 * <u>Examples:</u><br> 94 * 95 * <pre> 96 * {@code <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> 97 ... 98 <item name="lbImageCardViewType">Title|Content|IconOnLeft</item> 99 ... 100 </style>} 101 * </pre> 102 * 103 * <pre> 104 * {@code <style name="CustomImageCardViewStyle" parent="Widget.Leanback.ImageCardViewStyle"> 105 ... 106 <item name="lbImageCardViewType">ImageOnly</item> 107 ... 108 </style>} 109 * </pre> 110 * 111 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewStyle 112 * @attr ref androidx.leanback.R.styleable#lbImageCardView_lbImageCardViewType 113 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewTitleStyle 114 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewContentStyle 115 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewBadgeStyle 116 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewImageStyle 117 * @attr ref androidx.leanback.R.styleable#LeanbackTheme_imageCardViewInfoAreaStyle 118 */ 119 public class ImageCardView extends BaseCardView { 120 121 public static final int CARD_TYPE_FLAG_IMAGE_ONLY = 0; 122 public static final int CARD_TYPE_FLAG_TITLE = 1; 123 public static final int CARD_TYPE_FLAG_CONTENT = 2; 124 public static final int CARD_TYPE_FLAG_ICON_RIGHT = 4; 125 public static final int CARD_TYPE_FLAG_ICON_LEFT = 8; 126 127 private static final String ALPHA = "alpha"; 128 129 private ImageView mImageView; 130 private ViewGroup mInfoArea; 131 private TextView mTitleView; 132 private TextView mContentView; 133 private ImageView mBadgeImage; 134 private boolean mAttachedToWindow; 135 ObjectAnimator mFadeInAnimator; 136 137 /** 138 * Create an ImageCardView using a given theme for customization. 139 * 140 * @param context 141 * The Context the view is running in, through which it can 142 * access the current theme, resources, etc. 143 * @param themeResId 144 * The resourceId of the theme you want to apply to the ImageCardView. The theme 145 * includes attributes "imageCardViewStyle", "imageCardViewTitleStyle", 146 * "imageCardViewContentStyle" etc. to customize individual part of ImageCardView. 147 * @deprecated Calling this constructor inefficiently creates one ContextThemeWrapper per card, 148 * you should share it in card Presenter: wrapper = new ContextThemeWrapper(context, themResId); 149 * return new ImageCardView(wrapper); 150 */ 151 @Deprecated ImageCardView(Context context, int themeResId)152 public ImageCardView(Context context, int themeResId) { 153 this(new ContextThemeWrapper(context, themeResId)); 154 } 155 156 /** 157 * @see #View(Context, AttributeSet, int) 158 */ ImageCardView(Context context, AttributeSet attrs, int defStyleAttr)159 public ImageCardView(Context context, AttributeSet attrs, int defStyleAttr) { 160 super(context, attrs, defStyleAttr); 161 buildImageCardView(attrs, defStyleAttr, R.style.Widget_Leanback_ImageCardView); 162 } 163 buildImageCardView(AttributeSet attrs, int defStyleAttr, int defStyle)164 private void buildImageCardView(AttributeSet attrs, int defStyleAttr, int defStyle) { 165 // Make sure the ImageCardView is focusable. 166 setFocusable(true); 167 setFocusableInTouchMode(true); 168 169 LayoutInflater inflater = LayoutInflater.from(getContext()); 170 inflater.inflate(R.layout.lb_image_card_view, this); 171 TypedArray cardAttrs = getContext().obtainStyledAttributes(attrs, 172 R.styleable.lbImageCardView, defStyleAttr, defStyle); 173 int cardType = cardAttrs 174 .getInt(R.styleable.lbImageCardView_lbImageCardViewType, CARD_TYPE_FLAG_IMAGE_ONLY); 175 176 boolean hasImageOnly = cardType == CARD_TYPE_FLAG_IMAGE_ONLY; 177 boolean hasTitle = (cardType & CARD_TYPE_FLAG_TITLE) == CARD_TYPE_FLAG_TITLE; 178 boolean hasContent = (cardType & CARD_TYPE_FLAG_CONTENT) == CARD_TYPE_FLAG_CONTENT; 179 boolean hasIconRight = (cardType & CARD_TYPE_FLAG_ICON_RIGHT) == CARD_TYPE_FLAG_ICON_RIGHT; 180 boolean hasIconLeft = 181 !hasIconRight && (cardType & CARD_TYPE_FLAG_ICON_LEFT) == CARD_TYPE_FLAG_ICON_LEFT; 182 183 mImageView = findViewById(R.id.main_image); 184 if (mImageView.getDrawable() == null) { 185 mImageView.setVisibility(View.INVISIBLE); 186 } 187 // Set Object Animator for image view. 188 mFadeInAnimator = ObjectAnimator.ofFloat(mImageView, ALPHA, 1f); 189 mFadeInAnimator.setDuration( 190 mImageView.getResources().getInteger(android.R.integer.config_shortAnimTime)); 191 192 mInfoArea = findViewById(R.id.info_field); 193 if (hasImageOnly) { 194 removeView(mInfoArea); 195 cardAttrs.recycle(); 196 return; 197 } 198 // Create children 199 if (hasTitle) { 200 mTitleView = (TextView) inflater.inflate(R.layout.lb_image_card_view_themed_title, 201 mInfoArea, false); 202 mInfoArea.addView(mTitleView); 203 } 204 205 if (hasContent) { 206 mContentView = (TextView) inflater.inflate(R.layout.lb_image_card_view_themed_content, 207 mInfoArea, false); 208 mInfoArea.addView(mContentView); 209 } 210 211 if (hasIconRight || hasIconLeft) { 212 int layoutId = R.layout.lb_image_card_view_themed_badge_right; 213 if (hasIconLeft) { 214 layoutId = R.layout.lb_image_card_view_themed_badge_left; 215 } 216 mBadgeImage = (ImageView) inflater.inflate(layoutId, mInfoArea, false); 217 mInfoArea.addView(mBadgeImage); 218 } 219 220 // Set up LayoutParams for children 221 if (hasTitle && !hasContent && mBadgeImage != null) { 222 RelativeLayout.LayoutParams relativeLayoutParams = 223 (RelativeLayout.LayoutParams) mTitleView.getLayoutParams(); 224 // Adjust title TextView if there is an icon but no content 225 if (hasIconLeft) { 226 relativeLayoutParams.addRule(RelativeLayout.END_OF, mBadgeImage.getId()); 227 } else { 228 relativeLayoutParams.addRule(RelativeLayout.START_OF, mBadgeImage.getId()); 229 } 230 mTitleView.setLayoutParams(relativeLayoutParams); 231 } 232 233 // Set up LayoutParams for children 234 if (hasContent) { 235 RelativeLayout.LayoutParams relativeLayoutParams = 236 (RelativeLayout.LayoutParams) mContentView.getLayoutParams(); 237 if (!hasTitle) { 238 relativeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP); 239 } 240 // Adjust content TextView if icon is on the left 241 if (hasIconLeft) { 242 relativeLayoutParams.removeRule(RelativeLayout.START_OF); 243 relativeLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_START); 244 relativeLayoutParams.addRule(RelativeLayout.END_OF, mBadgeImage.getId()); 245 } 246 mContentView.setLayoutParams(relativeLayoutParams); 247 } 248 249 if (mBadgeImage != null) { 250 RelativeLayout.LayoutParams relativeLayoutParams = 251 (RelativeLayout.LayoutParams) mBadgeImage.getLayoutParams(); 252 if (hasContent) { 253 relativeLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mContentView.getId()); 254 } else if (hasTitle) { 255 relativeLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, mTitleView.getId()); 256 } 257 mBadgeImage.setLayoutParams(relativeLayoutParams); 258 } 259 260 // Backward compatibility: Newly created ImageCardViews should change 261 // the InfoArea's background color in XML using the corresponding style. 262 // However, since older implementations might make use of the 263 // 'infoAreaBackground' attribute, we have to make sure to support it. 264 // If the user has set a specific value here, it will differ from null. 265 // In this case, we do want to override the value set in the style. 266 Drawable background = cardAttrs.getDrawable(R.styleable.lbImageCardView_infoAreaBackground); 267 if (null != background) { 268 setInfoAreaBackground(background); 269 } 270 // Backward compatibility: There has to be an icon in the default 271 // version. If there is one, we have to set its visibility to 'GONE'. 272 // Disabling 'adjustIconVisibility' allows the user to set the icon's 273 // visibility state in XML rather than code. 274 if (mBadgeImage != null && mBadgeImage.getDrawable() == null) { 275 mBadgeImage.setVisibility(View.GONE); 276 } 277 cardAttrs.recycle(); 278 } 279 280 /** 281 * @see #View(Context) 282 */ ImageCardView(Context context)283 public ImageCardView(Context context) { 284 this(context, null); 285 } 286 287 /** 288 * @see #View(Context, AttributeSet) 289 */ ImageCardView(Context context, AttributeSet attrs)290 public ImageCardView(Context context, AttributeSet attrs) { 291 this(context, attrs, R.attr.imageCardViewStyle); 292 } 293 294 /** 295 * Returns the main image view. 296 */ getMainImageView()297 public final ImageView getMainImageView() { 298 return mImageView; 299 } 300 301 /** 302 * Enables or disables adjustment of view bounds on the main image. 303 */ setMainImageAdjustViewBounds(boolean adjustViewBounds)304 public void setMainImageAdjustViewBounds(boolean adjustViewBounds) { 305 if (mImageView != null) { 306 mImageView.setAdjustViewBounds(adjustViewBounds); 307 } 308 } 309 310 /** 311 * Sets the ScaleType of the main image. 312 */ setMainImageScaleType(ScaleType scaleType)313 public void setMainImageScaleType(ScaleType scaleType) { 314 if (mImageView != null) { 315 mImageView.setScaleType(scaleType); 316 } 317 } 318 319 /** 320 * Sets the image drawable with fade-in animation. 321 */ setMainImage(Drawable drawable)322 public void setMainImage(Drawable drawable) { 323 setMainImage(drawable, true); 324 } 325 326 /** 327 * Sets the image drawable with optional fade-in animation. 328 */ setMainImage(Drawable drawable, boolean fade)329 public void setMainImage(Drawable drawable, boolean fade) { 330 if (mImageView == null) { 331 return; 332 } 333 334 mImageView.setImageDrawable(drawable); 335 if (drawable == null) { 336 mFadeInAnimator.cancel(); 337 mImageView.setAlpha(1f); 338 mImageView.setVisibility(View.INVISIBLE); 339 } else { 340 mImageView.setVisibility(View.VISIBLE); 341 if (fade) { 342 fadeIn(); 343 } else { 344 mFadeInAnimator.cancel(); 345 mImageView.setAlpha(1f); 346 } 347 } 348 } 349 350 /** 351 * Sets the layout dimensions of the ImageView. 352 */ setMainImageDimensions(int width, int height)353 public void setMainImageDimensions(int width, int height) { 354 ViewGroup.LayoutParams lp = mImageView.getLayoutParams(); 355 lp.width = width; 356 lp.height = height; 357 mImageView.setLayoutParams(lp); 358 } 359 360 /** 361 * Returns the ImageView drawable. 362 */ getMainImage()363 public Drawable getMainImage() { 364 if (mImageView == null) { 365 return null; 366 } 367 368 return mImageView.getDrawable(); 369 } 370 371 /** 372 * Returns the info area background drawable. 373 */ getInfoAreaBackground()374 public Drawable getInfoAreaBackground() { 375 if (mInfoArea != null) { 376 return mInfoArea.getBackground(); 377 } 378 return null; 379 } 380 381 /** 382 * Sets the info area background drawable. 383 */ setInfoAreaBackground(Drawable drawable)384 public void setInfoAreaBackground(Drawable drawable) { 385 if (mInfoArea != null) { 386 mInfoArea.setBackground(drawable); 387 } 388 } 389 390 /** 391 * Sets the info area background color. 392 */ setInfoAreaBackgroundColor(@olorInt int color)393 public void setInfoAreaBackgroundColor(@ColorInt int color) { 394 if (mInfoArea != null) { 395 mInfoArea.setBackgroundColor(color); 396 } 397 } 398 399 /** 400 * Sets the title text. 401 */ setTitleText(CharSequence text)402 public void setTitleText(CharSequence text) { 403 if (mTitleView == null) { 404 return; 405 } 406 mTitleView.setText(text); 407 } 408 409 /** 410 * Returns the title text. 411 */ getTitleText()412 public CharSequence getTitleText() { 413 if (mTitleView == null) { 414 return null; 415 } 416 417 return mTitleView.getText(); 418 } 419 420 /** 421 * Sets the content text. 422 */ setContentText(CharSequence text)423 public void setContentText(CharSequence text) { 424 if (mContentView == null) { 425 return; 426 } 427 mContentView.setText(text); 428 } 429 430 /** 431 * Returns the content text. 432 */ getContentText()433 public CharSequence getContentText() { 434 if (mContentView == null) { 435 return null; 436 } 437 438 return mContentView.getText(); 439 } 440 441 /** 442 * Sets the badge image drawable. 443 */ setBadgeImage(Drawable drawable)444 public void setBadgeImage(Drawable drawable) { 445 if (mBadgeImage == null) { 446 return; 447 } 448 mBadgeImage.setImageDrawable(drawable); 449 if (drawable != null) { 450 mBadgeImage.setVisibility(View.VISIBLE); 451 } else { 452 mBadgeImage.setVisibility(View.GONE); 453 } 454 } 455 456 /** 457 * Returns the badge image drawable. 458 */ getBadgeImage()459 public Drawable getBadgeImage() { 460 if (mBadgeImage == null) { 461 return null; 462 } 463 464 return mBadgeImage.getDrawable(); 465 } 466 fadeIn()467 private void fadeIn() { 468 mImageView.setAlpha(0f); 469 if (mAttachedToWindow) { 470 mFadeInAnimator.start(); 471 } 472 } 473 474 @Override hasOverlappingRendering()475 public boolean hasOverlappingRendering() { 476 return false; 477 } 478 479 @Override onAttachedToWindow()480 protected void onAttachedToWindow() { 481 super.onAttachedToWindow(); 482 mAttachedToWindow = true; 483 if (mImageView.getAlpha() == 0) { 484 fadeIn(); 485 } 486 } 487 488 @Override onDetachedFromWindow()489 protected void onDetachedFromWindow() { 490 mAttachedToWindow = false; 491 mFadeInAnimator.cancel(); 492 mImageView.setAlpha(1f); 493 super.onDetachedFromWindow(); 494 } 495 } 496