1 /* 2 * Copyright (C) 2015 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.tuner.cc; 18 19 import android.content.Context; 20 import android.graphics.Paint; 21 import android.graphics.Rect; 22 import android.graphics.Typeface; 23 import android.text.Layout.Alignment; 24 import android.text.SpannableStringBuilder; 25 import android.text.Spanned; 26 import android.text.TextUtils; 27 import android.text.style.CharacterStyle; 28 import android.text.style.RelativeSizeSpan; 29 import android.text.style.StyleSpan; 30 import android.text.style.SubscriptSpan; 31 import android.text.style.SuperscriptSpan; 32 import android.text.style.UnderlineSpan; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.view.Gravity; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.accessibility.CaptioningManager; 39 import android.view.accessibility.CaptioningManager.CaptionStyle; 40 import android.view.accessibility.CaptioningManager.CaptioningChangeListener; 41 import android.widget.RelativeLayout; 42 43 import com.android.tv.common.flags.TunerFlags; 44 import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; 45 import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; 46 import com.android.tv.tuner.data.Cea708Data.CaptionWindow; 47 import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; 48 import com.android.tv.tuner.exoplayer.text.SubtitleView; 49 import com.android.tv.tuner.layout.ScaledLayout; 50 import com.google.android.exoplayer.text.CaptionStyleCompat; 51 import com.google.android.exoplayer2.text.Cue; 52 import com.google.auto.factory.AutoFactory; 53 import com.google.auto.factory.Provided; 54 55 import java.nio.charset.Charset; 56 import java.nio.charset.StandardCharsets; 57 import java.util.ArrayList; 58 import java.util.Arrays; 59 import java.util.Collections; 60 import java.util.List; 61 62 /** 63 * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that takes 64 * care of displaying the actual cc text. 65 */ 66 public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener { 67 private static final String TAG = "CaptionWindowLayout"; 68 private static final boolean DEBUG = false; 69 70 private static final float PROPORTION_PEN_SIZE_SMALL = .75f; 71 private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f; 72 73 // The following values indicates the maximum cell number of a window. 74 private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99; 75 private static final int ANCHOR_VERTICAL_MAX = 74; 76 private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159; 77 private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209; 78 79 // The following values indicates a gravity of a window. 80 private static final int ANCHOR_MODE_DIVIDER = 3; 81 private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0; 82 private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1; 83 private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2; 84 private static final int ANCHOR_VERTICAL_MODE_TOP = 0; 85 private static final int ANCHOR_VERTICAL_MODE_CENTER = 1; 86 private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2; 87 88 private static final int US_MAX_COLUMN_COUNT_16_9 = 42; 89 private static final int US_MAX_COLUMN_COUNT_4_3 = 32; 90 private static final int KR_MAX_COLUMN_COUNT_16_9 = 52; 91 private static final int KR_MAX_COLUMN_COUNT_4_3 = 40; 92 private static final int MAX_ROW_COUNT = 15; 93 94 private static final String KOR_ALPHABET = 95 new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); 96 private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f; 97 98 private CaptionLayout mCaptionLayout; 99 private CaptionStyleCompat mCaptionStyleCompat; 100 private com.google.android.exoplayer2.text.CaptionStyleCompat mCaptionStyleCompatExoV2; 101 102 // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}. 103 private final SubtitleView mSubtitleView; 104 private final com.google.android.exoplayer2.ui.SubtitleView mSubtitleViewExoV2; 105 private int mRowLimit = 0; 106 private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); 107 private final List<CharacterStyle> mCharacterStyles = new ArrayList<>(); 108 private int mCaptionWindowId; 109 private int mCurrentTextRow = -1; 110 private float mFontScale; 111 private float mTextSize; 112 private String mWidestChar; 113 private int mLastCaptionLayoutWidth; 114 private int mLastCaptionLayoutHeight; 115 private int mWindowJustify; 116 private int mPrintDirection; 117 private final TunerFlags mTunerFlags; 118 119 private class SystemWideCaptioningChangeListener extends CaptioningChangeListener { 120 @Override onUserStyleChanged(CaptionStyle userStyle)121 public void onUserStyleChanged(CaptionStyle userStyle) { 122 if (mTunerFlags.useExoplayerV2()) { 123 mCaptionStyleCompatExoV2 = com.google.android.exoplayer2.text.CaptionStyleCompat 124 .createFromCaptionStyle(userStyle); 125 mSubtitleViewExoV2.setStyle(mCaptionStyleCompatExoV2); 126 } else { 127 mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle); 128 mSubtitleView.setStyle(mCaptionStyleCompat); 129 } 130 updateWidestChar(); 131 } 132 133 @Override onFontScaleChanged(float fontScale)134 public void onFontScaleChanged(float fontScale) { 135 mFontScale = fontScale; 136 updateTextSize(); 137 } 138 } 139 140 /** 141 * Factory for {@link CaptionWindowLayout}. 142 * 143 * <p>This wrapper class keeps other classes from needing to reference the {@link AutoFactory} 144 * generated class. 145 */ 146 public interface Factory { create(Context context)147 public CaptionWindowLayout create(Context context); 148 } 149 150 @AutoFactory(implementing = Factory.class) CaptionWindowLayout(Context context, @Provided TunerFlags tunerFlags)151 public CaptionWindowLayout(Context context, @Provided TunerFlags tunerFlags) { 152 this(context, null, tunerFlags); 153 } 154 CaptionWindowLayout(Context context, AttributeSet attrs, TunerFlags tunerFlags)155 public CaptionWindowLayout(Context context, AttributeSet attrs, TunerFlags tunerFlags) { 156 this(context, attrs, 0, tunerFlags); 157 } 158 CaptionWindowLayout( Context context, AttributeSet attrs, int defStyleAttr, TunerFlags tunerFlags)159 public CaptionWindowLayout( 160 Context context, 161 AttributeSet attrs, 162 int defStyleAttr, 163 TunerFlags tunerFlags) { 164 super(context, attrs, defStyleAttr); 165 166 mTunerFlags = tunerFlags; 167 LayoutParams params = 168 new RelativeLayout.LayoutParams( 169 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 170 // Set the system wide cc preferences to the subtitle view. 171 CaptioningManager captioningManager = 172 (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); 173 mFontScale = captioningManager.getFontScale(); 174 175 // Add a subtitle view to the layout. 176 mSubtitleViewExoV2 = new com.google.android.exoplayer2.ui.SubtitleView(context); 177 mSubtitleView = new SubtitleView(context); 178 if (mTunerFlags.useExoplayerV2()) { 179 addView(mSubtitleViewExoV2, params); 180 mCaptionStyleCompatExoV2 = 181 com.google.android.exoplayer2.text.CaptionStyleCompat 182 .createFromCaptionStyle(captioningManager.getUserStyle()); 183 mSubtitleViewExoV2.setStyle(mCaptionStyleCompatExoV2); 184 } else { 185 addView(mSubtitleView, params); 186 mCaptionStyleCompat = 187 CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); 188 mSubtitleView.setStyle(mCaptionStyleCompat); 189 mSubtitleView.setText(""); 190 } 191 captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener()); 192 updateWidestChar(); 193 } 194 getCaptionWindowId()195 public int getCaptionWindowId() { 196 return mCaptionWindowId; 197 } 198 setCaptionWindowId(int captionWindowId)199 public void setCaptionWindowId(int captionWindowId) { 200 mCaptionWindowId = captionWindowId; 201 } 202 clear()203 public void clear() { 204 clearText(); 205 hide(); 206 } 207 show()208 public void show() { 209 setVisibility(View.VISIBLE); 210 requestLayout(); 211 } 212 hide()213 public void hide() { 214 setVisibility(View.INVISIBLE); 215 requestLayout(); 216 } 217 setPenAttr(CaptionPenAttr penAttr)218 public void setPenAttr(CaptionPenAttr penAttr) { 219 mCharacterStyles.clear(); 220 if (penAttr.italic) { 221 mCharacterStyles.add(new StyleSpan(Typeface.ITALIC)); 222 } 223 if (penAttr.underline) { 224 mCharacterStyles.add(new UnderlineSpan()); 225 } 226 switch (penAttr.penSize) { 227 case CaptionPenAttr.PEN_SIZE_SMALL: 228 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL)); 229 break; 230 case CaptionPenAttr.PEN_SIZE_LARGE: 231 mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE)); 232 break; 233 } 234 switch (penAttr.penOffset) { 235 case CaptionPenAttr.OFFSET_SUBSCRIPT: 236 mCharacterStyles.add(new SubscriptSpan()); 237 break; 238 case CaptionPenAttr.OFFSET_SUPERSCRIPT: 239 mCharacterStyles.add(new SuperscriptSpan()); 240 break; 241 } 242 } 243 setPenColor(CaptionPenColor penColor)244 public void setPenColor(CaptionPenColor penColor) { 245 // TODO: apply pen colors or skip this and use the style of system wide cc style as is. 246 } 247 setPenLocation(int row, int column)248 public void setPenLocation(int row, int column) { 249 // TODO: change the location of pen when window's justify isn't left. 250 // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within 251 // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate 252 // at row. Adding white space to make cursor locate at column. 253 if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) { 254 if (mCurrentTextRow >= 0) { 255 for (int r = mCurrentTextRow; r < row; ++r) { 256 appendText("\n"); 257 } 258 if (mCurrentTextRow <= row) { 259 for (int i = 0; i < column; ++i) { 260 appendText(" "); 261 } 262 } 263 } 264 } 265 mCurrentTextRow = row; 266 } 267 setWindowAttr(CaptionWindowAttr windowAttr)268 public void setWindowAttr(CaptionWindowAttr windowAttr) { 269 // TODO: apply window attrs or skip this and use the style of system wide cc style as is. 270 mWindowJustify = windowAttr.justify; 271 mPrintDirection = windowAttr.printDirection; 272 } 273 sendBuffer(String buffer)274 public void sendBuffer(String buffer) { 275 appendText(buffer); 276 } 277 sendControl(char control)278 public void sendControl(char control) { 279 // TODO: there are a bunch of ASCII-style control codes. 280 } 281 282 /** 283 * This method places the window on a given CaptionLayout along with the anchor of the window. 284 * 285 * <p>According to CEA-708B, the anchor id indicates the gravity of the window as the follows. 286 * For example, A value 7 of a anchor id says that a window is align with its parent bottom and 287 * is located at the center horizontally of its parent. 288 * 289 * <h4>Anchor id and the gravity of a window</h4> 290 * 291 * <table> 292 * <tr> 293 * <th>GRAVITY</th> 294 * <th>LEFT</th> 295 * <th>CENTER_HORIZONTAL</th> 296 * <th>RIGHT</th> 297 * </tr> 298 * <tr> 299 * <th>TOP</th> 300 * <td>0</td> 301 * <td>1</td> 302 * <td>2</td> 303 * </tr> 304 * <tr> 305 * <th>CENTER_VERTICAL</th> 306 * <td>3</td> 307 * <td>4</td> 308 * <td>5</td> 309 * </tr> 310 * <tr> 311 * <th>BOTTOM</th> 312 * <td>6</td> 313 * <td>7</td> 314 * <td>8</td> 315 * </tr> 316 * </table> 317 * 318 * <p>In order to handle the gravity of a window, there are two steps. First, set the size of 319 * the window. Since the window will be positioned at {@link ScaledLayout}, the size factors are 320 * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is 321 * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view, 322 * {@link SubtitleView}. 323 * 324 * <p>The gravity of the window is also related to its size. When it should be pushed to a one 325 * of the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a 326 * boundary of the window. When it should be pushed in the horizontal/vertical center of its 327 * container, the horizontal/vertical center point of the window should be the same as the 328 * anchor point. 329 * 330 * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area 331 * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the 332 * window 333 */ initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow)334 public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) { 335 if (DEBUG) { 336 Log.d( 337 TAG, 338 "initWindow with " 339 + (captionLayout != null ? captionLayout.getCaptionTrack() : null)); 340 } 341 if (mCaptionLayout != captionLayout) { 342 if (mCaptionLayout != null) { 343 mCaptionLayout.removeOnLayoutChangeListener(this); 344 } 345 mCaptionLayout = captionLayout; 346 mCaptionLayout.addOnLayoutChangeListener(this); 347 updateWidestChar(); 348 } 349 350 // Both anchor vertical and horizontal indicates the position cell number of the window. 351 float scaleRow = 352 (float) captionWindow.anchorVertical 353 / (captionWindow.relativePositioning 354 ? ANCHOR_RELATIVE_POSITIONING_MAX 355 : ANCHOR_VERTICAL_MAX); 356 float scaleCol = 357 (float) captionWindow.anchorHorizontal 358 / (captionWindow.relativePositioning 359 ? ANCHOR_RELATIVE_POSITIONING_MAX 360 : (isWideAspectRatio() 361 ? ANCHOR_HORIZONTAL_16_9_MAX 362 : ANCHOR_HORIZONTAL_4_3_MAX)); 363 364 // The range of scaleRow/Col need to be verified to be in [0, 1]. 365 // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}. 366 if (scaleRow < 0 || scaleRow > 1) { 367 Log.i( 368 TAG, 369 "The vertical position of the anchor point should be at the range of 0 and 1" 370 + " but " 371 + scaleRow); 372 scaleRow = Math.max(0, Math.min(scaleRow, 1)); 373 } 374 if (scaleCol < 0 || scaleCol > 1) { 375 Log.i( 376 TAG, 377 "The horizontal position of the anchor point should be at the range of 0 and" 378 + " 1 but " 379 + scaleCol); 380 scaleCol = Math.max(0, Math.min(scaleCol, 1)); 381 } 382 int gravity = Gravity.CENTER; 383 int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER; 384 int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER; 385 float scaleStartRow = 0; 386 float scaleEndRow = 1; 387 float scaleStartCol = 0; 388 float scaleEndCol = 1; 389 switch (horizontalMode) { 390 case ANCHOR_HORIZONTAL_MODE_LEFT: 391 gravity = Gravity.LEFT; 392 setCaptionsTextAlignment(Alignment.ALIGN_NORMAL); 393 scaleStartCol = scaleCol; 394 break; 395 case ANCHOR_HORIZONTAL_MODE_CENTER: 396 float gap = Math.min(1 - scaleCol, scaleCol); 397 398 // Since all TV sets use left text alignment instead of center text alignment 399 // for this case, we follow the industry convention if possible. 400 int columnCount = captionWindow.columnCount + 1; 401 if (isKoreanLanguageTrack()) { 402 columnCount /= 2; 403 } 404 columnCount = Math.min(getScreenColumnCount(), columnCount); 405 StringBuilder widestTextBuilder = new StringBuilder(); 406 for (int i = 0; i < columnCount; ++i) { 407 widestTextBuilder.append(mWidestChar); 408 } 409 Paint paint = new Paint(); 410 if (!mTunerFlags.useExoplayerV2()) { 411 paint.setTypeface(mCaptionStyleCompat.typeface); 412 } 413 paint.setTextSize(mTextSize); 414 float maxWindowWidth = paint.measureText(widestTextBuilder.toString()); 415 float halfMaxWidthScale = 416 mCaptionLayout.getWidth() > 0 417 ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f) 418 : 0.0f; 419 if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) { 420 // Calculate the expected max window size based on the column count of the 421 // caption window multiplied by average alphabets char width, then align the 422 // left side of the window with the left side of the expected max window. 423 gravity = Gravity.LEFT; 424 setCaptionsTextAlignment(Alignment.ALIGN_NORMAL); 425 scaleStartCol = scaleCol - halfMaxWidthScale; 426 scaleEndCol = 1.0f; 427 } else { 428 // The gap will be the minimum distance value of the distances from both 429 // horizontal end points to the anchor point. 430 // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2]. 431 // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1]. 432 // The anchor point is located at the horizontal center of the window in both 433 // cases. 434 gravity = Gravity.CENTER_HORIZONTAL; 435 setCaptionsTextAlignment(Alignment.ALIGN_CENTER); 436 scaleStartCol = scaleCol - gap; 437 scaleEndCol = scaleCol + gap; 438 } 439 break; 440 case ANCHOR_HORIZONTAL_MODE_RIGHT: 441 gravity = Gravity.RIGHT; 442 setCaptionsTextAlignment(Alignment.ALIGN_OPPOSITE); 443 scaleEndCol = scaleCol; 444 break; 445 } 446 switch (verticalMode) { 447 case ANCHOR_VERTICAL_MODE_TOP: 448 gravity |= Gravity.TOP; 449 scaleStartRow = scaleRow; 450 break; 451 case ANCHOR_VERTICAL_MODE_CENTER: 452 gravity |= Gravity.CENTER_VERTICAL; 453 454 // See the above comment. 455 float gap = Math.min(1 - scaleRow, scaleRow); 456 scaleStartRow = scaleRow - gap; 457 scaleEndRow = scaleRow + gap; 458 break; 459 case ANCHOR_VERTICAL_MODE_BOTTOM: 460 gravity |= Gravity.BOTTOM; 461 scaleEndRow = scaleRow; 462 break; 463 } 464 mCaptionLayout.addOrUpdateViewToSafeTitleArea( 465 this, 466 new ScaledLayout.ScaledLayoutParams( 467 scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); 468 setCaptionWindowId(captionWindow.id); 469 setRowLimit(captionWindow.rowCount); 470 setGravity(gravity); 471 setWindowStyle(captionWindow.windowStyle); 472 if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) { 473 setCaptionsTextAlignment(Alignment.ALIGN_CENTER); 474 } 475 if (captionWindow.visible) { 476 show(); 477 } else { 478 hide(); 479 } 480 } 481 482 @Override onLayoutChange( View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)483 public void onLayoutChange( 484 View v, 485 int left, 486 int top, 487 int right, 488 int bottom, 489 int oldLeft, 490 int oldTop, 491 int oldRight, 492 int oldBottom) { 493 int width = right - left; 494 int height = bottom - top; 495 if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) { 496 mLastCaptionLayoutWidth = width; 497 mLastCaptionLayoutHeight = height; 498 updateTextSize(); 499 } 500 } 501 isKoreanLanguageTrack()502 private boolean isKoreanLanguageTrack() { 503 return mCaptionLayout != null 504 && mCaptionLayout.getCaptionTrack() != null 505 && mCaptionLayout.getCaptionTrack().hasLanguage() 506 && "KOR".equalsIgnoreCase(mCaptionLayout.getCaptionTrack().getLanguage()); 507 } 508 isWideAspectRatio()509 private boolean isWideAspectRatio() { 510 return mCaptionLayout != null 511 && mCaptionLayout.getCaptionTrack() != null 512 && mCaptionLayout.getCaptionTrack().getWideAspectRatio(); 513 } 514 updateWidestChar()515 private void updateWidestChar() { 516 if (isKoreanLanguageTrack()) { 517 mWidestChar = KOR_ALPHABET; 518 } else { 519 Paint paint = new Paint(); 520 if (!mTunerFlags.useExoplayerV2()) { 521 paint.setTypeface(mCaptionStyleCompat.typeface); 522 } 523 Charset latin1 = Charset.forName("ISO-8859-1"); 524 float widestCharWidth = 0f; 525 for (int i = 0; i < 256; ++i) { 526 String ch = new String(new byte[] {(byte) i}, latin1); 527 float charWidth = paint.measureText(ch); 528 if (widestCharWidth < charWidth) { 529 widestCharWidth = charWidth; 530 mWidestChar = ch; 531 } 532 } 533 } 534 updateTextSize(); 535 } 536 setCaptionsTextAlignment(Alignment textAlignment)537 private void setCaptionsTextAlignment(Alignment textAlignment){ 538 if (mTunerFlags.useExoplayerV2()){ 539 switch (textAlignment) { 540 case ALIGN_NORMAL: 541 mSubtitleViewExoV2.setTextAlignment(View.TEXT_ALIGNMENT_INHERIT); 542 break; 543 case ALIGN_OPPOSITE: 544 mSubtitleViewExoV2.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_END); 545 break; 546 case ALIGN_CENTER: 547 mSubtitleViewExoV2.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); 548 break; 549 default: 550 mSubtitleViewExoV2.setTextAlignment(View.TEXT_ALIGNMENT_INHERIT); 551 break; 552 } 553 } else { 554 mSubtitleView.setTextAlignment(textAlignment); 555 } 556 } 557 updateTextSize()558 private void updateTextSize() { 559 if (mCaptionLayout == null) return; 560 561 // Calculate text size based on the max window size. 562 StringBuilder widestTextBuilder = new StringBuilder(); 563 int screenColumnCount = getScreenColumnCount(); 564 for (int i = 0; i < screenColumnCount; ++i) { 565 widestTextBuilder.append(mWidestChar); 566 } 567 String widestText = widestTextBuilder.toString(); 568 Paint paint = new Paint(); 569 if (!mTunerFlags.useExoplayerV2()) { 570 paint.setTypeface(mCaptionStyleCompat.typeface); 571 } 572 float startFontSize = 0f; 573 float endFontSize = 255f; 574 Rect boundRect = new Rect(); 575 while (startFontSize < endFontSize) { 576 float testTextSize = (startFontSize + endFontSize) / 2f; 577 paint.setTextSize(testTextSize); 578 float width = paint.measureText(widestText); 579 paint.getTextBounds(widestText, 0, widestText.length(), boundRect); 580 float height = boundRect.height() + width - boundRect.width(); 581 // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller 582 // than 1/15 of the height of the safe-title area, and the width shouldn't wider than 583 // 1/{@code getScreenColumnCount()} of the width of the safe-title area. 584 if (mCaptionLayout.getWidth() * 0.8f > width 585 && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) { 586 startFontSize = testTextSize + 0.01f; 587 } else { 588 endFontSize = testTextSize - 0.01f; 589 } 590 } 591 mTextSize = endFontSize * mFontScale; 592 paint.setTextSize(mTextSize); 593 float whiteSpaceWidth = paint.measureText(" "); 594 595 if (mTunerFlags.useExoplayerV2()) { 596 mSubtitleViewExoV2.setFixedTextSize(0, mTextSize); 597 } else { 598 mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth); 599 mSubtitleView.setTextSize(mTextSize); 600 } 601 } 602 getScreenColumnCount()603 private int getScreenColumnCount() { 604 float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight(); 605 boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD; 606 if (isKoreanLanguageTrack()) { 607 // Each korean character consumes two slots. 608 if (isWideAspectRationScreen || isWideAspectRatio()) { 609 return KR_MAX_COLUMN_COUNT_16_9 / 2; 610 } else { 611 return KR_MAX_COLUMN_COUNT_4_3 / 2; 612 } 613 } else { 614 if (isWideAspectRationScreen || isWideAspectRatio()) { 615 return US_MAX_COLUMN_COUNT_16_9; 616 } else { 617 return US_MAX_COLUMN_COUNT_4_3; 618 } 619 } 620 } 621 removeFromCaptionView()622 public void removeFromCaptionView() { 623 if (mCaptionLayout != null) { 624 mCaptionLayout.removeViewFromSafeTitleArea(this); 625 mCaptionLayout.removeOnLayoutChangeListener(this); 626 mCaptionLayout = null; 627 } 628 } 629 setText(String text)630 public void setText(String text) { 631 updateText(text, false); 632 } 633 appendText(String text)634 public void appendText(String text) { 635 updateText(text, true); 636 } 637 clearText()638 public void clearText() { 639 mBuilder.clear(); 640 if (mTunerFlags.useExoplayerV2()) { 641 mSubtitleViewExoV2.setCues(Collections.emptyList()); 642 } else { 643 mSubtitleView.setText(""); 644 } 645 } 646 setCues(List<Cue> cues)647 public void setCues(List<Cue> cues) { 648 mSubtitleViewExoV2.setCues(cues); 649 } 650 updateText(String text, boolean appended)651 private void updateText(String text, boolean appended) { 652 if (!appended) { 653 mBuilder.clear(); 654 } 655 if (text != null && text.length() > 0) { 656 int length = mBuilder.length(); 657 mBuilder.append(text); 658 for (CharacterStyle characterStyle : mCharacterStyles) { 659 mBuilder.setSpan( 660 characterStyle, 661 length, 662 mBuilder.length(), 663 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 664 } 665 } 666 String[] lines = TextUtils.split(mBuilder.toString(), "\n"); 667 668 // Truncate text not to exceed the row limit. 669 // Plus one here since the range of the rows is [0, mRowLimit]. 670 int startRow = Math.max(0, lines.length - (mRowLimit + 1)); 671 String truncatedText = 672 TextUtils.join("\n", Arrays.copyOfRange(lines, startRow, lines.length)); 673 mBuilder.delete(0, mBuilder.length() - truncatedText.length()); 674 mCurrentTextRow = lines.length - startRow - 1; 675 676 // Trim the buffer first then set text to {@link SubtitleView}. 677 int start = 0, last = mBuilder.length() - 1; 678 int end = last; 679 while ((start <= end) && (mBuilder.charAt(start) <= ' ')) { 680 ++start; 681 } 682 while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') { 683 --start; 684 } 685 while ((end >= start) && (mBuilder.charAt(end) <= ' ')) { 686 --end; 687 } 688 if (start == 0 && end == last) { 689 mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder)); 690 mSubtitleView.setText(mBuilder); 691 } else { 692 SpannableStringBuilder trim = new SpannableStringBuilder(); 693 trim.append(mBuilder); 694 if (end < last) { 695 trim.delete(end + 1, last + 1); 696 } 697 if (start > 0) { 698 trim.delete(0, start); 699 } 700 mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim)); 701 mSubtitleView.setText(trim); 702 } 703 } 704 getPrefixSpaces(SpannableStringBuilder builder)705 private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) { 706 ArrayList<Integer> prefixSpaces = new ArrayList<>(); 707 String[] lines = TextUtils.split(builder.toString(), "\n"); 708 for (String line : lines) { 709 int start = 0; 710 while (start < line.length() && line.charAt(start) <= ' ') { 711 start++; 712 } 713 prefixSpaces.add(start); 714 } 715 return prefixSpaces; 716 } 717 setRowLimit(int rowLimit)718 public void setRowLimit(int rowLimit) { 719 if (rowLimit < 0) { 720 throw new IllegalArgumentException("A rowLimit should have a positive number"); 721 } 722 mRowLimit = rowLimit; 723 } 724 setWindowStyle(int windowStyle)725 private void setWindowStyle(int windowStyle) { 726 // TODO: Set other attributes of window style. Like fill opacity and fill color. 727 switch (windowStyle) { 728 case 2: 729 mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; 730 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 731 break; 732 case 3: 733 mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; 734 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 735 break; 736 case 4: 737 mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; 738 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 739 break; 740 case 5: 741 mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; 742 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 743 break; 744 case 6: 745 mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; 746 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 747 break; 748 case 7: 749 mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; 750 mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM; 751 break; 752 default: 753 if (windowStyle != 0 && windowStyle != 1) { 754 Log.e(TAG, "Error predefined window style:" + windowStyle); 755 } 756 mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; 757 mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; 758 break; 759 } 760 } 761 } 762