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