• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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