Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 1 | package com.android.systemui.media |
| 2 | |
| 3 | import android.content.Context |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 4 | import android.graphics.Color |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 5 | import android.view.LayoutInflater |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 6 | import android.view.GestureDetector |
| 7 | import android.view.MotionEvent |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 8 | import android.view.View |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 9 | import android.view.ViewGroup |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 10 | import android.widget.HorizontalScrollView |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 11 | import android.widget.LinearLayout |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 12 | import androidx.core.view.GestureDetectorCompat |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 13 | import com.android.systemui.R |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 14 | import com.android.systemui.qs.PageIndicator |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 15 | import com.android.systemui.statusbar.notification.VisualStabilityManager |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 16 | import com.android.systemui.statusbar.policy.ConfigurationController |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 17 | import com.android.systemui.util.animation.UniqueObjectHostView |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 18 | import com.android.systemui.util.animation.requiresRemeasuring |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 19 | import javax.inject.Inject |
Robert Snoeberger | 7cec542 | 2020-05-29 17:09:14 -0400 | [diff] [blame] | 20 | import javax.inject.Provider |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 21 | import javax.inject.Singleton |
| 22 | |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 23 | private const val FLING_SLOP = 1000000 |
| 24 | |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 25 | /** |
| 26 | * Class that is responsible for keeping the view carousel up to date. |
| 27 | * This also handles changes in state and applies them to the media carousel like the expansion. |
| 28 | */ |
| 29 | @Singleton |
| 30 | class MediaViewManager @Inject constructor( |
| 31 | private val context: Context, |
Robert Snoeberger | 7cec542 | 2020-05-29 17:09:14 -0400 | [diff] [blame] | 32 | private val mediaControlPanelFactory: Provider<MediaControlPanel>, |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 33 | private val visualStabilityManager: VisualStabilityManager, |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 34 | private val mediaHostStatesManager: MediaHostStatesManager, |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 35 | mediaManager: MediaDataCombineLatest, |
| 36 | configurationController: ConfigurationController |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 37 | ) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 38 | |
| 39 | /** |
| 40 | * The desired location where we'll be at the end of the transformation. Usually this matches |
| 41 | * the end location, except when we're still waiting on a state update call. |
| 42 | */ |
| 43 | @MediaLocation |
| 44 | private var desiredLocation: Int = -1 |
| 45 | |
| 46 | /** |
| 47 | * The ending location of the view where it ends when all animations and transitions have |
| 48 | * finished |
| 49 | */ |
| 50 | @MediaLocation |
| 51 | private var currentEndLocation: Int = -1 |
| 52 | |
| 53 | /** |
| 54 | * The ending location of the view where it ends when all animations and transitions have |
| 55 | * finished |
| 56 | */ |
| 57 | @MediaLocation |
| 58 | private var currentStartLocation: Int = -1 |
| 59 | |
| 60 | /** |
| 61 | * The progress of the transition or 1.0 if there is no transition happening |
| 62 | */ |
| 63 | private var currentTransitionProgress: Float = 1.0f |
| 64 | |
| 65 | /** |
| 66 | * The measured width of the carousel |
| 67 | */ |
| 68 | private var carouselMeasureWidth: Int = 0 |
| 69 | |
| 70 | /** |
| 71 | * The measured height of the carousel |
| 72 | */ |
| 73 | private var carouselMeasureHeight: Int = 0 |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 74 | private var playerWidthPlusPadding: Int = 0 |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 75 | private var desiredHostState: MediaHostState? = null |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 76 | private val mediaCarousel: HorizontalScrollView |
| 77 | val mediaFrame: ViewGroup |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 78 | val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 79 | private val mediaData: MutableMap<String, MediaData> = mutableMapOf() |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 80 | private val mediaContent: ViewGroup |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 81 | private val pageIndicator: PageIndicator |
| 82 | private val gestureDetector: GestureDetectorCompat |
Robert Snoeberger | 70d0d6b | 2020-05-14 16:47:02 -0400 | [diff] [blame] | 83 | private val visualStabilityCallback: VisualStabilityManager.Callback |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 84 | private var activeMediaIndex: Int = 0 |
Selim Cinek | 0260c75 | 2020-05-11 16:03:52 -0700 | [diff] [blame] | 85 | private var needsReordering: Boolean = false |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 86 | private var scrollIntoCurrentMedia: Int = 0 |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 87 | private var currentlyExpanded = true |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 88 | set(value) { |
| 89 | if (field != value) { |
| 90 | field = value |
| 91 | for (player in mediaPlayers.values) { |
| 92 | player.setListening(field) |
| 93 | } |
| 94 | } |
| 95 | } |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 96 | private val scrollChangedListener = object : View.OnScrollChangeListener { |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 97 | override fun onScrollChange( |
| 98 | v: View?, |
| 99 | scrollX: Int, |
| 100 | scrollY: Int, |
| 101 | oldScrollX: Int, |
| 102 | oldScrollY: Int |
| 103 | ) { |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 104 | if (playerWidthPlusPadding == 0) { |
| 105 | return |
| 106 | } |
| 107 | onMediaScrollingChanged(scrollX / playerWidthPlusPadding, |
| 108 | scrollX % playerWidthPlusPadding) |
| 109 | } |
| 110 | } |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 111 | private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { |
| 112 | override fun onFling( |
| 113 | eStart: MotionEvent?, |
| 114 | eCurrent: MotionEvent?, |
| 115 | vX: Float, |
| 116 | vY: Float |
| 117 | ): Boolean { |
| 118 | return this@MediaViewManager.onFling(eStart, eCurrent, vX, vY) |
| 119 | } |
| 120 | } |
| 121 | private val touchListener = object : View.OnTouchListener { |
| 122 | override fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { |
| 123 | return this@MediaViewManager.onTouch(view, motionEvent) |
| 124 | } |
| 125 | } |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 126 | private val configListener = object : ConfigurationController.ConfigurationListener { |
| 127 | override fun onDensityOrFontScaleChanged() { |
| 128 | recreatePlayers() |
| 129 | } |
| 130 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 131 | |
| 132 | init { |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 133 | gestureDetector = GestureDetectorCompat(context, gestureListener) |
| 134 | mediaFrame = inflateMediaCarousel() |
| 135 | mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) |
| 136 | pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 137 | mediaCarousel.setOnScrollChangeListener(scrollChangedListener) |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 138 | mediaCarousel.setOnTouchListener(touchListener) |
| 139 | mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 140 | mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 141 | configurationController.addCallback(configListener) |
Selim Cinek | 0260c75 | 2020-05-11 16:03:52 -0700 | [diff] [blame] | 142 | visualStabilityCallback = VisualStabilityManager.Callback { |
| 143 | if (needsReordering) { |
| 144 | needsReordering = false |
| 145 | reorderAllPlayers() |
| 146 | } |
| 147 | // Let's reset our scroll position |
| 148 | mediaCarousel.scrollX = 0 |
| 149 | } |
| 150 | visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback, |
| 151 | true /* persistent */) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 152 | mediaManager.addListener(object : MediaDataManager.Listener { |
Beth Thibodeau | f55bc6a | 2020-05-20 02:01:31 -0400 | [diff] [blame] | 153 | override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) { |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 154 | oldKey?.let { mediaData.remove(it) } |
| 155 | mediaData.put(key, data) |
| 156 | addOrUpdatePlayer(key, oldKey, data) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 157 | } |
| 158 | |
| 159 | override fun onMediaDataRemoved(key: String) { |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 160 | mediaData.remove(key) |
| 161 | removePlayer(key) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 162 | } |
| 163 | }) |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 164 | mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { |
| 165 | override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { |
| 166 | if (location == desiredLocation) { |
| 167 | onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false) |
| 168 | } |
| 169 | } |
| 170 | }) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 171 | } |
| 172 | |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 173 | private fun inflateMediaCarousel(): ViewGroup { |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 174 | return LayoutInflater.from(context).inflate(R.layout.media_carousel, |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 175 | UniqueObjectHostView(context), false) as ViewGroup |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 176 | } |
| 177 | |
| 178 | private fun reorderAllPlayers() { |
| 179 | for (mediaPlayer in mediaPlayers.values) { |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 180 | val view = mediaPlayer.view?.player |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 181 | if (mediaPlayer.isPlaying && mediaContent.indexOfChild(view) != 0) { |
| 182 | mediaContent.removeView(view) |
| 183 | mediaContent.addView(view, 0) |
| 184 | } |
| 185 | } |
| 186 | updateMediaPaddings() |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 187 | updatePlayerVisibilities() |
| 188 | } |
| 189 | |
| 190 | private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { |
| 191 | val wasScrolledIn = scrollIntoCurrentMedia != 0 |
| 192 | scrollIntoCurrentMedia = scrollInAmount |
| 193 | val nowScrolledIn = scrollIntoCurrentMedia != 0 |
| 194 | if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { |
| 195 | activeMediaIndex = newIndex |
| 196 | updatePlayerVisibilities() |
| 197 | } |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 198 | val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) |
| 199 | scrollInAmount.toFloat() / playerWidthPlusPadding else 0f |
| 200 | pageIndicator.setLocation(location) |
| 201 | } |
| 202 | |
| 203 | private fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { |
| 204 | if (gestureDetector.onTouchEvent(motionEvent)) { |
| 205 | return true |
| 206 | } |
| 207 | if (motionEvent?.getAction() == MotionEvent.ACTION_UP) { |
| 208 | val pos = mediaCarousel.scrollX % playerWidthPlusPadding |
| 209 | if (pos > playerWidthPlusPadding / 2) { |
| 210 | mediaCarousel.smoothScrollBy(playerWidthPlusPadding - pos, 0) |
| 211 | } else { |
| 212 | mediaCarousel.smoothScrollBy(-1 * pos, 0) |
| 213 | } |
| 214 | return true |
| 215 | } |
| 216 | return view.onTouchEvent(motionEvent) |
| 217 | } |
| 218 | |
| 219 | private fun onFling( |
| 220 | eStart: MotionEvent?, |
| 221 | eCurrent: MotionEvent?, |
| 222 | vX: Float, |
| 223 | vY: Float |
| 224 | ): Boolean { |
| 225 | if (vX * vX < 0.5 * vY * vY) { |
| 226 | return false |
| 227 | } |
| 228 | if (vX * vX < FLING_SLOP) { |
| 229 | return false |
| 230 | } |
| 231 | val pos = mediaCarousel.scrollX |
| 232 | val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 |
| 233 | var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex |
| 234 | destIndex = Math.max(0, destIndex) |
| 235 | destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) |
| 236 | val view = mediaContent.getChildAt(destIndex) |
| 237 | mediaCarousel.smoothScrollTo(view.left, mediaCarousel.scrollY) |
| 238 | return true |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 239 | } |
| 240 | |
| 241 | private fun updatePlayerVisibilities() { |
| 242 | val scrolledIn = scrollIntoCurrentMedia != 0 |
| 243 | for (i in 0 until mediaContent.childCount) { |
| 244 | val view = mediaContent.getChildAt(i) |
| 245 | val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) |
| 246 | view.visibility = if (visible) View.VISIBLE else View.INVISIBLE |
| 247 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 248 | } |
| 249 | |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 250 | private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { |
Beth Thibodeau | f55bc6a | 2020-05-20 02:01:31 -0400 | [diff] [blame] | 251 | // If the key was changed, update entry |
| 252 | val oldData = mediaPlayers[oldKey] |
| 253 | if (oldData != null) { |
| 254 | val oldData = mediaPlayers.remove(oldKey) |
| 255 | mediaPlayers.put(key, oldData!!) |
| 256 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 257 | var existingPlayer = mediaPlayers[key] |
| 258 | if (existingPlayer == null) { |
Robert Snoeberger | 7cec542 | 2020-05-29 17:09:14 -0400 | [diff] [blame] | 259 | existingPlayer = mediaControlPanelFactory.get() |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 260 | existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), |
| 261 | mediaContent)) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 262 | mediaPlayers[key] = existingPlayer |
| 263 | val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, |
| 264 | ViewGroup.LayoutParams.WRAP_CONTENT) |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 265 | existingPlayer.view?.player?.setLayoutParams(lp) |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 266 | existingPlayer.setListening(currentlyExpanded) |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 267 | updatePlayerToState(existingPlayer, noAnimation = true) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 268 | if (existingPlayer.isPlaying) { |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 269 | mediaContent.addView(existingPlayer.view?.player, 0) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 270 | } else { |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 271 | mediaContent.addView(existingPlayer.view?.player) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 272 | } |
| 273 | } else if (existingPlayer.isPlaying && |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 274 | mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 275 | if (visualStabilityManager.isReorderingAllowed) { |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 276 | mediaContent.removeView(existingPlayer.view?.player) |
| 277 | mediaContent.addView(existingPlayer.view?.player, 0) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 278 | } else { |
Selim Cinek | 0260c75 | 2020-05-11 16:03:52 -0700 | [diff] [blame] | 279 | needsReordering = true |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 280 | } |
| 281 | } |
Robert Snoeberger | 7cec542 | 2020-05-29 17:09:14 -0400 | [diff] [blame] | 282 | existingPlayer?.bind(data) |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 283 | updateMediaPaddings() |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 284 | updatePageIndicator() |
Robert Snoeberger | d4efdae | 2020-06-12 15:42:25 -0400 | [diff] [blame] | 285 | updatePlayerVisibilities() |
| 286 | mediaCarousel.requiresRemeasuring = true |
| 287 | } |
| 288 | |
| 289 | private fun removePlayer(key: String) { |
| 290 | val removed = mediaPlayers.remove(key) |
| 291 | removed?.apply { |
| 292 | val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= |
| 293 | activeMediaIndex |
| 294 | mediaContent.removeView(removed.view?.player) |
| 295 | removed.onDestroy() |
| 296 | updateMediaPaddings() |
| 297 | if (beforeActive) { |
| 298 | // also update the index here since the scroll below might not always lead |
| 299 | // to a scrolling changed |
| 300 | activeMediaIndex = Math.max(0, activeMediaIndex - 1) |
| 301 | mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX - |
| 302 | playerWidthPlusPadding, 0) |
| 303 | } |
| 304 | updatePlayerVisibilities() |
| 305 | updatePageIndicator() |
| 306 | } |
| 307 | } |
| 308 | |
| 309 | private fun recreatePlayers() { |
| 310 | // Note that this will scramble the order of players. Actively playing sessions will, at |
| 311 | // least, still be put in the front. If we want to maintain order, then more work is |
| 312 | // needed. |
| 313 | mediaData.forEach { |
| 314 | key, data -> |
| 315 | removePlayer(key) |
| 316 | addOrUpdatePlayer(key = key, oldKey = null, data = data) |
| 317 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 318 | } |
| 319 | |
| 320 | private fun updateMediaPaddings() { |
| 321 | val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) |
| 322 | val childCount = mediaContent.childCount |
| 323 | for (i in 0 until childCount) { |
| 324 | val mediaView = mediaContent.getChildAt(i) |
| 325 | val desiredPaddingEnd = if (i == childCount - 1) 0 else padding |
| 326 | val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams |
| 327 | if (layoutParams.marginEnd != desiredPaddingEnd) { |
| 328 | layoutParams.marginEnd = desiredPaddingEnd |
| 329 | mediaView.layoutParams = layoutParams |
| 330 | } |
| 331 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 332 | } |
| 333 | |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 334 | private fun updatePageIndicator() { |
| 335 | val numPages = mediaContent.getChildCount() |
| 336 | pageIndicator.setNumPages(numPages, Color.WHITE) |
| 337 | if (numPages == 1) { |
| 338 | pageIndicator.setLocation(0f) |
| 339 | } |
| 340 | } |
| 341 | |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 342 | /** |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 343 | * Set a new interpolated state for all players. This is a state that is usually controlled |
| 344 | * by a finger movement where the user drags from one state to the next. |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 345 | */ |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 346 | fun setCurrentState( |
| 347 | @MediaLocation startLocation: Int, |
| 348 | @MediaLocation endLocation: Int, |
| 349 | progress: Float, |
| 350 | immediately: Boolean |
| 351 | ) { |
Robert Snoeberger | 52dfb62 | 2020-05-21 16:31:29 -0400 | [diff] [blame] | 352 | // Hack: Since the indicator doesn't move with the player expansion, just make it disappear |
| 353 | // and then reappear at the end. |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 354 | pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f |
| 355 | if (startLocation != currentStartLocation || |
| 356 | endLocation != currentEndLocation || |
| 357 | progress != currentTransitionProgress || |
| 358 | immediately |
| 359 | ) { |
| 360 | currentStartLocation = startLocation |
| 361 | currentEndLocation = endLocation |
| 362 | currentTransitionProgress = progress |
| 363 | for (mediaPlayer in mediaPlayers.values) { |
| 364 | updatePlayerToState(mediaPlayer, immediately) |
| 365 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 366 | } |
Selim Cinek | 9e517f7 | 2020-04-28 12:24:34 -0700 | [diff] [blame] | 367 | } |
Selim Cinek | 3df592e | 2020-04-28 13:51:43 -0700 | [diff] [blame] | 368 | |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 369 | private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) { |
| 370 | mediaPlayer.mediaViewController.setCurrentState( |
| 371 | startLocation = currentStartLocation, |
| 372 | endLocation = currentEndLocation, |
| 373 | transitionProgress = currentTransitionProgress, |
| 374 | applyImmediately = noAnimation) |
| 375 | } |
| 376 | |
Selim Cinek | 3df592e | 2020-04-28 13:51:43 -0700 | [diff] [blame] | 377 | /** |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 378 | * The desired location of this view has changed. We should remeasure the view to match |
| 379 | * the new bounds and kick off bounds animations if necessary. |
| 380 | * If an animation is happening, an animation is kicked of externally, which sets a new |
| 381 | * current state until we reach the targetState. |
| 382 | * |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 383 | * @param desiredLocation the location we're going to |
| 384 | * @param desiredHostState the target state we're transitioning to |
Selim Cinek | 3df592e | 2020-04-28 13:51:43 -0700 | [diff] [blame] | 385 | * @param animate should this be animated |
| 386 | */ |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 387 | fun onDesiredLocationChanged( |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 388 | desiredLocation: Int, |
| 389 | desiredHostState: MediaHostState?, |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 390 | animate: Boolean, |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 391 | duration: Long = 200, |
| 392 | startDelay: Long = 0 |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 393 | ) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 394 | desiredHostState?.let { |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 395 | // This is a hosting view, let's remeasure our players |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 396 | this.desiredLocation = desiredLocation |
| 397 | this.desiredHostState = it |
| 398 | currentlyExpanded = it.expansion > 0 |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 399 | for (mediaPlayer in mediaPlayers.values) { |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 400 | if (animate) { |
| 401 | mediaPlayer.mediaViewController.animatePendingStateChange( |
| 402 | duration = duration, |
| 403 | delay = startDelay) |
| 404 | } |
| 405 | mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 406 | } |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 407 | updateCarouselSize() |
Selim Cinek | 3df592e | 2020-04-28 13:51:43 -0700 | [diff] [blame] | 408 | } |
Selim Cinek | 3df592e | 2020-04-28 13:51:43 -0700 | [diff] [blame] | 409 | } |
| 410 | |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 411 | /** |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 412 | * Update the size of the carousel, remeasuring it if necessary. |
Selim Cinek | 5480962 | 2020-04-30 19:04:44 -0700 | [diff] [blame] | 413 | */ |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 414 | private fun updateCarouselSize() { |
| 415 | val width = desiredHostState?.measurementInput?.width ?: 0 |
| 416 | val height = desiredHostState?.measurementInput?.height ?: 0 |
| 417 | if (width != carouselMeasureWidth && width != 0 || |
| 418 | height != carouselMeasureWidth && height != 0) { |
| 419 | carouselMeasureWidth = width |
| 420 | carouselMeasureHeight = height |
| 421 | playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( |
| 422 | R.dimen.qs_media_padding) |
| 423 | // The player width has changed, let's update the scroll position to make sure |
| 424 | // it's still at the same place |
| 425 | var newScroll = activeMediaIndex * playerWidthPlusPadding |
| 426 | if (scrollIntoCurrentMedia > playerWidthPlusPadding) { |
| 427 | newScroll += playerWidthPlusPadding - |
| 428 | (scrollIntoCurrentMedia - playerWidthPlusPadding) |
| 429 | } else { |
| 430 | newScroll += scrollIntoCurrentMedia |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 431 | } |
Selim Cinek | 2de5ebb | 2020-05-20 15:39:03 -0700 | [diff] [blame] | 432 | mediaCarousel.scrollX = newScroll |
| 433 | // Let's remeasure the carousel |
| 434 | val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 |
| 435 | val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 |
| 436 | mediaCarousel.measure(widthSpec, heightSpec) |
| 437 | mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight) |
Selim Cinek | f418bb0 | 2020-05-04 17:16:58 -0700 | [diff] [blame] | 438 | } |
| 439 | } |
Robert Snoeberger | eb49e94 | 2020-05-12 16:31:09 -0400 | [diff] [blame] | 440 | } |