Split some of SubtitleView out into SubtitleTextView

SubtitleView now becomes a ViewGroup that owns a SubtitleTextView. It
handles some common styling defaults, but delegates the underlying
values down into SubtitleTextView through the SubtitleView.Output
interface.

When I add a SubtitleWebView this will also be a ViewGroup containing
a WebView and will implement SubtitleView.Output and convert Cue styling
into HTML & CSS.

PiperOrigin-RevId: 291903294
This commit is contained in:
ibaker 2020-01-28 11:58:59 +00:00 committed by Ian Baker
parent 331edb4fb2
commit f3157e703f
3 changed files with 265 additions and 144 deletions

View file

@ -0,0 +1,175 @@
/*
* Copyright (C) 2016 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.exoplayer2.ui;
import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION;
import static com.google.android.exoplayer2.ui.SubtitleView.DEFAULT_TEXT_SIZE_FRACTION;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link SubtitleView.Output} that uses Android's native text tooling via {@link
* SubtitlePainter}.
*/
/* package */ final class SubtitleTextView extends View implements SubtitleView.Output {
private final List<SubtitlePainter> painters;
private List<Cue> cues;
@Cue.TextSizeType private int textSizeType;
private float textSize;
private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes;
private CaptionStyleCompat style;
private float bottomPaddingFraction;
public SubtitleTextView(Context context) {
this(context, /* attrs= */ null);
}
public SubtitleTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
painters = new ArrayList<>();
cues = Collections.emptyList();
textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true;
style = CaptionStyleCompat.DEFAULT;
bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;
}
@Override
public void onCues(List<Cue> cues) {
if (this.cues == cues || this.cues.isEmpty() && cues.isEmpty()) {
return;
}
this.cues = cues;
// Ensure we have sufficient painters.
while (painters.size() < cues.size()) {
painters.add(new SubtitlePainter(getContext()));
}
// Invalidate to trigger drawing.
invalidate();
}
@Override
public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) {
return;
}
this.textSizeType = textSizeType;
this.textSize = textSize;
invalidate();
}
@Override
public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {
if (this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.applyEmbeddedFontSizes == applyEmbeddedStyles) {
return;
}
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.applyEmbeddedFontSizes = applyEmbeddedStyles;
invalidate();
}
@Override
public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {
if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) {
return;
}
this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
invalidate();
}
@Override
public void setStyle(CaptionStyleCompat style) {
if (this.style == style) {
return;
}
this.style = style;
invalidate();
}
@Override
public void setBottomPaddingFraction(float bottomPaddingFraction) {
if (this.bottomPaddingFraction == bottomPaddingFraction) {
return;
}
this.bottomPaddingFraction = bottomPaddingFraction;
invalidate();
}
@Override
public void dispatchDraw(Canvas canvas) {
@Nullable List<Cue> cues = this.cues;
if (cues.isEmpty()) {
return;
}
int rawViewHeight = getHeight();
// Calculate the cue box bounds relative to the canvas after padding is taken into account.
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int bottom = rawViewHeight - getPaddingBottom();
if (bottom <= top || right <= left) {
// No space to draw subtitles.
return;
}
int viewHeightMinusPadding = bottom - top;
float defaultViewTextSizePx =
SubtitleViewUtils.resolveTextSize(
textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
if (defaultViewTextSizePx <= 0) {
// Text has no height.
return;
}
int cueCount = cues.size();
for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i);
float cueTextSizePx =
SubtitleViewUtils.resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction,
canvas,
left,
top,
right,
bottom);
}
}
}

View file

@ -12,34 +12,32 @@
* 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.exoplayer2.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A view for displaying subtitle {@link Cue}s.
*/
public final class SubtitleView extends View implements TextOutput {
/** A view for displaying subtitle {@link Cue}s. */
public final class SubtitleView extends ViewGroup implements TextOutput {
/**
* The default fractional text size.
*
* @see #setFractionalTextSize(float, boolean)
* @see SubtitleView#setFractionalTextSize(float, boolean)
*/
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
@ -51,31 +49,20 @@ public final class SubtitleView extends View implements TextOutput {
*/
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
private final List<SubtitlePainter> painters;
@Nullable private List<Cue> cues;
@Cue.TextSizeType private int textSizeType;
private float textSize;
private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes;
private CaptionStyleCompat style;
private float bottomPaddingFraction;
private final SubtitleTextView subtitleTextView;
public SubtitleView(Context context) {
this(context, /* attrs= */ null);
this(context, null);
}
// The null checker doesn't like the `addView()` call,
@SuppressWarnings("nullness:method.invocation.invalid")
public SubtitleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
painters = new ArrayList<>();
textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true;
style = CaptionStyleCompat.DEFAULT;
bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;
subtitleTextView = new SubtitleTextView(context, attrs);
addView(subtitleTextView);
}
@Override
public void onCues(List<Cue> cues) {
setCues(cues);
@ -87,17 +74,14 @@ public final class SubtitleView extends View implements TextOutput {
* @param cues The cues to display, or null to clear the cues.
*/
public void setCues(@Nullable List<Cue> cues) {
if (this.cues == cues) {
return;
subtitleTextView.onCues(cues != null ? cues : Collections.emptyList());
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
subtitleTextView.layout(l, t, r, b);
}
this.cues = cues;
// Ensure we have sufficient painters.
int cueCount = (cues == null) ? 0 : cues.size();
while (painters.size() < cueCount) {
painters.add(new SubtitlePainter(getContext()));
}
// Invalidate to trigger drawing.
invalidate();
}
/**
@ -160,13 +144,7 @@ public final class SubtitleView extends View implements TextOutput {
}
private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) {
return;
}
this.textSizeType = textSizeType;
this.textSize = textSize;
// Invalidate to trigger drawing.
invalidate();
subtitleTextView.setTextSize(textSizeType, textSize);
}
/**
@ -176,14 +154,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param applyEmbeddedStyles Whether styling embedded within the cues should be applied.
*/
public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {
if (this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.applyEmbeddedFontSizes == applyEmbeddedStyles) {
return;
}
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.applyEmbeddedFontSizes = applyEmbeddedStyles;
// Invalidate to trigger drawing.
invalidate();
subtitleTextView.setApplyEmbeddedStyles(applyEmbeddedStyles);
}
/**
@ -193,12 +164,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied.
*/
public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {
if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) {
return;
}
this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
// Invalidate to trigger drawing.
invalidate();
subtitleTextView.setApplyEmbeddedFontSizes(applyEmbeddedFontSizes);
}
/**
@ -218,12 +184,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param style A style for the view.
*/
public void setStyle(CaptionStyleCompat style) {
if (this.style == style) {
return;
}
this.style = style;
// Invalidate to trigger drawing.
invalidate();
subtitleTextView.setStyle(style);
}
/**
@ -236,87 +197,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param bottomPaddingFraction The bottom padding fraction.
*/
public void setBottomPaddingFraction(float bottomPaddingFraction) {
if (this.bottomPaddingFraction == bottomPaddingFraction) {
return;
}
this.bottomPaddingFraction = bottomPaddingFraction;
// Invalidate to trigger drawing.
invalidate();
}
@Override
public void dispatchDraw(Canvas canvas) {
List<Cue> cues = this.cues;
if (cues == null || cues.isEmpty()) {
return;
}
int rawViewHeight = getHeight();
// Calculate the cue box bounds relative to the canvas after padding is taken into account.
int left = getPaddingLeft();
int top = getPaddingTop();
int right = getWidth() - getPaddingRight();
int bottom = rawViewHeight - getPaddingBottom();
if (bottom <= top || right <= left) {
// No space to draw subtitles.
return;
}
int viewHeightMinusPadding = bottom - top;
float defaultViewTextSizePx =
resolveTextSize(textSizeType, textSize, rawViewHeight, viewHeightMinusPadding);
if (defaultViewTextSizePx <= 0) {
// Text has no height.
return;
}
int cueCount = cues.size();
for (int i = 0; i < cueCount; i++) {
Cue cue = cues.get(i);
float cueTextSizePx = resolveCueTextSize(cue, rawViewHeight, viewHeightMinusPadding);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
applyEmbeddedStyles,
applyEmbeddedFontSizes,
style,
defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction,
canvas,
left,
top,
right,
bottom);
}
}
private float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return 0;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return Math.max(defaultCueTextSizePx, 0);
}
private float resolveTextSize(
@Cue.TextSizeType int textSizeType,
float textSize,
int rawViewHeight,
int viewHeightMinusPadding) {
switch (textSizeType) {
case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
return textSize;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
return textSize * viewHeightMinusPadding;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
return textSize * rawViewHeight;
case Cue.TYPE_UNSET:
default:
return Cue.DIMEN_UNSET;
}
subtitleTextView.setBottomPaddingFraction(bottomPaddingFraction);
}
@TargetApi(19)
@ -340,4 +221,17 @@ public final class SubtitleView extends View implements TextOutput {
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
}
/* package */ interface Output {
void onCues(List<Cue> cues);
void setTextSize(@Cue.TextSizeType int textSizeType, float textSize);
void setApplyEmbeddedStyles(boolean applyEmbeddedStyles);
void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes);
void setStyle(CaptionStyleCompat style);
void setBottomPaddingFraction(float bottomPaddingFraction);
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (C) 2020 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.exoplayer2.ui;
import com.google.android.exoplayer2.text.Cue;
/** Utility class for subtitle layout logic. */
/* package */ final class SubtitleViewUtils {
public static float resolveCueTextSize(Cue cue, int rawViewHeight, int viewHeightMinusPadding) {
if (cue.textSizeType == Cue.TYPE_UNSET || cue.textSize == Cue.DIMEN_UNSET) {
return 0;
}
float defaultCueTextSizePx =
resolveTextSize(cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
return Math.max(defaultCueTextSizePx, 0);
}
public static float resolveTextSize(
@Cue.TextSizeType int textSizeType,
float textSize,
int rawViewHeight,
int viewHeightMinusPadding) {
switch (textSizeType) {
case Cue.TEXT_SIZE_TYPE_ABSOLUTE:
return textSize;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL:
return textSize * viewHeightMinusPadding;
case Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING:
return textSize * rawViewHeight;
case Cue.TYPE_UNSET:
default:
return Cue.DIMEN_UNSET;
}
}
private SubtitleViewUtils() {}
}