Class MediaControls

java.lang.Object
limn.scene.Widget<MediaControls>
limn.components.MediaControls

public final class MediaControls extends Widget<MediaControls>
Playback controls for a VideoView: play/pause, a scrub bar and a position clock — the bar every player lays over its picture. Everything here drives the view and only the view (VideoView.setPaused(boolean), VideoView.seek(long, VideoStreamSource.SeekMode), VideoView.positionMicros()), so it works with a frozen capture, a source with no soundtrack, and a player the application built itself.

It reads left to right in either direction, by default. The playback cluster inherits its arrangement from tape decks and the scrub bar advances the way the tape ran; media playback is the standing exception in every platform's bidi guidance, so this widget declares LTR for itself when it is built. That is a declaration and not a law: an application that wants the controls to follow the tree (some platforms do mirror them) calls setLayoutDirection(null) to inherit, or passes an explicit direction — the same knob every widget has (see docs/design/direction-axis.md).

Sound, and which half of it is the toolkit's. A mute button and a volume slider are built in, because a gain on the soundtrack the player already holds is the toolkit's to turn (MediaPlayer.audio()). By default they appear only while the media can actually sound (MediaControls.Sound.AUTO) and step aside otherwise, so a silent clip does not carry a dead volume; setSound(limn.components.MediaControls.Sound) forces them MediaControls.Sound.ON or MediaControls.Sound.OFF. Muting is gain zero and never a teardown. Everything behind the gain stays the application's: one that opens its soundtrack lazily, switches audio tracks or offers subtitles turns the built-ins off and wires its own widgets into the slots.

The slots. addLeading(limn.scene.Widget<?>) places a widget between the play button and the scrub bar (where players put their volume), addTrailing(limn.scene.Widget<?>) between the scrub bar and the clock, and observeRefresh(java.lang.Runnable) gives injected controls a ride on the same heartbeat the built-in ones update on. setShowPosition(boolean) and setBackdrop(boolean) trim the built-ins.

The bar polls on a timer rather than a ticker, so parked over a paused picture it costs nothing: every write below is guarded, and a frame is only asked for when a number actually moved.

To an assistive technology this is one tool bar holding the transport, and nothing more: the play and mute buttons are real widgets that describe themselves as buttons named by the tooltips this bar keeps in step with their state, the two sliders and the clock describe themselves, and the row and the boxes between them are scaffolding the tree deletes. The bar cannot say anything about a control on the buttons' behalf — its only child is the row — and so it does not try to. A press from a screen reader reaches the same activation a click and a Space release reach.

The tooltips are written where the state they name changes, and not on the heartbeat. The tree is published before the paint of the same frame, so a tooltip written from the paint reaches a reader one frame after the change it names; the mute button's name in particular follows muted, which the setters and the volume slider all write, so each of those writes the tooltip as it goes.

  • Constructor Details

    • MediaControls

      public MediaControls(VideoView view)
      A transport for view: play and pause, the scrub bar and the clock, and mute and volume while the media can sound.
      Parameters:
      view - the view it controls; never null
  • Method Details

    • view

      public VideoView view()
      The view these controls drive.
    • addLeading

      public MediaControls addLeading(Widget<?> widget)
      Adds a widget between the play button and the scrub bar — where players put their volume. Order of calls is the order on screen.
    • addTrailing

      public MediaControls addTrailing(Widget<?> widget)
      Adds a widget between the scrub bar and the position clock.
    • setShowPosition

      public MediaControls setShowPosition(boolean show)
      Whether the position clock is shown (default true).
    • setBackdrop

      public MediaControls setBackdrop(boolean paint)
      Whether the bar paints its own translucent panel (default true). A host that already draws chrome behind the controls — a glass strip over the picture — turns it off.
    • observeRefresh

      public Subscription observeRefresh(Runnable listener)
      Observes every refresh of the built-in controls, so a widget injected through addLeading(limn.scene.Widget<?>)/addTrailing(limn.scene.Widget<?>) can update on the same heartbeat instead of arming a timer of its own. The heartbeat is the poll tick: a paint announces nothing and so refreshes nothing, which makes a tick a truer heartbeat than a paint rate was.
      Parameters:
      listener - what to run after each refresh; never null
      Returns:
      a handle that unregisters; cancelling it twice is a no-op. UI thread
    • setInk

      public MediaControls setInk(@Nullable Color newInk, @Nullable Color newMutedInk)
      Overrides the ink the built-ins write in — the play glyph and the clock. A bar drawn over someone else's chrome (a glass strip over the picture) sits on a surface the palette knows nothing about, and a theme ink chosen for the theme's own surfaces can land near-black on it. nulls restore the theme's answer.
    • setSound

      public MediaControls setSound(MediaControls.Sound mode)
      Whether the mute button and volume slider are offered (default MediaControls.Sound.AUTO).
    • setVolume

      public MediaControls setVolume(float newLevel)
      Volume in [0, 1], applied as a gain on the soundtrack the player holds; zero also mutes. This is what unmuting restores.
    • volume

      public float volume()
      The volume unmuting restores, in [0, 1].
    • setMuted

      public MediaControls setMuted(boolean newMuted)
      Gain zero and never a teardown: a mute that closed the stream would cost a decoder flush every time somebody silenced a video for a moment.
    • isMuted

      public boolean isMuted()
      Whether the soundtrack is silenced (gain zero; the stream keeps running).
    • onAccessibility

      protected void onAccessibility(Accessibility a)
      Describes this bar as one horizontal tool bar that controls its view, and nothing else.

      The role is unconditional and does not follow setBackdrop(boolean): the class paints the backdrop, so a bar that declared nothing would be deleted as scaffolding with a warning naming a toolkit class in an application's log, and the node is wanted with the panel off as much as with it on, because either way this is the bar that holds the transport. The orientation is fixed by the class, which lays its controls in a row and only a row. The relation names the view these controls drive; it is resolved to the view's own node once the view publishes one and is dropped until then.

      No name is declared, because the bar holds no string that names itself; an application that wants one calls setAccessibleName. Nothing is declared about the controls: each button, slider and the clock describes itself, and the row and the boxes between them are removed by the tree's own rule.

      Overrides:
      onAccessibility in class Widget<MediaControls>
      Parameters:
      a - the node being described
    • onMeasure

      protected Size onMeasure(Constraints constraints)
      Description copied from class: Widget
      Reports the size this widget wants within constraints. Called once per layout pass, and the result is cached against the constraints and the resolved axes (size step, layout direction, locale), so it must be a pure function of them and of this widget's own state.

      Resolve the ControlSize and the LayoutDirection once each here and thread them down; never read either in a constructor. The locale needs no threading: it is in scope, and I18n.locale() answers it wherever text is resolved.

      Specified by:
      onMeasure in class Widget<MediaControls>
    • onLayout

      protected void onLayout()
      Description copied from class: Widget
      Containers position children here (measure + Widget.layoutBox(float, float, float, float) per child).
      Overrides:
      onLayout in class Widget<MediaControls>
    • paintOutset

      protected float paintOutset()
      What is left of the theme's focus ring reach past the padding round the row, which is all of it without the backdrop: the play button is then flush with the bar's leading edge, and its ring, and that of any control an application added at either end, hangs past the bar. With the backdrop the padding is wider than the ring at every step and nothing is left.
      Overrides:
      paintOutset in class Widget<MediaControls>
    • onPaint

      protected void onPaint(Canvas canvas)
      Paints the backdrop and arms the poll. It refreshes nothing: a paint may not announce a change, and refresh() writes widget state that announces -- the bar's value on every advance, the clock's text once a second -- so the refresh runs from the poll tick alone. What that costs is latency, at most one poll interval, and a repaint that no tick follows (a resize, an occlusion redraw) no longer refreshes the transport on its own. What it keeps is the re-arm-on-paint pattern the animations use: the poll stops itself when the bar leaves the screen, and the paint that brings it back is what starts it again.
      Overrides:
      onPaint in class Widget<MediaControls>