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