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