Home | History | Annotate | Download | only in cc
      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