Update TtmlDecoder to keep only one Span of each type
The current code relies on Android's evaluation order of spans, which doesn't seem to be defined anywhere. PiperOrigin-RevId: 288700011
This commit is contained in:
parent
8e26505ee8
commit
14e401f53a
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
/**
|
||||
* Utility methods for Android <a href="https://developer.android.com/guide/topics/text/spans">span
|
||||
* styling</a>.
|
||||
*/
|
||||
public final class SpanUtil {
|
||||
|
||||
/**
|
||||
* Adds {@code span} to {@code spannable} between {@code start} and {@code end}, removing any
|
||||
* existing spans of the same type and with the same indices and flags.
|
||||
*
|
||||
* <p>This is useful for types of spans that don't make sense to duplicate and where the
|
||||
* evaluation order might have an unexpected impact on the final text, e.g. {@link
|
||||
* ForegroundColorSpan}.
|
||||
*
|
||||
* @param spannable The {@link Spannable} to add {@code span} to.
|
||||
* @param span The span object to be added.
|
||||
* @param start The start index to add the new span at.
|
||||
* @param end The end index to add the new span at.
|
||||
* @param spanFlags The flags to pass to {@link Spannable#setSpan(Object, int, int, int)}.
|
||||
*/
|
||||
public static void addOrReplaceSpan(
|
||||
Spannable spannable, Object span, int start, int end, int spanFlags) {
|
||||
Object[] existingSpans = spannable.getSpans(start, end, span.getClass());
|
||||
for (Object existingSpan : existingSpans) {
|
||||
if (spannable.getSpanStart(existingSpan) == start
|
||||
&& spannable.getSpanEnd(existingSpan) == end
|
||||
&& spannable.getSpanFlags(existingSpan) == spanFlags) {
|
||||
spannable.removeSpan(existingSpan);
|
||||
}
|
||||
}
|
||||
spannable.setSpan(span, start, end, spanFlags);
|
||||
}
|
||||
|
||||
private SpanUtil() {}
|
||||
}
|
@ -15,7 +15,6 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.text.ttml;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.AbsoluteSizeSpan;
|
||||
@ -27,6 +26,7 @@ import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import com.google.android.exoplayer2.text.SpanUtil;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@ -77,32 +77,60 @@ import java.util.Map;
|
||||
builder.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasFontColor()) {
|
||||
builder.setSpan(new ForegroundColorSpan(style.getFontColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new ForegroundColorSpan(style.getFontColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasBackgroundColor()) {
|
||||
builder.setSpan(new BackgroundColorSpan(style.getBackgroundColor()), start, end,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new BackgroundColorSpan(style.getBackgroundColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getFontFamily() != null) {
|
||||
builder.setSpan(new TypefaceSpan(style.getFontFamily()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new TypefaceSpan(style.getFontFamily()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getTextAlign() != null) {
|
||||
builder.setSpan(new AlignmentSpan.Standard(style.getTextAlign()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new AlignmentSpan.Standard(style.getTextAlign()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
switch (style.getFontSizeUnit()) {
|
||||
case TtmlStyle.FONT_SIZE_UNIT_PIXEL:
|
||||
builder.setSpan(new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new AbsoluteSizeSpan((int) style.getFontSize(), true),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.FONT_SIZE_UNIT_EM:
|
||||
builder.setSpan(new RelativeSizeSpan(style.getFontSize()), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new RelativeSizeSpan(style.getFontSize()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.FONT_SIZE_UNIT_PERCENT:
|
||||
builder.setSpan(new RelativeSizeSpan(style.getFontSize() / 100), start, end,
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
builder,
|
||||
new RelativeSizeSpan(style.getFontSize() / 100),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case TtmlStyle.UNSPECIFIED:
|
||||
|
@ -15,11 +15,11 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.text.webvtt;
|
||||
|
||||
import static com.google.android.exoplayer2.text.SpanUtil.addOrReplaceSpan;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Layout;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.SpannedString;
|
||||
@ -535,7 +535,12 @@ public final class WebvttCueParser {
|
||||
return;
|
||||
}
|
||||
if (style.getStyle() != WebvttCssStyle.UNSPECIFIED) {
|
||||
addOrReplaceSpan(spannedText, new StyleSpan(style.getStyle()), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new StyleSpan(style.getStyle()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.isLinethrough()) {
|
||||
spannedText.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
@ -544,29 +549,62 @@ public final class WebvttCueParser {
|
||||
spannedText.setSpan(new UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasFontColor()) {
|
||||
addOrReplaceSpan(spannedText, new ForegroundColorSpan(style.getFontColor()), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new ForegroundColorSpan(style.getFontColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.hasBackgroundColor()) {
|
||||
addOrReplaceSpan(
|
||||
spannedText, new BackgroundColorSpan(style.getBackgroundColor()), start, end);
|
||||
spannedText,
|
||||
new BackgroundColorSpan(style.getBackgroundColor()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (style.getFontFamily() != null) {
|
||||
addOrReplaceSpan(spannedText, new TypefaceSpan(style.getFontFamily()), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new TypefaceSpan(style.getFontFamily()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
Layout.Alignment textAlign = style.getTextAlign();
|
||||
if (textAlign != null) {
|
||||
addOrReplaceSpan(spannedText, new AlignmentSpan.Standard(textAlign), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new AlignmentSpan.Standard(textAlign),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
switch (style.getFontSizeUnit()) {
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_PIXEL:
|
||||
addOrReplaceSpan(
|
||||
spannedText, new AbsoluteSizeSpan((int) style.getFontSize(), true), start, end);
|
||||
spannedText,
|
||||
new AbsoluteSizeSpan((int) style.getFontSize(), true),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_EM:
|
||||
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize()), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new RelativeSizeSpan(style.getFontSize()),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.FONT_SIZE_UNIT_PERCENT:
|
||||
addOrReplaceSpan(spannedText, new RelativeSizeSpan(style.getFontSize() / 100), start, end);
|
||||
addOrReplaceSpan(
|
||||
spannedText,
|
||||
new RelativeSizeSpan(style.getFontSize() / 100),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
case WebvttCssStyle.UNSPECIFIED:
|
||||
// Do nothing.
|
||||
@ -578,26 +616,6 @@ public final class WebvttCueParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@code span} to {@code spannedText} between {@code start} and {@code end}, removing any
|
||||
* existing spans of the same type and with the same indices.
|
||||
*
|
||||
* <p>This is useful for types of spans that don't make sense to duplicate and where the
|
||||
* evaluation order might have an unexpected impact on the final text, e.g. {@link
|
||||
* ForegroundColorSpan}.
|
||||
*/
|
||||
private static void addOrReplaceSpan(
|
||||
SpannableStringBuilder spannedText, Object span, int start, int end) {
|
||||
Object[] existingSpans = spannedText.getSpans(start, end, span.getClass());
|
||||
for (Object existingSpan : existingSpans) {
|
||||
if (spannedText.getSpanStart(existingSpan) == start
|
||||
&& spannedText.getSpanEnd(existingSpan) == end) {
|
||||
spannedText.removeSpan(existingSpan);
|
||||
}
|
||||
}
|
||||
spannedText.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tag name for the given tag contents.
|
||||
*
|
||||
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.text;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Tests for {@link SpanUtil}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SpanUtilTest {
|
||||
|
||||
@Test
|
||||
public void addOrReplaceSpan_replacesSameTypeAndIndexes() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
spannable.setSpan(
|
||||
new ForegroundColorSpan(Color.CYAN),
|
||||
/* start= */ 2,
|
||||
/* end= */ 5,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
ForegroundColorSpan newSpan = new ForegroundColorSpan(Color.BLUE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
spannable, newSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
|
||||
assertThat(spans).asList().containsExactly(newSpan);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addOrReplaceSpan_ignoresDifferentType() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
|
||||
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
BackgroundColorSpan newSpan = new BackgroundColorSpan(Color.BLUE);
|
||||
SpanUtil.addOrReplaceSpan(spannable, newSpan, 2, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
|
||||
assertThat(spans).asList().containsExactly(originalSpan, newSpan).inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void addOrReplaceSpan_ignoresDifferentStartEndAndFlags() {
|
||||
Spannable spannable = SpannableString.valueOf("test text");
|
||||
ForegroundColorSpan originalSpan = new ForegroundColorSpan(Color.CYAN);
|
||||
spannable.setSpan(originalSpan, /* start= */ 2, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
ForegroundColorSpan differentStart = new ForegroundColorSpan(Color.GREEN);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
spannable, differentStart, /* start= */ 3, /* end= */ 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ForegroundColorSpan differentEnd = new ForegroundColorSpan(Color.BLUE);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
spannable, differentEnd, /* start= */ 2, /* end= */ 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
ForegroundColorSpan differentFlags = new ForegroundColorSpan(Color.GREEN);
|
||||
SpanUtil.addOrReplaceSpan(
|
||||
spannable, differentFlags, /* start= */ 2, /* end= */ 5, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
|
||||
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
|
||||
assertThat(spans)
|
||||
.asList()
|
||||
.containsExactly(originalSpan, differentStart, differentEnd, differentFlags)
|
||||
.inOrder();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user