Home | History | Annotate | Download | only in widget
      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
    152     public ImageCardView(Context context, int themeResId) {
    153         this(new ContextThemeWrapper(context, themeResId));
    154     }
    155 
    156     /**
    157      * @see #View(Context, AttributeSet, int)
    158      */
    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 
    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      */
    283     public ImageCardView(Context context) {
    284         this(context, null);
    285     }
    286 
    287     /**
    288      * @see #View(Context, AttributeSet)
    289      */
    290     public ImageCardView(Context context, AttributeSet attrs) {
    291         this(context, attrs, R.attr.imageCardViewStyle);
    292     }
    293 
    294     /**
    295      * Returns the main image view.
    296      */
    297     public final ImageView getMainImageView() {
    298         return mImageView;
    299     }
    300 
    301     /**
    302      * Enables or disables adjustment of view bounds on the main image.
    303      */
    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      */
    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      */
    322     public void setMainImage(Drawable drawable) {
    323         setMainImage(drawable, true);
    324     }
    325 
    326     /**
    327      * Sets the image drawable with optional fade-in animation.
    328      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    459     public Drawable getBadgeImage() {
    460         if (mBadgeImage == null) {
    461             return null;
    462         }
    463 
    464         return mBadgeImage.getDrawable();
    465     }
    466 
    467     private void fadeIn() {
    468         mImageView.setAlpha(0f);
    469         if (mAttachedToWindow) {
    470             mFadeInAnimator.start();
    471         }
    472     }
    473 
    474     @Override
    475     public boolean hasOverlappingRendering() {
    476         return false;
    477     }
    478 
    479     @Override
    480     protected void onAttachedToWindow() {
    481         super.onAttachedToWindow();
    482         mAttachedToWindow = true;
    483         if (mImageView.getAlpha() == 0) {
    484             fadeIn();
    485         }
    486     }
    487 
    488     @Override
    489     protected void onDetachedFromWindow() {
    490         mAttachedToWindow = false;
    491         mFadeInAnimator.cancel();
    492         mImageView.setAlpha(1f);
    493         super.onDetachedFromWindow();
    494     }
    495 }
    496