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