Merge "Add WebVTT caption renderer" into klp-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
013634cc44
@@ -29053,12 +29053,12 @@ package android.view.accessibility {
|
||||
}
|
||||
|
||||
public class CaptioningManager {
|
||||
method public void addCaptioningStateChangeListener(android.view.accessibility.CaptioningManager.CaptioningChangeListener);
|
||||
method public void addCaptioningChangeListener(android.view.accessibility.CaptioningManager.CaptioningChangeListener);
|
||||
method public final float getFontScale();
|
||||
method public final java.util.Locale getLocale();
|
||||
method public android.view.accessibility.CaptioningManager.CaptionStyle getUserStyle();
|
||||
method public final boolean isEnabled();
|
||||
method public void removeCaptioningStateChangeListener(android.view.accessibility.CaptioningManager.CaptioningChangeListener);
|
||||
method public void removeCaptioningChangeListener(android.view.accessibility.CaptioningManager.CaptioningChangeListener);
|
||||
}
|
||||
|
||||
public static final class CaptioningManager.CaptionStyle {
|
||||
@@ -29072,7 +29072,7 @@ package android.view.accessibility {
|
||||
field public final int foregroundColor;
|
||||
}
|
||||
|
||||
public abstract class CaptioningManager.CaptioningChangeListener {
|
||||
public static abstract class CaptioningManager.CaptioningChangeListener {
|
||||
ctor public CaptioningManager.CaptioningChangeListener();
|
||||
method public void onEnabledChanged(boolean);
|
||||
method public void onFontScaleChanged(float);
|
||||
|
||||
@@ -140,7 +140,7 @@ public class CaptioningManager {
|
||||
*
|
||||
* @param listener the listener to add
|
||||
*/
|
||||
public void addCaptioningStateChangeListener(CaptioningChangeListener listener) {
|
||||
public void addCaptioningChangeListener(CaptioningChangeListener listener) {
|
||||
synchronized (mListeners) {
|
||||
if (mListeners.isEmpty()) {
|
||||
registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
|
||||
@@ -163,11 +163,11 @@ public class CaptioningManager {
|
||||
|
||||
/**
|
||||
* Removes a listener previously added using
|
||||
* {@link #addCaptioningStateChangeListener}.
|
||||
* {@link #addCaptioningChangeListener}.
|
||||
*
|
||||
* @param listener the listener to remove
|
||||
*/
|
||||
public void removeCaptioningStateChangeListener(CaptioningChangeListener listener) {
|
||||
public void removeCaptioningChangeListener(CaptioningChangeListener listener) {
|
||||
synchronized (mListeners) {
|
||||
mListeners.remove(listener);
|
||||
|
||||
@@ -366,7 +366,7 @@ public class CaptioningManager {
|
||||
* Listener for changes in captioning properties, including enabled state
|
||||
* and user style preferences.
|
||||
*/
|
||||
public abstract class CaptioningChangeListener {
|
||||
public static abstract class CaptioningChangeListener {
|
||||
/**
|
||||
* Called when the captioning enabled state changes.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@ import android.media.MediaPlayer.OnErrorListener;
|
||||
import android.media.MediaPlayer.OnInfoListener;
|
||||
import android.media.Metadata;
|
||||
import android.media.SubtitleController;
|
||||
import android.media.SubtitleTrack.RenderingWidget;
|
||||
import android.media.WebVttRenderer;
|
||||
import android.net.Uri;
|
||||
import android.util.AttributeSet;
|
||||
@@ -46,7 +47,6 @@ import android.widget.MediaController.MediaPlayerControl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
|
||||
@@ -100,14 +100,11 @@ public class VideoView extends SurfaceView
|
||||
private boolean mCanSeekBack;
|
||||
private boolean mCanSeekForward;
|
||||
|
||||
/** List of views overlaid on top of the video. */
|
||||
private ArrayList<View> mOverlays;
|
||||
/** Subtitle rendering widget overlaid on top of the video. */
|
||||
private RenderingWidget mSubtitleWidget;
|
||||
|
||||
/**
|
||||
* Listener for overlay layout changes. Invalidates the video view to ensure
|
||||
* that captions are redrawn whenever their layout changes.
|
||||
*/
|
||||
private OnLayoutChangeListener mOverlayLayoutListener;
|
||||
/** Listener for changes to subtitle data, used to redraw when needed. */
|
||||
private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
|
||||
|
||||
public VideoView(Context context) {
|
||||
super(context);
|
||||
@@ -302,11 +299,10 @@ public class VideoView extends SurfaceView
|
||||
mMediaPlayer = new MediaPlayer();
|
||||
// TODO: create SubtitleController in MediaPlayer, but we need
|
||||
// a context for the subtitle renderers
|
||||
SubtitleController controller = new SubtitleController(
|
||||
getContext(),
|
||||
mMediaPlayer.getMediaTimeProvider(),
|
||||
mMediaPlayer);
|
||||
controller.registerRenderer(new WebVttRenderer(getContext(), null));
|
||||
final Context context = getContext();
|
||||
final SubtitleController controller = new SubtitleController(
|
||||
context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
|
||||
controller.registerRenderer(new WebVttRenderer(context));
|
||||
mMediaPlayer.setSubtitleAnchor(controller, this);
|
||||
|
||||
if (mAudioSession != 0) {
|
||||
@@ -791,13 +787,30 @@ public class VideoView extends SurfaceView
|
||||
return mAudioSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
if (mSubtitleWidget != null) {
|
||||
mSubtitleWidget.onAttachedToWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
if (mSubtitleWidget != null) {
|
||||
mSubtitleWidget.onDetachedFromWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||
super.onLayout(changed, left, top, right, bottom);
|
||||
|
||||
// Layout overlay views, if necessary.
|
||||
if (changed && mOverlays != null && !mOverlays.isEmpty()) {
|
||||
measureAndLayoutOverlays();
|
||||
if (mSubtitleWidget != null) {
|
||||
measureAndLayoutSubtitleWidget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -805,104 +818,65 @@ public class VideoView extends SurfaceView
|
||||
public void draw(Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
|
||||
final int count = mOverlays.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View overlay = mOverlays.get(i);
|
||||
overlay.draw(canvas);
|
||||
if (mSubtitleWidget != null) {
|
||||
final int saveCount = canvas.save();
|
||||
canvas.translate(getPaddingLeft(), getPaddingTop());
|
||||
mSubtitleWidget.draw(canvas);
|
||||
canvas.restoreToCount(saveCount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a view to be overlaid on top of this video view. During layout, the
|
||||
* view will be forced to match the bounds, less padding, of the video view.
|
||||
* <p>
|
||||
* Overlays are drawn in the order they are added. The last added overlay
|
||||
* will be drawn on top.
|
||||
*
|
||||
* @param overlay the view to overlay
|
||||
* @see #removeOverlay(View)
|
||||
*/
|
||||
private void addOverlay(View overlay) {
|
||||
if (mOverlays == null) {
|
||||
mOverlays = new ArrayList<View>(1);
|
||||
}
|
||||
|
||||
if (mOverlayLayoutListener == null) {
|
||||
mOverlayLayoutListener = new OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom,
|
||||
int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (mOverlays.isEmpty()) {
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
mOverlays.add(overlay);
|
||||
overlay.addOnLayoutChangeListener(mOverlayLayoutListener);
|
||||
measureAndLayoutOverlays();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a view previously added using {@link #addOverlay}.
|
||||
*
|
||||
* @param overlay the view to remove
|
||||
* @see #addOverlay(View)
|
||||
*/
|
||||
private void removeOverlay(View overlay) {
|
||||
if (mOverlays == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
overlay.removeOnLayoutChangeListener(mOverlayLayoutListener);
|
||||
mOverlays.remove(overlay);
|
||||
|
||||
if (mOverlays.isEmpty()) {
|
||||
setWillNotDraw(true);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a measurement and layout pass for all overlaid views.
|
||||
*
|
||||
* @see #addOverlay(View)
|
||||
* @see #setSubtitleWidget(RenderingWidget)
|
||||
*/
|
||||
private void measureAndLayoutOverlays() {
|
||||
final int left = getPaddingLeft();
|
||||
final int top = getPaddingTop();
|
||||
final int right = getWidth() - left - getPaddingRight();
|
||||
final int bottom = getHeight() - top - getPaddingBottom();
|
||||
final int widthSpec = MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY);
|
||||
final int heightSpec = MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY);
|
||||
private void measureAndLayoutSubtitleWidget() {
|
||||
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
final int height = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
|
||||
final int count = mOverlays.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final View overlay = mOverlays.get(i);
|
||||
overlay.measure(widthSpec, heightSpec);
|
||||
overlay.layout(left, top, right, bottom);
|
||||
}
|
||||
mSubtitleWidget.setSize(width, height);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public void setSubtitleView(View view) {
|
||||
if (mSubtitleView == view) {
|
||||
public void setSubtitleWidget(RenderingWidget subtitleWidget) {
|
||||
if (mSubtitleWidget == subtitleWidget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mSubtitleView != null) {
|
||||
removeOverlay(mSubtitleView);
|
||||
}
|
||||
mSubtitleView = view;
|
||||
if (mSubtitleView != null) {
|
||||
addOverlay(mSubtitleView);
|
||||
}
|
||||
}
|
||||
final boolean attachedToWindow = isAttachedToWindow();
|
||||
if (mSubtitleWidget != null) {
|
||||
if (attachedToWindow) {
|
||||
mSubtitleWidget.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
private View mSubtitleView;
|
||||
mSubtitleWidget.setOnChangedListener(null);
|
||||
}
|
||||
|
||||
mSubtitleWidget = subtitleWidget;
|
||||
|
||||
if (subtitleWidget != null) {
|
||||
if (mSubtitlesChangedListener == null) {
|
||||
mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
|
||||
@Override
|
||||
public void onChanged(RenderingWidget renderingWidget) {
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setWillNotDraw(false);
|
||||
subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
|
||||
|
||||
if (attachedToWindow) {
|
||||
subtitleWidget.onAttachedToWindow();
|
||||
requestLayout();
|
||||
}
|
||||
} else {
|
||||
setWillNotDraw(true);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ public class SubtitleView extends View {
|
||||
private float mSpacingAdd = 0;
|
||||
private int mInnerPaddingX = 0;
|
||||
|
||||
public SubtitleView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public SubtitleView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ import java.util.Locale;
|
||||
import java.util.Vector;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.MediaPlayer.OnSubtitleDataListener;
|
||||
import android.view.View;
|
||||
import android.media.SubtitleTrack.RenderingWidget;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
|
||||
/**
|
||||
@@ -32,7 +31,6 @@ import android.view.accessibility.CaptioningManager;
|
||||
* @hide
|
||||
*/
|
||||
public class SubtitleController {
|
||||
private Context mContext;
|
||||
private MediaTimeProvider mTimeProvider;
|
||||
private Vector<Renderer> mRenderers;
|
||||
private Vector<SubtitleTrack> mTracks;
|
||||
@@ -50,7 +48,6 @@ public class SubtitleController {
|
||||
Context context,
|
||||
MediaTimeProvider timeProvider,
|
||||
Listener listener) {
|
||||
mContext = context;
|
||||
mTimeProvider = timeProvider;
|
||||
mListener = listener;
|
||||
|
||||
@@ -79,11 +76,11 @@ public class SubtitleController {
|
||||
return mSelectedTrack;
|
||||
}
|
||||
|
||||
private View getSubtitleView() {
|
||||
private RenderingWidget getRenderingWidget() {
|
||||
if (mSelectedTrack == null) {
|
||||
return null;
|
||||
}
|
||||
return mSelectedTrack.getView();
|
||||
return mSelectedTrack.getRenderingWidget();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,7 +107,7 @@ public class SubtitleController {
|
||||
}
|
||||
|
||||
mSelectedTrack = track;
|
||||
mAnchor.setSubtitleView(getSubtitleView());
|
||||
mAnchor.setSubtitleWidget(getRenderingWidget());
|
||||
|
||||
if (mSelectedTrack != null) {
|
||||
mSelectedTrack.setTimeProvider(mTimeProvider);
|
||||
@@ -268,17 +265,16 @@ public class SubtitleController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtitle anchor, an object that is able to display a subtitle view,
|
||||
* Subtitle anchor, an object that is able to display a subtitle renderer,
|
||||
* e.g. a VideoView.
|
||||
*/
|
||||
public interface Anchor {
|
||||
/**
|
||||
* Anchor should set the subtitle view to the supplied view,
|
||||
* or none, if the supplied view is null.
|
||||
*
|
||||
* @param view subtitle view, or null
|
||||
* Anchor should use the supplied subtitle rendering widget, or
|
||||
* none if it is null.
|
||||
* @hide
|
||||
*/
|
||||
public void setSubtitleView(View view);
|
||||
public void setSubtitleWidget(RenderingWidget subtitleWidget);
|
||||
}
|
||||
|
||||
private Anchor mAnchor;
|
||||
@@ -290,11 +286,11 @@ public class SubtitleController {
|
||||
}
|
||||
|
||||
if (mAnchor != null) {
|
||||
mAnchor.setSubtitleView(null);
|
||||
mAnchor.setSubtitleWidget(null);
|
||||
}
|
||||
mAnchor = anchor;
|
||||
if (mAnchor != null) {
|
||||
mAnchor.setSubtitleView(getSubtitleView());
|
||||
mAnchor.setSubtitleWidget(getRenderingWidget());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
package android.media;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.util.LongSparseArray;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
@@ -98,16 +98,16 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
|
||||
public abstract void onData(String data, boolean eos, long runID);
|
||||
|
||||
/**
|
||||
* Called when adding the subtitle rendering view to the view hierarchy, as
|
||||
* well as when showing or hiding the subtitle track, or when the video
|
||||
* Called when adding the subtitle rendering widget to the view hierarchy,
|
||||
* as well as when showing or hiding the subtitle track, or when the video
|
||||
* surface position has changed.
|
||||
*
|
||||
* @return the view object that displays this subtitle track. For most
|
||||
* renderers there should be a single shared view instance that is used
|
||||
* for all tracks supported by that renderer, as at most one subtitle
|
||||
* track is visible at one time.
|
||||
* @return the widget that renders this subtitle track. For most renderers
|
||||
* there should be a single shared instance that is used for all
|
||||
* tracks supported by that renderer, as at most one subtitle track
|
||||
* is visible at one time.
|
||||
*/
|
||||
public abstract View getView();
|
||||
public abstract RenderingWidget getRenderingWidget();
|
||||
|
||||
/**
|
||||
* Called when the active cues have changed, and the contents of the subtitle
|
||||
@@ -268,7 +268,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
|
||||
}
|
||||
|
||||
mVisible = true;
|
||||
getView().setVisibility(View.VISIBLE);
|
||||
getRenderingWidget().setVisible(true);
|
||||
if (mTimeProvider != null) {
|
||||
mTimeProvider.scheduleUpdate(this);
|
||||
}
|
||||
@@ -283,7 +283,7 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
|
||||
if (mTimeProvider != null) {
|
||||
mTimeProvider.cancelNotifications(this);
|
||||
}
|
||||
getView().setVisibility(View.INVISIBLE);
|
||||
getRenderingWidget().setVisible(false);
|
||||
mVisible = false;
|
||||
}
|
||||
|
||||
@@ -645,4 +645,61 @@ public abstract class SubtitleTrack implements MediaTimeProvider.OnMediaTimeList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for rendering subtitles onto a Canvas.
|
||||
*/
|
||||
public interface RenderingWidget {
|
||||
/**
|
||||
* Sets the widget's callback, which is used to send updates when the
|
||||
* rendered data has changed.
|
||||
*
|
||||
* @param callback update callback
|
||||
*/
|
||||
public void setOnChangedListener(OnChangedListener callback);
|
||||
|
||||
/**
|
||||
* Sets the widget's size.
|
||||
*
|
||||
* @param width width in pixels
|
||||
* @param height height in pixels
|
||||
*/
|
||||
public void setSize(int width, int height);
|
||||
|
||||
/**
|
||||
* Sets whether the widget should draw subtitles.
|
||||
*
|
||||
* @param visible true if subtitles should be drawn, false otherwise
|
||||
*/
|
||||
public void setVisible(boolean visible);
|
||||
|
||||
/**
|
||||
* Renders subtitles onto a {@link Canvas}.
|
||||
*
|
||||
* @param c canvas on which to render subtitles
|
||||
*/
|
||||
public void draw(Canvas c);
|
||||
|
||||
/**
|
||||
* Called when the widget is attached to a window.
|
||||
*/
|
||||
public void onAttachedToWindow();
|
||||
|
||||
/**
|
||||
* Called when the widget is detached from a window.
|
||||
*/
|
||||
public void onDetachedFromWindow();
|
||||
|
||||
/**
|
||||
* Callback used to send updates about changes to rendering data.
|
||||
*/
|
||||
public interface OnChangedListener {
|
||||
/**
|
||||
* Called when the rendering data has changed.
|
||||
*
|
||||
* @param renderingWidget the widget whose data has changed
|
||||
*/
|
||||
public void onChanged(RenderingWidget renderingWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 android.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.TextView;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
import android.view.accessibility.CaptioningManager.CaptionStyle;
|
||||
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.internal.widget.SubtitleView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -14,10 +39,12 @@ import java.util.Vector;
|
||||
|
||||
/** @hide */
|
||||
public class WebVttRenderer extends SubtitleController.Renderer {
|
||||
private TextView mMyTextView;
|
||||
private final Context mContext;
|
||||
|
||||
public WebVttRenderer(Context context, AttributeSet attrs) {
|
||||
mMyTextView = new WebVttView(context, attrs);
|
||||
private WebVttRenderingWidget mRenderingWidget;
|
||||
|
||||
public WebVttRenderer(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,19 +57,11 @@ public class WebVttRenderer extends SubtitleController.Renderer {
|
||||
|
||||
@Override
|
||||
public SubtitleTrack createTrack(MediaFormat format) {
|
||||
return new WebVttTrack(format, mMyTextView);
|
||||
}
|
||||
}
|
||||
if (mRenderingWidget == null) {
|
||||
mRenderingWidget = new WebVttRenderingWidget(mContext);
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
class WebVttView extends TextView {
|
||||
public WebVttView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setTextColor(0xffffff00);
|
||||
setTextSize(46);
|
||||
setTextAlignment(TextView.TEXT_ALIGNMENT_CENTER);
|
||||
setLayoutParams(new LayoutParams(
|
||||
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
|
||||
return new WebVttTrack(mRenderingWidget, format);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,26 +973,26 @@ interface WebVttCueListener {
|
||||
class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
|
||||
private static final String TAG = "WebVttTrack";
|
||||
|
||||
private final TextView mTextView;
|
||||
|
||||
private final WebVttParser mParser = new WebVttParser(this);
|
||||
private final UnstyledTextExtractor mExtractor =
|
||||
new UnstyledTextExtractor();
|
||||
private final Tokenizer mTokenizer = new Tokenizer(mExtractor);
|
||||
private final Vector<Long> mTimestamps = new Vector<Long>();
|
||||
private final WebVttRenderingWidget mRenderingWidget;
|
||||
|
||||
private final Map<String, TextTrackRegion> mRegions =
|
||||
new HashMap<String, TextTrackRegion>();
|
||||
private Long mCurrentRunID;
|
||||
|
||||
WebVttTrack(MediaFormat format, TextView textView) {
|
||||
WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
|
||||
super(format);
|
||||
mTextView = textView;
|
||||
|
||||
mRenderingWidget = renderingWidget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return mTextView;
|
||||
public WebVttRenderingWidget getRenderingWidget() {
|
||||
return mRenderingWidget;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1051,6 +1070,7 @@ class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
|
||||
if (!mVisible) {
|
||||
// don't keep the state if we are not visible
|
||||
@@ -1066,29 +1086,737 @@ class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
|
||||
Log.d(TAG, "at (illegal state) the active cues are:");
|
||||
}
|
||||
}
|
||||
StringBuilder text = new StringBuilder();
|
||||
StringBuilder lineBuilder = new StringBuilder();
|
||||
for (Cue o: activeCues) {
|
||||
TextTrackCue cue = (TextTrackCue)o;
|
||||
if (DEBUG) Log.d(TAG, cue.toString());
|
||||
for (TextTrackCueSpan[] line: cue.mLines) {
|
||||
for (TextTrackCueSpan span: line) {
|
||||
if (!span.mEnabled) {
|
||||
continue;
|
||||
}
|
||||
lineBuilder.append(span.mText);
|
||||
|
||||
mRenderingWidget.setActiveCues(activeCues);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget capable of rendering WebVTT captions.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
|
||||
private static final boolean DEBUG = false;
|
||||
private static final int DEBUG_REGION_BACKGROUND = 0x800000FF;
|
||||
private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000;
|
||||
|
||||
/** WebVtt specifies line height as 5.3% of the viewport height. */
|
||||
private static final float LINE_HEIGHT_RATIO = 0.0533f;
|
||||
|
||||
/** Map of active regions, used to determine enter/exit. */
|
||||
private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes =
|
||||
new ArrayMap<TextTrackRegion, RegionLayout>();
|
||||
|
||||
/** Map of active cues, used to determine enter/exit. */
|
||||
private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes =
|
||||
new ArrayMap<TextTrackCue, CueLayout>();
|
||||
|
||||
/** Captioning manager, used to obtain and track caption properties. */
|
||||
private final CaptioningManager mManager;
|
||||
|
||||
/** Callback for rendering changes. */
|
||||
private OnChangedListener mListener;
|
||||
|
||||
/** Current caption style. */
|
||||
private CaptionStyle mCaptionStyle;
|
||||
|
||||
/** Current font size, computed from font scaling factor and height. */
|
||||
private float mFontSize;
|
||||
|
||||
/** Whether a caption style change listener is registered. */
|
||||
private boolean mHasChangeListener;
|
||||
|
||||
public WebVttRenderingWidget(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public WebVttRenderingWidget(Context context, AttributeSet attrs) {
|
||||
this(context, null, 0);
|
||||
}
|
||||
|
||||
public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
|
||||
// Cannot render text over video when layer type is hardware.
|
||||
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
|
||||
mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
mCaptionStyle = mManager.getUserStyle();
|
||||
mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSize(int width, int height) {
|
||||
final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
|
||||
final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
|
||||
|
||||
measure(widthSpec, heightSpec);
|
||||
layout(0, 0, width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
manageChangeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
|
||||
manageChangeListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnChangedListener(OnChangedListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean visible) {
|
||||
if (visible) {
|
||||
setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
manageChangeListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages whether this renderer is listening for caption style changes.
|
||||
*/
|
||||
private void manageChangeListener() {
|
||||
final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
|
||||
if (mHasChangeListener != needsListener) {
|
||||
mHasChangeListener = needsListener;
|
||||
|
||||
if (needsListener) {
|
||||
mManager.addCaptioningChangeListener(mCaptioningListener);
|
||||
|
||||
final CaptionStyle captionStyle = mManager.getUserStyle();
|
||||
final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
|
||||
setCaptionStyle(captionStyle, fontSize);
|
||||
} else {
|
||||
mManager.removeCaptioningChangeListener(mCaptioningListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
|
||||
final Context context = getContext();
|
||||
final CaptionStyle captionStyle = mCaptionStyle;
|
||||
final float fontSize = mFontSize;
|
||||
|
||||
prepForPrune();
|
||||
|
||||
// Ensure we have all necessary cue and region boxes.
|
||||
final int count = activeCues.size();
|
||||
for (int i = 0; i < count; i++) {
|
||||
final TextTrackCue cue = (TextTrackCue) activeCues.get(i);
|
||||
final TextTrackRegion region = cue.mRegion;
|
||||
if (region != null) {
|
||||
RegionLayout regionBox = mRegionBoxes.get(region);
|
||||
if (regionBox == null) {
|
||||
regionBox = new RegionLayout(context, region, captionStyle, fontSize);
|
||||
mRegionBoxes.put(region, regionBox);
|
||||
addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
if (lineBuilder.length() > 0) {
|
||||
text.append(lineBuilder.toString()).append("\n");
|
||||
lineBuilder.delete(0, lineBuilder.length());
|
||||
regionBox.put(cue);
|
||||
} else {
|
||||
CueLayout cueBox = mCueBoxes.get(cue);
|
||||
if (cueBox == null) {
|
||||
cueBox = new CueLayout(context, cue, captionStyle, fontSize);
|
||||
mCueBoxes.put(cue, cueBox);
|
||||
addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
cueBox.update();
|
||||
cueBox.setOrder(i);
|
||||
}
|
||||
}
|
||||
|
||||
prune();
|
||||
|
||||
// Force measurement and layout.
|
||||
final int width = getWidth();
|
||||
final int height = getHeight();
|
||||
setSize(width, height);
|
||||
|
||||
if (mListener != null) {
|
||||
mListener.onChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
|
||||
mCaptionStyle = captionStyle;
|
||||
mFontSize = fontSize;
|
||||
|
||||
final int cueCount = mCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mCueBoxes.valueAt(i);
|
||||
cueBox.setCaptionStyle(captionStyle, fontSize);
|
||||
}
|
||||
|
||||
final int regionCount = mRegionBoxes.size();
|
||||
for (int i = 0; i < regionCount; i++) {
|
||||
final RegionLayout regionBox = mRegionBoxes.valueAt(i);
|
||||
regionBox.setCaptionStyle(captionStyle, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inactive cues and regions.
|
||||
*/
|
||||
private void prune() {
|
||||
int regionCount = mRegionBoxes.size();
|
||||
for (int i = 0; i < regionCount; i++) {
|
||||
final RegionLayout regionBox = mRegionBoxes.valueAt(i);
|
||||
if (regionBox.prune()) {
|
||||
removeView(regionBox);
|
||||
mRegionBoxes.removeAt(i);
|
||||
regionCount--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
int cueCount = mCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mCueBoxes.valueAt(i);
|
||||
if (!cueBox.isActive()) {
|
||||
removeView(cueBox);
|
||||
mCueBoxes.removeAt(i);
|
||||
cueCount--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset active cues and regions.
|
||||
*/
|
||||
private void prepForPrune() {
|
||||
final int regionCount = mRegionBoxes.size();
|
||||
for (int i = 0; i < regionCount; i++) {
|
||||
final RegionLayout regionBox = mRegionBoxes.valueAt(i);
|
||||
regionBox.prepForPrune();
|
||||
}
|
||||
|
||||
final int cueCount = mCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mCueBoxes.valueAt(i);
|
||||
cueBox.prepForPrune();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
|
||||
final int regionCount = mRegionBoxes.size();
|
||||
for (int i = 0; i < regionCount; i++) {
|
||||
final RegionLayout regionBox = mRegionBoxes.valueAt(i);
|
||||
regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
final int cueCount = mCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mCueBoxes.valueAt(i);
|
||||
cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||
final int viewportWidth = r - l;
|
||||
final int viewportHeight = b - t;
|
||||
|
||||
setCaptionStyle(mCaptionStyle,
|
||||
mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight);
|
||||
|
||||
final int regionCount = mRegionBoxes.size();
|
||||
for (int i = 0; i < regionCount; i++) {
|
||||
final RegionLayout regionBox = mRegionBoxes.valueAt(i);
|
||||
layoutRegion(viewportWidth, viewportHeight, regionBox);
|
||||
}
|
||||
|
||||
final int cueCount = mCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mCueBoxes.valueAt(i);
|
||||
layoutCue(viewportWidth, viewportHeight, cueBox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out a region within the viewport. The region handles layout for
|
||||
* contained cues.
|
||||
*/
|
||||
private void layoutRegion(
|
||||
int viewportWidth, int viewportHeight,
|
||||
RegionLayout regionBox) {
|
||||
final TextTrackRegion region = regionBox.getRegion();
|
||||
final int regionHeight = regionBox.getMeasuredHeight();
|
||||
final int regionWidth = regionBox.getMeasuredWidth();
|
||||
|
||||
// TODO: Account for region anchor point.
|
||||
final float x = region.mViewportAnchorPointX;
|
||||
final float y = region.mViewportAnchorPointY;
|
||||
final int left = (int) (x * (viewportWidth - regionWidth) / 100);
|
||||
final int top = (int) (y * (viewportHeight - regionHeight) / 100);
|
||||
|
||||
regionBox.layout(left, top, left + regionWidth, top + regionHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out a cue within the viewport.
|
||||
*/
|
||||
private void layoutCue(
|
||||
int viewportWidth, int viewportHeight, CueLayout cueBox) {
|
||||
final TextTrackCue cue = cueBox.getCue();
|
||||
final int direction = getLayoutDirection();
|
||||
final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
|
||||
final boolean cueSnapToLines = cue.mSnapToLines;
|
||||
|
||||
int size = 100 * cueBox.getMeasuredWidth() / viewportWidth;
|
||||
|
||||
// Determine raw x-position.
|
||||
int xPosition;
|
||||
switch (absAlignment) {
|
||||
case TextTrackCue.ALIGNMENT_LEFT:
|
||||
xPosition = cue.mTextPosition;
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_RIGHT:
|
||||
xPosition = cue.mTextPosition - size;
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_MIDDLE:
|
||||
default:
|
||||
xPosition = cue.mTextPosition - size / 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Adjust x-position for layout.
|
||||
if (direction == LAYOUT_DIRECTION_RTL) {
|
||||
xPosition = 100 - xPosition;
|
||||
}
|
||||
|
||||
// If the text track cue snap-to-lines flag is set, adjust
|
||||
// x-position and size for padding. This is equivalent to placing the
|
||||
// cue within the title-safe area.
|
||||
if (cueSnapToLines) {
|
||||
final int paddingLeft = 100 * getPaddingLeft() / viewportWidth;
|
||||
final int paddingRight = 100 * getPaddingRight() / viewportWidth;
|
||||
if (xPosition < paddingLeft && xPosition + size > paddingLeft) {
|
||||
xPosition += paddingLeft;
|
||||
size -= paddingLeft;
|
||||
}
|
||||
final float rightEdge = 100 - paddingRight;
|
||||
if (xPosition < rightEdge && xPosition + size > rightEdge) {
|
||||
size -= paddingRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute absolute left position and width.
|
||||
final int left = xPosition * viewportWidth / 100;
|
||||
final int width = size * viewportWidth / 100;
|
||||
|
||||
// Determine initial y-position.
|
||||
final int yPosition = calculateLinePosition(cueBox);
|
||||
|
||||
// Compute absolute final top position and height.
|
||||
final int height = cueBox.getMeasuredHeight();
|
||||
final int top;
|
||||
if (yPosition < 0) {
|
||||
// TODO: This needs to use the actual height of prior boxes.
|
||||
top = viewportHeight + yPosition * height;
|
||||
} else {
|
||||
top = yPosition * (viewportHeight - height) / 100;
|
||||
}
|
||||
|
||||
// Layout cue in final position.
|
||||
cueBox.layout(left, top, left + width, top + height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the line position for a cue.
|
||||
* <p>
|
||||
* If the resulting position is negative, it represents a bottom-aligned
|
||||
* position relative to the number of active cues. Otherwise, it represents
|
||||
* a percentage [0-100] of the viewport height.
|
||||
*/
|
||||
private int calculateLinePosition(CueLayout cueBox) {
|
||||
final TextTrackCue cue = cueBox.getCue();
|
||||
final Integer linePosition = cue.mLinePosition;
|
||||
final boolean snapToLines = cue.mSnapToLines;
|
||||
final boolean autoPosition = (linePosition == null);
|
||||
|
||||
if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) {
|
||||
// Invalid line position defaults to 100.
|
||||
return 100;
|
||||
} else if (!autoPosition) {
|
||||
// Use the valid, supplied line position.
|
||||
return linePosition;
|
||||
} else if (!snapToLines) {
|
||||
// Automatic, non-snapped line position defaults to 100.
|
||||
return 100;
|
||||
} else {
|
||||
// Automatic snapped line position uses active cue order.
|
||||
return -(cueBox.mOrder + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves cue alignment according to the specified layout direction.
|
||||
*/
|
||||
private static int resolveCueAlignment(int layoutDirection, int alignment) {
|
||||
switch (alignment) {
|
||||
case TextTrackCue.ALIGNMENT_START:
|
||||
return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
|
||||
TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT;
|
||||
case TextTrackCue.ALIGNMENT_END:
|
||||
return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
|
||||
TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT;
|
||||
}
|
||||
return alignment;
|
||||
}
|
||||
|
||||
private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
|
||||
@Override
|
||||
public void onFontScaleChanged(float fontScale) {
|
||||
final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO;
|
||||
setCaptionStyle(mCaptionStyle, fontSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserStyleChanged(CaptionStyle userStyle) {
|
||||
setCaptionStyle(userStyle, mFontSize);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A text track region represents a portion of the video viewport and
|
||||
* provides a rendering area for text track cues.
|
||||
*/
|
||||
private static class RegionLayout extends LinearLayout {
|
||||
private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>();
|
||||
private final TextTrackRegion mRegion;
|
||||
|
||||
private CaptionStyle mCaptionStyle;
|
||||
private float mFontSize;
|
||||
|
||||
public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle,
|
||||
float fontSize) {
|
||||
super(context);
|
||||
|
||||
mRegion = region;
|
||||
mCaptionStyle = captionStyle;
|
||||
mFontSize = fontSize;
|
||||
|
||||
// TODO: Add support for vertical text
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
if (DEBUG) {
|
||||
setBackgroundColor(DEBUG_REGION_BACKGROUND);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
|
||||
mCaptionStyle = captionStyle;
|
||||
mFontSize = fontSize;
|
||||
|
||||
final int cueCount = mRegionCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mRegionCueBoxes.get(i);
|
||||
cueBox.setCaptionStyle(captionStyle, fontSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the parent's measurement responsibilities, then
|
||||
* automatically performs its own measurement.
|
||||
*/
|
||||
public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final TextTrackRegion region = mRegion;
|
||||
final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
final int width = (int) region.mWidth;
|
||||
|
||||
// Determine the absolute maximum region size as the requested size.
|
||||
final int size = width * specWidth / 100;
|
||||
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
|
||||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
|
||||
measure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this region for pruning by setting all tracks as inactive.
|
||||
* <p>
|
||||
* Tracks that are added or updated using {@link #put(TextTrackCue)}
|
||||
* after this calling this method will be marked as active.
|
||||
*/
|
||||
public void prepForPrune() {
|
||||
final int cueCount = mRegionCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mRegionCueBoxes.get(i);
|
||||
cueBox.prepForPrune();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a {@link TextTrackCue} to this region. If the track had already
|
||||
* been added, updates its active state.
|
||||
*
|
||||
* @param cue
|
||||
*/
|
||||
public void put(TextTrackCue cue) {
|
||||
final int cueCount = mRegionCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mRegionCueBoxes.get(i);
|
||||
if (cueBox.getCue() == cue) {
|
||||
cueBox.update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize);
|
||||
addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
|
||||
if (getChildCount() > mRegion.mLines) {
|
||||
removeViewAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all inactive tracks from this region.
|
||||
*
|
||||
* @return true if this region is empty and should be pruned
|
||||
*/
|
||||
public boolean prune() {
|
||||
int cueCount = mRegionCueBoxes.size();
|
||||
for (int i = 0; i < cueCount; i++) {
|
||||
final CueLayout cueBox = mRegionCueBoxes.get(i);
|
||||
if (!cueBox.isActive()) {
|
||||
mRegionCueBoxes.remove(i);
|
||||
removeView(cueBox);
|
||||
cueCount--;
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return mRegionCueBoxes.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the region data backing this layout
|
||||
*/
|
||||
public TextTrackRegion getRegion() {
|
||||
return mRegion;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A text track cue is the unit of time-sensitive data in a text track,
|
||||
* corresponding for instance for subtitles and captions to the text that
|
||||
* appears at a particular time and disappears at another time.
|
||||
* <p>
|
||||
* A single cue may contain multiple {@link SpanLayout}s, each representing a
|
||||
* single line of text.
|
||||
*/
|
||||
private static class CueLayout extends LinearLayout {
|
||||
public final TextTrackCue mCue;
|
||||
|
||||
private CaptionStyle mCaptionStyle;
|
||||
private float mFontSize;
|
||||
|
||||
private boolean mActive;
|
||||
private int mOrder;
|
||||
|
||||
public CueLayout(
|
||||
Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) {
|
||||
super(context);
|
||||
|
||||
mCue = cue;
|
||||
mCaptionStyle = captionStyle;
|
||||
mFontSize = fontSize;
|
||||
|
||||
// TODO: Add support for vertical text.
|
||||
final boolean horizontal = cue.mWritingDirection
|
||||
== TextTrackCue.WRITING_DIRECTION_HORIZONTAL;
|
||||
setOrientation(horizontal ? VERTICAL : HORIZONTAL);
|
||||
|
||||
switch (cue.mAlignment) {
|
||||
case TextTrackCue.ALIGNMENT_END:
|
||||
setGravity(Gravity.END);
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_LEFT:
|
||||
setGravity(Gravity.LEFT);
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_MIDDLE:
|
||||
setGravity(horizontal
|
||||
? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_RIGHT:
|
||||
setGravity(Gravity.RIGHT);
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_START:
|
||||
setGravity(Gravity.START);
|
||||
break;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
setBackgroundColor(DEBUG_CUE_BACKGROUND);
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
public void setCaptionStyle(CaptionStyle style, float fontSize) {
|
||||
mCaptionStyle = style;
|
||||
mFontSize = fontSize;
|
||||
|
||||
final int n = getChildCount();
|
||||
for (int i = 0; i < n; i++) {
|
||||
final View child = getChildAt(i);
|
||||
if (child instanceof SpanLayout) {
|
||||
((SpanLayout) child).setCaptionStyle(style, fontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mTextView != null) {
|
||||
if (DEBUG) Log.d(TAG, "updating to " + text.toString());
|
||||
mTextView.setText(text.toString());
|
||||
mTextView.postInvalidate();
|
||||
public void prepForPrune() {
|
||||
mActive = false;
|
||||
}
|
||||
|
||||
public void update() {
|
||||
mActive = true;
|
||||
|
||||
removeAllViews();
|
||||
|
||||
final CaptionStyle captionStyle = mCaptionStyle;
|
||||
final float fontSize = mFontSize;
|
||||
final TextTrackCueSpan[][] lines = mCue.mLines;
|
||||
final int lineCount = lines.length;
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]);
|
||||
lineBox.setCaptionStyle(captionStyle, fontSize);
|
||||
|
||||
addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the parent's measurement responsibilities, then
|
||||
* automatically performs its own measurement.
|
||||
*/
|
||||
public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
final TextTrackCue cue = mCue;
|
||||
final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
|
||||
final int direction = getLayoutDirection();
|
||||
final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
|
||||
|
||||
// Determine the maximum size of cue based on its starting position
|
||||
// and the direction in which it grows.
|
||||
final int maximumSize;
|
||||
switch (absAlignment) {
|
||||
case TextTrackCue.ALIGNMENT_LEFT:
|
||||
maximumSize = 100 - cue.mTextPosition;
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_RIGHT:
|
||||
maximumSize = cue.mTextPosition;
|
||||
break;
|
||||
case TextTrackCue.ALIGNMENT_MIDDLE:
|
||||
if (cue.mTextPosition <= 50) {
|
||||
maximumSize = cue.mTextPosition * 2;
|
||||
} else {
|
||||
maximumSize = (100 - cue.mTextPosition) * 2;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
maximumSize = 0;
|
||||
}
|
||||
|
||||
// Determine absolute maximum cue size as the smaller of the
|
||||
// requested size and the maximum theoretical size.
|
||||
final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100;
|
||||
widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
|
||||
heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
|
||||
measure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the order of this cue in the list of active cues.
|
||||
*
|
||||
* @param order the order of this cue in the list of active cues
|
||||
*/
|
||||
public void setOrder(int order) {
|
||||
mOrder = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether this cue is marked as active
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return mActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the cue data backing this layout
|
||||
*/
|
||||
public TextTrackCue getCue() {
|
||||
return mCue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A text track line represents a single line of text within a cue.
|
||||
* <p>
|
||||
* A single line may contain multiple spans, each representing a section of
|
||||
* text that may be enabled or disabled at a particular time.
|
||||
*/
|
||||
private static class SpanLayout extends SubtitleView {
|
||||
private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
|
||||
private final TextTrackCueSpan[] mSpans;
|
||||
|
||||
public SpanLayout(Context context, TextTrackCueSpan[] spans) {
|
||||
super(context);
|
||||
|
||||
mSpans = spans;
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
public void update() {
|
||||
final SpannableStringBuilder builder = mBuilder;
|
||||
final TextTrackCueSpan[] spans = mSpans;
|
||||
|
||||
builder.clear();
|
||||
builder.clearSpans();
|
||||
|
||||
final int spanCount = spans.length;
|
||||
for (int i = 0; i < spanCount; i++) {
|
||||
final TextTrackCueSpan span = spans[i];
|
||||
if (span.mEnabled) {
|
||||
builder.append(spans[i].mText);
|
||||
}
|
||||
}
|
||||
|
||||
setText(builder);
|
||||
}
|
||||
|
||||
public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
|
||||
setBackgroundColor(captionStyle.backgroundColor);
|
||||
setForegroundColor(captionStyle.foregroundColor);
|
||||
setEdgeColor(captionStyle.edgeColor);
|
||||
setEdgeType(captionStyle.edgeType);
|
||||
setTypeface(captionStyle.getTypeface());
|
||||
setTextSize(fontSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user