1511 lines
47 KiB
Java
1511 lines
47 KiB
Java
/*
|
|
* Copyright (C) 2014 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.media;
|
|
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Rect;
|
|
import android.graphics.Typeface;
|
|
import android.text.Spannable;
|
|
import android.text.SpannableStringBuilder;
|
|
import android.text.TextPaint;
|
|
import android.text.style.CharacterStyle;
|
|
import android.text.style.StyleSpan;
|
|
import android.text.style.UnderlineSpan;
|
|
import android.text.style.UpdateAppearance;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.util.TypedValue;
|
|
import android.view.Gravity;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.accessibility.CaptioningManager;
|
|
import android.view.accessibility.CaptioningManager.CaptionStyle;
|
|
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
|
|
import android.widget.LinearLayout;
|
|
import android.widget.TextView;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Vector;
|
|
|
|
/** @hide */
|
|
public class ClosedCaptionRenderer extends SubtitleController.Renderer {
|
|
private final Context mContext;
|
|
private Cea608CCWidget mCCWidget;
|
|
|
|
public ClosedCaptionRenderer(Context context) {
|
|
mContext = context;
|
|
}
|
|
|
|
@Override
|
|
public boolean supports(MediaFormat format) {
|
|
if (format.containsKey(MediaFormat.KEY_MIME)) {
|
|
String mimeType = format.getString(MediaFormat.KEY_MIME);
|
|
return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
public SubtitleTrack createTrack(MediaFormat format) {
|
|
String mimeType = format.getString(MediaFormat.KEY_MIME);
|
|
if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
|
|
if (mCCWidget == null) {
|
|
mCCWidget = new Cea608CCWidget(mContext);
|
|
}
|
|
return new Cea608CaptionTrack(mCCWidget, format);
|
|
}
|
|
throw new RuntimeException("No matching format: " + format.toString());
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
class Cea608CaptionTrack extends SubtitleTrack {
|
|
private final Cea608CCParser mCCParser;
|
|
private final Cea608CCWidget mRenderingWidget;
|
|
|
|
Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
|
|
super(format);
|
|
|
|
mRenderingWidget = renderingWidget;
|
|
mCCParser = new Cea608CCParser(mRenderingWidget);
|
|
}
|
|
|
|
@Override
|
|
public void onData(byte[] data, boolean eos, long runID) {
|
|
mCCParser.parse(data);
|
|
}
|
|
|
|
@Override
|
|
public RenderingWidget getRenderingWidget() {
|
|
return mRenderingWidget;
|
|
}
|
|
|
|
@Override
|
|
public void updateView(Vector<Cue> activeCues) {
|
|
// Overriding with NO-OP, CC rendering by-passes this
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Abstract widget class to render a closed caption track.
|
|
*
|
|
* @hide
|
|
*/
|
|
abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
|
|
|
|
/** @hide */
|
|
interface ClosedCaptionLayout {
|
|
void setCaptionStyle(CaptionStyle captionStyle);
|
|
void setFontScale(float scale);
|
|
}
|
|
|
|
private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
|
|
|
|
/** Captioning manager, used to obtain and track caption properties. */
|
|
private final CaptioningManager mManager;
|
|
|
|
/** Current caption style. */
|
|
protected CaptionStyle mCaptionStyle;
|
|
|
|
/** Callback for rendering changes. */
|
|
protected OnChangedListener mListener;
|
|
|
|
/** Concrete layout of CC. */
|
|
protected ClosedCaptionLayout mClosedCaptionLayout;
|
|
|
|
/** Whether a caption style change listener is registered. */
|
|
private boolean mHasChangeListener;
|
|
|
|
public ClosedCaptionWidget(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public ClosedCaptionWidget(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
|
|
this(context, attrs, defStyle, 0);
|
|
}
|
|
|
|
public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
|
|
// Cannot render text over video when layer type is hardware.
|
|
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
|
|
|
mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
|
|
mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle());
|
|
|
|
mClosedCaptionLayout = createCaptionLayout(context);
|
|
mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
|
|
mClosedCaptionLayout.setFontScale(mManager.getFontScale());
|
|
addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
|
|
LayoutParams.MATCH_PARENT);
|
|
|
|
requestLayout();
|
|
}
|
|
|
|
public abstract ClosedCaptionLayout createCaptionLayout(Context context);
|
|
|
|
@Override
|
|
public void setOnChangedListener(OnChangedListener listener) {
|
|
mListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public void setSize(int width, int height) {
|
|
final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
|
|
final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
|
|
|
|
measure(widthSpec, heightSpec);
|
|
layout(0, 0, width, height);
|
|
}
|
|
|
|
@Override
|
|
public void setVisible(boolean visible) {
|
|
if (visible) {
|
|
setVisibility(View.VISIBLE);
|
|
} else {
|
|
setVisibility(View.GONE);
|
|
}
|
|
|
|
manageChangeListener();
|
|
}
|
|
|
|
@Override
|
|
public void onAttachedToWindow() {
|
|
super.onAttachedToWindow();
|
|
|
|
manageChangeListener();
|
|
}
|
|
|
|
@Override
|
|
public void onDetachedFromWindow() {
|
|
super.onDetachedFromWindow();
|
|
|
|
manageChangeListener();
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
|
|
}
|
|
|
|
/**
|
|
* Manages whether this renderer is listening for caption style changes.
|
|
*/
|
|
private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
|
|
@Override
|
|
public void onUserStyleChanged(CaptionStyle userStyle) {
|
|
mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle);
|
|
mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
|
|
}
|
|
|
|
@Override
|
|
public void onFontScaleChanged(float fontScale) {
|
|
mClosedCaptionLayout.setFontScale(fontScale);
|
|
}
|
|
};
|
|
|
|
private void manageChangeListener() {
|
|
final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
|
|
if (mHasChangeListener != needsListener) {
|
|
mHasChangeListener = needsListener;
|
|
|
|
if (needsListener) {
|
|
mManager.addCaptioningChangeListener(mCaptioningListener);
|
|
} else {
|
|
mManager.removeCaptioningChangeListener(mCaptioningListener);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
*
|
|
* CCParser processes CEA-608 closed caption data.
|
|
*
|
|
* It calls back into OnDisplayChangedListener upon
|
|
* display change with styled text for rendering.
|
|
*
|
|
*/
|
|
class Cea608CCParser {
|
|
public static final int MAX_ROWS = 15;
|
|
public static final int MAX_COLS = 32;
|
|
|
|
private static final String TAG = "Cea608CCParser";
|
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
|
|
|
private static final int INVALID = -1;
|
|
|
|
// EIA-CEA-608: Table 70 - Control Codes
|
|
private static final int RCL = 0x20;
|
|
private static final int BS = 0x21;
|
|
private static final int AOF = 0x22;
|
|
private static final int AON = 0x23;
|
|
private static final int DER = 0x24;
|
|
private static final int RU2 = 0x25;
|
|
private static final int RU3 = 0x26;
|
|
private static final int RU4 = 0x27;
|
|
private static final int FON = 0x28;
|
|
private static final int RDC = 0x29;
|
|
private static final int TR = 0x2a;
|
|
private static final int RTD = 0x2b;
|
|
private static final int EDM = 0x2c;
|
|
private static final int CR = 0x2d;
|
|
private static final int ENM = 0x2e;
|
|
private static final int EOC = 0x2f;
|
|
|
|
// Transparent Space
|
|
private static final char TS = '\u00A0';
|
|
|
|
// Captioning Modes
|
|
private static final int MODE_UNKNOWN = 0;
|
|
private static final int MODE_PAINT_ON = 1;
|
|
private static final int MODE_ROLL_UP = 2;
|
|
private static final int MODE_POP_ON = 3;
|
|
private static final int MODE_TEXT = 4;
|
|
|
|
private final DisplayListener mListener;
|
|
|
|
private int mMode = MODE_PAINT_ON;
|
|
private int mRollUpSize = 4;
|
|
private int mPrevCtrlCode = INVALID;
|
|
|
|
private CCMemory mDisplay = new CCMemory();
|
|
private CCMemory mNonDisplay = new CCMemory();
|
|
private CCMemory mTextMem = new CCMemory();
|
|
|
|
Cea608CCParser(DisplayListener listener) {
|
|
mListener = listener;
|
|
}
|
|
|
|
public void parse(byte[] data) {
|
|
CCData[] ccData = CCData.fromByteArray(data);
|
|
|
|
for (int i = 0; i < ccData.length; i++) {
|
|
if (DEBUG) {
|
|
Log.d(TAG, ccData[i].toString());
|
|
}
|
|
|
|
if (handleCtrlCode(ccData[i])
|
|
|| handleTabOffsets(ccData[i])
|
|
|| handlePACCode(ccData[i])
|
|
|| handleMidRowCode(ccData[i])) {
|
|
continue;
|
|
}
|
|
|
|
handleDisplayableChars(ccData[i]);
|
|
}
|
|
}
|
|
|
|
interface DisplayListener {
|
|
void onDisplayChanged(SpannableStringBuilder[] styledTexts);
|
|
CaptionStyle getCaptionStyle();
|
|
}
|
|
|
|
private CCMemory getMemory() {
|
|
// get the CC memory to operate on for current mode
|
|
switch (mMode) {
|
|
case MODE_POP_ON:
|
|
return mNonDisplay;
|
|
case MODE_TEXT:
|
|
// TODO(chz): support only caption mode for now,
|
|
// in text mode, dump everything to text mem.
|
|
return mTextMem;
|
|
case MODE_PAINT_ON:
|
|
case MODE_ROLL_UP:
|
|
return mDisplay;
|
|
default:
|
|
Log.w(TAG, "unrecoginized mode: " + mMode);
|
|
}
|
|
return mDisplay;
|
|
}
|
|
|
|
private boolean handleDisplayableChars(CCData ccData) {
|
|
if (!ccData.isDisplayableChar()) {
|
|
return false;
|
|
}
|
|
|
|
// Extended char includes 1 automatic backspace
|
|
if (ccData.isExtendedChar()) {
|
|
getMemory().bs();
|
|
}
|
|
|
|
getMemory().writeText(ccData.getDisplayText());
|
|
|
|
if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
|
|
updateDisplay();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private boolean handleMidRowCode(CCData ccData) {
|
|
StyleCode m = ccData.getMidRow();
|
|
if (m != null) {
|
|
getMemory().writeMidRowCode(m);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean handlePACCode(CCData ccData) {
|
|
PAC pac = ccData.getPAC();
|
|
|
|
if (pac != null) {
|
|
if (mMode == MODE_ROLL_UP) {
|
|
getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
|
|
}
|
|
getMemory().writePAC(pac);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean handleTabOffsets(CCData ccData) {
|
|
int tabs = ccData.getTabOffset();
|
|
|
|
if (tabs > 0) {
|
|
getMemory().tab(tabs);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean handleCtrlCode(CCData ccData) {
|
|
int ctrlCode = ccData.getCtrlCode();
|
|
|
|
if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
|
|
// discard double ctrl codes (but if there's a 3rd one, we still take that)
|
|
mPrevCtrlCode = INVALID;
|
|
return true;
|
|
}
|
|
|
|
switch(ctrlCode) {
|
|
case RCL:
|
|
// select pop-on style
|
|
mMode = MODE_POP_ON;
|
|
break;
|
|
case BS:
|
|
getMemory().bs();
|
|
break;
|
|
case DER:
|
|
getMemory().der();
|
|
break;
|
|
case RU2:
|
|
case RU3:
|
|
case RU4:
|
|
mRollUpSize = (ctrlCode - 0x23);
|
|
// erase memory if currently in other style
|
|
if (mMode != MODE_ROLL_UP) {
|
|
mDisplay.erase();
|
|
mNonDisplay.erase();
|
|
}
|
|
// select roll-up style
|
|
mMode = MODE_ROLL_UP;
|
|
break;
|
|
case FON:
|
|
Log.i(TAG, "Flash On");
|
|
break;
|
|
case RDC:
|
|
// select paint-on style
|
|
mMode = MODE_PAINT_ON;
|
|
break;
|
|
case TR:
|
|
mMode = MODE_TEXT;
|
|
mTextMem.erase();
|
|
break;
|
|
case RTD:
|
|
mMode = MODE_TEXT;
|
|
break;
|
|
case EDM:
|
|
// erase display memory
|
|
mDisplay.erase();
|
|
updateDisplay();
|
|
break;
|
|
case CR:
|
|
if (mMode == MODE_ROLL_UP) {
|
|
getMemory().rollUp(mRollUpSize);
|
|
} else {
|
|
getMemory().cr();
|
|
}
|
|
if (mMode == MODE_ROLL_UP) {
|
|
updateDisplay();
|
|
}
|
|
break;
|
|
case ENM:
|
|
// erase non-display memory
|
|
mNonDisplay.erase();
|
|
break;
|
|
case EOC:
|
|
// swap display/non-display memory
|
|
swapMemory();
|
|
// switch to pop-on style
|
|
mMode = MODE_POP_ON;
|
|
updateDisplay();
|
|
break;
|
|
case INVALID:
|
|
default:
|
|
mPrevCtrlCode = INVALID;
|
|
return false;
|
|
}
|
|
|
|
mPrevCtrlCode = ctrlCode;
|
|
|
|
// handled
|
|
return true;
|
|
}
|
|
|
|
private void updateDisplay() {
|
|
if (mListener != null) {
|
|
CaptionStyle captionStyle = mListener.getCaptionStyle();
|
|
mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
|
|
}
|
|
}
|
|
|
|
private void swapMemory() {
|
|
CCMemory temp = mDisplay;
|
|
mDisplay = mNonDisplay;
|
|
mNonDisplay = temp;
|
|
}
|
|
|
|
private static class StyleCode {
|
|
static final int COLOR_WHITE = 0;
|
|
static final int COLOR_GREEN = 1;
|
|
static final int COLOR_BLUE = 2;
|
|
static final int COLOR_CYAN = 3;
|
|
static final int COLOR_RED = 4;
|
|
static final int COLOR_YELLOW = 5;
|
|
static final int COLOR_MAGENTA = 6;
|
|
static final int COLOR_INVALID = 7;
|
|
|
|
static final int STYLE_ITALICS = 0x00000001;
|
|
static final int STYLE_UNDERLINE = 0x00000002;
|
|
|
|
static final String[] mColorMap = {
|
|
"WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
|
|
};
|
|
|
|
final int mStyle;
|
|
final int mColor;
|
|
|
|
static StyleCode fromByte(byte data2) {
|
|
int style = 0;
|
|
int color = (data2 >> 1) & 0x7;
|
|
|
|
if ((data2 & 0x1) != 0) {
|
|
style |= STYLE_UNDERLINE;
|
|
}
|
|
|
|
if (color == COLOR_INVALID) {
|
|
// WHITE ITALICS
|
|
color = COLOR_WHITE;
|
|
style |= STYLE_ITALICS;
|
|
}
|
|
|
|
return new StyleCode(style, color);
|
|
}
|
|
|
|
StyleCode(int style, int color) {
|
|
mStyle = style;
|
|
mColor = color;
|
|
}
|
|
|
|
boolean isItalics() {
|
|
return (mStyle & STYLE_ITALICS) != 0;
|
|
}
|
|
|
|
boolean isUnderline() {
|
|
return (mStyle & STYLE_UNDERLINE) != 0;
|
|
}
|
|
|
|
int getColor() {
|
|
return mColor;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder str = new StringBuilder();
|
|
str.append("{");
|
|
str.append(mColorMap[mColor]);
|
|
if ((mStyle & STYLE_ITALICS) != 0) {
|
|
str.append(", ITALICS");
|
|
}
|
|
if ((mStyle & STYLE_UNDERLINE) != 0) {
|
|
str.append(", UNDERLINE");
|
|
}
|
|
str.append("}");
|
|
|
|
return str.toString();
|
|
}
|
|
}
|
|
|
|
private static class PAC extends StyleCode {
|
|
final int mRow;
|
|
final int mCol;
|
|
|
|
static PAC fromBytes(byte data1, byte data2) {
|
|
int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
|
|
int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
|
|
int style = 0;
|
|
if ((data2 & 1) != 0) {
|
|
style |= STYLE_UNDERLINE;
|
|
}
|
|
if ((data2 & 0x10) != 0) {
|
|
// indent code
|
|
int indent = (data2 >> 1) & 0x7;
|
|
return new PAC(row, indent * 4, style, COLOR_WHITE);
|
|
} else {
|
|
// style code
|
|
int color = (data2 >> 1) & 0x7;
|
|
|
|
if (color == COLOR_INVALID) {
|
|
// WHITE ITALICS
|
|
color = COLOR_WHITE;
|
|
style |= STYLE_ITALICS;
|
|
}
|
|
return new PAC(row, -1, style, color);
|
|
}
|
|
}
|
|
|
|
PAC(int row, int col, int style, int color) {
|
|
super(style, color);
|
|
mRow = row;
|
|
mCol = col;
|
|
}
|
|
|
|
boolean isIndentPAC() {
|
|
return (mCol >= 0);
|
|
}
|
|
|
|
int getRow() {
|
|
return mRow;
|
|
}
|
|
|
|
int getCol() {
|
|
return mCol;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("{%d, %d}, %s",
|
|
mRow, mCol, super.toString());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
|
|
*
|
|
* @hide
|
|
*/
|
|
public static class MutableBackgroundColorSpan extends CharacterStyle
|
|
implements UpdateAppearance {
|
|
private int mColor;
|
|
|
|
public MutableBackgroundColorSpan(int color) {
|
|
mColor = color;
|
|
}
|
|
|
|
public void setBackgroundColor(int color) {
|
|
mColor = color;
|
|
}
|
|
|
|
public int getBackgroundColor() {
|
|
return mColor;
|
|
}
|
|
|
|
@Override
|
|
public void updateDrawState(TextPaint ds) {
|
|
ds.bgColor = mColor;
|
|
}
|
|
}
|
|
|
|
/* CCLineBuilder keeps track of displayable chars, as well as
|
|
* MidRow styles and PACs, for a single line of CC memory.
|
|
*
|
|
* It generates styled text via getStyledText() method.
|
|
*/
|
|
private static class CCLineBuilder {
|
|
private final StringBuilder mDisplayChars;
|
|
private final StyleCode[] mMidRowStyles;
|
|
private final StyleCode[] mPACStyles;
|
|
|
|
CCLineBuilder(String str) {
|
|
mDisplayChars = new StringBuilder(str);
|
|
mMidRowStyles = new StyleCode[mDisplayChars.length()];
|
|
mPACStyles = new StyleCode[mDisplayChars.length()];
|
|
}
|
|
|
|
void setCharAt(int index, char ch) {
|
|
mDisplayChars.setCharAt(index, ch);
|
|
mMidRowStyles[index] = null;
|
|
}
|
|
|
|
void setMidRowAt(int index, StyleCode m) {
|
|
mDisplayChars.setCharAt(index, ' ');
|
|
mMidRowStyles[index] = m;
|
|
}
|
|
|
|
void setPACAt(int index, PAC pac) {
|
|
mPACStyles[index] = pac;
|
|
}
|
|
|
|
char charAt(int index) {
|
|
return mDisplayChars.charAt(index);
|
|
}
|
|
|
|
int length() {
|
|
return mDisplayChars.length();
|
|
}
|
|
|
|
void applyStyleSpan(
|
|
SpannableStringBuilder styledText,
|
|
StyleCode s, int start, int end) {
|
|
if (s.isItalics()) {
|
|
styledText.setSpan(
|
|
new StyleSpan(android.graphics.Typeface.ITALIC),
|
|
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
}
|
|
if (s.isUnderline()) {
|
|
styledText.setSpan(
|
|
new UnderlineSpan(),
|
|
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
}
|
|
}
|
|
|
|
SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
|
|
SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
|
|
int start = -1, next = 0;
|
|
int styleStart = -1;
|
|
StyleCode curStyle = null;
|
|
while (next < mDisplayChars.length()) {
|
|
StyleCode newStyle = null;
|
|
if (mMidRowStyles[next] != null) {
|
|
// apply mid-row style change
|
|
newStyle = mMidRowStyles[next];
|
|
} else if (mPACStyles[next] != null
|
|
&& (styleStart < 0 || start < 0)) {
|
|
// apply PAC style change, only if:
|
|
// 1. no style set, or
|
|
// 2. style set, but prev char is none-displayable
|
|
newStyle = mPACStyles[next];
|
|
}
|
|
if (newStyle != null) {
|
|
curStyle = newStyle;
|
|
if (styleStart >= 0 && start >= 0) {
|
|
applyStyleSpan(styledText, newStyle, styleStart, next);
|
|
}
|
|
styleStart = next;
|
|
}
|
|
|
|
if (mDisplayChars.charAt(next) != TS) {
|
|
if (start < 0) {
|
|
start = next;
|
|
}
|
|
} else if (start >= 0) {
|
|
int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
|
|
int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
|
|
styledText.setSpan(
|
|
new MutableBackgroundColorSpan(captionStyle.backgroundColor),
|
|
expandedStart, expandedEnd,
|
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
if (styleStart >= 0) {
|
|
applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
|
|
}
|
|
start = -1;
|
|
}
|
|
next++;
|
|
}
|
|
|
|
return styledText;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* CCMemory models a console-style display.
|
|
*/
|
|
private static class CCMemory {
|
|
private final String mBlankLine;
|
|
private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
|
|
private int mRow;
|
|
private int mCol;
|
|
|
|
CCMemory() {
|
|
char[] blank = new char[MAX_COLS + 2];
|
|
Arrays.fill(blank, TS);
|
|
mBlankLine = new String(blank);
|
|
}
|
|
|
|
void erase() {
|
|
// erase all lines
|
|
for (int i = 0; i < mLines.length; i++) {
|
|
mLines[i] = null;
|
|
}
|
|
mRow = MAX_ROWS;
|
|
mCol = 1;
|
|
}
|
|
|
|
void der() {
|
|
if (mLines[mRow] != null) {
|
|
for (int i = 0; i < mCol; i++) {
|
|
if (mLines[mRow].charAt(i) != TS) {
|
|
for (int j = mCol; j < mLines[mRow].length(); j++) {
|
|
mLines[j].setCharAt(j, TS);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
mLines[mRow] = null;
|
|
}
|
|
}
|
|
|
|
void tab(int tabs) {
|
|
moveCursorByCol(tabs);
|
|
}
|
|
|
|
void bs() {
|
|
moveCursorByCol(-1);
|
|
if (mLines[mRow] != null) {
|
|
mLines[mRow].setCharAt(mCol, TS);
|
|
if (mCol == MAX_COLS - 1) {
|
|
// Spec recommendation:
|
|
// if cursor was at col 32, move cursor
|
|
// back to col 31 and erase both col 31&32
|
|
mLines[mRow].setCharAt(MAX_COLS, TS);
|
|
}
|
|
}
|
|
}
|
|
|
|
void cr() {
|
|
moveCursorTo(mRow + 1, 1);
|
|
}
|
|
|
|
void rollUp(int windowSize) {
|
|
int i;
|
|
for (i = 0; i <= mRow - windowSize; i++) {
|
|
mLines[i] = null;
|
|
}
|
|
int startRow = mRow - windowSize + 1;
|
|
if (startRow < 1) {
|
|
startRow = 1;
|
|
}
|
|
for (i = startRow; i < mRow; i++) {
|
|
mLines[i] = mLines[i + 1];
|
|
}
|
|
for (i = mRow; i < mLines.length; i++) {
|
|
// clear base row
|
|
mLines[i] = null;
|
|
}
|
|
// default to col 1, in case PAC is not sent
|
|
mCol = 1;
|
|
}
|
|
|
|
void writeText(String text) {
|
|
for (int i = 0; i < text.length(); i++) {
|
|
getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
|
|
moveCursorByCol(1);
|
|
}
|
|
}
|
|
|
|
void writeMidRowCode(StyleCode m) {
|
|
getLineBuffer(mRow).setMidRowAt(mCol, m);
|
|
moveCursorByCol(1);
|
|
}
|
|
|
|
void writePAC(PAC pac) {
|
|
if (pac.isIndentPAC()) {
|
|
moveCursorTo(pac.getRow(), pac.getCol());
|
|
} else {
|
|
moveCursorTo(pac.getRow(), 1);
|
|
}
|
|
getLineBuffer(mRow).setPACAt(mCol, pac);
|
|
}
|
|
|
|
SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
|
|
ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
|
|
for (int i = 1; i <= MAX_ROWS; i++) {
|
|
rows.add(mLines[i] != null ?
|
|
mLines[i].getStyledText(captionStyle) : null);
|
|
}
|
|
return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
|
|
}
|
|
|
|
private static int clamp(int x, int min, int max) {
|
|
return x < min ? min : (x > max ? max : x);
|
|
}
|
|
|
|
private void moveCursorTo(int row, int col) {
|
|
mRow = clamp(row, 1, MAX_ROWS);
|
|
mCol = clamp(col, 1, MAX_COLS);
|
|
}
|
|
|
|
private void moveCursorToRow(int row) {
|
|
mRow = clamp(row, 1, MAX_ROWS);
|
|
}
|
|
|
|
private void moveCursorByCol(int col) {
|
|
mCol = clamp(mCol + col, 1, MAX_COLS);
|
|
}
|
|
|
|
private void moveBaselineTo(int baseRow, int windowSize) {
|
|
if (mRow == baseRow) {
|
|
return;
|
|
}
|
|
int actualWindowSize = windowSize;
|
|
if (baseRow < actualWindowSize) {
|
|
actualWindowSize = baseRow;
|
|
}
|
|
if (mRow < actualWindowSize) {
|
|
actualWindowSize = mRow;
|
|
}
|
|
|
|
int i;
|
|
if (baseRow < mRow) {
|
|
// copy from bottom to top row
|
|
for (i = actualWindowSize - 1; i >= 0; i--) {
|
|
mLines[baseRow - i] = mLines[mRow - i];
|
|
}
|
|
} else {
|
|
// copy from top to bottom row
|
|
for (i = 0; i < actualWindowSize; i++) {
|
|
mLines[baseRow - i] = mLines[mRow - i];
|
|
}
|
|
}
|
|
// clear rest of the rows
|
|
for (i = 0; i <= baseRow - windowSize; i++) {
|
|
mLines[i] = null;
|
|
}
|
|
for (i = baseRow + 1; i < mLines.length; i++) {
|
|
mLines[i] = null;
|
|
}
|
|
}
|
|
|
|
private CCLineBuilder getLineBuffer(int row) {
|
|
if (mLines[row] == null) {
|
|
mLines[row] = new CCLineBuilder(mBlankLine);
|
|
}
|
|
return mLines[row];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* CCData parses the raw CC byte pair into displayable chars,
|
|
* misc control codes, Mid-Row or Preamble Address Codes.
|
|
*/
|
|
private static class CCData {
|
|
private final byte mType;
|
|
private final byte mData1;
|
|
private final byte mData2;
|
|
|
|
private static final String[] mCtrlCodeMap = {
|
|
"RCL", "BS" , "AOF", "AON",
|
|
"DER", "RU2", "RU3", "RU4",
|
|
"FON", "RDC", "TR" , "RTD",
|
|
"EDM", "CR" , "ENM", "EOC",
|
|
};
|
|
|
|
private static final String[] mSpecialCharMap = {
|
|
"\u00AE",
|
|
"\u00B0",
|
|
"\u00BD",
|
|
"\u00BF",
|
|
"\u2122",
|
|
"\u00A2",
|
|
"\u00A3",
|
|
"\u266A", // Eighth note
|
|
"\u00E0",
|
|
"\u00A0", // Transparent space
|
|
"\u00E8",
|
|
"\u00E2",
|
|
"\u00EA",
|
|
"\u00EE",
|
|
"\u00F4",
|
|
"\u00FB",
|
|
};
|
|
|
|
private static final String[] mSpanishCharMap = {
|
|
// Spanish and misc chars
|
|
"\u00C1", // A
|
|
"\u00C9", // E
|
|
"\u00D3", // I
|
|
"\u00DA", // O
|
|
"\u00DC", // U
|
|
"\u00FC", // u
|
|
"\u2018", // opening single quote
|
|
"\u00A1", // inverted exclamation mark
|
|
"*",
|
|
"'",
|
|
"\u2014", // em dash
|
|
"\u00A9", // Copyright
|
|
"\u2120", // Servicemark
|
|
"\u2022", // round bullet
|
|
"\u201C", // opening double quote
|
|
"\u201D", // closing double quote
|
|
// French
|
|
"\u00C0",
|
|
"\u00C2",
|
|
"\u00C7",
|
|
"\u00C8",
|
|
"\u00CA",
|
|
"\u00CB",
|
|
"\u00EB",
|
|
"\u00CE",
|
|
"\u00CF",
|
|
"\u00EF",
|
|
"\u00D4",
|
|
"\u00D9",
|
|
"\u00F9",
|
|
"\u00DB",
|
|
"\u00AB",
|
|
"\u00BB"
|
|
};
|
|
|
|
private static final String[] mProtugueseCharMap = {
|
|
// Portuguese
|
|
"\u00C3",
|
|
"\u00E3",
|
|
"\u00CD",
|
|
"\u00CC",
|
|
"\u00EC",
|
|
"\u00D2",
|
|
"\u00F2",
|
|
"\u00D5",
|
|
"\u00F5",
|
|
"{",
|
|
"}",
|
|
"\\",
|
|
"^",
|
|
"_",
|
|
"|",
|
|
"~",
|
|
// German and misc chars
|
|
"\u00C4",
|
|
"\u00E4",
|
|
"\u00D6",
|
|
"\u00F6",
|
|
"\u00DF",
|
|
"\u00A5",
|
|
"\u00A4",
|
|
"\u2502", // vertical bar
|
|
"\u00C5",
|
|
"\u00E5",
|
|
"\u00D8",
|
|
"\u00F8",
|
|
"\u250C", // top-left corner
|
|
"\u2510", // top-right corner
|
|
"\u2514", // lower-left corner
|
|
"\u2518", // lower-right corner
|
|
};
|
|
|
|
static CCData[] fromByteArray(byte[] data) {
|
|
CCData[] ccData = new CCData[data.length / 3];
|
|
|
|
for (int i = 0; i < ccData.length; i++) {
|
|
ccData[i] = new CCData(
|
|
data[i * 3],
|
|
data[i * 3 + 1],
|
|
data[i * 3 + 2]);
|
|
}
|
|
|
|
return ccData;
|
|
}
|
|
|
|
CCData(byte type, byte data1, byte data2) {
|
|
mType = type;
|
|
mData1 = data1;
|
|
mData2 = data2;
|
|
}
|
|
|
|
int getCtrlCode() {
|
|
if ((mData1 == 0x14 || mData1 == 0x1c)
|
|
&& mData2 >= 0x20 && mData2 <= 0x2f) {
|
|
return mData2;
|
|
}
|
|
return INVALID;
|
|
}
|
|
|
|
StyleCode getMidRow() {
|
|
// only support standard Mid-row codes, ignore
|
|
// optional background/foreground mid-row codes
|
|
if ((mData1 == 0x11 || mData1 == 0x19)
|
|
&& mData2 >= 0x20 && mData2 <= 0x2f) {
|
|
return StyleCode.fromByte(mData2);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
PAC getPAC() {
|
|
if ((mData1 & 0x70) == 0x10
|
|
&& (mData2 & 0x40) == 0x40
|
|
&& ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
|
|
return PAC.fromBytes(mData1, mData2);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
int getTabOffset() {
|
|
if ((mData1 == 0x17 || mData1 == 0x1f)
|
|
&& mData2 >= 0x21 && mData2 <= 0x23) {
|
|
return mData2 & 0x3;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
boolean isDisplayableChar() {
|
|
return isBasicChar() || isSpecialChar() || isExtendedChar();
|
|
}
|
|
|
|
String getDisplayText() {
|
|
String str = getBasicChars();
|
|
|
|
if (str == null) {
|
|
str = getSpecialChar();
|
|
|
|
if (str == null) {
|
|
str = getExtendedChar();
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
private String ctrlCodeToString(int ctrlCode) {
|
|
return mCtrlCodeMap[ctrlCode - 0x20];
|
|
}
|
|
|
|
private boolean isBasicChar() {
|
|
return mData1 >= 0x20 && mData1 <= 0x7f;
|
|
}
|
|
|
|
private boolean isSpecialChar() {
|
|
return ((mData1 == 0x11 || mData1 == 0x19)
|
|
&& mData2 >= 0x30 && mData2 <= 0x3f);
|
|
}
|
|
|
|
private boolean isExtendedChar() {
|
|
return ((mData1 == 0x12 || mData1 == 0x1A
|
|
|| mData1 == 0x13 || mData1 == 0x1B)
|
|
&& mData2 >= 0x20 && mData2 <= 0x3f);
|
|
}
|
|
|
|
private char getBasicChar(byte data) {
|
|
char c;
|
|
// replace the non-ASCII ones
|
|
switch (data) {
|
|
case 0x2A: c = '\u00E1'; break;
|
|
case 0x5C: c = '\u00E9'; break;
|
|
case 0x5E: c = '\u00ED'; break;
|
|
case 0x5F: c = '\u00F3'; break;
|
|
case 0x60: c = '\u00FA'; break;
|
|
case 0x7B: c = '\u00E7'; break;
|
|
case 0x7C: c = '\u00F7'; break;
|
|
case 0x7D: c = '\u00D1'; break;
|
|
case 0x7E: c = '\u00F1'; break;
|
|
case 0x7F: c = '\u2588'; break; // Full block
|
|
default: c = (char) data; break;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
private String getBasicChars() {
|
|
if (mData1 >= 0x20 && mData1 <= 0x7f) {
|
|
StringBuilder builder = new StringBuilder(2);
|
|
builder.append(getBasicChar(mData1));
|
|
if (mData2 >= 0x20 && mData2 <= 0x7f) {
|
|
builder.append(getBasicChar(mData2));
|
|
}
|
|
return builder.toString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private String getSpecialChar() {
|
|
if ((mData1 == 0x11 || mData1 == 0x19)
|
|
&& mData2 >= 0x30 && mData2 <= 0x3f) {
|
|
return mSpecialCharMap[mData2 - 0x30];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private String getExtendedChar() {
|
|
if ((mData1 == 0x12 || mData1 == 0x1A)
|
|
&& mData2 >= 0x20 && mData2 <= 0x3f){
|
|
// 1 Spanish/French char
|
|
return mSpanishCharMap[mData2 - 0x20];
|
|
} else if ((mData1 == 0x13 || mData1 == 0x1B)
|
|
&& mData2 >= 0x20 && mData2 <= 0x3f){
|
|
// 1 Portuguese/German/Danish char
|
|
return mProtugueseCharMap[mData2 - 0x20];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
String str;
|
|
|
|
if (mData1 < 0x10 && mData2 < 0x10) {
|
|
// Null Pad, ignore
|
|
return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
|
|
}
|
|
|
|
int ctrlCode = getCtrlCode();
|
|
if (ctrlCode != INVALID) {
|
|
return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
|
|
}
|
|
|
|
int tabOffset = getTabOffset();
|
|
if (tabOffset > 0) {
|
|
return String.format("[%d]Tab%d", mType, tabOffset);
|
|
}
|
|
|
|
PAC pac = getPAC();
|
|
if (pac != null) {
|
|
return String.format("[%d]PAC: %s", mType, pac.toString());
|
|
}
|
|
|
|
StyleCode m = getMidRow();
|
|
if (m != null) {
|
|
return String.format("[%d]Mid-row: %s", mType, m.toString());
|
|
}
|
|
|
|
if (isDisplayableChar()) {
|
|
return String.format("[%d]Displayable: %s (%02x %02x)",
|
|
mType, getDisplayText(), mData1, mData2);
|
|
}
|
|
|
|
return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Widget capable of rendering CEA-608 closed captions.
|
|
*
|
|
* @hide
|
|
*/
|
|
class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
|
|
private static final Rect mTextBounds = new Rect();
|
|
private static final String mDummyText = "1234567890123456789012345678901234";
|
|
|
|
public Cea608CCWidget(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public Cea608CCWidget(Context context, AttributeSet attrs) {
|
|
this(context, attrs, 0);
|
|
}
|
|
|
|
public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
|
|
this(context, attrs, defStyle, 0);
|
|
}
|
|
|
|
public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
}
|
|
|
|
@Override
|
|
public ClosedCaptionLayout createCaptionLayout(Context context) {
|
|
return new CCLayout(context);
|
|
}
|
|
|
|
@Override
|
|
public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
|
|
((CCLayout) mClosedCaptionLayout).update(styledTexts);
|
|
|
|
if (mListener != null) {
|
|
mListener.onChanged(this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public CaptionStyle getCaptionStyle() {
|
|
return mCaptionStyle;
|
|
}
|
|
|
|
private static class CCLineBox extends TextView {
|
|
private static final float FONT_PADDING_RATIO = 0.75f;
|
|
private static final float EDGE_OUTLINE_RATIO = 0.1f;
|
|
private static final float EDGE_SHADOW_RATIO = 0.05f;
|
|
private float mOutlineWidth;
|
|
private float mShadowRadius;
|
|
private float mShadowOffset;
|
|
|
|
private int mTextColor = Color.WHITE;
|
|
private int mBgColor = Color.BLACK;
|
|
private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
|
|
private int mEdgeColor = Color.TRANSPARENT;
|
|
|
|
CCLineBox(Context context) {
|
|
super(context);
|
|
setGravity(Gravity.CENTER);
|
|
setBackgroundColor(Color.TRANSPARENT);
|
|
setTextColor(Color.WHITE);
|
|
setTypeface(Typeface.MONOSPACE);
|
|
setVisibility(View.INVISIBLE);
|
|
|
|
final Resources res = getContext().getResources();
|
|
|
|
// get the default (will be updated later during measure)
|
|
mOutlineWidth = res.getDimensionPixelSize(
|
|
com.android.internal.R.dimen.subtitle_outline_width);
|
|
mShadowRadius = res.getDimensionPixelSize(
|
|
com.android.internal.R.dimen.subtitle_shadow_radius);
|
|
mShadowOffset = res.getDimensionPixelSize(
|
|
com.android.internal.R.dimen.subtitle_shadow_offset);
|
|
}
|
|
|
|
void setCaptionStyle(CaptionStyle captionStyle) {
|
|
mTextColor = captionStyle.foregroundColor;
|
|
mBgColor = captionStyle.backgroundColor;
|
|
mEdgeType = captionStyle.edgeType;
|
|
mEdgeColor = captionStyle.edgeColor;
|
|
|
|
setTextColor(mTextColor);
|
|
if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
|
|
setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
|
|
} else {
|
|
setShadowLayer(0, 0, 0, 0);
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
|
|
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);
|
|
|
|
mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
|
|
mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;;
|
|
mShadowOffset = mShadowRadius;
|
|
|
|
// set font scale in the X direction to match the required width
|
|
setScaleX(1.0f);
|
|
getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds);
|
|
float actualTextWidth = mTextBounds.width();
|
|
float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
|
|
setScaleX(requiredTextWidth / actualTextWidth);
|
|
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
}
|
|
|
|
@Override
|
|
protected void onDraw(Canvas c) {
|
|
if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
|
|
|| mEdgeType == CaptionStyle.EDGE_TYPE_NONE
|
|
|| mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
|
|
// these edge styles don't require a second pass
|
|
super.onDraw(c);
|
|
return;
|
|
}
|
|
|
|
if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
|
|
drawEdgeOutline(c);
|
|
} else {
|
|
// Raised or depressed
|
|
drawEdgeRaisedOrDepressed(c);
|
|
}
|
|
}
|
|
|
|
private void drawEdgeOutline(Canvas c) {
|
|
TextPaint textPaint = getPaint();
|
|
|
|
Paint.Style previousStyle = textPaint.getStyle();
|
|
Paint.Join previousJoin = textPaint.getStrokeJoin();
|
|
float previousWidth = textPaint.getStrokeWidth();
|
|
|
|
setTextColor(mEdgeColor);
|
|
textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
|
textPaint.setStrokeJoin(Paint.Join.ROUND);
|
|
textPaint.setStrokeWidth(mOutlineWidth);
|
|
|
|
// Draw outline and background only.
|
|
super.onDraw(c);
|
|
|
|
// Restore original settings.
|
|
setTextColor(mTextColor);
|
|
textPaint.setStyle(previousStyle);
|
|
textPaint.setStrokeJoin(previousJoin);
|
|
textPaint.setStrokeWidth(previousWidth);
|
|
|
|
// Remove the background.
|
|
setBackgroundSpans(Color.TRANSPARENT);
|
|
// Draw foreground only.
|
|
super.onDraw(c);
|
|
// Restore the background.
|
|
setBackgroundSpans(mBgColor);
|
|
}
|
|
|
|
private void drawEdgeRaisedOrDepressed(Canvas c) {
|
|
TextPaint textPaint = getPaint();
|
|
|
|
Paint.Style previousStyle = textPaint.getStyle();
|
|
textPaint.setStyle(Paint.Style.FILL);
|
|
|
|
final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
|
|
final int colorUp = raised ? Color.WHITE : mEdgeColor;
|
|
final int colorDown = raised ? mEdgeColor : Color.WHITE;
|
|
final float offset = mShadowRadius / 2f;
|
|
|
|
// Draw background and text with shadow up
|
|
setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
|
|
super.onDraw(c);
|
|
|
|
// Remove the background.
|
|
setBackgroundSpans(Color.TRANSPARENT);
|
|
|
|
// Draw text with shadow down
|
|
setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
|
|
super.onDraw(c);
|
|
|
|
// Restore settings
|
|
textPaint.setStyle(previousStyle);
|
|
|
|
// Restore the background.
|
|
setBackgroundSpans(mBgColor);
|
|
}
|
|
|
|
private void setBackgroundSpans(int color) {
|
|
CharSequence text = getText();
|
|
if (text instanceof Spannable) {
|
|
Spannable spannable = (Spannable) text;
|
|
Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
|
|
0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
|
|
for (int i = 0; i < bgSpans.length; i++) {
|
|
bgSpans[i].setBackgroundColor(color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class CCLayout extends LinearLayout implements ClosedCaptionLayout {
|
|
private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
|
|
private static final float SAFE_AREA_RATIO = 0.9f;
|
|
|
|
private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];
|
|
|
|
CCLayout(Context context) {
|
|
super(context);
|
|
setGravity(Gravity.START);
|
|
setOrientation(LinearLayout.VERTICAL);
|
|
for (int i = 0; i < MAX_ROWS; i++) {
|
|
mLineBoxes[i] = new CCLineBox(getContext());
|
|
addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setCaptionStyle(CaptionStyle captionStyle) {
|
|
for (int i = 0; i < MAX_ROWS; i++) {
|
|
mLineBoxes[i].setCaptionStyle(captionStyle);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void setFontScale(float fontScale) {
|
|
// Ignores the font scale changes of the system wide CC preference.
|
|
}
|
|
|
|
void update(SpannableStringBuilder[] textBuffer) {
|
|
for (int i = 0; i < MAX_ROWS; i++) {
|
|
if (textBuffer[i] != null) {
|
|
mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
|
|
mLineBoxes[i].setVisibility(View.VISIBLE);
|
|
} else {
|
|
mLineBoxes[i].setVisibility(View.INVISIBLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
|
|
int safeWidth = getMeasuredWidth();
|
|
int safeHeight = getMeasuredHeight();
|
|
|
|
// CEA-608 assumes 4:3 video
|
|
if (safeWidth * 3 >= safeHeight * 4) {
|
|
safeWidth = safeHeight * 4 / 3;
|
|
} else {
|
|
safeHeight = safeWidth * 3 / 4;
|
|
}
|
|
safeWidth *= SAFE_AREA_RATIO;
|
|
safeHeight *= SAFE_AREA_RATIO;
|
|
|
|
int lineHeight = safeHeight / MAX_ROWS;
|
|
int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
|
|
lineHeight, MeasureSpec.EXACTLY);
|
|
int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
|
|
safeWidth, MeasureSpec.EXACTLY);
|
|
|
|
for (int i = 0; i < MAX_ROWS; i++) {
|
|
mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
// safe caption area
|
|
int viewPortWidth = r - l;
|
|
int viewPortHeight = b - t;
|
|
int safeWidth, safeHeight;
|
|
// CEA-608 assumes 4:3 video
|
|
if (viewPortWidth * 3 >= viewPortHeight * 4) {
|
|
safeWidth = viewPortHeight * 4 / 3;
|
|
safeHeight = viewPortHeight;
|
|
} else {
|
|
safeWidth = viewPortWidth;
|
|
safeHeight = viewPortWidth * 3 / 4;
|
|
}
|
|
safeWidth *= SAFE_AREA_RATIO;
|
|
safeHeight *= SAFE_AREA_RATIO;
|
|
int left = (viewPortWidth - safeWidth) / 2;
|
|
int top = (viewPortHeight - safeHeight) / 2;
|
|
|
|
for (int i = 0; i < MAX_ROWS; i++) {
|
|
mLineBoxes[i].layout(
|
|
left,
|
|
top + safeHeight * i / MAX_ROWS,
|
|
left + safeWidth,
|
|
top + safeHeight * (i + 1) / MAX_ROWS);
|
|
}
|
|
}
|
|
}
|
|
}
|