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:
TreeHugger Robot
2020-06-12 05:25:11 +00:00
committed by Automerger Merge Worker
3 changed files with 132 additions and 15 deletions

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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. */