/* * 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; /** * 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 boolean applyEmbeddedStyles; private int foregroundColor; private int backgroundColor; private int windowColor; private int edgeColor; private int edgeType; private float fontScale; private float bottomPaddingFraction; 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 applyEmbeddedStyles Whether styling embedded within the cue should be applied. * @param style The style to use when drawing the cue text. * @param fontScale The font scale. * @param bottomPaddingFraction The bottom padding fraction to apply when {@link Cue#line} is * {@link Cue#UNSET_VALUE}, as a fraction of the viewport height * @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, boolean applyEmbeddedStyles, CaptionStyleCompat style, float fontScale, float bottomPaddingFraction, Canvas canvas, int cueBoxLeft, int cueBoxTop, int cueBoxRight, int cueBoxBottom) { CharSequence cueText = cue.text; if (TextUtils.isEmpty(cueText)) { // Nothing to draw. return; } if (!applyEmbeddedStyles) { // Strip out any embedded styling. cueText = cueText.toString(); } if (areCharSequencesEqual(this.cueText, cueText) && this.cuePosition == cue.position && Util.areEqual(this.cueAlignment, cue.alignment) && this.applyEmbeddedStyles == applyEmbeddedStyles && this.foregroundColor == style.foregroundColor && this.backgroundColor == style.backgroundColor && this.windowColor == style.windowColor && this.edgeType == style.edgeType && this.edgeColor == style.edgeColor && Util.areEqual(this.textPaint.getTypeface(), style.typeface) && this.fontScale == fontScale && this.bottomPaddingFraction == bottomPaddingFraction && this.parentLeft == cueBoxLeft && this.parentTop == cueBoxTop && this.parentRight == cueBoxRight && this.parentBottom == cueBoxBottom) { // We can use the cached layout. drawLayout(canvas); return; } this.cueText = cueText; this.cuePosition = cue.position; this.cueAlignment = cue.alignment; this.applyEmbeddedStyles = applyEmbeddedStyles; this.foregroundColor = style.foregroundColor; this.backgroundColor = style.backgroundColor; this.windowColor = style.windowColor; this.edgeType = style.edgeType; this.edgeColor = style.edgeColor; this.textPaint.setTypeface(style.typeface); this.fontScale = fontScale; this.bottomPaddingFraction = bottomPaddingFraction; this.parentLeft = cueBoxLeft; this.parentTop = cueBoxTop; this.parentRight = cueBoxRight; this.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 * bottomPaddingFraction); 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); } /** * This method is used instead of {@link TextUtils#equals(CharSequence, CharSequence)} because the * latter only checks the text of each sequence, and does not check for equality of styling that * may be embedded within the {@link CharSequence}s. */ private static boolean areCharSequencesEqual(CharSequence first, CharSequence second) { // Some CharSequence implementations don't perform a cheap referential equality check in their // equals methods, so we perform one explicitly here. return first == second || (first != null && first.equals(second)); } }