Optimize captions.
SubtitleLayout no longer trigger re-layouts of the view hierarchy. Instead, the SubtitleLayout just invalidates itself. This is made possible by making SubtitleLayout a regular View that draws each Cue directly onto the canvas, rather than having SubtitleLayout be a ViewGroup with a child View for each Cue.
This commit is contained in:
parent
8b08efce79
commit
884e7a4170
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer.text;
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Join;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.RectF;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Draws {@link Cue}s.
|
||||
*/
|
||||
/* package */ final class CuePainter {
|
||||
|
||||
private static final String TAG = "CuePainter";
|
||||
|
||||
/**
|
||||
* Ratio of inner padding to font size.
|
||||
*/
|
||||
private static final float INNER_PADDING_RATIO = 0.125f;
|
||||
|
||||
/**
|
||||
* Use the same line height ratio as WebVtt to match the display with the preview.
|
||||
* WebVtt specifies line height as 5.3% of the viewport height.
|
||||
*/
|
||||
private static final float LINE_HEIGHT_FRACTION = 0.0533f;
|
||||
|
||||
/**
|
||||
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, as a
|
||||
* fraction of the viewport height.
|
||||
*/
|
||||
private static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
|
||||
|
||||
/**
|
||||
* Temporary rectangle used for computing line bounds.
|
||||
*/
|
||||
private final RectF lineBounds = new RectF();
|
||||
|
||||
// Styled dimensions.
|
||||
private final float cornerRadius;
|
||||
private final float outlineWidth;
|
||||
private final float shadowRadius;
|
||||
private final float shadowOffset;
|
||||
private final float spacingMult;
|
||||
private final float spacingAdd;
|
||||
|
||||
private final TextPaint textPaint;
|
||||
private final Paint paint;
|
||||
|
||||
// Previous input variables.
|
||||
private CharSequence cueText;
|
||||
private int cuePosition;
|
||||
private Alignment cueAlignment;
|
||||
private int foregroundColor;
|
||||
private int backgroundColor;
|
||||
private int windowColor;
|
||||
private int edgeColor;
|
||||
private int edgeType;
|
||||
private int parentLeft;
|
||||
private int parentTop;
|
||||
private int parentRight;
|
||||
private int parentBottom;
|
||||
|
||||
// Derived drawing variables.
|
||||
private StaticLayout textLayout;
|
||||
private int textLeft;
|
||||
private int textTop;
|
||||
private int textPaddingX;
|
||||
|
||||
public CuePainter(Context context) {
|
||||
int[] viewAttr = {android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};
|
||||
TypedArray styledAttributes = context.obtainStyledAttributes(null, viewAttr, 0, 0);
|
||||
spacingAdd = styledAttributes.getDimensionPixelSize(0, 0);
|
||||
spacingMult = styledAttributes.getFloat(1, 1);
|
||||
styledAttributes.recycle();
|
||||
|
||||
Resources resources = context.getResources();
|
||||
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
|
||||
int twoDpInPx = Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
|
||||
cornerRadius = twoDpInPx;
|
||||
outlineWidth = twoDpInPx;
|
||||
shadowRadius = twoDpInPx;
|
||||
shadowOffset = twoDpInPx;
|
||||
|
||||
textPaint = new TextPaint();
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setSubpixelText(true);
|
||||
|
||||
paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStyle(Style.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the provided {@link Cue} into a canvas with the specified styling.
|
||||
* <p>
|
||||
* A call to this method is able to use cached results of calculations made during the previous
|
||||
* call, and so an instance of this class is able to optimize repeated calls to this method in
|
||||
* which the same parameters are passed.
|
||||
*
|
||||
* @param cue The cue to draw.
|
||||
* @param style The style to use when drawing the cue text.
|
||||
* @param fontScale The font scale.
|
||||
* @param canvas The canvas into which to draw.
|
||||
* @param cueBoxLeft The left position of the enclosing cue box.
|
||||
* @param cueBoxTop The top position of the enclosing cue box.
|
||||
* @param cueBoxRight The right position of the enclosing cue box.
|
||||
* @param cueBoxBottom The bottom position of the enclosing cue box.
|
||||
*/
|
||||
public void draw(Cue cue, CaptionStyleCompat style, float fontScale, Canvas canvas,
|
||||
int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) {
|
||||
if (TextUtils.equals(cueText, cue.text)
|
||||
&& cuePosition == cue.position
|
||||
&& Util.areEqual(cueAlignment, cue.alignment)
|
||||
&& foregroundColor == style.foregroundColor
|
||||
&& backgroundColor == style.backgroundColor
|
||||
&& windowColor == style.windowColor
|
||||
&& edgeType == style.edgeType
|
||||
&& edgeColor == style.edgeColor
|
||||
&& Util.areEqual(textPaint.getTypeface(), style.typeface)
|
||||
&& parentLeft == cueBoxLeft
|
||||
&& parentTop == cueBoxTop
|
||||
&& parentRight == cueBoxRight
|
||||
&& parentBottom == cueBoxBottom) {
|
||||
// We can use the cached layout.
|
||||
drawLayout(canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
cueText = cue.text;
|
||||
cuePosition = cue.position;
|
||||
cueAlignment = cue.alignment;
|
||||
foregroundColor = style.foregroundColor;
|
||||
backgroundColor = style.backgroundColor;
|
||||
windowColor = style.windowColor;
|
||||
edgeType = style.edgeType;
|
||||
edgeColor = style.edgeColor;
|
||||
textPaint.setTypeface(style.typeface);
|
||||
parentLeft = cueBoxLeft;
|
||||
parentTop = cueBoxTop;
|
||||
parentRight = cueBoxRight;
|
||||
parentBottom = cueBoxBottom;
|
||||
|
||||
int parentWidth = parentRight - parentLeft;
|
||||
int parentHeight = parentBottom - parentTop;
|
||||
|
||||
float textSize = LINE_HEIGHT_FRACTION * parentHeight * fontScale;
|
||||
textPaint.setTextSize(textSize);
|
||||
int textPaddingX = (int) (textSize * INNER_PADDING_RATIO + 0.5f);
|
||||
int availableWidth = parentWidth - textPaddingX * 2;
|
||||
if (availableWidth <= 0) {
|
||||
Log.w(TAG, "Skipped drawing subtitle cue (insufficient space)");
|
||||
return;
|
||||
}
|
||||
|
||||
Alignment layoutAlignment = cueAlignment == null ? Alignment.ALIGN_CENTER : cueAlignment;
|
||||
textLayout = new StaticLayout(cueText, textPaint, availableWidth, layoutAlignment, spacingMult,
|
||||
spacingAdd, true);
|
||||
|
||||
int textHeight = textLayout.getHeight();
|
||||
int textWidth = 0;
|
||||
int lineCount = textLayout.getLineCount();
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
textWidth = Math.max((int) Math.ceil(textLayout.getLineWidth(i)), textWidth);
|
||||
}
|
||||
textWidth += textPaddingX * 2;
|
||||
|
||||
int textLeft = (parentWidth - textWidth) / 2;
|
||||
int textRight = textLeft + textWidth;
|
||||
int textTop = parentBottom - textHeight
|
||||
- (int) (parentHeight * DEFAULT_BOTTOM_PADDING_FRACTION);
|
||||
int textBottom = textTop + textHeight;
|
||||
|
||||
if (cue.position != Cue.UNSET_VALUE) {
|
||||
if (cue.alignment == Alignment.ALIGN_OPPOSITE) {
|
||||
textRight = (parentWidth * cue.position) / 100 + parentLeft;
|
||||
textLeft = Math.max(textRight - textWidth, parentLeft);
|
||||
} else {
|
||||
textLeft = (parentWidth * cue.position) / 100 + parentLeft;
|
||||
textRight = Math.min(textLeft + textWidth, parentRight);
|
||||
}
|
||||
}
|
||||
if (cue.line != Cue.UNSET_VALUE) {
|
||||
textTop = (parentHeight * cue.line) / 100 + parentTop;
|
||||
textBottom = textTop + textHeight;
|
||||
if (textBottom > parentBottom) {
|
||||
textTop = parentBottom - textHeight;
|
||||
textBottom = parentBottom;
|
||||
}
|
||||
}
|
||||
textWidth = textRight - textLeft;
|
||||
|
||||
// Update the derived drawing variables.
|
||||
this.textLayout = new StaticLayout(cueText, textPaint, textWidth, layoutAlignment, spacingMult,
|
||||
spacingAdd, true);
|
||||
this.textLeft = textLeft;
|
||||
this.textTop = textTop;
|
||||
this.textPaddingX = textPaddingX;
|
||||
|
||||
drawLayout(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws {@link #textLayout} into the provided canvas.
|
||||
*
|
||||
* @param canvas The canvas into which to draw.
|
||||
*/
|
||||
private void drawLayout(Canvas canvas) {
|
||||
final StaticLayout layout = textLayout;
|
||||
if (layout == null) {
|
||||
// Nothing to draw.
|
||||
return;
|
||||
}
|
||||
|
||||
int saveCount = canvas.save();
|
||||
canvas.translate(textLeft, textTop);
|
||||
|
||||
if (Color.alpha(windowColor) > 0) {
|
||||
paint.setColor(windowColor);
|
||||
canvas.drawRect(-textPaddingX, 0, layout.getWidth() + textPaddingX, layout.getHeight(),
|
||||
paint);
|
||||
}
|
||||
|
||||
if (Color.alpha(backgroundColor) > 0) {
|
||||
paint.setColor(backgroundColor);
|
||||
float previousBottom = layout.getLineTop(0);
|
||||
int lineCount = layout.getLineCount();
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
lineBounds.left = layout.getLineLeft(i) - textPaddingX;
|
||||
lineBounds.right = layout.getLineRight(i) + textPaddingX;
|
||||
lineBounds.top = previousBottom;
|
||||
lineBounds.bottom = layout.getLineBottom(i);
|
||||
previousBottom = lineBounds.bottom;
|
||||
canvas.drawRoundRect(lineBounds, cornerRadius, cornerRadius, paint);
|
||||
}
|
||||
}
|
||||
|
||||
if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
|
||||
textPaint.setStrokeJoin(Join.ROUND);
|
||||
textPaint.setStrokeWidth(outlineWidth);
|
||||
textPaint.setColor(edgeColor);
|
||||
textPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
layout.draw(canvas);
|
||||
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {
|
||||
textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);
|
||||
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED
|
||||
|| edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {
|
||||
boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;
|
||||
int colorUp = raised ? Color.WHITE : edgeColor;
|
||||
int colorDown = raised ? edgeColor : Color.WHITE;
|
||||
float offset = shadowRadius / 2f;
|
||||
textPaint.setColor(foregroundColor);
|
||||
textPaint.setStyle(Style.FILL);
|
||||
textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);
|
||||
layout.draw(canvas);
|
||||
textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);
|
||||
}
|
||||
|
||||
textPaint.setColor(foregroundColor);
|
||||
textPaint.setStyle(Style.FILL);
|
||||
layout.draw(canvas);
|
||||
textPaint.setShadowLayer(0, 0, 0, 0);
|
||||
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
}
|
@ -16,9 +16,9 @@
|
||||
package com.google.android.exoplayer.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -26,28 +26,13 @@ import java.util.List;
|
||||
/**
|
||||
* A view for rendering rich-formatted captions.
|
||||
*/
|
||||
public final class SubtitleLayout extends ViewGroup {
|
||||
public final class SubtitleLayout extends View {
|
||||
|
||||
/**
|
||||
* Use the same line height ratio as WebVtt to match the display with the preview.
|
||||
* WebVtt specifies line height as 5.3% of the viewport height.
|
||||
*/
|
||||
private static final float LINE_HEIGHT_FRACTION = 0.0533f;
|
||||
|
||||
/**
|
||||
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#UNSET_VALUE}, as a
|
||||
* fraction of the viewport height.
|
||||
*/
|
||||
private static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
|
||||
|
||||
private final List<SubtitleView> subtitleViews;
|
||||
|
||||
private List<Cue> subtitleCues;
|
||||
private int viewsInUse;
|
||||
private final List<CuePainter> painters;
|
||||
|
||||
private List<Cue> cues;
|
||||
private float fontScale;
|
||||
private float textSize;
|
||||
private CaptionStyleCompat captionStyle;
|
||||
private CaptionStyleCompat style;
|
||||
|
||||
public SubtitleLayout(Context context) {
|
||||
this(context, null);
|
||||
@ -55,9 +40,9 @@ public final class SubtitleLayout extends ViewGroup {
|
||||
|
||||
public SubtitleLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
subtitleViews = new ArrayList<>();
|
||||
painters = new ArrayList<>();
|
||||
fontScale = 1;
|
||||
captionStyle = CaptionStyleCompat.DEFAULT;
|
||||
style = CaptionStyleCompat.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -66,131 +51,54 @@ public final class SubtitleLayout extends ViewGroup {
|
||||
* @param cues The cues to display.
|
||||
*/
|
||||
public void setCues(List<Cue> cues) {
|
||||
subtitleCues = cues;
|
||||
int size = (cues == null) ? 0 : cues.size();
|
||||
|
||||
// create new subtitle views if necessary
|
||||
if (size > subtitleViews.size()) {
|
||||
for (int i = subtitleViews.size(); i < size; i++) {
|
||||
SubtitleView newView = createSubtitleView();
|
||||
subtitleViews.add(newView);
|
||||
}
|
||||
if (this.cues == cues) {
|
||||
return;
|
||||
}
|
||||
|
||||
// add the views we currently need, if necessary
|
||||
for (int i = viewsInUse; i < size; i++) {
|
||||
addView(subtitleViews.get(i));
|
||||
this.cues = cues;
|
||||
// Ensure we have sufficient painters.
|
||||
int cueCount = (cues == null) ? 0 : cues.size();
|
||||
while (painters.size() < cueCount) {
|
||||
painters.add(new CuePainter(getContext()));
|
||||
}
|
||||
|
||||
// remove the views we don't currently need, if necessary
|
||||
for (int i = size; i < viewsInUse; i++) {
|
||||
removeView(subtitleViews.get(i));
|
||||
}
|
||||
|
||||
viewsInUse = size;
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
subtitleViews.get(i).setText(cues.get(i).text);
|
||||
}
|
||||
|
||||
requestLayout();
|
||||
// Invalidate to trigger drawing.
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scale of the font.
|
||||
*
|
||||
* @param scale The scale of the font.
|
||||
* @param fontScale The scale of the font.
|
||||
*/
|
||||
public void setFontScale(float scale) {
|
||||
fontScale = scale;
|
||||
updateSubtitlesTextSize(getHeight());
|
||||
|
||||
for (SubtitleView subtitleView : subtitleViews) {
|
||||
subtitleView.setTextSize(textSize);
|
||||
public void setFontScale(float fontScale) {
|
||||
if (this.fontScale == fontScale) {
|
||||
return;
|
||||
}
|
||||
requestLayout();
|
||||
this.fontScale = fontScale;
|
||||
// Invalidate to trigger drawing.
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the view according to the given style.
|
||||
*
|
||||
* @param captionStyle A style for the view.
|
||||
* @param style A style for the view.
|
||||
*/
|
||||
public void setStyle(CaptionStyleCompat captionStyle) {
|
||||
this.captionStyle = captionStyle;
|
||||
|
||||
for (SubtitleView subtitleView : subtitleViews) {
|
||||
subtitleView.setStyle(captionStyle);
|
||||
public void setStyle(CaptionStyleCompat style) {
|
||||
if (this.style == style) {
|
||||
return;
|
||||
}
|
||||
requestLayout();
|
||||
this.style = style;
|
||||
// Invalidate to trigger drawing.
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
setMeasuredDimension(width, height);
|
||||
|
||||
updateSubtitlesTextSize(height);
|
||||
|
||||
for (int i = 0; i < viewsInUse; i++) {
|
||||
subtitleViews.get(i).setTextSize(textSize);
|
||||
subtitleViews.get(i).measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
|
||||
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
|
||||
public void dispatchDraw(Canvas canvas) {
|
||||
int cueCount = (cues == null) ? 0 : cues.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
painters.get(i).draw(cues.get(i), style, fontScale, canvas, getLeft(), getTop(), getRight(),
|
||||
getBottom());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
int width = right - left;
|
||||
int height = bottom - top;
|
||||
|
||||
for (int i = 0; i < viewsInUse; i++) {
|
||||
SubtitleView subtitleView = subtitleViews.get(i);
|
||||
Cue subtitleCue = subtitleCues.get(i);
|
||||
|
||||
int viewLeft = (width - subtitleView.getMeasuredWidth()) / 2;
|
||||
int viewRight = viewLeft + subtitleView.getMeasuredWidth();
|
||||
int viewTop = bottom - subtitleView.getMeasuredHeight()
|
||||
- (int) (height * DEFAULT_BOTTOM_PADDING_FRACTION);
|
||||
int viewBottom = bottom;
|
||||
|
||||
if (subtitleCue.alignment != null) {
|
||||
subtitleView.setTextAlignment(subtitleCue.alignment);
|
||||
} else {
|
||||
subtitleView.setTextAlignment(Alignment.ALIGN_CENTER);
|
||||
}
|
||||
if (subtitleCue.position != Cue.UNSET_VALUE) {
|
||||
if (subtitleCue.alignment == Alignment.ALIGN_OPPOSITE) {
|
||||
viewRight = (int) ((width * (double) subtitleCue.position) / 100) + left;
|
||||
viewLeft = Math.max(viewRight - subtitleView.getMeasuredWidth(), left);
|
||||
} else {
|
||||
viewLeft = (int) ((width * (double) subtitleCue.position) / 100) + left;
|
||||
viewRight = Math.min(viewLeft + subtitleView.getMeasuredWidth(), right);
|
||||
}
|
||||
}
|
||||
if (subtitleCue.line != Cue.UNSET_VALUE) {
|
||||
viewTop = (int) (height * (double) subtitleCue.line / 100) + top;
|
||||
viewBottom = viewTop + subtitleView.getMeasuredHeight();
|
||||
if (viewBottom > bottom) {
|
||||
viewTop = bottom - subtitleView.getMeasuredHeight();
|
||||
viewBottom = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
subtitleView.layout(viewLeft, viewTop, viewRight, viewBottom);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSubtitlesTextSize(int height) {
|
||||
textSize = LINE_HEIGHT_FRACTION * height * fontScale;
|
||||
}
|
||||
|
||||
private SubtitleView createSubtitleView() {
|
||||
SubtitleView view = new SubtitleView(getContext());
|
||||
view.setStyle(captionStyle);
|
||||
view.setTextSize(textSize);
|
||||
return view;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,302 +0,0 @@
|
||||
/*
|
||||
* 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 com.google.android.exoplayer.text;
|
||||
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Paint.Join;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Layout.Alignment;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A view for rendering a single caption.
|
||||
*/
|
||||
public class SubtitleView extends View {
|
||||
|
||||
/**
|
||||
* Ratio of inner padding to font size.
|
||||
*/
|
||||
private static final float INNER_PADDING_RATIO = 0.125f;
|
||||
|
||||
/**
|
||||
* Temporary rectangle used for computing line bounds.
|
||||
*/
|
||||
private final RectF lineBounds = new RectF();
|
||||
|
||||
// Styled dimensions.
|
||||
private final float cornerRadius;
|
||||
private final float outlineWidth;
|
||||
private final float shadowRadius;
|
||||
private final float shadowOffset;
|
||||
|
||||
private TextPaint textPaint;
|
||||
private Paint paint;
|
||||
|
||||
private CharSequence text;
|
||||
|
||||
private int foregroundColor;
|
||||
private int backgroundColor;
|
||||
private int edgeColor;
|
||||
private int edgeType;
|
||||
|
||||
private boolean hasMeasurements;
|
||||
private int lastMeasuredWidth;
|
||||
private StaticLayout layout;
|
||||
|
||||
private Alignment alignment;
|
||||
private float spacingMult;
|
||||
private float spacingAdd;
|
||||
private int innerPaddingX;
|
||||
|
||||
public SubtitleView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SubtitleView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
int[] viewAttr = {android.R.attr.text, android.R.attr.textSize,
|
||||
android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier};
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0);
|
||||
CharSequence text = a.getText(0);
|
||||
int textSize = a.getDimensionPixelSize(1, 15);
|
||||
spacingAdd = a.getDimensionPixelSize(2, 0);
|
||||
spacingMult = a.getFloat(3, 1);
|
||||
a.recycle();
|
||||
|
||||
Resources resources = getContext().getResources();
|
||||
DisplayMetrics displayMetrics = resources.getDisplayMetrics();
|
||||
int twoDpInPx = Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT);
|
||||
cornerRadius = twoDpInPx;
|
||||
outlineWidth = twoDpInPx;
|
||||
shadowRadius = twoDpInPx;
|
||||
shadowOffset = twoDpInPx;
|
||||
|
||||
textPaint = new TextPaint();
|
||||
textPaint.setAntiAlias(true);
|
||||
textPaint.setSubpixelText(true);
|
||||
|
||||
alignment = Alignment.ALIGN_CENTER;
|
||||
|
||||
paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
|
||||
innerPaddingX = 0;
|
||||
setText(text);
|
||||
setTextSize(textSize);
|
||||
setStyle(CaptionStyleCompat.DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBackgroundColor(int color) {
|
||||
backgroundColor = color;
|
||||
forceUpdate(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed by the view.
|
||||
*
|
||||
* @param text The text to display.
|
||||
*/
|
||||
public void setText(CharSequence text) {
|
||||
this.text = text;
|
||||
forceUpdate(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text size in pixels.
|
||||
*
|
||||
* @param size The text size in pixels.
|
||||
*/
|
||||
public void setTextSize(float size) {
|
||||
if (textPaint.getTextSize() != size) {
|
||||
textPaint.setTextSize(size);
|
||||
innerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f);
|
||||
forceUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text alignment.
|
||||
*
|
||||
* @param textAlignment The text alignment.
|
||||
*/
|
||||
public void setTextAlignment(Alignment textAlignment) {
|
||||
alignment = textAlignment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the view according to the given style.
|
||||
*
|
||||
* @param style A style for the view.
|
||||
*/
|
||||
public void setStyle(CaptionStyleCompat style) {
|
||||
foregroundColor = style.foregroundColor;
|
||||
backgroundColor = style.backgroundColor;
|
||||
edgeType = style.edgeType;
|
||||
edgeColor = style.edgeColor;
|
||||
setTypeface(style.typeface);
|
||||
super.setBackgroundColor(style.windowColor);
|
||||
forceUpdate(true);
|
||||
}
|
||||
|
||||
private void setTypeface(Typeface typeface) {
|
||||
if (textPaint.getTypeface() != typeface) {
|
||||
textPaint.setTypeface(typeface);
|
||||
forceUpdate(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void forceUpdate(boolean needsLayout) {
|
||||
if (needsLayout) {
|
||||
hasMeasurements = false;
|
||||
requestLayout();
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final int widthSpec = MeasureSpec.getSize(widthMeasureSpec);
|
||||
|
||||
if (computeMeasurements(widthSpec)) {
|
||||
final StaticLayout layout = this.layout;
|
||||
final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2;
|
||||
final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom();
|
||||
int width = 0;
|
||||
int lineCount = layout.getLineCount();
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width);
|
||||
}
|
||||
width += paddingX;
|
||||
setMeasuredDimension(width, height);
|
||||
} else if (Util.SDK_INT >= 11) {
|
||||
setTooSmallMeasureDimensionV11();
|
||||
} else {
|
||||
setMeasuredDimension(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
private void setTooSmallMeasureDimensionV11() {
|
||||
setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
final int width = r - l;
|
||||
computeMeasurements(width);
|
||||
}
|
||||
|
||||
private boolean computeMeasurements(int maxWidth) {
|
||||
if (hasMeasurements && maxWidth == lastMeasuredWidth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Account for padding.
|
||||
final int paddingX = getPaddingLeft() + getPaddingRight() + innerPaddingX * 2;
|
||||
maxWidth -= paddingX;
|
||||
if (maxWidth <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasMeasurements = true;
|
||||
lastMeasuredWidth = maxWidth;
|
||||
layout = new StaticLayout(text, textPaint, maxWidth, alignment, spacingMult, spacingAdd, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas c) {
|
||||
final StaticLayout layout = this.layout;
|
||||
if (layout == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int saveCount = c.save();
|
||||
final int innerPaddingX = this.innerPaddingX;
|
||||
c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop());
|
||||
|
||||
final int lineCount = layout.getLineCount();
|
||||
final Paint textPaint = this.textPaint;
|
||||
final Paint paint = this.paint;
|
||||
final RectF bounds = lineBounds;
|
||||
|
||||
if (Color.alpha(backgroundColor) > 0) {
|
||||
final float cornerRadius = this.cornerRadius;
|
||||
float previousBottom = layout.getLineTop(0);
|
||||
|
||||
paint.setColor(backgroundColor);
|
||||
paint.setStyle(Style.FILL);
|
||||
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
bounds.left = layout.getLineLeft(i) - innerPaddingX;
|
||||
bounds.right = layout.getLineRight(i) + innerPaddingX;
|
||||
bounds.top = previousBottom;
|
||||
bounds.bottom = layout.getLineBottom(i);
|
||||
previousBottom = bounds.bottom;
|
||||
|
||||
c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
|
||||
}
|
||||
}
|
||||
|
||||
if (edgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) {
|
||||
textPaint.setStrokeJoin(Join.ROUND);
|
||||
textPaint.setStrokeWidth(outlineWidth);
|
||||
textPaint.setColor(edgeColor);
|
||||
textPaint.setStyle(Style.FILL_AND_STROKE);
|
||||
layout.draw(c);
|
||||
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) {
|
||||
textPaint.setShadowLayer(shadowRadius, shadowOffset, shadowOffset, edgeColor);
|
||||
} else if (edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED
|
||||
|| edgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) {
|
||||
boolean raised = edgeType == CaptionStyleCompat.EDGE_TYPE_RAISED;
|
||||
int colorUp = raised ? Color.WHITE : edgeColor;
|
||||
int colorDown = raised ? edgeColor : Color.WHITE;
|
||||
float offset = shadowRadius / 2f;
|
||||
textPaint.setColor(foregroundColor);
|
||||
textPaint.setStyle(Style.FILL);
|
||||
textPaint.setShadowLayer(shadowRadius, -offset, -offset, colorUp);
|
||||
layout.draw(c);
|
||||
textPaint.setShadowLayer(shadowRadius, offset, offset, colorDown);
|
||||
}
|
||||
|
||||
textPaint.setColor(foregroundColor);
|
||||
textPaint.setStyle(Style.FILL);
|
||||
layout.draw(c);
|
||||
textPaint.setShadowLayer(0, 0, 0, 0);
|
||||
c.restoreToCount(saveCount);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user