Merge "Help reduce falsing on the seek bar" into rvc-dev am: 70bec8cc36 am: 89a5c51eca am: a9e2502c8c
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11702457 Change-Id: I9edab0977a47de42d7a83fd9c680e54ca018c8c4
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -198,11 +199,11 @@ 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(
|
||||
val viewModel: SeekBarViewModel,
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user