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 onUserStyleChanged(CaptionStyle userStyle)113 public void onUserStyleChanged(CaptionStyle userStyle) { 114 mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle); 115 mSubtitleView.setStyle(mCaptionStyleCompat); 116 updateWidestChar(); 117 } 118 119 @Override onFontScaleChanged(float fontScale)120 public void onFontScaleChanged(float fontScale) { 121 mFontScale = fontScale; 122 updateTextSize(); 123 } 124 } 125 CaptionWindowLayout(Context context)126 public CaptionWindowLayout(Context context) { 127 this(context, null); 128 } 129 CaptionWindowLayout(Context context, AttributeSet attrs)130 public CaptionWindowLayout(Context context, AttributeSet attrs) { 131 this(context, attrs, 0); 132 } 133 CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr)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 getCaptionWindowId()155 public int getCaptionWindowId() { 156 return mCaptionWindowId; 157 } 158 setCaptionWindowId(int captionWindowId)159 public void setCaptionWindowId(int captionWindowId) { 160 mCaptionWindowId = captionWindowId; 161 } 162 clear()163 public void clear() { 164 clearText(); 165 hide(); 166 } 167 show()168 public void show() { 169 setVisibility(View.VISIBLE); 170 requestLayout(); 171 } 172 hide()173 public void hide() { 174 setVisibility(View.INVISIBLE); 175 requestLayout(); 176 } 177 setPenAttr(CaptionPenAttr penAttr)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 setPenColor(CaptionPenColor penColor)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 setPenLocation(int row, int column)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 setWindowAttr(CaptionWindowAttr windowAttr)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 sendBuffer(String buffer)234 public void sendBuffer(String buffer) { 235 appendText(buffer); 236 } 237 sendControl(char control)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 */ initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow)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 onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)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 isKoreanLanguageTrack()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 isWideAspectRatio()440 private boolean isWideAspectRatio() { 441 return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null 442 && mCaptionLayout.getCaptionTrack().wideAspectRatio; 443 } 444 updateWidestChar()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 updateTextSize()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 getScreenColumnCount()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 removeFromCaptionView()522 public void removeFromCaptionView() { 523 if (mCaptionLayout != null) { 524 mCaptionLayout.removeViewFromSafeTitleArea(this); 525 mCaptionLayout.removeOnLayoutChangeListener(this); 526 mCaptionLayout = null; 527 } 528 } 529 setText(String text)530 public void setText(String text) { 531 updateText(text, false); 532 } 533 appendText(String text)534 public void appendText(String text) { 535 updateText(text, true); 536 } 537 clearText()538 public void clearText() { 539 mBuilder.clear(); 540 mSubtitleView.setText(""); 541 } 542 updateText(String text, boolean appended)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 getPrefixSpaces(SpannableStringBuilder builder)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 setRowLimit(int rowLimit)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 setWindowStyle(int windowStyle)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