diff options
| -rw-r--r-- | api/current.txt | 6 | ||||
| -rw-r--r-- | core/java/android/view/accessibility/CaptioningManager.java | 8 | ||||
| -rw-r--r-- | core/java/android/widget/VideoView.java | 162 | ||||
| -rw-r--r-- | core/java/com/android/internal/widget/SubtitleView.java | 4 | ||||
| -rw-r--r-- | media/java/android/media/SubtitleController.java | 26 | ||||
| -rw-r--r-- | media/java/android/media/SubtitleTrack.java | 77 | ||||
| -rw-r--r-- | media/java/android/media/WebVttRenderer.java | 810 |
7 files changed, 926 insertions, 167 deletions
diff --git a/api/current.txt b/api/current.txt index eb3596ac21b3..2d33fc5b00f4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -28991,12 +28991,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 { @@ -29010,7 +29010,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); diff --git a/core/java/android/view/accessibility/CaptioningManager.java b/core/java/android/view/accessibility/CaptioningManager.java index d4c6abe69abf..557239f6096d 100644 --- a/core/java/android/view/accessibility/CaptioningManager.java +++ b/core/java/android/view/accessibility/CaptioningManager.java @@ -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. * diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index f449797d8053..009b729ffc75 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -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) { @@ -792,117 +788,95 @@ public class VideoView extends SurfaceView } @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); + protected void onAttachedToWindow() { + super.onAttachedToWindow(); - // Layout overlay views, if necessary. - if (changed && mOverlays != null && !mOverlays.isEmpty()) { - measureAndLayoutOverlays(); + if (mSubtitleWidget != null) { + mSubtitleWidget.onAttachedToWindow(); } } @Override - public void draw(Canvas canvas) { - super.draw(canvas); + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); - final int count = mOverlays.size(); - for (int i = 0; i < count; i++) { - final View overlay = mOverlays.get(i); - overlay.draw(canvas); + if (mSubtitleWidget != null) { + mSubtitleWidget.onDetachedFromWindow(); } } - /** - * 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(); - } - }; - } + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); - if (mOverlays.isEmpty()) { - setWillNotDraw(false); + if (mSubtitleWidget != null) { + measureAndLayoutSubtitleWidget(); } - - 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); + @Override + public void draw(Canvas canvas) { + super.draw(canvas); - if (mOverlays.isEmpty()) { - setWillNotDraw(true); + if (mSubtitleWidget != null) { + final int saveCount = canvas.save(); + canvas.translate(getPaddingLeft(), getPaddingTop()); + mSubtitleWidget.draw(canvas); + canvas.restoreToCount(saveCount); } - - 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); + final boolean attachedToWindow = isAttachedToWindow(); + if (mSubtitleWidget != null) { + if (attachedToWindow) { + mSubtitleWidget.onDetachedFromWindow(); + } + + mSubtitleWidget.setOnChangedListener(null); } - mSubtitleView = view; - if (mSubtitleView != null) { - addOverlay(mSubtitleView); + + 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); } - } - private View mSubtitleView; + invalidate(); + } } diff --git a/core/java/com/android/internal/widget/SubtitleView.java b/core/java/com/android/internal/widget/SubtitleView.java index 131015124d87..356401c2a322 100644 --- a/core/java/com/android/internal/widget/SubtitleView.java +++ b/core/java/com/android/internal/widget/SubtitleView.java @@ -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); } diff --git a/media/java/android/media/SubtitleController.java b/media/java/android/media/SubtitleController.java index 2cf1b2db71b5..e83c5bae99f3 100644 --- a/media/java/android/media/SubtitleController.java +++ b/media/java/android/media/SubtitleController.java @@ -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()); } } diff --git a/media/java/android/media/SubtitleTrack.java b/media/java/android/media/SubtitleTrack.java index 09fb3f21ace3..cb689af7a43e 100644 --- a/media/java/android/media/SubtitleTrack.java +++ b/media/java/android/media/SubtitleTrack.java @@ -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); + } + } } diff --git a/media/java/android/media/WebVttRenderer.java b/media/java/android/media/WebVttRenderer.java index 527c57f6401c..74773a8be437 100644 --- a/media/java/android/media/WebVttRenderer.java +++ b/media/java/android/media/WebVttRenderer.java @@ -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--; } } - if (mTextView != null) { - if (DEBUG) Log.d(TAG, "updating to " + text.toString()); - mTextView.setText(text.toString()); - mTextView.postInvalidate(); + 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); + } + } + } + + 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); } } } |