diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java new file mode 100644 index 0000000000..39aaebc755 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleTextView.java @@ -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 painters; + + private List 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 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 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); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 5fa2e4816b..e4ac70a507 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -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 painters; - - @Nullable private List 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 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 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 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 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); + } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java new file mode 100644 index 0000000000..4c1eb3298e --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleViewUtils.java @@ -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() {} +}