blob: 670314e463b67fb17b9c5598ed1847ed6520d5cd [file] [log] [blame]
/*
* 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 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.Color;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.Base64;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.webkit.WebView;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Util;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
/**
* A {@link SubtitleView.Output} that uses a {@link WebView} to render subtitles.
*
* <p>This is useful for subtitle styling not supported by Android's native text libraries such as
* vertical text.
*
* <p>NOTE: This is currently extremely experimental and doesn't support most {@link Cue} styling
* properties.
*/
/* package */ final class SubtitleWebView extends ViewGroup implements SubtitleView.Output {
private final WebView webView;
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 SubtitleWebView(Context context) {
this(context, null);
}
public SubtitleWebView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
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;
webView =
new WebView(context, attrs) {
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
// Return false so that touch events are allowed down into @id/exo_content_frame below.
return false;
}
@Override
public boolean performClick() {
super.performClick();
// Return false so that clicks are allowed down into @id/exo_content_frame below.
return false;
}
};
webView.setBackgroundColor(Color.TRANSPARENT);
addView(webView);
}
@Override
public void onCues(List<Cue> cues) {
this.cues = cues;
updateWebView();
}
@Override
public void setTextSize(@Cue.TextSizeType int textSizeType, float textSize) {
if (this.textSizeType == textSizeType && this.textSize == textSize) {
return;
}
this.textSizeType = textSizeType;
this.textSize = textSize;
updateWebView();
}
@Override
public void setApplyEmbeddedStyles(boolean applyEmbeddedStyles) {
if (this.applyEmbeddedStyles == applyEmbeddedStyles
&& this.applyEmbeddedFontSizes == applyEmbeddedStyles) {
return;
}
this.applyEmbeddedStyles = applyEmbeddedStyles;
this.applyEmbeddedFontSizes = applyEmbeddedStyles;
updateWebView();
}
@Override
public void setApplyEmbeddedFontSizes(boolean applyEmbeddedFontSizes) {
if (this.applyEmbeddedFontSizes == applyEmbeddedFontSizes) {
return;
}
this.applyEmbeddedFontSizes = applyEmbeddedFontSizes;
updateWebView();
}
@Override
public void setStyle(CaptionStyleCompat style) {
if (this.style == style) {
return;
}
this.style = style;
updateWebView();
}
@Override
public void setBottomPaddingFraction(float bottomPaddingFraction) {
if (this.bottomPaddingFraction == bottomPaddingFraction) {
return;
}
this.bottomPaddingFraction = bottomPaddingFraction;
updateWebView();
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
webView.layout(l, t, r, b);
}
}
private void updateWebView() {
StringBuilder html = new StringBuilder();
html.append("<html><body>")
.append("<div style=\"")
.append("-webkit-user-select:none;")
.append("position:fixed;")
.append("top:0;")
.append("bottom:0;")
.append("left:0;")
.append("right:0;")
.append("font-size:20px;")
.append("color:red;")
.append("\">");
for (int i = 0; i < cues.size(); i++) {
Cue cue = cues.get(i);
float positionPercent = (cue.position != Cue.DIMEN_UNSET) ? (cue.position * 100) : 50;
int positionAnchorTranslatePercent = anchorTypeToTranslatePercent(cue.positionAnchor);
float linePercent;
int lineTranslatePercent;
if (cue.line != Cue.DIMEN_UNSET) {
switch (cue.lineType) {
case Cue.LINE_TYPE_NUMBER:
if (cue.line >= 0) {
linePercent = 0;
lineTranslatePercent = Math.round(cue.line) * 100;
} else {
linePercent = 100;
lineTranslatePercent = Math.round(cue.line + 1) * 100;
}
break;
case Cue.LINE_TYPE_FRACTION:
case Cue.TYPE_UNSET:
default:
linePercent = cue.line * 100;
lineTranslatePercent = 0;
}
} else {
linePercent = 100;
lineTranslatePercent = 0;
}
int lineAnchorTranslatePercent =
cue.verticalType == Cue.VERTICAL_TYPE_RL
? -anchorTypeToTranslatePercent(cue.lineAnchor)
: anchorTypeToTranslatePercent(cue.lineAnchor);
String size =
cue.size != Cue.DIMEN_UNSET
? Util.formatInvariant("%.2f%%", cue.size * 100)
: "fit-content";
String textAlign = convertAlignmentToCss(cue.textAlignment);
String writingMode = convertVerticalTypeToCss(cue.verticalType);
String positionProperty;
String lineProperty;
switch (cue.verticalType) {
case Cue.VERTICAL_TYPE_LR:
lineProperty = "left";
positionProperty = "top";
break;
case Cue.VERTICAL_TYPE_RL:
lineProperty = "right";
positionProperty = "top";
break;
case Cue.TYPE_UNSET:
default:
lineProperty = "top";
positionProperty = "left";
}
String sizeProperty;
int horizontalTranslatePercent;
int verticalTranslatePercent;
if (cue.verticalType == Cue.VERTICAL_TYPE_LR || cue.verticalType == Cue.VERTICAL_TYPE_RL) {
sizeProperty = "height";
horizontalTranslatePercent = lineTranslatePercent + lineAnchorTranslatePercent;
verticalTranslatePercent = positionAnchorTranslatePercent;
} else {
sizeProperty = "width";
horizontalTranslatePercent = positionAnchorTranslatePercent;
verticalTranslatePercent = lineTranslatePercent + lineAnchorTranslatePercent;
}
html.append(
Util.formatInvariant(
"<div style=\""
+ "position:absolute;"
+ "%s:%.2f%%;"
+ "%s:%.2f%%;"
+ "%s:%s;"
+ "text-align:%s;"
+ "writing-mode:%s;"
+ "transform:translate(%s%%,%s%%);"
+ "\">",
positionProperty,
positionPercent,
lineProperty,
linePercent,
sizeProperty,
size,
textAlign,
writingMode,
horizontalTranslatePercent,
verticalTranslatePercent))
.append(SpannedToHtmlConverter.convert(cue.text))
.append("</div>");
}
html.append("</div></body></html>");
webView.loadData(
Base64.encodeToString(
html.toString().getBytes(Charset.forName(C.UTF8_NAME)), Base64.NO_PADDING),
"text/html",
"base64");
}
private String convertVerticalTypeToCss(@Cue.VerticalType int verticalType) {
switch (verticalType) {
case Cue.VERTICAL_TYPE_LR:
return "vertical-lr";
case Cue.VERTICAL_TYPE_RL:
return "vertical-rl";
case Cue.TYPE_UNSET:
default:
return "horizontal-tb";
}
}
private String convertAlignmentToCss(@Nullable Layout.Alignment alignment) {
if (alignment == null) {
return "unset";
}
switch (alignment) {
case ALIGN_NORMAL:
return "start";
case ALIGN_CENTER:
return "center";
case ALIGN_OPPOSITE:
return "end";
default:
return "unset";
}
}
/**
* Converts a {@link Cue.AnchorType} to a percentage for use in a CSS {@code transform:
* translate(x,y)} function.
*
* <p>We use {@code position: absolute} and always use the same CSS positioning property (top,
* bottom, left, right) regardless of the anchor type. The anchor is effectively 'moved' by using
* a CSS {@code translate(x,y)} operation on the value returned from this function.
*/
private static int anchorTypeToTranslatePercent(@Cue.AnchorType int anchorType) {
switch (anchorType) {
case Cue.ANCHOR_TYPE_END:
return -100;
case Cue.ANCHOR_TYPE_MIDDLE:
return -50;
case Cue.ANCHOR_TYPE_START:
case Cue.TYPE_UNSET:
default:
return 0;
}
}
}