SubtitleView.java
/*
* 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 androidx.media3.ui;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.SOURCE;
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.accessibility.CaptioningManager;
import android.webkit.WebView;
import android.widget.FrameLayout;
import androidx.annotation.Dimension;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** A view for displaying subtitle {@link Cue}s. */
@UnstableApi
public final class SubtitleView extends FrameLayout {
/**
* An output for displaying subtitles.
*
* <p>Implementations of this also need to extend {@link View} in order to be attached to the
* Android view hierarchy.
*/
/* package */ interface Output {
/**
* Updates the list of cues displayed.
*
* @param cues The cues to display.
* @param style A {@link CaptionStyleCompat} to use for styling unset properties of cues.
* @param defaultTextSize The default font size to apply when {@link Cue#textSize} is {@link
* Cue#DIMEN_UNSET}.
* @param defaultTextSizeType The type of {@code defaultTextSize}.
* @param bottomPaddingFraction The bottom padding to apply when {@link Cue#line} is {@link
* Cue#DIMEN_UNSET}, as a fraction of the view's remaining height after its top and bottom
* padding have been subtracted.
* @see #setStyle(CaptionStyleCompat)
* @see #setTextSize(int, float)
* @see #setBottomPaddingFraction(float)
*/
void update(
List<Cue> cues,
CaptionStyleCompat style,
float defaultTextSize,
@Cue.TextSizeType int defaultTextSizeType,
float bottomPaddingFraction);
}
/**
* The default fractional text size.
*
* @see SubtitleView#setFractionalTextSize(float, boolean)
*/
public static final float DEFAULT_TEXT_SIZE_FRACTION = 0.0533f;
/**
* The default bottom padding to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as a
* fraction of the viewport height.
*
* @see #setBottomPaddingFraction(float)
*/
public static final float DEFAULT_BOTTOM_PADDING_FRACTION = 0.08f;
/** Indicates subtitles should be displayed using a {@link Canvas}. This is the default. */
public static final int VIEW_TYPE_CANVAS = 1;
/**
* Indicates subtitles should be displayed using a {@link WebView}.
*
* <p>This will use CSS and HTML styling to render the subtitles. This supports some additional
* styling features beyond those supported by {@link #VIEW_TYPE_CANVAS} such as vertical text.
*/
public static final int VIEW_TYPE_WEB = 2;
/**
* The type of {@link View} to use to display subtitles.
*
* <p>One of:
*
* <ul>
* <li>{@link #VIEW_TYPE_CANVAS}
* <li>{@link #VIEW_TYPE_WEB}
* </ul>
*/
@Documented
@Retention(SOURCE)
@Target(TYPE_USE)
@IntDef({VIEW_TYPE_CANVAS, VIEW_TYPE_WEB})
public @interface ViewType {}
private List<Cue> cues;
private CaptionStyleCompat style;
private @Cue.TextSizeType int defaultTextSizeType;
private float defaultTextSize;
private float bottomPaddingFraction;
private boolean applyEmbeddedStyles;
private boolean applyEmbeddedFontSizes;
private @ViewType int viewType;
private Output output;
private View innerSubtitleView;
public SubtitleView(Context context) {
this(context, null);
}
public SubtitleView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
cues = Collections.emptyList();
style = CaptionStyleCompat.DEFAULT;
defaultTextSizeType = Cue.TEXT_SIZE_TYPE_FRACTIONAL;
defaultTextSize = DEFAULT_TEXT_SIZE_FRACTION;
bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;
applyEmbeddedStyles = true;
applyEmbeddedFontSizes = true;
CanvasSubtitleOutput canvasSubtitleOutput = new CanvasSubtitleOutput(context);
output = canvasSubtitleOutput;
innerSubtitleView = canvasSubtitleOutput;
addView(innerSubtitleView);
viewType = VIEW_TYPE_CANVAS;
}
/**
* Sets the cues to be displayed by the view.
*
* @param cues The cues to display, or null to clear the cues.
*/
public void setCues(@Nullable List<Cue> cues) {
this.cues = (cues != null ? cues : Collections.emptyList());
updateOutput();
}
/**
* Sets the type of {@link View} used to display subtitles.
*
* <p>NOTE: {@link #VIEW_TYPE_WEB} is currently very experimental, and doesn't support most
* styling and layout properties of {@link Cue}.
*
* @param viewType The {@link ViewType} to use.
*/
public void setViewType(@ViewType int viewType) {
if (this.viewType == viewType) {
return;
}
switch (viewType) {
case VIEW_TYPE_CANVAS:
setView(new CanvasSubtitleOutput(getContext()));
break;
case VIEW_TYPE_WEB:
setView(new WebViewSubtitleOutput(getContext()));
break;
default:
throw new IllegalArgumentException();
}
this.viewType = viewType;
}
private <T extends View & Output> void setView(T view) {
removeView(innerSubtitleView);
if (innerSubtitleView instanceof WebViewSubtitleOutput) {
((WebViewSubtitleOutput) innerSubtitleView).destroy();
}
innerSubtitleView = view;
output = view;
addView(view);
}
/**
* Sets the text size to a given unit and value.
*
* <p>See {@link TypedValue} for the possible dimension units.
*
* @param unit The desired dimension unit.
* @param size The desired size in the given units.
*/
public void setFixedTextSize(@Dimension int unit, float size) {
Context context = getContext();
Resources resources;
if (context == null) {
resources = Resources.getSystem();
} else {
resources = context.getResources();
}
setTextSize(
Cue.TEXT_SIZE_TYPE_ABSOLUTE,
TypedValue.applyDimension(unit, size, resources.getDisplayMetrics()));
}
/**
* Sets the text size based on {@link CaptioningManager#getFontScale()} if {@link
* CaptioningManager} is available and enabled.
*
* <p>Otherwise (and always before API level 19) uses a default font scale of 1.0.
*/
public void setUserDefaultTextSize() {
setFractionalTextSize(DEFAULT_TEXT_SIZE_FRACTION * getUserCaptionFontScale());
}
/**
* Sets the text size to be a fraction of the view's remaining height after its top and bottom
* padding have been subtracted.
*
* <p>Equivalent to {@code #setFractionalTextSize(fractionOfHeight, false)}.
*
* @param fractionOfHeight A fraction between 0 and 1.
*/
public void setFractionalTextSize(float fractionOfHeight) {
setFractionalTextSize(fractionOfHeight, false);
}
/**
* Sets the text size to be a fraction of the height of this view.
*
* @param fractionOfHeight A fraction between 0 and 1.
* @param ignorePadding Set to true if {@code fractionOfHeight} should be interpreted as a
* fraction of this view's height ignoring any top and bottom padding. Set to false if {@code
* fractionOfHeight} should be interpreted as a fraction of this view's remaining height after
* the top and bottom padding has been subtracted.
*/
public void setFractionalTextSize(float fractionOfHeight, boolean ignorePadding) {
setTextSize(
ignorePadding
? Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING
: Cue.TEXT_SIZE_TYPE_FRACTIONAL,
fractionOfHeight);
}
private void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
this.defaultTextSizeType = textSizeType;
this.defaultTextSize = textSize;
updateOutput();
}
/**
* Sets whether styling embedded within the cues should be applied. Enabled by default. Overrides
* any setting made with {@link SubtitleView#setApplyEmbeddedFontSizes}.
*
* @param applyEmbeddedStyles Whether styling embedded within the cues should be applied.
*/
public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {
this.applyEmbeddedStyles = applyEmbeddedStyles;
updateOutput();
}
/**
* Sets whether font sizes embedded within the cues should be applied. Enabled by default. Only
* takes effect if {@link SubtitleView#setApplyEmbeddedStyles} is set to true.
*
* @param applyEmbeddedFontSizes Whether font sizes embedded within the cues should be applied.
*/
public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {
this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
updateOutput();
}
/**
* Styles the captions using {@link CaptioningManager#getUserStyle()} if {@link CaptioningManager}
* is available and enabled.
*
* <p>Otherwise (and always before API level 19) uses a default style.
*/
public void setUserDefaultStyle() {
setStyle(getUserCaptionStyle());
}
/**
* Sets the caption style.
*
* @param style A style for the view.
*/
public void setStyle(CaptionStyleCompat style) {
this.style = style;
updateOutput();
}
/**
* Sets the bottom padding fraction to apply when {@link Cue#line} is {@link Cue#DIMEN_UNSET}, as
* a fraction of the view's remaining height after its top and bottom padding have been
* subtracted.
*
* <p>Note that this padding is applied in addition to any standard view padding.
*
* @param bottomPaddingFraction The bottom padding fraction.
*/
public void setBottomPaddingFraction(float bottomPaddingFraction) {
this.bottomPaddingFraction = bottomPaddingFraction;
updateOutput();
}
private float getUserCaptionFontScale() {
if (Util.SDK_INT < 19 || isInEditMode()) {
return 1f;
}
@Nullable
CaptioningManager captioningManager =
(CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE);
return captioningManager != null && captioningManager.isEnabled()
? captioningManager.getFontScale()
: 1f;
}
private CaptionStyleCompat getUserCaptionStyle() {
if (Util.SDK_INT < 19 || isInEditMode()) {
return CaptionStyleCompat.DEFAULT;
}
@Nullable
CaptioningManager captioningManager =
(CaptioningManager) getContext().getSystemService(Context.CAPTIONING_SERVICE);
return captioningManager != null && captioningManager.isEnabled()
? CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle())
: CaptionStyleCompat.DEFAULT;
}
private void updateOutput() {
output.update(
getCuesWithStylingPreferencesApplied(),
style,
defaultTextSize,
defaultTextSizeType,
bottomPaddingFraction);
}
/**
* Returns {@link #cues} with {@link #applyEmbeddedStyles} and {@link #applyEmbeddedFontSizes}
* applied.
*
* <p>If {@link #applyEmbeddedStyles} is false then all styling spans are removed from {@link
* Cue#text}, {@link Cue#textSize} and {@link Cue#textSizeType} are set to {@link Cue#DIMEN_UNSET}
* and {@link Cue#windowColorSet} is set to false.
*
* <p>Otherwise if {@link #applyEmbeddedFontSizes} is false then only size-related styling spans
* are removed from {@link Cue#text} and {@link Cue#textSize} and {@link Cue#textSizeType} are set
* to {@link Cue#DIMEN_UNSET}
*/
private List<Cue> getCuesWithStylingPreferencesApplied() {
if (applyEmbeddedStyles && applyEmbeddedFontSizes) {
return cues;
}
List<Cue> strippedCues = new ArrayList<>(cues.size());
for (int i = 0; i < cues.size(); i++) {
strippedCues.add(removeEmbeddedStyling(cues.get(i)));
}
return strippedCues;
}
private Cue removeEmbeddedStyling(Cue cue) {
Cue.Builder strippedCue = cue.buildUpon();
if (!applyEmbeddedStyles) {
SubtitleViewUtils.removeAllEmbeddedStyling(strippedCue);
} else if (!applyEmbeddedFontSizes) {
SubtitleViewUtils.removeEmbeddedFontSizes(strippedCue);
}
return strippedCue.build();
}
}