Merge "Fix MediaCarousel in RTL" into rvc-dev am: b01bd6a560

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11988335

Change-Id: I2df164d6b9c7308a245cdb95837a92c1bc8f2b86
This commit is contained in:
TreeHugger Robot
2020-06-25 21:39:12 +00:00
committed by Automerger Merge Worker
3 changed files with 142 additions and 49 deletions

View File

@@ -2,6 +2,7 @@ package com.android.systemui.media
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -96,7 +97,6 @@ class MediaCarouselController @Inject constructor(
* The measured height of the carousel * The measured height of the carousel
*/ */
private var carouselMeasureHeight: Int = 0 private var carouselMeasureHeight: Int = 0
private var playerWidthPlusPadding: Int = 0
private var desiredHostState: MediaHostState? = null private var desiredHostState: MediaHostState? = null
private val mediaCarousel: MediaScrollView private val mediaCarousel: MediaScrollView
private val mediaCarouselScrollHandler: MediaCarouselScrollHandler private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
@@ -108,6 +108,15 @@ class MediaCarouselController @Inject constructor(
private val pageIndicator: PageIndicator private val pageIndicator: PageIndicator
private val visualStabilityCallback: VisualStabilityManager.Callback private val visualStabilityCallback: VisualStabilityManager.Callback
private var needsReordering: Boolean = false private var needsReordering: Boolean = false
private var isRtl: Boolean = false
set(value) {
if (value != field) {
field = value
mediaFrame.layoutDirection =
if (value) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
mediaCarouselScrollHandler.scrollToStart()
}
}
private var currentlyExpanded = true private var currentlyExpanded = true
set(value) { set(value) {
if (field != value) { if (field != value) {
@@ -126,6 +135,11 @@ class MediaCarouselController @Inject constructor(
override fun onOverlayChanged() { override fun onOverlayChanged() {
inflateSettingsButton() inflateSettingsButton()
} }
override fun onConfigChanged(newConfig: Configuration?) {
if (newConfig == null) return
isRtl = newConfig.layoutDirection == View.LAYOUT_DIRECTION_RTL
}
} }
init { init {
@@ -135,6 +149,7 @@ class MediaCarouselController @Inject constructor(
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
falsingManager) falsingManager)
isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
inflateSettingsButton() inflateSettingsButton()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
configurationController.addCallback(configListener) configurationController.addCallback(configListener)
@@ -144,7 +159,7 @@ class MediaCarouselController @Inject constructor(
reorderAllPlayers() reorderAllPlayers()
} }
// Let's reset our scroll position // Let's reset our scroll position
mediaCarousel.scrollX = 0 mediaCarouselScrollHandler.scrollToStart()
} }
visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
true /* persistent */) true /* persistent */)
@@ -196,8 +211,13 @@ class MediaCarouselController @Inject constructor(
} }
private fun inflateMediaCarousel(): ViewGroup { private fun inflateMediaCarousel(): ViewGroup {
return LayoutInflater.from(context).inflate(R.layout.media_carousel, val mediaCarousel = LayoutInflater.from(context).inflate(R.layout.media_carousel,
UniqueObjectHostView(context), false) as ViewGroup UniqueObjectHostView(context), false) as ViewGroup
// Because this is inflated when not attached to the true view hierarchy, it resolves some
// potential issues to force that the layout direction is defined by the locale
// (rather than inherited from the parent, which would resolve to LTR when unattached).
mediaCarousel.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
return mediaCarousel
} }
private fun reorderAllPlayers() { private fun reorderAllPlayers() {
@@ -313,8 +333,12 @@ class MediaCarouselController @Inject constructor(
private fun updatePageIndicatorLocation() { private fun updatePageIndicatorLocation() {
// Update the location of the page indicator, carousel clipping // Update the location of the page indicator, carousel clipping
pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f + val translationX = if (isRtl) {
mediaCarouselScrollHandler.contentTranslation (pageIndicator.width - currentCarouselWidth) / 2.0f
} else {
(currentCarouselWidth - pageIndicator.width) / 2.0f
}
pageIndicator.translationX = translationX + mediaCarouselScrollHandler.contentTranslation
val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
layoutParams.bottomMargin).toFloat() layoutParams.bottomMargin).toFloat()
@@ -334,7 +358,8 @@ class MediaCarouselController @Inject constructor(
if (width != currentCarouselWidth || height != currentCarouselHeight) { if (width != currentCarouselWidth || height != currentCarouselHeight) {
currentCarouselWidth = width currentCarouselWidth = width
currentCarouselHeight = height currentCarouselHeight = height
mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight) mediaCarouselScrollHandler.setCarouselBounds(
currentCarouselWidth, currentCarouselHeight)
updatePageIndicatorLocation() updatePageIndicatorLocation()
} }
} }
@@ -348,7 +373,7 @@ class MediaCarouselController @Inject constructor(
if (currentlyShowingOnlyActive != endShowsActive || if (currentlyShowingOnlyActive != endShowsActive ||
((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
startShowsActive != endShowsActive)) { startShowsActive != endShowsActive)) {
/// Whenever we're transitioning from between differing states or the endstate differs // Whenever we're transitioning from between differing states or the endstate differs
// we reset the translation // we reset the translation
currentlyShowingOnlyActive = endShowsActive currentlyShowingOnlyActive = endShowsActive
mediaCarouselScrollHandler.resetTranslation(animate = true) mediaCarouselScrollHandler.resetTranslation(animate = true)
@@ -416,14 +441,15 @@ class MediaCarouselController @Inject constructor(
height != carouselMeasureWidth && height != 0) { height != carouselMeasureWidth && height != 0) {
carouselMeasureWidth = width carouselMeasureWidth = width
carouselMeasureHeight = height carouselMeasureHeight = height
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( val playerWidthPlusPadding = carouselMeasureWidth +
R.dimen.qs_media_padding) context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
// Let's remeasure the carousel // Let's remeasure the carousel
val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
mediaCarousel.measure(widthSpec, heightSpec) mediaCarousel.measure(widthSpec, heightSpec)
mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
// Update the padding after layout; view widths are used in RTL to calculate scrollX
mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
} }
} }
} }

View File

@@ -58,6 +58,10 @@ class MediaCarouselScrollHandler(
private var translationChangedListener: () -> Unit, private var translationChangedListener: () -> Unit,
private val falsingManager: FalsingManager private val falsingManager: FalsingManager
) { ) {
/**
* Is the view in RTL
*/
val isRtl: Boolean get() = scrollView.isLayoutRtl
/** /**
* Do we need falsing protection? * Do we need falsing protection?
*/ */
@@ -121,14 +125,14 @@ class MediaCarouselScrollHandler(
field = value field = value
// The player width has changed, let's update the scroll position to make sure // The player width has changed, let's update the scroll position to make sure
// it's still at the same place // it's still at the same place
var newScroll = activeMediaIndex * playerWidthPlusPadding var newRelativeScroll = activeMediaIndex * playerWidthPlusPadding
if (scrollIntoCurrentMedia > playerWidthPlusPadding) { if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
newScroll += playerWidthPlusPadding - newRelativeScroll += playerWidthPlusPadding -
(scrollIntoCurrentMedia - playerWidthPlusPadding) (scrollIntoCurrentMedia - playerWidthPlusPadding)
} else { } else {
newScroll += scrollIntoCurrentMedia newRelativeScroll += scrollIntoCurrentMedia
} }
scrollView.scrollX = newScroll scrollView.relativeScrollX = newRelativeScroll
} }
/** /**
@@ -184,8 +188,9 @@ class MediaCarouselScrollHandler(
if (playerWidthPlusPadding == 0) { if (playerWidthPlusPadding == 0) {
return return
} }
onMediaScrollingChanged(scrollX / playerWidthPlusPadding, val relativeScrollX = scrollView.relativeScrollX
scrollX % playerWidthPlusPadding) onMediaScrollingChanged(relativeScrollX / playerWidthPlusPadding,
relativeScrollX % playerWidthPlusPadding)
} }
} }
@@ -222,11 +227,19 @@ class MediaCarouselScrollHandler(
Math.abs(contentTranslation)) Math.abs(contentTranslation))
val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width *
SETTINGS_BUTTON_TRANSLATION_FRACTION SETTINGS_BUTTON_TRANSLATION_FRACTION
val newTranslationX: Float val newTranslationX = if (isRtl) {
// In RTL, the 0-placement is on the right side of the view, not the left...
if (contentTranslation > 0) { if (contentTranslation > 0) {
newTranslationX = settingsTranslation -(scrollView.width - settingsTranslation - settingsButton.width)
} else { } else {
newTranslationX = scrollView.width - settingsTranslation - settingsButton.width -settingsTranslation
}
} else {
if (contentTranslation > 0) {
settingsTranslation
} else {
scrollView.width - settingsTranslation - settingsButton.width
}
} }
val rotation = (1.0f - settingsOffset) * 50 val rotation = (1.0f - settingsOffset) * 50
settingsButton.rotation = rotation * -Math.signum(contentTranslation) settingsButton.rotation = rotation * -Math.signum(contentTranslation)
@@ -259,26 +272,26 @@ class MediaCarouselScrollHandler(
} }
if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
// It's an up and the fling didn't take it above // It's an up and the fling didn't take it above
val pos = scrollView.scrollX % playerWidthPlusPadding val relativePos = scrollView.relativeScrollX % playerWidthPlusPadding
val scollXAmount: Int val scrollXAmount: Int
if (pos > playerWidthPlusPadding / 2) { if (relativePos > playerWidthPlusPadding / 2) {
scollXAmount = playerWidthPlusPadding - pos scrollXAmount = playerWidthPlusPadding - relativePos
} else { } else {
scollXAmount = -1 * pos scrollXAmount = -1 * relativePos
} }
if (scollXAmount != 0) { if (scrollXAmount != 0) {
// Delay the scrolling since scrollView calls springback which cancels // Delay the scrolling since scrollView calls springback which cancels
// the animation again.. // the animation again..
mainExecutor.execute { mainExecutor.execute {
scrollView.smoothScrollBy(scollXAmount, 0) scrollView.smoothScrollBy(if (isRtl) -scrollXAmount else scrollXAmount, 0)
} }
} }
val currentTranslation = scrollView.getContentTranslation() val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f) { if (currentTranslation != 0.0f) {
// We started a Swipe but didn't end up with a fling. Let's either go to the // We started a Swipe but didn't end up with a fling. Let's either go to the
// dismissed position or go back. // dismissed position or go back.
val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 ||
|| isFalseTouch() isFalseTouch()
val newTranslation: Float val newTranslation: Float
if (springBack) { if (springBack) {
newTranslation = 0.0f newTranslation = 0.0f
@@ -313,9 +326,11 @@ class MediaCarouselScrollHandler(
return gestureDetector.onTouchEvent(motionEvent) return gestureDetector.onTouchEvent(motionEvent)
} }
fun onScroll(down: MotionEvent, fun onScroll(
down: MotionEvent,
lastMotion: MotionEvent, lastMotion: MotionEvent,
distanceX: Float): Boolean { distanceX: Float
): Boolean {
val totalX = lastMotion.x - down.x val totalX = lastMotion.x - down.x
val currentTranslation = scrollView.getContentTranslation() val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f || if (currentTranslation != 0.0f ||
@@ -339,8 +354,8 @@ class MediaCarouselScrollHandler(
} // Otherwise we don't have do do anything, and will remove the unrubberbanded } // Otherwise we don't have do do anything, and will remove the unrubberbanded
// translation // translation
} }
if (Math.signum(newTranslation) != Math.signum(currentTranslation) if (Math.signum(newTranslation) != Math.signum(currentTranslation) &&
&& currentTranslation != 0.0f) { currentTranslation != 0.0f) {
// We crossed the 0.0 threshold of the translation. Let's see if we're allowed // We crossed the 0.0 threshold of the translation. Let's see if we're allowed
// to scroll into the new direction // to scroll into the new direction
if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
@@ -394,9 +409,10 @@ class MediaCarouselScrollHandler(
scrollView.animationTargetX = newTranslation scrollView.animationTargetX = newTranslation
} else { } else {
// We're flinging the player! Let's go either to the previous or to the next player // We're flinging the player! Let's go either to the previous or to the next player
val pos = scrollView.scrollX val pos = scrollView.relativeScrollX
val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex val flungTowardEnd = if (isRtl) vX > 0 else vX < 0
var destIndex = if (flungTowardEnd) currentIndex + 1 else currentIndex
destIndex = Math.max(0, destIndex) destIndex = Math.max(0, destIndex)
destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
val view = mediaContent.getChildAt(destIndex) val view = mediaContent.getChildAt(destIndex)
@@ -438,8 +454,14 @@ class MediaCarouselScrollHandler(
activeMediaIndex = newIndex activeMediaIndex = newIndex
updatePlayerVisibilities() updatePlayerVisibilities()
} }
val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
scrollInAmount.toFloat() / playerWidthPlusPadding else 0f scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
// Fix the location, because PageIndicator does not handle RTL internally
val location = if (isRtl) {
mediaContent.childCount - relativeLocation - 1
} else {
relativeLocation
}
pageIndicator.setLocation(location) pageIndicator.setLocation(location)
updateClipToOutline() updateClipToOutline()
} }
@@ -480,13 +502,20 @@ class MediaCarouselScrollHandler(
* where it was and update our scroll position. * where it was and update our scroll position.
*/ */
fun onPrePlayerRemoved(removed: MediaControlPanel) { fun onPrePlayerRemoved(removed: MediaControlPanel) {
val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex val removedIndex = mediaContent.indexOfChild(removed.view?.player)
// If the removed index is less than the activeMediaIndex, then we need to decrement it.
// RTL has no effect on this, because indices are always relative (start-to-end).
// Update the index 'manually' since we won't always get a call to onMediaScrollingChanged
val beforeActive = removedIndex <= activeMediaIndex
if (beforeActive) { if (beforeActive) {
// also update the index here since the scroll below might not always lead
// to a scrolling changed
activeMediaIndex = Math.max(0, activeMediaIndex - 1) activeMediaIndex = Math.max(0, activeMediaIndex - 1)
scrollView.scrollX = Math.max(scrollView.scrollX - }
playerWidthPlusPadding, 0) // If the removed media item is "left of" the active one (in an absolute sense), we need to
// scroll the view to keep that player in view. This is because scroll position is always
// calculated from left to right.
val leftOfActive = if (isRtl) !beforeActive else beforeActive
if (leftOfActive) {
scrollView.scrollX = Math.max(scrollView.scrollX - playerWidthPlusPadding, 0)
} }
} }
@@ -501,6 +530,13 @@ class MediaCarouselScrollHandler(
} }
} }
/**
* Reset the MediaScrollView to the start.
*/
fun scrollToStart() {
scrollView.relativeScrollX = 0
}
companion object { companion object {
private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>(
"contentTranslation") { "contentTranslation") {

View File

@@ -15,7 +15,10 @@ import com.android.systemui.util.animation.physicsAnimator
* when only measuring children but not the parent, when trying to apply a new scroll position * when only measuring children but not the parent, when trying to apply a new scroll position
*/ */
class MediaScrollView @JvmOverloads constructor( class MediaScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
)
: HorizontalScrollView(context, attrs, defStyleAttr) { : HorizontalScrollView(context, attrs, defStyleAttr) {
lateinit var contentContainer: ViewGroup lateinit var contentContainer: ViewGroup
@@ -37,6 +40,26 @@ class MediaScrollView @JvmOverloads constructor(
contentContainer.translationX contentContainer.translationX
} }
/**
* Convert between the absolute (left-to-right) and relative (start-to-end) scrollX of the media
* carousel. The player indices are always relative (start-to-end) and the scrollView.scrollX
* is always absolute. This function is its own inverse.
*/
private fun transformScrollX(scrollX: Int): Int = if (isLayoutRtl) {
contentContainer.width - width - scrollX
} else {
scrollX
}
/**
* Get the layoutDirection-relative (start-to-end) scroll X position of the carousel.
*/
var relativeScrollX: Int
get() = transformScrollX(scrollX)
set(value) {
scrollX = transformScrollX(value)
}
/** /**
* Allow all scrolls to go through, use base implementation * Allow all scrolls to go through, use base implementation
*/ */
@@ -55,15 +78,15 @@ class MediaScrollView @JvmOverloads constructor(
} }
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false; var intercept = false
touchListener?.let { touchListener?.let {
intercept = it.onInterceptTouchEvent(ev) intercept = it.onInterceptTouchEvent(ev)
} }
return super.onInterceptTouchEvent(ev) || intercept; return super.onInterceptTouchEvent(ev) || intercept
} }
override fun onTouchEvent(ev: MotionEvent?): Boolean { override fun onTouchEvent(ev: MotionEvent?): Boolean {
var touch = false; var touch = false
touchListener?.let { touchListener?.let {
touch = it.onTouchEvent(ev) touch = it.onTouchEvent(ev)
} }
@@ -75,9 +98,17 @@ class MediaScrollView @JvmOverloads constructor(
contentContainer = getChildAt(0) as ViewGroup contentContainer = getChildAt(0) as ViewGroup
} }
override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int, override fun overScrollBy(
scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int, deltaX: Int,
maxOverScrollY: Int, isTouchEvent: Boolean): Boolean { deltaY: Int,
scrollX: Int,
scrollY: Int,
scrollRangeX: Int,
scrollRangeY: Int,
maxOverScrollX: Int,
maxOverScrollY: Int,
isTouchEvent: Boolean
): Boolean {
if (getContentTranslation() != 0.0f) { if (getContentTranslation() != 0.0f) {
// When we're dismissing we ignore all the scrolling // When we're dismissing we ignore all the scrolling
return false return false