Commit f3157e70 by ibaker Committed by Ian Baker

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
parent 331edb4f
/*
* 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);
}
}
}
...@@ -12,34 +12,32 @@ ...@@ -12,34 +12,32 @@
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList; import java.util.Collections;
import java.util.List; import java.util.List;
/** /** A view for displaying subtitle {@link Cue}s. */
* A view for displaying subtitle {@link Cue}s. public final class SubtitleView extends ViewGroup implements TextOutput {
*/
public final class SubtitleView extends View implements TextOutput {
/** /**
* The default fractional text size. * The default fractional text size.
* *
* @see #setFractionalTextSize(float, boolean) * @see SubtitleView#setFractionalTextSize(float, boolean)
*/ */
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f; public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
...@@ -51,31 +49,20 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -51,31 +49,20 @@ public final class SubtitleView extends View implements TextOutput {
*/ */
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f; public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
private final List<SubtitlePainter> painters; private final SubtitleTextView subtitleTextView;
@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;
public SubtitleView(Context context) { 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) { public SubtitleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs); super(context, attrs);
painters = new ArrayList<>(); subtitleTextView = new SubtitleTextView(context, attrs);
textSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL; addView(subtitleTextView);
textSize = DEFAULT_TEXT_SIZE_FRACTION;
applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true;
style = CaptionStyleCompat.DEFAULT;
bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;
} }
@Override @Override
public void onCues(List<Cue> cues) { public void onCues(List<Cue> cues) {
setCues(cues); setCues(cues);
...@@ -87,17 +74,14 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -87,17 +74,14 @@ public final class SubtitleView extends View implements TextOutput {
* @param cues The cues to display, or null to clear the cues. * @param cues The cues to display, or null to clear the cues.
*/ */
public void setCues(@Nullable List<Cue> cues) { public void setCues(@Nullable List<Cue> cues) {
if (this.cues == cues) { subtitleTextView.onCues(cues != null ? cues : Collections.emptyList());
return; }
}
this.cues = cues; @Override
// Ensure we have sufficient painters. protected void onLayout(boolean changed, int l, int t, int r, int b) {
int cueCount = (cues == null) ? 0 : cues.size(); if (changed) {
while (painters.size() < cueCount) { subtitleTextView.layout(l, t, r, b);
painters.add(new SubtitlePainter(getContext()));
} }
// Invalidate to trigger drawing.
invalidate();
} }
/** /**
...@@ -160,13 +144,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -160,13 +144,7 @@ public final class SubtitleView extends View implements TextOutput {
} }
private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) { private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) { subtitleTextView.setTextSize(textSizeType, textSize);
return;
}
this.textSizeType = textSizeType;
this.textSize = textSize;
// Invalidate to trigger drawing.
invalidate();
} }
/** /**
...@@ -176,14 +154,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -176,14 +154,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param applyEmbeddedStyles Whether styling embedded within the cues should be applied. * @param applyEmbeddedStyles Whether styling embedded within the cues should be applied.
*/ */
public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) { public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {
if (this.applyEmbeddedStyles == applyEmbeddedStyles subtitleTextView.setApplyEmbeddedStyles(applyEmbeddedStyles);
&& this.applyEmbeddedFontSizes == applyEmbeddedStyles) {
return;
}
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.applyEmbeddedFontSizes = applyEmbeddedStyles;
// Invalidate to trigger drawing.
invalidate();
} }
/** /**
...@@ -193,12 +164,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -193,12 +164,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied. * @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied.
*/ */
public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) { public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {
if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) { subtitleTextView.setApplyEmbeddedFontSizes(applyEmbeddedFontSizes);
return;
}
this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
// Invalidate to trigger drawing.
invalidate();
} }
/** /**
...@@ -218,12 +184,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -218,12 +184,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param style A style for the view. * @param style A style for the view.
*/ */
public void setStyle(CaptionStyleCompat style) { public void setStyle(CaptionStyleCompat style) {
if (this.style == style) { subtitleTextView.setStyle(style);
return;
}
this.style = style;
// Invalidate to trigger drawing.
invalidate();
} }
/** /**
...@@ -236,87 +197,7 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -236,87 +197,7 @@ public final class SubtitleView extends View implements TextOutput {
* @param bottomPaddingFraction The bottom padding fraction. * @param bottomPaddingFraction The bottom padding fraction.
*/ */
public void setBottomPaddingFraction(float bottomPaddingFraction) { public void setBottomPaddingFraction(float bottomPaddingFraction) {
if (this.bottomPaddingFraction == bottomPaddingFraction) { subtitleTextView.setBottomPaddingFraction(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;
}
} }
@TargetApi(19) @TargetApi(19)
...@@ -340,4 +221,17 @@ public final class SubtitleView extends View implements TextOutput { ...@@ -340,4 +221,17 @@ public final class SubtitleView extends View implements TextOutput {
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); 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);
}
} }
/*
* 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() {}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment