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
17package com.android.tv.dvr.ui.browse;
18
19import android.app.Activity;
20import android.animation.Animator;
21import android.animation.AnimatorSet;
22import android.animation.ObjectAnimator;
23import android.animation.PropertyValuesHolder;
24import android.content.Context;
25import android.graphics.Paint;
26import android.graphics.Paint.FontMetricsInt;
27import android.support.v17.leanback.widget.Presenter;
28import android.text.TextUtils;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.ViewTreeObserver;
33import android.view.accessibility.AccessibilityManager;
34import android.widget.LinearLayout;
35import android.widget.TextView;
36
37import com.android.tv.R;
38import com.android.tv.ui.ViewUtils;
39import com.android.tv.util.Utils;
40
41/**
42 * An {@link Presenter} for rendering a detailed description of an DVR item.
43 * Typically this Presenter will be used in a
44 * {@link android.support.v17.leanback.widget.DetailsOverviewRowPresenter}.
45 * Most codes of this class is originated from
46 * {@link android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter}.
47 * The latter class are re-used to provide a customized version of
48 * {@link android.support.v17.leanback.widget.DetailsOverviewRow}.
49 */
50class DetailsContentPresenter extends Presenter {
51    /**
52     * The ViewHolder for the {@link DetailsContentPresenter}.
53     */
54    public static class ViewHolder extends Presenter.ViewHolder {
55        final TextView mTitle;
56        final TextView mSubtitle;
57        final LinearLayout mDescriptionContainer;
58        final TextView mBody;
59        final TextView mReadMoreView;
60        final int mTitleMargin;
61        final int mUnderTitleBaselineMargin;
62        final int mUnderSubtitleBaselineMargin;
63        final int mTitleLineSpacing;
64        final int mBodyLineSpacing;
65        final int mBodyMaxLines;
66        final int mBodyMinLines;
67        final FontMetricsInt mTitleFontMetricsInt;
68        final FontMetricsInt mSubtitleFontMetricsInt;
69        final FontMetricsInt mBodyFontMetricsInt;
70        final int mTitleMaxLines;
71
72        private Activity mActivity;
73        private boolean mFullTextMode;
74        private int mFullTextAnimationDuration;
75        private boolean mIsListeningToPreDraw;
76
77        private ViewTreeObserver.OnPreDrawListener mPreDrawListener =
78                new ViewTreeObserver.OnPreDrawListener() {
79                    @Override
80                    public boolean onPreDraw() {
81                        if (mSubtitle.getVisibility() == View.VISIBLE
82                                && mSubtitle.getTop() > view.getHeight()
83                                && mTitle.getLineCount() > 1) {
84                            mTitle.setMaxLines(mTitle.getLineCount() - 1);
85                            return false;
86                        }
87                        final int bodyLines = mBody.getLineCount();
88                        int maxLines = mFullTextMode ? bodyLines :
89                                (mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines);
90                        if (bodyLines > maxLines) {
91                            mReadMoreView.setVisibility(View.VISIBLE);
92                            mDescriptionContainer.setFocusable(true);
93                            mDescriptionContainer.setClickable(true);
94                            mDescriptionContainer.setOnClickListener(new View.OnClickListener() {
95                                @Override
96                                public void onClick(View view) {
97                                    mFullTextMode = true;
98                                    mReadMoreView.setVisibility(View.GONE);
99                                    mDescriptionContainer.setFocusable((
100                                            (AccessibilityManager) view.getContext()
101                                                    .getSystemService(
102                                                            Context.ACCESSIBILITY_SERVICE))
103                                            .isEnabled());
104                                    mDescriptionContainer.setClickable(false);
105                                    mDescriptionContainer.setOnClickListener(null);
106                                    int oldMaxLines = mBody.getMaxLines();
107                                    mBody.setMaxLines(bodyLines);
108                                    // Minus 1 from line difference to eliminate the space
109                                    // originally occupied by "READ MORE"
110                                    showFullText((bodyLines - oldMaxLines - 1) * mBodyLineSpacing);
111                                }
112                            });
113                        }
114                        if (mReadMoreView.getVisibility() == View.VISIBLE
115                                && mSubtitle.getVisibility() == View.VISIBLE) {
116                            // If both "READ MORE" and subtitle is shown, the capable maximum lines
117                            // will be one line less.
118                            maxLines -= 1;
119                        }
120                        if (mBody.getMaxLines() != maxLines) {
121                            mBody.setMaxLines(maxLines);
122                            return false;
123                        } else {
124                            removePreDrawListener();
125                            return true;
126                        }
127                    }
128                };
129
130        public ViewHolder(final View view) {
131            super(view);
132            view.addOnAttachStateChangeListener(
133                    new View.OnAttachStateChangeListener() {
134                        @Override
135                        public void onViewAttachedToWindow(View v) {
136                            // In case predraw listener was removed in detach, make sure
137                            // we have the proper layout.
138                            addPreDrawListener();
139                        }
140
141                        @Override
142                        public void onViewDetachedFromWindow(View v) {
143                            removePreDrawListener();
144                        }
145                    });
146            mTitle = (TextView) view.findViewById(R.id.dvr_details_description_title);
147            mSubtitle = (TextView) view.findViewById(R.id.dvr_details_description_subtitle);
148            mBody = (TextView) view.findViewById(R.id.dvr_details_description_body);
149            mDescriptionContainer =
150                    (LinearLayout) view.findViewById(R.id.dvr_details_description_container);
151            // We have to explicitly set focusable to true here for accessibility, since we might
152            // set the view's focusable state when we need to show "READ MORE", which would remove
153            // the default focusable state for accessibility.
154            mDescriptionContainer.setFocusable(((AccessibilityManager) view.getContext()
155                    .getSystemService(Context.ACCESSIBILITY_SERVICE)).isEnabled());
156            mReadMoreView = (TextView) view.findViewById(R.id.dvr_details_description_read_more);
157
158            FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle);
159            final int titleAscent = view.getResources().getDimensionPixelSize(
160                    R.dimen.lb_details_description_title_baseline);
161            // Ascent is negative
162            mTitleMargin = titleAscent + titleFontMetricsInt.ascent;
163
164            mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize(
165                    R.dimen.lb_details_description_under_title_baseline_margin);
166            mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize(
167                    R.dimen.dvr_details_description_under_subtitle_baseline_margin);
168
169            mTitleLineSpacing = view.getResources().getDimensionPixelSize(
170                    R.dimen.lb_details_description_title_line_spacing);
171            mBodyLineSpacing = view.getResources().getDimensionPixelSize(
172                    R.dimen.lb_details_description_body_line_spacing);
173
174            mBodyMaxLines = view.getResources().getInteger(
175                    R.integer.lb_details_description_body_max_lines);
176            mBodyMinLines = view.getResources().getInteger(
177                    R.integer.lb_details_description_body_min_lines);
178            mTitleMaxLines = mTitle.getMaxLines();
179
180            mTitleFontMetricsInt = getFontMetricsInt(mTitle);
181            mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle);
182            mBodyFontMetricsInt = getFontMetricsInt(mBody);
183        }
184
185        void addPreDrawListener() {
186            if (!mIsListeningToPreDraw) {
187                mIsListeningToPreDraw = true;
188                view.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
189            }
190        }
191
192        void removePreDrawListener() {
193            if (mIsListeningToPreDraw) {
194                view.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
195                mIsListeningToPreDraw = false;
196            }
197        }
198
199        public TextView getTitle() {
200            return mTitle;
201        }
202
203        public TextView getSubtitle() {
204            return mSubtitle;
205        }
206
207        public TextView getBody() {
208            return mBody;
209        }
210
211        private FontMetricsInt getFontMetricsInt(TextView textView) {
212            Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
213            paint.setTextSize(textView.getTextSize());
214            paint.setTypeface(textView.getTypeface());
215            return paint.getFontMetricsInt();
216        }
217
218        private void showFullText(int heightDiff) {
219            final ViewGroup detailsFrame = (ViewGroup) mActivity.findViewById(R.id.details_frame);
220            int nowHeight = ViewUtils.getLayoutHeight(detailsFrame);
221            Animator expandAnimator = ViewUtils.createHeightAnimator(
222                    detailsFrame, nowHeight, nowHeight + heightDiff);
223            expandAnimator.setDuration(mFullTextAnimationDuration);
224            Animator shiftAnimator = ObjectAnimator.ofPropertyValuesHolder(detailsFrame,
225                    PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
226                            0f, -(heightDiff / 2)));
227            shiftAnimator.setDuration(mFullTextAnimationDuration);
228            AnimatorSet fullTextAnimator = new AnimatorSet();
229            fullTextAnimator.playTogether(expandAnimator, shiftAnimator);
230            fullTextAnimator.start();
231        }
232    }
233
234    private final Activity mActivity;
235    private final int mFullTextAnimationDuration;
236
237    public DetailsContentPresenter(Activity activity) {
238        super();
239        mActivity = activity;
240        mFullTextAnimationDuration = mActivity.getResources()
241                .getInteger(R.integer.dvr_details_full_text_animation_duration);
242    }
243
244    @Override
245    public final ViewHolder onCreateViewHolder(ViewGroup parent) {
246        View v = LayoutInflater.from(parent.getContext())
247                .inflate(R.layout.dvr_details_description, parent, false);
248        return new ViewHolder(v);
249    }
250
251    @Override
252    public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
253        final ViewHolder vh = (ViewHolder) viewHolder;
254        final DetailsContent detailsContent = (DetailsContent) item;
255
256        vh.mActivity = mActivity;
257        vh.mFullTextAnimationDuration = mFullTextAnimationDuration;
258
259        boolean hasTitle = true;
260        if (TextUtils.isEmpty(detailsContent.getTitle())) {
261            vh.mTitle.setVisibility(View.GONE);
262            hasTitle = false;
263        } else {
264            vh.mTitle.setText(detailsContent.getTitle());
265            vh.mTitle.setVisibility(View.VISIBLE);
266            vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight()
267                    + vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier());
268            vh.mTitle.setMaxLines(vh.mTitleMaxLines);
269        }
270        setTopMargin(vh.mTitle, vh.mTitleMargin);
271
272        boolean hasSubtitle = true;
273        if (detailsContent.getStartTimeUtcMillis() != DetailsContent.INVALID_TIME
274                && detailsContent.getEndTimeUtcMillis() != DetailsContent.INVALID_TIME) {
275            vh.mSubtitle.setText(Utils.getDurationString(viewHolder.view.getContext(),
276                    detailsContent.getStartTimeUtcMillis(),
277                    detailsContent.getEndTimeUtcMillis(), false));
278            vh.mSubtitle.setVisibility(View.VISIBLE);
279            if (hasTitle) {
280                setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin
281                        + vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent);
282            } else {
283                setTopMargin(vh.mSubtitle, 0);
284            }
285        } else {
286            vh.mSubtitle.setVisibility(View.GONE);
287            hasSubtitle = false;
288        }
289
290        if (TextUtils.isEmpty(detailsContent.getDescription())) {
291            vh.mBody.setVisibility(View.GONE);
292        } else {
293            vh.mBody.setText(detailsContent.getDescription());
294            vh.mBody.setVisibility(View.VISIBLE);
295            vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight()
296                    + vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier());
297            if (hasSubtitle) {
298                setTopMargin(vh.mDescriptionContainer, vh.mUnderSubtitleBaselineMargin
299                        + vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent
300                        - vh.mBody.getPaddingTop());
301            } else if (hasTitle) {
302                setTopMargin(vh.mDescriptionContainer, vh.mUnderTitleBaselineMargin
303                        + vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent
304                        - vh.mBody.getPaddingTop());
305            } else {
306                setTopMargin(vh.mDescriptionContainer, 0);
307            }
308        }
309    }
310
311    @Override
312    public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { }
313
314    private void setTopMargin(View view, int topMargin) {
315        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
316        lp.topMargin = topMargin;
317        view.setLayoutParams(lp);
318    }
319}