CanvasSubtitleOutput.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 androidx.media3.ui.SubtitleView.DEFAULT_BOTTOM_PADDING_FRACTION;
import static androidx.media3.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 androidx.media3.common.text.Cue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A {@link SubtitleView.Output} that uses Android's native layout framework via {@link
* SubtitlePainter}.
*/
/* package */ final class CanvasSubtitleOutput extends View implements SubtitleView.Output {
private final List<SubtitlePainter> painters;
private List<Cue> cues;
private @Cue.TextSizeType int textSizeType;
private float textSize;
private CaptionStyleCompat style;
private float bottomPaddingFraction;
public CanvasSubtitleOutput(Context context) {
this(context, /* attrs= */ null);
}
public CanvasSubtitleOutput(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;
style = CaptionStyleCompat.DEFAULT;
bottomPaddingFraction = DEFAULT_BOTTOM_PADDING_FRACTION;
}
@Override
public void update(
List<Cue> cues,
CaptionStyleCompat style,
float textSize,
@Cue.TextSizeType int textSizeType,
float bottomPaddingFraction) {
this.cues = cues;
this.style = style;
this.textSize = textSize;
this.textSizeType = textSizeType;
this.bottomPaddingFraction = bottomPaddingFraction;
// Ensure we have sufficient painters.
while (painters.size() < cues.size()) {
painters.add(new SubtitlePainter(getContext()));
}
// Invalidate to trigger drawing.
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);
if (cue.verticalType != Cue.TYPE_UNSET) {
cue = repositionVerticalCue(cue);
}
float cueTextSizePx =
SubtitleViewUtils.resolveTextSize(
cue.textSizeType, cue.textSize, rawViewHeight, viewHeightMinusPadding);
SubtitlePainter painter = painters.get(i);
painter.draw(
cue,
style,
defaultViewTextSizePx,
cueTextSizePx,
bottomPaddingFraction,
canvas,
left,
top,
right,
bottom);
}
}
/**
* Reposition a vertical cue for horizontal display.
*
* <p>This class doesn't support rendering vertical text, but if we naively interpret vertical
* {@link Cue#position} and{@link Cue#line} values for horizontal display then the cues will often
* be displayed in unexpected positions. For example, the 'default' position for vertical-rl
* subtitles is the right-hand edge of the viewport, so cues that would appear vertically in this
* position should appear horizontally at the bottom of the viewport (generally the default
* position). Similarly left-edge vertical-rl cues should be shown at the top of a horizontal
* viewport.
*
* <p>There isn't a meaningful way to transform {@link Cue#position} and related values (e.g.
* alignment), so we clear these and allow {@link SubtitlePainter} to do the default behaviour of
* centering the cue.
*/
private static Cue repositionVerticalCue(Cue cue) {
Cue.Builder cueBuilder =
cue.buildUpon()
.setPosition(Cue.DIMEN_UNSET)
.setPositionAnchor(Cue.TYPE_UNSET)
.setTextAlignment(null);
if (cue.lineType == Cue.LINE_TYPE_FRACTION) {
cueBuilder.setLine(1.0f - cue.line, Cue.LINE_TYPE_FRACTION);
} else {
cueBuilder.setLine(-cue.line - 1f, Cue.LINE_TYPE_NUMBER);
}
switch (cue.lineAnchor) {
case Cue.ANCHOR_TYPE_END:
cueBuilder.setLineAnchor(Cue.ANCHOR_TYPE_START);
break;
case Cue.ANCHOR_TYPE_START:
cueBuilder.setLineAnchor(Cue.ANCHOR_TYPE_END);
break;
case Cue.ANCHOR_TYPE_MIDDLE:
case Cue.TYPE_UNSET:
default:
// Fall through
}
return cueBuilder.build();
}
}