Loading packages/SystemUI/res/layout/media_view.xml +2 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ android:layout_alignParentLeft="true" android:fontFamily="@*android:string/config_bodyFontFamily" android:textColor="@color/media_primary_text" android:gravity="left" android:gravity="start" android:textSize="14sp" /> <TextView Loading @@ -49,7 +49,7 @@ android:layout_alignParentRight="true" android:fontFamily="@*android:string/config_bodyFontFamily" android:textColor="@color/media_primary_text" android:gravity="right" android:gravity="end" android:textSize="14sp" /> </FrameLayout> Loading packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +1 −4 Original line number Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.NonNull; Loading Loading @@ -165,9 +164,7 @@ public class MediaControlPanel { TransitionLayout player = vh.getPlayer(); mSeekBarObserver = new SeekBarObserver(vh); mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); SeekBar bar = vh.getSeekBar(); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mMediaViewController.attach(player); } Loading packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +129 −9 Original line number Diff line number Diff line Loading @@ -20,11 +20,13 @@ import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.PlaybackState import android.os.SystemClock import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.widget.SeekBar import androidx.annotation.AnyThread import androidx.annotation.WorkerThread import androidx.core.view.GestureDetectorCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.LiveData import com.android.systemui.dagger.qualifiers.Background Loading Loading @@ -68,7 +70,6 @@ private fun PlaybackState.computePosition(duration: Long): Long { /** ViewModel for seek bar in QS media player. */ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: RepeatableExecutor) { private var _data = Progress(false, false, null, null) set(value) { field = value Loading Loading @@ -198,10 +199,10 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R return SeekBarChangeListener(this, bgExecutor) } /** Gets a listener to attach to the seek bar to disable touch intercepting. */ val seekBarTouchListener: View.OnTouchListener get() { return SeekBarTouchListener() /** Attach touch handlers to the seek bar view. */ fun attachTouchHandlers(bar: SeekBar) { bar.setOnSeekBarChangeListener(seekBarListener) bar.setOnTouchListener(SeekBarTouchListener(bar)) } private class SeekBarChangeListener( Loading @@ -225,11 +226,130 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R } } private class SeekBarTouchListener : View.OnTouchListener { /** * Responsible for intercepting touch events before they reach the seek bar. * * This reduces the gestures seen by the seek bar so that users don't accidentially seek when * they intend to scroll the carousel. */ private class SeekBarTouchListener( private val bar: SeekBar ) : View.OnTouchListener, GestureDetector.OnGestureListener { // Gesture detector helps decide which touch events to intercept. private val detector = GestureDetectorCompat(bar.context, this) // Defines a tap target around the thumb at the beginning of a gesture. private var onDownTargetBoxMinX: Int = -1 private var onDownTargetBoxMaxX: Int = -1 /** * Decide which touch events to intercept before they reach the seek bar. * * Based on the gesture detected, we decide whether we want the event to reach the seek bar. * If we want the seek bar to see the event, then we return false so that the event isn't * handled here and it will be passed along. If, however, we don't want the seek bar to see * the event, then return true so that the event is handled here. * * When the seek bar is contained in the carousel, the carousel still has the ability to * intercept the touch event. So, even though we may handle the event here, the carousel can * still intercept the event. This way, gestures that we consider falses on the seek bar can * still be used by the carousel for paging. * * Returns true for events that we don't want dispatched to the seek bar. */ override fun onTouch(view: View, event: MotionEvent): Boolean { view.parent.requestDisallowInterceptTouchEvent(true) return view.onTouchEvent(event) if (view != bar) { return false } val shouldGoToSeekBar = detector.onTouchEvent(event) return !shouldGoToSeekBar } /** * Handle down events that press down on the thumb. * * On the down action, determine a target box around the thumb to know when a scroll * gesture starts by clicking on the thumb. The target box will be used by subsequent * onScroll events. * * Returns true when the down event hits within the target box of the thumb. */ override fun onDown(event: MotionEvent): Boolean { val padL = bar.paddingLeft val padR = bar.paddingRight // Compute the X location of the thumb as a function of the seek bar progress. // TODO: account for thumb offset val progress = bar.getProgress() val range = bar.max - bar.min val widthFraction = if (range > 0) { (progress - bar.min).toDouble() / range } else { 0.0 } val availableWidth = bar.width - padL - padR val thumbX = if (bar.isLayoutRtl()) { padL + availableWidth * (1 - widthFraction) } else { padL + availableWidth * widthFraction } // Set the min, max boundaries of the thumb box. // I'm cheating by using the height of the seek bar as the width of the box. val halfHeight: Int = bar.height / 2 onDownTargetBoxMinX = (Math.round(thumbX) - halfHeight).toInt() onDownTargetBoxMaxX = (Math.round(thumbX) + halfHeight).toInt() // If the x position of the down event is within the box, then request that the parent // not intercept the event. val x = Math.round(event.x) val accept = x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX if (accept) { bar.parent?.requestDisallowInterceptTouchEvent(true) } return accept } /** * Always handle single tap up. * * This enables the user to single tap anywhere on the seek bar to seek to that position. */ override fun onSingleTapUp(event: MotionEvent) = true /** * Handle scroll events when the down event is on the thumb. * * Returns true when the down event of the scroll hits within the target box of the thumb. */ override fun onScroll( eventStart: MotionEvent, event: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { val x = Math.round(eventStart.x) return x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX } /** * Handle fling events when the down event is on the thumb. * * TODO: Ignore entire gesture when it includes a fling. * If a user is flinging, then they are probably trying to page the carousel. It would be * better to ignore the entire gesture when it includes a fling. This could be achieved by * reseting the seek bar position to where it was when the gesture started. */ override fun onFling( eventStart: MotionEvent, event: MotionEvent, velocityX: Float, velocityY: Float ): Boolean { val x = Math.round(eventStart.x) return x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX } override fun onShowPress(event: MotionEvent) {} override fun onLongPress(event: MotionEvent) {} } /** State seen by seek bar UI. */ Loading Loading
packages/SystemUI/res/layout/media_view.xml +2 −2 Original line number Diff line number Diff line Loading @@ -39,7 +39,7 @@ android:layout_alignParentLeft="true" android:fontFamily="@*android:string/config_bodyFontFamily" android:textColor="@color/media_primary_text" android:gravity="left" android:gravity="start" android:textSize="14sp" /> <TextView Loading @@ -49,7 +49,7 @@ android:layout_alignParentRight="true" android:fontFamily="@*android:string/config_bodyFontFamily" android:textColor="@color/media_primary_text" android:gravity="right" android:gravity="end" android:textSize="14sp" /> </FrameLayout> Loading
packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java +1 −4 Original line number Diff line number Diff line Loading @@ -35,7 +35,6 @@ import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.NonNull; Loading Loading @@ -165,9 +164,7 @@ public class MediaControlPanel { TransitionLayout player = vh.getPlayer(); mSeekBarObserver = new SeekBarObserver(vh); mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); SeekBar bar = vh.getSeekBar(); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); mMediaViewController.attach(player); } Loading
packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt +129 −9 Original line number Diff line number Diff line Loading @@ -20,11 +20,13 @@ import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.PlaybackState import android.os.SystemClock import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.widget.SeekBar import androidx.annotation.AnyThread import androidx.annotation.WorkerThread import androidx.core.view.GestureDetectorCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.LiveData import com.android.systemui.dagger.qualifiers.Background Loading Loading @@ -68,7 +70,6 @@ private fun PlaybackState.computePosition(duration: Long): Long { /** ViewModel for seek bar in QS media player. */ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: RepeatableExecutor) { private var _data = Progress(false, false, null, null) set(value) { field = value Loading Loading @@ -198,10 +199,10 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R return SeekBarChangeListener(this, bgExecutor) } /** Gets a listener to attach to the seek bar to disable touch intercepting. */ val seekBarTouchListener: View.OnTouchListener get() { return SeekBarTouchListener() /** Attach touch handlers to the seek bar view. */ fun attachTouchHandlers(bar: SeekBar) { bar.setOnSeekBarChangeListener(seekBarListener) bar.setOnTouchListener(SeekBarTouchListener(bar)) } private class SeekBarChangeListener( Loading @@ -225,11 +226,130 @@ class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: R } } private class SeekBarTouchListener : View.OnTouchListener { /** * Responsible for intercepting touch events before they reach the seek bar. * * This reduces the gestures seen by the seek bar so that users don't accidentially seek when * they intend to scroll the carousel. */ private class SeekBarTouchListener( private val bar: SeekBar ) : View.OnTouchListener, GestureDetector.OnGestureListener { // Gesture detector helps decide which touch events to intercept. private val detector = GestureDetectorCompat(bar.context, this) // Defines a tap target around the thumb at the beginning of a gesture. private var onDownTargetBoxMinX: Int = -1 private var onDownTargetBoxMaxX: Int = -1 /** * Decide which touch events to intercept before they reach the seek bar. * * Based on the gesture detected, we decide whether we want the event to reach the seek bar. * If we want the seek bar to see the event, then we return false so that the event isn't * handled here and it will be passed along. If, however, we don't want the seek bar to see * the event, then return true so that the event is handled here. * * When the seek bar is contained in the carousel, the carousel still has the ability to * intercept the touch event. So, even though we may handle the event here, the carousel can * still intercept the event. This way, gestures that we consider falses on the seek bar can * still be used by the carousel for paging. * * Returns true for events that we don't want dispatched to the seek bar. */ override fun onTouch(view: View, event: MotionEvent): Boolean { view.parent.requestDisallowInterceptTouchEvent(true) return view.onTouchEvent(event) if (view != bar) { return false } val shouldGoToSeekBar = detector.onTouchEvent(event) return !shouldGoToSeekBar } /** * Handle down events that press down on the thumb. * * On the down action, determine a target box around the thumb to know when a scroll * gesture starts by clicking on the thumb. The target box will be used by subsequent * onScroll events. * * Returns true when the down event hits within the target box of the thumb. */ override fun onDown(event: MotionEvent): Boolean { val padL = bar.paddingLeft val padR = bar.paddingRight // Compute the X location of the thumb as a function of the seek bar progress. // TODO: account for thumb offset val progress = bar.getProgress() val range = bar.max - bar.min val widthFraction = if (range > 0) { (progress - bar.min).toDouble() / range } else { 0.0 } val availableWidth = bar.width - padL - padR val thumbX = if (bar.isLayoutRtl()) { padL + availableWidth * (1 - widthFraction) } else { padL + availableWidth * widthFraction } // Set the min, max boundaries of the thumb box. // I'm cheating by using the height of the seek bar as the width of the box. val halfHeight: Int = bar.height / 2 onDownTargetBoxMinX = (Math.round(thumbX) - halfHeight).toInt() onDownTargetBoxMaxX = (Math.round(thumbX) + halfHeight).toInt() // If the x position of the down event is within the box, then request that the parent // not intercept the event. val x = Math.round(event.x) val accept = x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX if (accept) { bar.parent?.requestDisallowInterceptTouchEvent(true) } return accept } /** * Always handle single tap up. * * This enables the user to single tap anywhere on the seek bar to seek to that position. */ override fun onSingleTapUp(event: MotionEvent) = true /** * Handle scroll events when the down event is on the thumb. * * Returns true when the down event of the scroll hits within the target box of the thumb. */ override fun onScroll( eventStart: MotionEvent, event: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { val x = Math.round(eventStart.x) return x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX } /** * Handle fling events when the down event is on the thumb. * * TODO: Ignore entire gesture when it includes a fling. * If a user is flinging, then they are probably trying to page the carousel. It would be * better to ignore the entire gesture when it includes a fling. This could be achieved by * reseting the seek bar position to where it was when the gesture started. */ override fun onFling( eventStart: MotionEvent, event: MotionEvent, velocityX: Float, velocityY: Float ): Boolean { val x = Math.round(eventStart.x) return x >= onDownTargetBoxMinX && x <= onDownTargetBoxMaxX } override fun onShowPress(event: MotionEvent) {} override fun onLongPress(event: MotionEvent) {} } /** State seen by seek bar UI. */ Loading