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}