Home | History | Annotate | Download | only in browse
      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.app.Activity;
     20 import android.animation.Animator;
     21 import android.animation.AnimatorSet;
     22 import android.animation.ObjectAnimator;
     23 import android.animation.PropertyValuesHolder;
     24 import android.content.Context;
     25 import android.graphics.Paint;
     26 import android.graphics.Paint.FontMetricsInt;
     27 import android.support.v17.leanback.widget.Presenter;
     28 import android.text.TextUtils;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.view.ViewTreeObserver;
     33 import android.view.accessibility.AccessibilityManager;
     34 import android.widget.LinearLayout;
     35 import android.widget.TextView;
     36 
     37 import com.android.tv.R;
     38 import com.android.tv.ui.ViewUtils;
     39 import 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  */
     50 class 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 }