1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.dvr.ui.browse;
18 
19 import android.animation.ValueAnimator;
20 import android.content.Context;
21 import android.graphics.Bitmap;
22 import android.graphics.Rect;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.drawable.Drawable;
25 import android.support.annotation.Nullable;
26 import android.text.Layout;
27 import android.text.TextUtils;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewTreeObserver;
31 import android.widget.FrameLayout;
32 import android.widget.ImageView;
33 import android.widget.ProgressBar;
34 import android.widget.TextView;
35 import androidx.leanback.widget.BaseCardView;
36 import com.android.tv.R;
37 import com.android.tv.dvr.data.RecordedProgram;
38 import com.android.tv.ui.ViewUtils;
39 import com.android.tv.util.images.ImageLoader;
40 
41 /**
42  * A CardView for displaying info about a {@link com.android.tv.dvr.data.ScheduledRecording} or
43  * {@link RecordedProgram} or {@link com.android.tv.dvr.data.SeriesRecording}.
44  */
45 public class RecordingCardView extends BaseCardView {
46     // This value should be the same with
47     // androidx.leanback.widget.FocusHighlightHelper.BrowseItemFocusHighlight.DURATION_MS
48     private static final int ANIMATION_DURATION = 150;
49     private final ImageView mImageView;
50     private final int mImageWidth;
51     private final int mImageHeight;
52     private String mImageUri;
53     private final ImageView mContentIconView;
54     private final TextView mMajorContentView;
55     private final TextView mMinorContentView;
56     private final ProgressBar mProgressBar;
57     private final Drawable mDefaultImage;
58     private final FrameLayout mTitleArea;
59     private final TextView mFoldedTitleView;
60     private final TextView mExpandedTitleView;
61     private final ValueAnimator mExpandTitleAnimator;
62     private final int mFoldedTitleHeight;
63     private final int mExpandedTitleHeight;
64     private final boolean mExpandTitleWhenFocused;
65     private boolean mExpanded;
66     private String mDetailBackgroundImageUri;
67     private Layout mTitleViewLayout;
68 
RecordingCardView(Context context)69     public RecordingCardView(Context context) {
70         this(context, false);
71     }
72 
RecordingCardView(Context context, boolean expandTitleWhenFocused)73     public RecordingCardView(Context context, boolean expandTitleWhenFocused) {
74         this(
75                 context,
76                 context.getResources()
77                         .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_width),
78                 context.getResources()
79                         .getDimensionPixelSize(R.dimen.dvr_library_card_image_layout_height),
80                 expandTitleWhenFocused);
81     }
82 
RecordingCardView( Context context, int imageWidth, int imageHeight, boolean expandTitleWhenFocused)83     public RecordingCardView(
84             Context context, int imageWidth, int imageHeight, boolean expandTitleWhenFocused) {
85         super(context);
86         // TODO(dvr): move these to the layout XML.
87         setCardType(BaseCardView.CARD_TYPE_INFO_UNDER_WITH_EXTRA);
88         setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS);
89         setFocusable(true);
90         setFocusableInTouchMode(true);
91         mDefaultImage = getResources().getDrawable(R.drawable.dvr_default_poster, null);
92 
93         LayoutInflater inflater = LayoutInflater.from(getContext());
94         inflater.inflate(R.layout.dvr_recording_card_view, this);
95         mImageView = (ImageView) findViewById(R.id.image);
96         mImageWidth = imageWidth;
97         mImageHeight = imageHeight;
98         mProgressBar = (ProgressBar) findViewById(R.id.recording_progress);
99         mContentIconView = (ImageView) findViewById(R.id.content_icon);
100         mMajorContentView = (TextView) findViewById(R.id.content_major);
101         mMinorContentView = (TextView) findViewById(R.id.content_minor);
102         mTitleArea = (FrameLayout) findViewById(R.id.title_area);
103         mFoldedTitleView = (TextView) findViewById(R.id.title_one_line);
104         mExpandedTitleView = (TextView) findViewById(R.id.title_two_lines);
105         mFoldedTitleHeight =
106                 getResources().getDimensionPixelSize(R.dimen.dvr_library_card_folded_title_height);
107         mExpandedTitleHeight =
108                 getResources()
109                         .getDimensionPixelSize(R.dimen.dvr_library_card_expanded_title_height);
110         mExpandTitleAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(ANIMATION_DURATION);
111         mExpandTitleAnimator.addUpdateListener(
112                 new ValueAnimator.AnimatorUpdateListener() {
113                     @Override
114                     public void onAnimationUpdate(ValueAnimator valueAnimator) {
115                         float value = (Float) valueAnimator.getAnimatedValue();
116                         mExpandedTitleView.setAlpha(value);
117                         mFoldedTitleView.setAlpha(1.0f - value);
118                         ViewUtils.setLayoutHeight(
119                                 mTitleArea,
120                                 (int)
121                                         (mFoldedTitleHeight
122                                                 + (mExpandedTitleHeight - mFoldedTitleHeight)
123                                                         * value));
124                     }
125                 });
126         getViewTreeObserver().addOnGlobalLayoutListener(
127                 new ViewTreeObserver.OnGlobalLayoutListener() {
128                     @Override
129                     public void onGlobalLayout() {
130                         getViewTreeObserver().removeOnGlobalLayoutListener(this);
131                         mTitleViewLayout = mFoldedTitleView.getLayout();
132                     }
133                 });
134         mExpandTitleWhenFocused = expandTitleWhenFocused;
135     }
136 
137     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)138     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
139         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
140         // Preload the background image going to be used in detail fragments here to prevent
141         // loading and drawing background images during activity transitions.
142         if (gainFocus) {
143             if (!TextUtils.isEmpty(mDetailBackgroundImageUri)) {
144                 ImageLoader.loadBitmap(
145                         getContext(),
146                         mDetailBackgroundImageUri,
147                         Integer.MAX_VALUE,
148                         Integer.MAX_VALUE,
149                         null);
150             }
151         }
152         if (mExpandTitleWhenFocused) {
153             if (gainFocus) {
154                 expandTitle(true, true);
155             } else {
156                 expandTitle(false, true);
157             }
158         }
159     }
160 
161     /**
162      * Expands/folds the title area to show program title with two/one lines.
163      *
164      * @param expand {@code true} to expand the title area, or {@code false} to fold it.
165      * @param withAnimation {@code true} to expand/fold with animation.
166      */
expandTitle(boolean expand, boolean withAnimation)167     public void expandTitle(boolean expand, boolean withAnimation) {
168         if (expand != mExpanded && mTitleViewLayout != null
169                 && mTitleViewLayout.getEllipsisCount(0) > 0) {
170             if (withAnimation) {
171                 if (expand) {
172                     mExpandTitleAnimator.start();
173                 } else {
174                     mExpandTitleAnimator.reverse();
175                 }
176             } else {
177                 if (expand) {
178                     mFoldedTitleView.setAlpha(0.0f);
179                     mExpandedTitleView.setAlpha(1.0f);
180                     ViewUtils.setLayoutHeight(mTitleArea, mExpandedTitleHeight);
181                 } else {
182                     mFoldedTitleView.setAlpha(1.0f);
183                     mExpandedTitleView.setAlpha(0.0f);
184                     ViewUtils.setLayoutHeight(mTitleArea, mFoldedTitleHeight);
185                 }
186             }
187             mExpanded = expand;
188         }
189     }
190 
setTitle(CharSequence title)191     void setTitle(CharSequence title) {
192         mFoldedTitleView.setText(title);
193         mExpandedTitleView.setText(title);
194     }
195 
setContent(CharSequence majorContent, CharSequence minorContent)196     void setContent(CharSequence majorContent, CharSequence minorContent) {
197         mContentIconView.setVisibility(View.GONE);
198         if (!TextUtils.isEmpty(majorContent)) {
199             mMajorContentView.setText(majorContent);
200             mMajorContentView.setVisibility(View.VISIBLE);
201         } else {
202             mMajorContentView.setVisibility(View.GONE);
203         }
204         if (!TextUtils.isEmpty(minorContent)) {
205             mMinorContentView.setText(minorContent);
206             mMinorContentView.setVisibility(View.VISIBLE);
207         } else {
208             mMinorContentView.setVisibility(View.GONE);
209         }
210     }
211 
setRecordingFailedContent(Context context)212     void setRecordingFailedContent(Context context) {
213         mContentIconView.setVisibility(View.VISIBLE);
214         mContentIconView.setImageResource(R.drawable.ic_error_outline_pink_24dp);
215         mMajorContentView.setText(context.getString(R.string.dvr_recording_failed_no_period));
216         mMajorContentView.setVisibility(View.VISIBLE);
217         mMajorContentView.setTextColor(
218                 getResources().getColor(R.color.dvr_recording_failed_text_color, null));
219     }
220 
setRecordingConflictContent(Context context)221     void setRecordingConflictContent(Context context) {
222         mContentIconView.setVisibility(View.VISIBLE);
223         mContentIconView.setImageResource(R.drawable.ic_warning_yellow_24dp);
224         mMajorContentView.setText(context.getString(R.string.dvr_recording_conflict));
225         mMajorContentView.setVisibility(View.VISIBLE);
226         mMajorContentView.setTextColor(
227                 getResources().getColor(R.color.dvr_recording_conflict_text_color, null));
228     }
229 
230     /** Sets progress bar. If progress is {@code null}, hides progress bar. */
setProgressBar(Integer progress)231     void setProgressBar(Integer progress) {
232         if (progress == null) {
233             mProgressBar.setVisibility(View.GONE);
234         } else {
235             mProgressBar.setProgress(progress);
236             mProgressBar.setVisibility(View.VISIBLE);
237         }
238     }
239 
240     /** Sets the color of progress bar. */
setProgressBarColor(int color)241     void setProgressBarColor(int color) {
242         mProgressBar.getProgressDrawable().setTint(color);
243     }
244 
245     /**
246      * Sets the image URI of the poster should be shown on the card view.
247      *
248      * @param isChannelLogo {@code true} if the image is from channels' logo.
249      */
setImageUri(String uri, boolean isChannelLogo)250     void setImageUri(String uri, boolean isChannelLogo) {
251         if (isChannelLogo) {
252             mImageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
253         } else {
254             mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
255         }
256         mImageUri = uri;
257         if (TextUtils.isEmpty(uri)) {
258             mImageView.setImageDrawable(mDefaultImage);
259         } else {
260             ImageLoader.loadBitmap(
261                     getContext(),
262                     uri,
263                     mImageWidth,
264                     mImageHeight,
265                     new RecordingCardImageLoaderCallback(this, uri));
266         }
267     }
268 
269     /** Sets the {@link Drawable} of the poster should be shown on the card view. */
setImage(Drawable image)270     public void setImage(Drawable image) {
271         if (image != null) {
272             mImageView.setImageDrawable(image);
273         }
274     }
275 
276     /**
277      * Sets the background image URI of the card view, which will be displayed as background when
278      * the view is clicked and shows its details fragment.
279      */
setDetailBackgroundImageUri(String uri)280     public void setDetailBackgroundImageUri(String uri) {
281         mDetailBackgroundImageUri = uri;
282     }
283 
284     /** Returns image view. */
getImageView()285     public ImageView getImageView() {
286         return mImageView;
287     }
288 
289     private static class RecordingCardImageLoaderCallback
290             extends ImageLoader.ImageLoaderCallback<RecordingCardView> {
291         private final String mUri;
292 
RecordingCardImageLoaderCallback(RecordingCardView referent, String uri)293         RecordingCardImageLoaderCallback(RecordingCardView referent, String uri) {
294             super(referent);
295             mUri = uri;
296         }
297 
298         @Override
onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap)299         public void onBitmapLoaded(RecordingCardView view, @Nullable Bitmap bitmap) {
300             if (bitmap == null || !mUri.equals(view.mImageUri)) {
301                 view.mImageView.setImageDrawable(view.mDefaultImage);
302             } else {
303                 view.mImageView.setImageDrawable(new BitmapDrawable(view.getResources(), bitmap));
304             }
305         }
306     }
307 
reset()308     public void reset() {
309         mFoldedTitleView.setText(null);
310         mExpandedTitleView.setText(null);
311         setContent(null, null);
312         mImageView.setImageDrawable(mDefaultImage);
313     }
314 }
315