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