diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b824358866..420dcd0e49 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ * Use `defStyleAttr` when obtaining styled attributes in `StyledPlayerView`, `PlayerView` and `PlayerControlView` ([#9024](https://github.com/google/ExoPlayer/issues/9024)). + * Fix accessibility focus in `PlayerControlView` + ([#9111](https://github.com/google/ExoPlayer/issues/9111)). * Remove deprecated symbols: * Remove `Renderer.VIDEO_SCALING_MODE_*` constants. Use identically named constants in `C` instead. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 2b0dce52d0..3db156f4b4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -512,6 +512,9 @@ public class DefaultTimeBar extends View implements TimeBar { @Override public void setPosition(long position) { + if (this.position == position) { + return; + } this.position = position; setContentDescription(getProgressText()); update(); @@ -519,12 +522,18 @@ public class DefaultTimeBar extends View implements TimeBar { @Override public void setBufferedPosition(long bufferedPosition) { + if (this.bufferedPosition == bufferedPosition) { + return; + } this.bufferedPosition = bufferedPosition; update(); } @Override public void setDuration(long duration) { + if (this.duration == duration) { + return; + } this.duration = duration; if (scrubbing && duration == C.TIME_UNSET) { stopScrubbing(/* canceled= */ true); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java index 7fc028ccc1..1d2d38784e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -41,10 +41,13 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.ExoPlayer; @@ -339,6 +342,8 @@ public class PlayerControlView extends FrameLayout { private long[] extraAdGroupTimesMs; private boolean[] extraPlayedAdGroups; private long currentWindowOffset; + private long currentPosition; + private long currentBufferedPosition; public PlayerControlView(Context context) { this(context, /* attrs= */ null); @@ -783,6 +788,7 @@ public class PlayerControlView extends FrameLayout { } updateAll(); requestPlayPauseFocus(); + requestPlayPauseAccessibilityFocus(); } // Call hideAfterTimeout even if already visible to reset the timeout. hideAfterTimeout(); @@ -831,18 +837,30 @@ public class PlayerControlView extends FrameLayout { return; } boolean requestPlayPauseFocus = false; + boolean requestPlayPauseAccessibilityFocus = false; boolean shouldShowPauseButton = shouldShowPauseButton(); if (playButton != null) { requestPlayPauseFocus |= shouldShowPauseButton && playButton.isFocused(); + requestPlayPauseAccessibilityFocus |= + Util.SDK_INT < 21 + ? requestPlayPauseFocus + : (shouldShowPauseButton && Api21.isAccessibilityFocused(playButton)); playButton.setVisibility(shouldShowPauseButton ? GONE : VISIBLE); } if (pauseButton != null) { requestPlayPauseFocus |= !shouldShowPauseButton && pauseButton.isFocused(); + requestPlayPauseAccessibilityFocus |= + Util.SDK_INT < 21 + ? requestPlayPauseFocus + : (!shouldShowPauseButton && Api21.isAccessibilityFocused(pauseButton)); pauseButton.setVisibility(shouldShowPauseButton ? VISIBLE : GONE); } if (requestPlayPauseFocus) { requestPlayPauseFocus(); } + if (requestPlayPauseAccessibilityFocus) { + requestPlayPauseAccessibilityFocus(); + } } private void updateNavigation() { @@ -1021,14 +1039,21 @@ public class PlayerControlView extends FrameLayout { position = currentWindowOffset + player.getContentPosition(); bufferedPosition = currentWindowOffset + player.getContentBufferedPosition(); } - if (positionView != null && !scrubbing) { + boolean positionChanged = position != currentPosition; + boolean bufferedPositionChanged = bufferedPosition != currentBufferedPosition; + currentPosition = position; + currentBufferedPosition = bufferedPosition; + + // Only update the TextView if the position has changed, else TalkBack will repeatedly read the + // same position to the user. + if (positionView != null && !scrubbing && positionChanged) { positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); } if (timeBar != null) { timeBar.setPosition(position); timeBar.setBufferedPosition(bufferedPosition); } - if (progressUpdateListener != null) { + if (progressUpdateListener != null && (positionChanged || bufferedPositionChanged)) { progressUpdateListener.onProgressUpdate(position, bufferedPosition); } @@ -1065,6 +1090,15 @@ public class PlayerControlView extends FrameLayout { } } + private void requestPlayPauseAccessibilityFocus() { + boolean shouldShowPauseButton = shouldShowPauseButton(); + if (!shouldShowPauseButton && playButton != null) { + playButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } else if (shouldShowPauseButton && pauseButton != null) { + pauseButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); + } + } + private void updateButton(boolean visible, boolean enabled, @Nullable View view) { if (view == null) { return; @@ -1339,4 +1373,12 @@ public class PlayerControlView extends FrameLayout { } } } + + @RequiresApi(21) + private static final class Api21 { + @DoNotInline + public static boolean isAccessibilityFocused(View view) { + return view.isAccessibilityFocused(); + } + } }