From 52dfb62937831295761181131e593e9deaab16c8 Mon Sep 17 00:00:00 2001 From: Robert Snoeberger Date: Thu, 21 May 2020 16:31:29 -0400 Subject: [PATCH] Paged carousel for media players This is a stopgap implementation on top of HorizontalScrollView. It will take much more work to move to a ViewPager2 implementation. In the meantime, this at least adds a page indicator to that users will realize that there are multiple pages. Added in this change: - page indicator - fling to next page - centering current page when scrolled into Bug: 153170704 Test: manual - Paged between three players in the carousel. Verify that the page indicator showed correct page. Test: manual - Scoll into player and release. Verify that page snaps back to center. Change-Id: Iae51d9136184a0a7de083177ea105f2db9a72c99 --- .../SystemUI/res/layout/media_carousel.xml | 35 +++++-- .../systemui/media/MediaHierarchyManager.kt | 4 +- .../systemui/media/MediaViewManager.kt | 92 ++++++++++++++++++- .../android/systemui/qs/PageIndicator.java | 13 ++- 4 files changed, 125 insertions(+), 19 deletions(-) diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index 03e74676f03e4..dc917316bef1b 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -16,22 +16,39 @@ --> - - - - - + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 3e7661ac0dd91..cce1d3efd6e54 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -58,7 +58,7 @@ class MediaHierarchyManager @Inject constructor( private var rootOverlay: ViewGroupOverlay? = null private lateinit var currentState: MediaState private val mediaCarousel - get() = mediaViewManager.mediaCarousel + get() = mediaViewManager.mediaFrame private var animationStartState: MediaState? = null private var statusbarState: Int = statusBarStateController.state private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { @@ -424,4 +424,4 @@ class MediaHierarchyManager @Inject constructor( */ const val IN_OVERLAY = -1000 } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt index 17e8404fe705f..16302d1632b5b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt @@ -1,15 +1,20 @@ package com.android.systemui.media import android.content.Context +import android.graphics.Color import android.view.LayoutInflater +import android.view.GestureDetector +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout +import androidx.core.view.GestureDetectorCompat import com.android.systemui.R import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.qs.PageIndicator import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.util.animation.MeasurementOutput import com.android.systemui.util.animation.UniqueObjectHostView @@ -18,6 +23,8 @@ import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Singleton +private const val FLING_SLOP = 1000000 + /** * Class that is responsible for keeping the view carousel up to date. * This also handles changes in state and applies them to the media carousel like the expansion. @@ -35,9 +42,12 @@ class MediaViewManager @Inject constructor( private var playerWidthPlusPadding: Int = 0 private var desiredState: MediaHost.MediaHostState? = null private var currentState: MediaState? = null - val mediaCarousel: HorizontalScrollView + private val mediaCarousel: HorizontalScrollView + val mediaFrame: ViewGroup private val mediaContent: ViewGroup private val mediaPlayers: MutableMap = mutableMapOf() + private val pageIndicator: PageIndicator + private val gestureDetector: GestureDetectorCompat private val visualStabilityCallback: VisualStabilityManager.Callback private var activeMediaIndex: Int = 0 private var needsReordering: Boolean = false @@ -66,10 +76,30 @@ class MediaViewManager @Inject constructor( scrollX % playerWidthPlusPadding) } } + private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { + override fun onFling( + eStart: MotionEvent?, + eCurrent: MotionEvent?, + vX: Float, + vY: Float + ): Boolean { + return this@MediaViewManager.onFling(eStart, eCurrent, vX, vY) + } + } + private val touchListener = object : View.OnTouchListener { + override fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { + return this@MediaViewManager.onTouch(view, motionEvent) + } + } init { - mediaCarousel = inflateMediaCarousel() + gestureDetector = GestureDetectorCompat(context, gestureListener) + mediaFrame = inflateMediaCarousel() + mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) + pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) mediaCarousel.setOnScrollChangeListener(scrollChangedListener) + mediaCarousel.setOnTouchListener(touchListener) + mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER) mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) visualStabilityCallback = VisualStabilityManager.Callback { if (needsReordering) { @@ -103,14 +133,15 @@ class MediaViewManager @Inject constructor( playerWidthPlusPadding, 0) } updatePlayerVisibilities() + updatePageIndicator() } } }) } - private fun inflateMediaCarousel(): HorizontalScrollView { + private fun inflateMediaCarousel(): ViewGroup { return LayoutInflater.from(context).inflate(R.layout.media_carousel, - UniqueObjectHostView(context), false) as HorizontalScrollView + UniqueObjectHostView(context), false) as ViewGroup } private fun reorderAllPlayers() { @@ -133,6 +164,47 @@ class MediaViewManager @Inject constructor( activeMediaIndex = newIndex updatePlayerVisibilities() } + val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) + scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + pageIndicator.setLocation(location) + } + + private fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { + if (gestureDetector.onTouchEvent(motionEvent)) { + return true + } + if (motionEvent?.getAction() == MotionEvent.ACTION_UP) { + val pos = mediaCarousel.scrollX % playerWidthPlusPadding + if (pos > playerWidthPlusPadding / 2) { + mediaCarousel.smoothScrollBy(playerWidthPlusPadding - pos, 0) + } else { + mediaCarousel.smoothScrollBy(-1 * pos, 0) + } + return true + } + return view.onTouchEvent(motionEvent) + } + + private fun onFling( + eStart: MotionEvent?, + eCurrent: MotionEvent?, + vX: Float, + vY: Float + ): Boolean { + if (vX * vX < 0.5 * vY * vY) { + return false + } + if (vX * vX < FLING_SLOP) { + return false + } + val pos = mediaCarousel.scrollX + val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 + var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex + destIndex = Math.max(0, destIndex) + destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) + val view = mediaContent.getChildAt(destIndex) + mediaCarousel.smoothScrollTo(view.left, mediaCarousel.scrollY) + return true } private fun updatePlayerVisibilities() { @@ -176,6 +248,7 @@ class MediaViewManager @Inject constructor( // motion model existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f updateMediaPaddings() + updatePageIndicator() } private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) { @@ -199,6 +272,14 @@ class MediaViewManager @Inject constructor( } } + private fun updatePageIndicator() { + val numPages = mediaContent.getChildCount() + pageIndicator.setNumPages(numPages, Color.WHITE) + if (numPages == 1) { + pageIndicator.setLocation(0f) + } + } + /** * Set the current state of a view. This is updated often during animations and we shouldn't * do anything expensive. @@ -206,6 +287,9 @@ class MediaViewManager @Inject constructor( fun setCurrentState(state: MediaState) { currentState = state currentlyExpanded = state.expansion > 0 + // Hack: Since the indicator doesn't move with the player expansion, just make it disappear + // and then reappear at the end. + pageIndicator.alpha = if (state.expansion == 1f || state.expansion == 0f) 1f else 0f for (mediaPlayer in mediaPlayers.values) { val view = mediaPlayer.view?.player view?.progress = state.expansion diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java index d5c5ba401e997..69337f988080c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java @@ -45,6 +45,15 @@ public class PageIndicator extends ViewGroup { } public void setNumPages(int numPages) { + TypedArray array = getContext().obtainStyledAttributes( + new int[]{android.R.attr.colorControlActivated}); + int color = array.getColor(0, 0); + array.recycle(); + setNumPages(numPages, color); + } + + /** Oveload of setNumPages that allows the indicator color to be specified.*/ + public void setNumPages(int numPages, int color) { setVisibility(numPages > 1 ? View.VISIBLE : View.GONE); if (mAnimating) { Log.w(TAG, "setNumPages during animation"); @@ -52,10 +61,6 @@ public class PageIndicator extends ViewGroup { while (numPages < getChildCount()) { removeViewAt(getChildCount() - 1); } - TypedArray array = getContext().obtainStyledAttributes( - new int[]{android.R.attr.colorControlActivated}); - int color = array.getColor(0, 0); - array.recycle(); while (numPages > getChildCount()) { ImageView v = new ImageView(mContext); v.setImageResource(R.drawable.minor_a_b);