Merge "Making the media carousel dismissable" into rvc-dev

This commit is contained in:
Selim Cinek
2020-06-22 18:36:48 +00:00
committed by Android (Google) Code Review
24 changed files with 979 additions and 237 deletions

View File

@@ -30,7 +30,7 @@ import java.io.PrintWriter;
*/ */
@ProvidesInterface(version = FalsingManager.VERSION) @ProvidesInterface(version = FalsingManager.VERSION)
public interface FalsingManager { public interface FalsingManager {
int VERSION = 3; int VERSION = 4;
void onSuccessfulUnlock(); void onSuccessfulUnlock();
@@ -88,11 +88,11 @@ public interface FalsingManager {
void onScreenOff(); void onScreenOff();
void onNotificatonStopDismissing(); void onNotificationStopDismissing();
void onNotificationDismissed(); void onNotificationDismissed();
void onNotificatonStartDismissing(); void onNotificationStartDismissing();
void onNotificationDoubleTap(boolean accepted, float dx, float dy); void onNotificationDoubleTap(boolean accepted, float dx, float dy);

View File

@@ -23,7 +23,7 @@
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
> >
<com.android.systemui.media.UnboundHorizontalScrollView <com.android.systemui.media.MediaScrollView
android:id="@+id/media_carousel_scroller" android:id="@+id/media_carousel_scroller"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -41,14 +41,12 @@
> >
<!-- QSMediaPlayers will be added here dynamically --> <!-- QSMediaPlayers will be added here dynamically -->
</LinearLayout> </LinearLayout>
</com.android.systemui.media.UnboundHorizontalScrollView> </com.android.systemui.media.MediaScrollView>
<com.android.systemui.qs.PageIndicator <com.android.systemui.qs.PageIndicator
android:id="@+id/media_page_indicator" android:id="@+id/media_page_indicator"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="48dp" android:layout_height="48dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:layout_gravity="center_horizontal|bottom"
android:gravity="center"
android:tint="@color/media_primary_text" android:tint="@color/media_primary_text"
/> />
</FrameLayout> </FrameLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settings_cog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/controls_media_settings_button"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:paddingBottom="20dp"
android:paddingTop="20dp"
android:src="@drawable/ic_settings"
android:tint="@color/notification_gear_color"
android:visibility="invisible"
android:forceHasOverlappingRendering="false"/>

View File

@@ -201,7 +201,7 @@ public class FalsingManagerFake implements FalsingManager {
} }
@Override @Override
public void onNotificatonStopDismissing() { public void onNotificationStopDismissing() {
} }
@@ -211,7 +211,7 @@ public class FalsingManagerFake implements FalsingManager {
} }
@Override @Override
public void onNotificatonStartDismissing() { public void onNotificationStartDismissing() {
} }

View File

@@ -481,15 +481,15 @@ public class FalsingManagerImpl implements FalsingManager {
mDataCollector.onNotificationDismissed(); mDataCollector.onNotificationDismissed();
} }
public void onNotificatonStartDismissing() { public void onNotificationStartDismissing() {
if (FalsingLog.ENABLED) { if (FalsingLog.ENABLED) {
FalsingLog.i("onNotificatonStartDismissing", ""); FalsingLog.i("onNotificationStartDismissing", "");
} }
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS); mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
mDataCollector.onNotificatonStartDismissing(); mDataCollector.onNotificatonStartDismissing();
} }
public void onNotificatonStopDismissing() { public void onNotificationStopDismissing() {
mDataCollector.onNotificatonStopDismissing(); mDataCollector.onNotificatonStopDismissing();
} }

View File

@@ -302,8 +302,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable {
} }
@Override @Override
public void onNotificatonStopDismissing() { public void onNotificationStopDismissing() {
mInternalFalsingManager.onNotificatonStopDismissing(); mInternalFalsingManager.onNotificationStopDismissing();
} }
@Override @Override
@@ -312,8 +312,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable {
} }
@Override @Override
public void onNotificatonStartDismissing() { public void onNotificationStartDismissing() {
mInternalFalsingManager.onNotificatonStartDismissing(); mInternalFalsingManager.onNotificationStartDismissing();
} }
@Override @Override

View File

@@ -380,7 +380,7 @@ public class BrightLineFalsingManager implements FalsingManager {
@Override @Override
public void onNotificatonStopDismissing() { public void onNotificationStopDismissing() {
} }
@Override @Override
@@ -388,7 +388,7 @@ public class BrightLineFalsingManager implements FalsingManager {
} }
@Override @Override
public void onNotificatonStartDismissing() { public void onNotificationStartDismissing() {
updateInteractionType(Classifier.NOTIFICATION_DISMISS); updateInteractionType(Classifier.NOTIFICATION_DISMISS);
} }

View File

@@ -46,7 +46,9 @@ class KeyguardMediaController @Inject constructor(
}) })
} }
private var view: MediaHeaderView? = null var visibilityChangedListener: ((Boolean) -> Unit)? = null
var view: MediaHeaderView? = null
private set
/** /**
* Attach this controller to a media view, initializing its state * Attach this controller to a media view, initializing its state
@@ -57,6 +59,7 @@ class KeyguardMediaController @Inject constructor(
mediaHost.visibleChangedListener = { updateVisibility() } mediaHost.visibleChangedListener = { updateVisibility() }
mediaHost.expansion = 0.0f mediaHost.expansion = 0.0f
mediaHost.showsOnlyActiveMedia = true mediaHost.showsOnlyActiveMedia = true
mediaHost.falsingProtectionNeeded = true
// Let's now initialize this view, which also creates the host view for us. // Let's now initialize this view, which also creates the host view for us.
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -70,6 +73,11 @@ class KeyguardMediaController @Inject constructor(
!bypassController.bypassEnabled && !bypassController.bypassEnabled &&
keyguardOrUserSwitcher && keyguardOrUserSwitcher &&
notifLockscreenUserManager.shouldShowLockscreenNotifications() notifLockscreenUserManager.shouldShowLockscreenNotifications()
view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE val previousVisibility = view?.visibility ?: View.GONE
val newVisibility = if (shouldBeVisible) View.VISIBLE else View.GONE
view?.visibility = newVisibility
if (previousVisibility != newVisibility) {
visibilityChangedListener?.invoke(shouldBeVisible)
}
} }
} }

View File

@@ -1,41 +1,65 @@
package com.android.systemui.media package com.android.systemui.media
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.animation.requiresRemeasuring import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider import javax.inject.Provider
import javax.inject.Singleton import javax.inject.Singleton
private const val FLING_SLOP = 1000000 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
/** /**
* Class that is responsible for keeping the view carousel up to date. * 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. * This also handles changes in state and applies them to the media carousel like the expansion.
*/ */
@Singleton @Singleton
class MediaViewManager @Inject constructor( class MediaCarouselController @Inject constructor(
private val context: Context, private val context: Context,
private val mediaControlPanelFactory: Provider<MediaControlPanel>, private val mediaControlPanelFactory: Provider<MediaControlPanel>,
private val visualStabilityManager: VisualStabilityManager, private val visualStabilityManager: VisualStabilityManager,
private val mediaHostStatesManager: MediaHostStatesManager, private val mediaHostStatesManager: MediaHostStatesManager,
private val activityStarter: ActivityStarter,
@Main executor: DelayableExecutor,
mediaManager: MediaDataCombineLatest, mediaManager: MediaDataCombineLatest,
configurationController: ConfigurationController configurationController: ConfigurationController,
mediaDataManager: MediaDataManager,
falsingManager: FalsingManager
) { ) {
/**
* The current width of the carousel
*/
private var currentCarouselWidth: Int = 0
/**
* The current height of the carousel
*/
private var currentCarouselHeight: Int = 0
/**
* Are we currently showing only active players
*/
private var currentlyShowingOnlyActive: Boolean = false
/**
* Is the player currently visible (at the end of the transformation
*/
private var playersVisible: Boolean = false
/** /**
* The desired location where we'll be at the end of the transformation. Usually this matches * The desired location where we'll be at the end of the transformation. Usually this matches
* the end location, except when we're still waiting on a state update call. * the end location, except when we're still waiting on a state update call.
@@ -73,17 +97,16 @@ class MediaViewManager @Inject constructor(
private var carouselMeasureHeight: Int = 0 private var carouselMeasureHeight: Int = 0
private var playerWidthPlusPadding: Int = 0 private var playerWidthPlusPadding: Int = 0
private var desiredHostState: MediaHostState? = null private var desiredHostState: MediaHostState? = null
private val mediaCarousel: HorizontalScrollView private val mediaCarousel: MediaScrollView
private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
val mediaFrame: ViewGroup val mediaFrame: ViewGroup
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private lateinit var settingsButton: View
private val mediaData: MutableMap<String, MediaData> = mutableMapOf() private val mediaData: MutableMap<String, MediaData> = mutableMapOf()
private val mediaContent: ViewGroup private val mediaContent: ViewGroup
private val pageIndicator: PageIndicator private val pageIndicator: PageIndicator
private val gestureDetector: GestureDetectorCompat
private val visualStabilityCallback: VisualStabilityManager.Callback private val visualStabilityCallback: VisualStabilityManager.Callback
private var activeMediaIndex: Int = 0
private var needsReordering: Boolean = false private var needsReordering: Boolean = false
private var scrollIntoCurrentMedia: Int = 0
private var currentlyExpanded = true private var currentlyExpanded = true
set(value) { set(value) {
if (field != value) { if (field != value) {
@@ -93,50 +116,25 @@ class MediaViewManager @Inject constructor(
} }
} }
} }
private val scrollChangedListener = object : View.OnScrollChangeListener {
override fun onScrollChange(
v: View?,
scrollX: Int,
scrollY: Int,
oldScrollX: Int,
oldScrollY: Int
) {
if (playerWidthPlusPadding == 0) {
return
}
onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
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)
}
}
private val configListener = object : ConfigurationController.ConfigurationListener { private val configListener = object : ConfigurationController.ConfigurationListener {
override fun onDensityOrFontScaleChanged() { override fun onDensityOrFontScaleChanged() {
recreatePlayers() recreatePlayers()
inflateSettingsButton()
}
override fun onOverlayChanged() {
inflateSettingsButton()
} }
} }
init { init {
gestureDetector = GestureDetectorCompat(context, gestureListener)
mediaFrame = inflateMediaCarousel() mediaFrame = inflateMediaCarousel()
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
mediaCarousel.setOnScrollChangeListener(scrollChangedListener) mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
mediaCarousel.setOnTouchListener(touchListener) executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER) falsingManager)
inflateSettingsButton()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
configurationController.addCallback(configListener) configurationController.addCallback(configListener)
visualStabilityCallback = VisualStabilityManager.Callback { visualStabilityCallback = VisualStabilityManager.Callback {
@@ -161,6 +159,11 @@ class MediaViewManager @Inject constructor(
removePlayer(key) removePlayer(key)
} }
}) })
mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
// The pageIndicator is not laid out yet when we get the current state update,
// Lets make sure we have the right dimensions
updatePageIndicatorLocation()
}
mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
if (location == desiredLocation) { if (location == desiredLocation) {
@@ -170,6 +173,20 @@ class MediaViewManager @Inject constructor(
}) })
} }
private fun inflateSettingsButton() {
val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
mediaFrame, false) as View
if (this::settingsButton.isInitialized) {
mediaFrame.removeView(settingsButton)
}
settingsButton = settings
mediaFrame.addView(settingsButton)
mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
settingsButton.setOnClickListener {
activityStarter.startActivity(settingsIntent, true /* dismissShade */)
}
}
private fun inflateMediaCarousel(): ViewGroup { private fun inflateMediaCarousel(): ViewGroup {
return LayoutInflater.from(context).inflate(R.layout.media_carousel, return LayoutInflater.from(context).inflate(R.layout.media_carousel,
UniqueObjectHostView(context), false) as ViewGroup UniqueObjectHostView(context), false) as ViewGroup
@@ -183,68 +200,7 @@ class MediaViewManager @Inject constructor(
mediaContent.addView(view, 0) mediaContent.addView(view, 0)
} }
} }
updateMediaPaddings() mediaCarouselScrollHandler.onPlayersChanged()
updatePlayerVisibilities()
}
private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
val wasScrolledIn = scrollIntoCurrentMedia != 0
scrollIntoCurrentMedia = scrollInAmount
val nowScrolledIn = scrollIntoCurrentMedia != 0
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
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() {
val scrolledIn = scrollIntoCurrentMedia != 0
for (i in 0 until mediaContent.childCount) {
val view = mediaContent.getChildAt(i)
val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
} }
private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
@@ -259,6 +215,7 @@ class MediaViewManager @Inject constructor(
existingPlayer = mediaControlPanelFactory.get() existingPlayer = mediaControlPanelFactory.get()
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent)) mediaContent))
existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
mediaPlayers[key] = existingPlayer mediaPlayers[key] = existingPlayer
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -280,28 +237,18 @@ class MediaViewManager @Inject constructor(
} }
} }
existingPlayer?.bind(data) existingPlayer?.bind(data)
updateMediaPaddings()
updatePageIndicator() updatePageIndicator()
updatePlayerVisibilities() mediaCarouselScrollHandler.onPlayersChanged()
mediaCarousel.requiresRemeasuring = true mediaCarousel.requiresRemeasuring = true
} }
private fun removePlayer(key: String) { private fun removePlayer(key: String) {
val removed = mediaPlayers.remove(key) val removed = mediaPlayers.remove(key)
removed?.apply { removed?.apply {
val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
activeMediaIndex
mediaContent.removeView(removed.view?.player) mediaContent.removeView(removed.view?.player)
removed.onDestroy() removed.onDestroy()
updateMediaPaddings() mediaCarouselScrollHandler.onPlayersChanged()
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)
mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX -
playerWidthPlusPadding, 0)
}
updatePlayerVisibilities()
updatePageIndicator() updatePageIndicator()
} }
} }
@@ -317,20 +264,6 @@ class MediaViewManager @Inject constructor(
} }
} }
private fun updateMediaPaddings() {
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
val childCount = mediaContent.childCount
for (i in 0 until childCount) {
val mediaView = mediaContent.getChildAt(i)
val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
if (layoutParams.marginEnd != desiredPaddingEnd) {
layoutParams.marginEnd = desiredPaddingEnd
mediaView.layoutParams = layoutParams
}
}
}
private fun updatePageIndicator() { private fun updatePageIndicator() {
val numPages = mediaContent.getChildCount() val numPages = mediaContent.getChildCount()
pageIndicator.setNumPages(numPages, Color.WHITE) pageIndicator.setNumPages(numPages, Color.WHITE)
@@ -342,6 +275,12 @@ class MediaViewManager @Inject constructor(
/** /**
* Set a new interpolated state for all players. This is a state that is usually controlled * Set a new interpolated state for all players. This is a state that is usually controlled
* by a finger movement where the user drags from one state to the next. * by a finger movement where the user drags from one state to the next.
*
* @param startLocation the start location of our state or -1 if this is directly set
* @param endLocation the ending location of our state.
* @param progress the progress of the transition between startLocation and endlocation. If
* this is not a guided transformation, this will be 1.0f
* @param immediately should this state be applied immediately, canceling all animations?
*/ */
fun setCurrentState( fun setCurrentState(
@MediaLocation startLocation: Int, @MediaLocation startLocation: Int,
@@ -349,9 +288,6 @@ class MediaViewManager @Inject constructor(
progress: Float, progress: Float,
immediately: Boolean immediately: Boolean
) { ) {
// Hack: Since the indicator doesn't move with the player expansion, just make it disappear
// and then reappear at the end.
pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
if (startLocation != currentStartLocation || if (startLocation != currentStartLocation ||
endLocation != currentEndLocation || endLocation != currentEndLocation ||
progress != currentTransitionProgress || progress != currentTransitionProgress ||
@@ -363,6 +299,51 @@ class MediaViewManager @Inject constructor(
for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in mediaPlayers.values) {
updatePlayerToState(mediaPlayer, immediately) updatePlayerToState(mediaPlayer, immediately)
} }
maybeResetSettingsCog()
}
}
private fun updatePageIndicatorLocation() {
// Update the location of the page indicator, carousel clipping
pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f +
mediaCarouselScrollHandler.contentTranslation
val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
layoutParams.bottomMargin).toFloat()
}
/**
* Update the dimension of this carousel.
*/
private fun updateCarouselDimensions() {
var width = 0
var height = 0
for (mediaPlayer in mediaPlayers.values) {
val controller = mediaPlayer.mediaViewController
width = Math.max(width, controller.currentWidth)
height = Math.max(height, controller.currentHeight)
}
if (width != currentCarouselWidth || height != currentCarouselHeight) {
currentCarouselWidth = width
currentCarouselHeight = height
mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight)
updatePageIndicatorLocation()
}
}
private fun maybeResetSettingsCog() {
val hostStates = mediaHostStatesManager.mediaHostStates
val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
?: true
val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
?: endShowsActive
if (currentlyShowingOnlyActive != endShowsActive ||
((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
startShowsActive != endShowsActive)) {
/// Whenever we're transitioning from between differing states or the endstate differs
// we reset the translation
currentlyShowingOnlyActive = endShowsActive
mediaCarouselScrollHandler.resetTranslation(animate = true)
} }
} }
@@ -404,6 +385,15 @@ class MediaViewManager @Inject constructor(
} }
mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
} }
mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
val nowVisible = it.visible
if (nowVisible != playersVisible) {
playersVisible = nowVisible
if (nowVisible) {
mediaCarouselScrollHandler.resetTranslation()
}
}
updateCarouselSize() updateCarouselSize()
} }
} }
@@ -420,16 +410,7 @@ class MediaViewManager @Inject constructor(
carouselMeasureHeight = height carouselMeasureHeight = height
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
R.dimen.qs_media_padding) R.dimen.qs_media_padding)
// The player width has changed, let's update the scroll position to make sure mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
// it's still at the same place
var newScroll = activeMediaIndex * playerWidthPlusPadding
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
newScroll += playerWidthPlusPadding -
(scrollIntoCurrentMedia - playerWidthPlusPadding)
} else {
newScroll += scrollIntoCurrentMedia
}
mediaCarousel.scrollX = newScroll
// 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

View File

@@ -0,0 +1,516 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.graphics.Outline
import android.util.MathUtils
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewOutlineProvider
import androidx.core.view.GestureDetectorCompat
import androidx.dynamicanimation.animation.FloatPropertyCompat
import androidx.dynamicanimation.animation.SpringForce
import com.android.settingslib.Utils
import com.android.systemui.Gefingerpoken
import com.android.systemui.qs.PageIndicator
import com.android.systemui.R
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.util.animation.PhysicsAnimator
import com.android.systemui.util.concurrency.DelayableExecutor
private const val FLING_SLOP = 1000000
private const val DISMISS_DELAY = 100L
private const val RUBBERBAND_FACTOR = 0.2f
private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f
/**
* Default spring configuration to use for animations where stiffness and/or damping ratio
* were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig].
*/
private val translationConfig = PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_MEDIUM,
SpringForce.DAMPING_RATIO_LOW_BOUNCY)
/**
* A controller class for the media scrollview, responsible for touch handling
*/
class MediaCarouselScrollHandler(
private val scrollView: MediaScrollView,
private val pageIndicator: PageIndicator,
private val mainExecutor: DelayableExecutor,
private val dismissCallback: () -> Unit,
private var translationChangedListener: () -> Unit,
private val falsingManager: FalsingManager
) {
/**
* Do we need falsing protection?
*/
var falsingProtectionNeeded: Boolean = false
/**
* The width of the carousel
*/
private var carouselWidth: Int = 0
/**
* The height of the carousel
*/
private var carouselHeight: Int = 0
/**
* How much are we scrolled into the current media?
*/
private var cornerRadius: Int = 0
/**
* The content where the players are added
*/
private var mediaContent: ViewGroup
/**
* The gesture detector to detect touch gestures
*/
private val gestureDetector: GestureDetectorCompat
/**
* The settings button view
*/
private lateinit var settingsButton: View
/**
* What's the currently active player index?
*/
var activeMediaIndex: Int = 0
private set
/**
* How much are we scrolled into the current media?
*/
private var scrollIntoCurrentMedia: Int = 0
/**
* how much is the content translated in X
*/
var contentTranslation = 0.0f
private set(value) {
field = value
mediaContent.translationX = value
updateSettingsPresentation()
translationChangedListener.invoke()
updateClipToOutline()
}
/**
* The width of a player including padding
*/
var playerWidthPlusPadding: Int = 0
set(value) {
field = value
// The player width has changed, let's update the scroll position to make sure
// it's still at the same place
var newScroll = activeMediaIndex * playerWidthPlusPadding
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
newScroll += playerWidthPlusPadding -
(scrollIntoCurrentMedia - playerWidthPlusPadding)
} else {
newScroll += scrollIntoCurrentMedia
}
scrollView.scrollX = newScroll
}
/**
* Does the dismiss currently show the setting cog?
*/
var showsSettingsButton: Boolean = false
/**
* A utility to detect gestures, used in the touch listener
*/
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onFling(
eStart: MotionEvent?,
eCurrent: MotionEvent?,
vX: Float,
vY: Float
) = onFling(vX, vY)
override fun onScroll(
down: MotionEvent?,
lastMotion: MotionEvent?,
distanceX: Float,
distanceY: Float
) = onScroll(down!!, lastMotion!!, distanceX)
override fun onDown(e: MotionEvent?): Boolean {
if (falsingProtectionNeeded) {
falsingManager.onNotificationStartDismissing()
}
return false
}
}
/**
* The touch listener for the scroll view
*/
private val touchListener = object : Gefingerpoken {
override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!)
override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!)
}
/**
* A listener that is invoked when the scrolling changes to update player visibilities
*/
private val scrollChangedListener = object : View.OnScrollChangeListener {
override fun onScrollChange(
v: View?,
scrollX: Int,
scrollY: Int,
oldScrollX: Int,
oldScrollY: Int
) {
if (playerWidthPlusPadding == 0) {
return
}
onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
scrollX % playerWidthPlusPadding)
}
}
init {
gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener)
scrollView.touchListener = touchListener
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER)
mediaContent = scrollView.contentContainer
scrollView.setOnScrollChangeListener(scrollChangedListener)
scrollView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat())
}
}
}
fun onSettingsButtonUpdated(button: View) {
settingsButton = button
// We don't have a context to resolve, lets use the settingsbuttons one since that is
// reinflated appropriately
cornerRadius = settingsButton.resources.getDimensionPixelSize(
Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius))
updateSettingsPresentation()
scrollView.invalidateOutline()
}
private fun updateSettingsPresentation() {
if (showsSettingsButton) {
val settingsOffset = MathUtils.map(
0.0f,
getMaxTranslation().toFloat(),
0.0f,
1.0f,
Math.abs(contentTranslation))
val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width *
SETTINGS_BUTTON_TRANSLATION_FRACTION
val newTranslationX: Float
if (contentTranslation > 0) {
newTranslationX = settingsTranslation
} else {
newTranslationX = scrollView.width - settingsTranslation - settingsButton.width
}
val rotation = (1.0f - settingsOffset) * 50
settingsButton.rotation = rotation * -Math.signum(contentTranslation)
val alpha = MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset)
settingsButton.alpha = alpha
settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE
settingsButton.translationX = newTranslationX
settingsButton.translationY = (scrollView.height - settingsButton.height) / 2.0f
} else {
settingsButton.visibility = View.INVISIBLE
}
}
private fun onTouch(motionEvent: MotionEvent): Boolean {
val isUp = motionEvent.action == MotionEvent.ACTION_UP
if (isUp && falsingProtectionNeeded) {
falsingManager.onNotificationStopDismissing()
}
if (gestureDetector.onTouchEvent(motionEvent)) {
if (isUp) {
// If this is an up and we're flinging, we don't want to have this touch reach
// the view, otherwise that would scroll, while we are trying to snap to the
// new page. Let's dispatch a cancel instead.
scrollView.cancelCurrentScroll()
return true
} else {
// Pass touches to the scrollView
return false
}
}
if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) {
// It's an up and the fling didn't take it above
val pos = scrollView.scrollX % playerWidthPlusPadding
val scollXAmount: Int
if (pos > playerWidthPlusPadding / 2) {
scollXAmount = playerWidthPlusPadding - pos
} else {
scollXAmount = -1 * pos
}
if (scollXAmount != 0) {
// Delay the scrolling since scrollView calls springback which cancels
// the animation again..
mainExecutor.execute {
scrollView.smoothScrollBy(scollXAmount, 0)
}
}
val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f) {
// We started a Swipe but didn't end up with a fling. Let's either go to the
// dismissed position or go back.
val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2
|| isFalseTouch()
val newTranslation: Float
if (springBack) {
newTranslation = 0.0f
} else {
newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
if (!showsSettingsButton) {
// Delay the dismiss a bit to avoid too much overlap. Waiting until the
// animation has finished also feels a bit too slow here.
mainExecutor.executeDelayed({
dismissCallback.invoke()
}, DISMISS_DELAY)
}
}
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
newTranslation, startVelocity = 0.0f, config = translationConfig).start()
scrollView.animationTargetX = newTranslation
}
}
// Always pass touches to the scrollView
return false
}
private fun isFalseTouch() = falsingProtectionNeeded && falsingManager.isFalseTouch
private fun getMaxTranslation() = if (showsSettingsButton) {
settingsButton.width
} else {
playerWidthPlusPadding
}
private fun onInterceptTouch(motionEvent: MotionEvent): Boolean {
return gestureDetector.onTouchEvent(motionEvent)
}
fun onScroll(down: MotionEvent,
lastMotion: MotionEvent,
distanceX: Float): Boolean {
val totalX = lastMotion.x - down.x
val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f ||
!scrollView.canScrollHorizontally((-totalX).toInt())) {
var newTranslation = currentTranslation - distanceX
val absTranslation = Math.abs(newTranslation)
if (absTranslation > getMaxTranslation()) {
// Rubberband all translation above the maximum
if (Math.signum(distanceX) != Math.signum(currentTranslation)) {
// The movement is in the same direction as our translation,
// Let's rubberband it.
if (Math.abs(currentTranslation) > getMaxTranslation()) {
// we were already overshooting before. Let's add the distance
// fully rubberbanded.
newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR
} else {
// We just crossed the boundary, let's rubberband it all
newTranslation = Math.signum(newTranslation) * (getMaxTranslation() +
(absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR)
}
} // Otherwise we don't have do do anything, and will remove the unrubberbanded
// translation
}
if (Math.signum(newTranslation) != Math.signum(currentTranslation)
&& currentTranslation != 0.0f) {
// We crossed the 0.0 threshold of the translation. Let's see if we're allowed
// to scroll into the new direction
if (scrollView.canScrollHorizontally(-newTranslation.toInt())) {
// We can actually scroll in the direction where we want to translate,
// Let's make sure to stop at 0
newTranslation = 0.0f
}
}
val physicsAnimator = PhysicsAnimator.getInstance(this)
if (physicsAnimator.isRunning()) {
physicsAnimator.spring(CONTENT_TRANSLATION,
newTranslation, startVelocity = 0.0f, config = translationConfig).start()
} else {
contentTranslation = newTranslation
}
scrollView.animationTargetX = newTranslation
return true
}
return false
}
private fun onFling(
vX: Float,
vY: Float
): Boolean {
if (vX * vX < 0.5 * vY * vY) {
return false
}
if (vX * vX < FLING_SLOP) {
return false
}
val currentTranslation = scrollView.getContentTranslation()
if (currentTranslation != 0.0f) {
// We're translated and flung. Let's see if the fling is in the same direction
val newTranslation: Float
if (Math.signum(vX) != Math.signum(currentTranslation) || isFalseTouch()) {
// The direction of the fling isn't the same as the translation, let's go to 0
newTranslation = 0.0f
} else {
newTranslation = getMaxTranslation() * Math.signum(currentTranslation)
// Delay the dismiss a bit to avoid too much overlap. Waiting until the animation
// has finished also feels a bit too slow here.
if (!showsSettingsButton) {
mainExecutor.executeDelayed({
dismissCallback.invoke()
}, DISMISS_DELAY)
}
}
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
newTranslation, startVelocity = vX, config = translationConfig).start()
scrollView.animationTargetX = newTranslation
} else {
// We're flinging the player! Let's go either to the previous or to the next player
val pos = scrollView.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)
// We need to post this since we're dispatching a touch to the underlying view to cancel
// but canceling will actually abort the animation.
mainExecutor.execute {
scrollView.smoothScrollTo(view.left, scrollView.scrollY)
}
}
return true
}
/**
* Reset the translation of the players when swiped
*/
fun resetTranslation(animate: Boolean = false) {
if (scrollView.getContentTranslation() != 0.0f) {
if (animate) {
PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION,
0.0f, config = translationConfig).start()
scrollView.animationTargetX = 0.0f
} else {
PhysicsAnimator.getInstance(this).cancel()
contentTranslation = 0.0f
}
}
}
private fun updateClipToOutline() {
val clip = contentTranslation != 0.0f || scrollIntoCurrentMedia != 0
scrollView.clipToOutline = clip
}
private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
val wasScrolledIn = scrollIntoCurrentMedia != 0
scrollIntoCurrentMedia = scrollInAmount
val nowScrolledIn = scrollIntoCurrentMedia != 0
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
activeMediaIndex = newIndex
updatePlayerVisibilities()
}
val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
pageIndicator.setLocation(location)
updateClipToOutline()
}
/**
* Notified whenever the players or their order has changed
*/
fun onPlayersChanged() {
updatePlayerVisibilities()
updateMediaPaddings()
}
private fun updateMediaPaddings() {
val padding = scrollView.context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
val childCount = mediaContent.childCount
for (i in 0 until childCount) {
val mediaView = mediaContent.getChildAt(i)
val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
if (layoutParams.marginEnd != desiredPaddingEnd) {
layoutParams.marginEnd = desiredPaddingEnd
mediaView.layoutParams = layoutParams
}
}
}
private fun updatePlayerVisibilities() {
val scrolledIn = scrollIntoCurrentMedia != 0
for (i in 0 until mediaContent.childCount) {
val view = mediaContent.getChildAt(i)
val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
}
}
/**
* Notify that a player will be removed right away. This gives us the opporunity to look
* where it was and update our scroll position.
*/
fun onPrePlayerRemoved(removed: MediaControlPanel) {
val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex
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)
scrollView.scrollX = Math.max(scrollView.scrollX -
playerWidthPlusPadding, 0)
}
}
/**
* Update the bounds of the carousel
*/
fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) {
if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) {
carouselWidth = currentCarouselWidth
carouselHeight = currentCarouselHeight
scrollView.invalidateOutline()
}
}
companion object {
private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>(
"contentTranslation") {
override fun getValue(handler: MediaCarouselScrollHandler): Float {
return handler.contentTranslation
}
override fun setValue(handler: MediaCarouselScrollHandler, value: Float) {
handler.contentTranslation = value
}
}
}
}

View File

@@ -592,6 +592,16 @@ class MediaDataManager(
} }
} }
/**
* Invoked when the user has dismissed the media carousel
*/
fun onSwipeToDismiss() {
val mediaKeys = mediaEntries.keys.toSet()
mediaKeys.forEach {
setTimedOut(it, timedOut = true)
}
}
interface Listener { interface Listener {
/** /**

View File

@@ -49,7 +49,7 @@ class MediaHierarchyManager @Inject constructor(
private val statusBarStateController: SysuiStatusBarStateController, private val statusBarStateController: SysuiStatusBarStateController,
private val keyguardStateController: KeyguardStateController, private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController, private val bypassController: KeyguardBypassController,
private val mediaViewManager: MediaViewManager, private val mediaCarouselController: MediaCarouselController,
private val notifLockscreenUserManager: NotificationLockscreenUserManager, private val notifLockscreenUserManager: NotificationLockscreenUserManager,
wakefulnessLifecycle: WakefulnessLifecycle wakefulnessLifecycle: WakefulnessLifecycle
) { ) {
@@ -65,7 +65,7 @@ class MediaHierarchyManager @Inject constructor(
private var animationStartBounds: Rect = Rect() private var animationStartBounds: Rect = Rect()
private var targetBounds: Rect = Rect() private var targetBounds: Rect = Rect()
private val mediaFrame private val mediaFrame
get() = mediaViewManager.mediaFrame get() = mediaCarouselController.mediaFrame
private var statusbarState: Int = statusBarStateController.state private var statusbarState: Int = statusBarStateController.state
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
interpolator = Interpolators.FAST_OUT_SLOW_IN interpolator = Interpolators.FAST_OUT_SLOW_IN
@@ -273,8 +273,8 @@ class MediaHierarchyManager @Inject constructor(
val animate = shouldAnimateTransition(desiredLocation, previousLocation) val animate = shouldAnimateTransition(desiredLocation, previousLocation)
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
val host = getHost(desiredLocation) val host = getHost(desiredLocation)
mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration, mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
delay) animDuration, delay)
performTransitionToNewLocation(isNewView, animate) performTransitionToNewLocation(isNewView, animate)
} }
} }
@@ -457,7 +457,7 @@ class MediaHierarchyManager @Inject constructor(
val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1 val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
val endLocation = desiredLocation val endLocation = desiredLocation
mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately) mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
updateHostAttachment() updateHostAttachment()
if (currentAttachmentLocation == IN_OVERLAY) { if (currentAttachmentLocation == IN_OVERLAY) {
mediaFrame.setLeftTopRightBottom( mediaFrame.setLeftTopRightBottom(

View File

@@ -113,8 +113,11 @@ class MediaHost @Inject constructor(
} else { } else {
mediaDataManager.hasAnyMedia() mediaDataManager.hasAnyMedia()
} }
hostView.visibility = if (visible) View.VISIBLE else View.GONE val newVisibility = if (visible) View.VISIBLE else View.GONE
visibleChangedListener?.invoke(visible) if (newVisibility != hostView.visibility) {
hostView.visibility = newVisibility
visibleChangedListener?.invoke(visible)
}
} }
class MediaHostStateHolder @Inject constructor() : MediaHostState { class MediaHostStateHolder @Inject constructor() : MediaHostState {
@@ -153,6 +156,15 @@ class MediaHost @Inject constructor(
changedListener?.invoke() changedListener?.invoke()
} }
override var falsingProtectionNeeded: Boolean = false
set(value) {
if (field == value) {
return
}
field = value
changedListener?.invoke()
}
override fun getPivotX(): Float = gonePivot.x override fun getPivotX(): Float = gonePivot.x
override fun getPivotY(): Float = gonePivot.y override fun getPivotY(): Float = gonePivot.y
override fun setGonePivot(x: Float, y: Float) { override fun setGonePivot(x: Float, y: Float) {
@@ -178,6 +190,7 @@ class MediaHost @Inject constructor(
mediaHostState.measurementInput = measurementInput?.copy() mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible mediaHostState.visible = visible
mediaHostState.gonePivot.set(gonePivot) mediaHostState.gonePivot.set(gonePivot)
mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
return mediaHostState return mediaHostState
} }
@@ -197,6 +210,9 @@ class MediaHost @Inject constructor(
if (visible != other.visible) { if (visible != other.visible) {
return false return false
} }
if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
return false
}
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) { if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
return false return false
} }
@@ -206,6 +222,7 @@ class MediaHost @Inject constructor(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0 var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode() result = 31 * result + expansion.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2 result = 31 * result + if (visible) 1 else 2
result = 31 * result + gonePivot.hashCode() result = 31 * result + gonePivot.hashCode()
@@ -238,6 +255,11 @@ interface MediaHostState {
*/ */
var visible: Boolean var visible: Boolean
/**
* Does this host need any falsing protection?
*/
var falsingProtectionNeeded: Boolean
/** /**
* Sets the pivot point when clipping the height or width. * Sets the pivot point when clipping the height or width.
* Clipping happens when animating visibility when we're visible in QS but not on QQS, * Clipping happens when animating visibility when we're visible in QS but not on QQS,

View File

@@ -0,0 +1,100 @@
package com.android.systemui.media
import android.content.Context
import android.os.SystemClock
import android.util.AttributeSet
import android.view.InputDevice
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import com.android.systemui.Gefingerpoken
import com.android.systemui.util.animation.physicsAnimator
/**
* A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful
* when only measuring children but not the parent, when trying to apply a new scroll position
*/
class MediaScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: HorizontalScrollView(context, attrs, defStyleAttr) {
lateinit var contentContainer: ViewGroup
private set
var touchListener: Gefingerpoken? = null
/**
* The target value of the translation X animation. Only valid if the physicsAnimator is running
*/
var animationTargetX = 0.0f
/**
* Get the current content translation. This is usually the normal translationX of the content,
* but when animating, it might differ
*/
fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) {
animationTargetX
} else {
contentContainer.translationX
}
/**
* Allow all scrolls to go through, use base implementation
*/
override fun scrollTo(x: Int, y: Int) {
if (mScrollX != x || mScrollY != y) {
val oldX: Int = mScrollX
val oldY: Int = mScrollY
mScrollX = x
mScrollY = y
invalidateParentCaches()
onScrollChanged(mScrollX, mScrollY, oldX, oldY)
if (!awakenScrollBars()) {
postInvalidateOnAnimation()
}
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
var intercept = false;
touchListener?.let {
intercept = it.onInterceptTouchEvent(ev)
}
return super.onInterceptTouchEvent(ev) || intercept;
}
override fun onTouchEvent(ev: MotionEvent?): Boolean {
var touch = false;
touchListener?.let {
touch = it.onTouchEvent(ev)
}
return super.onTouchEvent(ev) || touch
}
override fun onFinishInflate() {
super.onFinishInflate()
contentContainer = getChildAt(0) as ViewGroup
}
override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int,
scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int,
maxOverScrollY: Int, isTouchEvent: Boolean): Boolean {
if (getContentTranslation() != 0.0f) {
// When we're dismissing we ignore all the scrolling
return false
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
}
/**
* Cancel the current touch event going on.
*/
fun cancelCurrentScroll() {
val now = SystemClock.uptimeMillis()
val event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0)
event.source = InputDevice.SOURCE_TOUCHSCREEN
super.onTouchEvent(event)
event.recycle()
}
}

View File

@@ -35,6 +35,10 @@ class MediaViewController @Inject constructor(
private val mediaHostStatesManager: MediaHostStatesManager private val mediaHostStatesManager: MediaHostStatesManager
) { ) {
/**
* A listener when the current dimensions of the player change
*/
lateinit var sizeChangedListener: () -> Unit
private var firstRefresh: Boolean = true private var firstRefresh: Boolean = true
private var transitionLayout: TransitionLayout? = null private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController() private val layoutController = TransitionLayoutController()
@@ -75,6 +79,17 @@ class MediaViewController @Inject constructor(
*/ */
private val tmpPoint = PointF() private val tmpPoint = PointF()
/**
* The current width of the player. This might not factor in case the player is animating
* to the current state, but represents the end state
*/
var currentWidth: Int = 0
/**
* The current height of the player. This might not factor in case the player is animating
* to the current state, but represents the end state
*/
var currentHeight: Int = 0
/** /**
* A callback for media state changes * A callback for media state changes
*/ */
@@ -105,6 +120,11 @@ class MediaViewController @Inject constructor(
collapsedLayout.load(context, R.xml.media_collapsed) collapsedLayout.load(context, R.xml.media_collapsed)
expandedLayout.load(context, R.xml.media_expanded) expandedLayout.load(context, R.xml.media_expanded)
mediaHostStatesManager.addController(this) mediaHostStatesManager.addController(this)
layoutController.sizeChangedListener = { width: Int, height: Int ->
currentWidth = width
currentHeight = height
sizeChangedListener.invoke()
}
} }
/** /**
@@ -279,6 +299,8 @@ class MediaViewController @Inject constructor(
tmpPoint, tmpState) tmpPoint, tmpState)
tmpState tmpState
} }
currentWidth = result.width
currentHeight = result.height
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration, layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
animationDelay) animationDelay)
} }

View File

@@ -1,31 +0,0 @@
package com.android.systemui.media
import android.content.Context
import android.util.AttributeSet
import android.widget.HorizontalScrollView
/**
* A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful
* when only measuring children but not the parent, when trying to apply a new scroll position
*/
class UnboundHorizontalScrollView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: HorizontalScrollView(context, attrs, defStyleAttr) {
/**
* Allow all scrolls to go through, use base implementation
*/
override fun scrollTo(x: Int, y: Int) {
if (mScrollX != x || mScrollY != y) {
val oldX: Int = mScrollX
val oldY: Int = mScrollY
mScrollX = x
mScrollY = y
invalidateParentCaches()
onScrollChanged(mScrollX, mScrollY, oldX, oldY)
if (!awakenScrollBars()) {
postInvalidateOnAnimation()
}
}
}
}

View File

@@ -26,8 +26,12 @@ import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.qs.customize.QSCustomizer;
import com.android.systemui.util.animation.PhysicsAnimator;
/** /**
* Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader} * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
@@ -35,7 +39,22 @@ import com.android.systemui.qs.customize.QSCustomizer;
public class QSContainerImpl extends FrameLayout { public class QSContainerImpl extends FrameLayout {
private final Point mSizePoint = new Point(); private final Point mSizePoint = new Point();
private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM =
new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") {
@Override
public float getValue(QSContainerImpl qsImpl) {
return qsImpl.getBackgroundBottom();
}
@Override
public void setValue(QSContainerImpl background, float value) {
background.setBackgroundBottom((int) value);
}
};
private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING
= new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM,
SpringForce.DAMPING_RATIO_LOW_BOUNCY);
private int mBackgroundBottom = -1;
private int mHeightOverride = -1; private int mHeightOverride = -1;
private QSPanel mQSPanel; private QSPanel mQSPanel;
private View mQSDetail; private View mQSDetail;
@@ -53,6 +72,7 @@ public class QSContainerImpl extends FrameLayout {
private boolean mQsDisabled; private boolean mQsDisabled;
private int mContentPaddingStart = -1; private int mContentPaddingStart = -1;
private int mContentPaddingEnd = -1; private int mContentPaddingEnd = -1;
private boolean mAnimateBottomOnNextLayout;
public QSContainerImpl(Context context, AttributeSet attrs) { public QSContainerImpl(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -71,10 +91,30 @@ public class QSContainerImpl extends FrameLayout {
mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
updateResources(); updateResources();
mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> {
if (mHeader.getHeaderQsPanel().isShown()) {
mAnimateBottomOnNextLayout = true;
}
});
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
} }
private void setBackgroundBottom(int value) {
// We're saving the bottom separately since otherwise the bottom would be overridden in
// the layout and the animation wouldn't properly start at the old position.
mBackgroundBottom = value;
mBackground.setBottom(value);
}
private float getBackgroundBottom() {
if (mBackgroundBottom == -1) {
return mBackground.getBottom();
}
return mBackgroundBottom;
}
@Override @Override
protected void onConfigurationChanged(Configuration newConfig) { protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
@@ -140,7 +180,8 @@ public class QSContainerImpl extends FrameLayout {
@Override @Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom); super.onLayout(changed, left, top, right, bottom);
updateExpansion(); updateExpansion(mAnimateBottomOnNextLayout /* animate */);
mAnimateBottomOnNextLayout = false;
} }
public void disable(int state1, int state2, boolean animate) { public void disable(int state1, int state2, boolean animate) {
@@ -181,13 +222,31 @@ public class QSContainerImpl extends FrameLayout {
} }
public void updateExpansion() { public void updateExpansion() {
updateExpansion(false /* animate */);
}
public void updateExpansion(boolean animate) {
int height = calculateContainerHeight(); int height = calculateContainerHeight();
setBottom(getTop() + height); setBottom(getTop() + height);
mQSDetail.setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height);
// Pin the drag handle to the bottom of the panel. // Pin the drag handle to the bottom of the panel.
mDragHandle.setTranslationY(height - mDragHandle.getHeight()); mDragHandle.setTranslationY(height - mDragHandle.getHeight());
mBackground.setTop(mQSPanelContainer.getTop()); mBackground.setTop(mQSPanelContainer.getTop());
mBackground.setBottom(height); updateBackgroundBottom(height, animate);
}
private void updateBackgroundBottom(int height, boolean animated) {
PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this);
if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) {
// An animation is running or we want to animate
// Let's make sure to set the currentValue again, since the call below might only
// start in the next frame and otherwise we'd flicker
BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this));
physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start();
} else {
BACKGROUND_BOTTOM.setValue(this, height);
}
} }
protected int calculateContainerHeight() { protected int calculateContainerHeight() {

View File

@@ -65,6 +65,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
@@ -141,6 +142,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
private int mLastOrientation = -1; private int mLastOrientation = -1;
private int mMediaTotalBottomMargin; private int mMediaTotalBottomMargin;
private int mFooterMarginStartHorizontal; private int mFooterMarginStartHorizontal;
private Consumer<Boolean> mMediaVisibilityChangedListener;
@Inject @Inject
@@ -159,7 +161,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
R.dimen.quick_settings_bottom_margin_media); R.dimen.quick_settings_bottom_margin_media);
mMediaHost = mediaHost; mMediaHost = mediaHost;
mMediaHost.setVisibleChangedListener((visible) -> { mMediaHost.setVisibleChangedListener((visible) -> {
switchTileLayout(); onMediaVisibilityChanged(visible);
return null; return null;
}); });
mContext = context; mContext = context;
@@ -207,6 +209,13 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
updateResources(); updateResources();
} }
protected void onMediaVisibilityChanged(Boolean visible) {
switchTileLayout();
if (mMediaVisibilityChangedListener != null) {
mMediaVisibilityChangedListener.accept(visible);
}
}
protected void addSecurityFooter() { protected void addSecurityFooter() {
mSecurityFooter = new QSSecurityFooter(this, mContext); mSecurityFooter = new QSSecurityFooter(this, mContext);
} }
@@ -1065,6 +1074,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
mHeaderContainer = headerContainer; mHeaderContainer = headerContainer;
} }
public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) {
mMediaVisibilityChangedListener = visibilityChangedListener;
}
private class H extends Handler { private class H extends Handler {
private static final int SHOW_DETAIL = 1; private static final int SHOW_DETAIL = 1;
private static final int SET_TILE_VISIBILITY = 2; private static final int SET_TILE_VISIBILITY = 2;

View File

@@ -769,10 +769,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable {
return mContentTranslation; return mContentTranslation;
} }
public boolean wantsAddAndRemoveAnimations() {
return true;
}
/** Sets whether this view is the first notification in a section. */ /** Sets whether this view is the first notification in a section. */
public void setFirstInSection(boolean firstInSection) { public void setFirstInSection(boolean firstInSection) {
mFirstInSection = firstInSection; mFirstInSection = firstInSection;

View File

@@ -50,9 +50,4 @@ public class MediaHeaderView extends ExpandableView {
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
} }
@Override
public boolean wantsAddAndRemoveAnimations() {
return false;
}
} }

View File

@@ -97,6 +97,7 @@ import com.android.systemui.Interpolators;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SwipeHelper; import com.android.systemui.SwipeHelper;
import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
@@ -552,6 +553,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
SysuiStatusBarStateController statusBarStateController, SysuiStatusBarStateController statusBarStateController,
HeadsUpManagerPhone headsUpManager, HeadsUpManagerPhone headsUpManager,
KeyguardBypassController keyguardBypassController, KeyguardBypassController keyguardBypassController,
KeyguardMediaController keyguardMediaController,
FalsingManager falsingManager, FalsingManager falsingManager,
NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationLockscreenUserManager notificationLockscreenUserManager,
NotificationGutsManager notificationGutsManager, NotificationGutsManager notificationGutsManager,
@@ -670,6 +672,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
initializeForegroundServiceSection(fgsFeatureController); initializeForegroundServiceSection(fgsFeatureController);
mUiEventLogger = uiEventLogger; mUiEventLogger = uiEventLogger;
mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener); mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
keyguardMediaController.setVisibilityChangedListener((visible) -> {
if (visible) {
generateAddAnimation(keyguardMediaController.getView(), false /*fromMoreCard */);
} else {
generateRemoveAnimation(keyguardMediaController.getView());
}
requestChildrenUpdate();
return null;
});
} }
private void initializeForegroundServiceSection( private void initializeForegroundServiceSection(
@@ -3101,9 +3112,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
*/ */
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
private boolean generateRemoveAnimation(ExpandableView child) { private boolean generateRemoveAnimation(ExpandableView child) {
if (!child.wantsAddAndRemoveAnimations()) {
return false;
}
if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
mAddedHeadsUpChildren.remove(child); mAddedHeadsUpChildren.remove(child);
return false; return false;
@@ -3458,8 +3466,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@Override @Override
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) { public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden() if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
&& child.wantsAddAndRemoveAnimations()) {
// Generate Animations // Generate Animations
mChildrenToAddAnimated.add(child); mChildrenToAddAnimated.add(child);
if (fromMoreCard) { if (fromMoreCard) {
@@ -3654,6 +3661,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
ignoreChildren = false; ignoreChildren = false;
} }
childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth(); childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
} else if (child instanceof MediaHeaderView) {
childWasSwipedOut = true;
} }
if (!childWasSwipedOut) { if (!childWasSwipedOut) {
Rect clipBounds = child.getClipBounds(); Rect clipBounds = child.getClipBounds();
@@ -6370,7 +6379,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@Override @Override
public void onDragCancelled(View v) { public void onDragCancelled(View v) {
setSwipingInProgress(false); setSwipingInProgress(false);
mFalsingManager.onNotificatonStopDismissing(); mFalsingManager.onNotificationStopDismissing();
} }
/** /**
@@ -6470,7 +6479,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
@Override @Override
public void onBeginDrag(View v) { public void onBeginDrag(View v) {
mFalsingManager.onNotificatonStartDismissing(); mFalsingManager.onNotificationStartDismissing();
setSwipingInProgress(true); setSwipingInProgress(true);
mAmbientState.onBeginDrag((ExpandableView) v); mAmbientState.onBeginDrag((ExpandableView) v);
updateContinuousShadowDrawing(); updateContinuousShadowDrawing();

View File

@@ -46,6 +46,9 @@ open class TransitionLayoutController {
private var state = TransitionViewState() private var state = TransitionViewState()
private var pivot = PointF() private var pivot = PointF()
private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
private var currentHeight: Int = 0
private var currentWidth: Int = 0
var sizeChangedListener: ((Int, Int) -> Unit)? = null
init { init {
animator.apply { animator.apply {
@@ -67,7 +70,16 @@ open class TransitionLayoutController {
progress = animator.animatedFraction, progress = animator.animatedFraction,
pivot = pivot, pivot = pivot,
resultState = currentState) resultState = currentState)
view.setState(currentState) applyStateToLayout(currentState)
}
private fun applyStateToLayout(state: TransitionViewState) {
transitionLayout?.setState(state)
if (currentHeight != state.height || currentWidth != state.width) {
currentHeight = state.height
currentWidth = state.width
sizeChangedListener?.invoke(currentWidth, currentHeight)
}
} }
/** /**
@@ -213,7 +225,7 @@ open class TransitionLayoutController {
this.state = state.copy() this.state = state.copy()
if (applyImmediately || transitionLayout == null) { if (applyImmediately || transitionLayout == null) {
animator.cancel() animator.cancel()
transitionLayout?.setState(this.state) applyStateToLayout(this.state)
currentState = state.copy(reusedState = currentState) currentState = state.copy(reusedState = currentState)
} else if (animated) { } else if (animated) {
animationStartState = currentState.copy() animationStartState = currentState.copy()
@@ -221,7 +233,7 @@ open class TransitionLayoutController {
animator.startDelay = delay animator.startDelay = delay
animator.start() animator.start()
} else if (!animator.isRunning) { } else if (!animator.isRunning) {
transitionLayout?.setState(this.state) applyStateToLayout(this.state)
currentState = state.copy(reusedState = currentState) currentState = state.copy(reusedState = currentState)
} }
// otherwise the desired state was updated and the animation will go to the new target // otherwise the desired state was updated and the animation will go to the new target

View File

@@ -70,7 +70,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Mock @Mock
private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
@Mock @Mock
private lateinit var mediaViewManager: MediaViewManager private lateinit var mediaCarouselController: MediaCarouselController
@Mock @Mock
private lateinit var wakefulnessLifecycle: WakefulnessLifecycle private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Captor @Captor
@@ -82,13 +82,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Before @Before
fun setup() { fun setup() {
`when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame) `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
mediaHiearchyManager = MediaHierarchyManager( mediaHiearchyManager = MediaHierarchyManager(
context, context,
statusBarStateController, statusBarStateController,
keyguardStateController, keyguardStateController,
bypassController, bypassController,
mediaViewManager, mediaCarouselController,
notificationLockscreenUserManager, notificationLockscreenUserManager,
wakefulnessLifecycle) wakefulnessLifecycle)
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
@@ -97,7 +97,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE) `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
// We'll use the viewmanager to verify a few calls below, let's reset this. // We'll use the viewmanager to verify a few calls below, let's reset this.
clearInvocations(mediaViewManager) clearInvocations(mediaCarouselController)
} }
@@ -118,14 +118,14 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
fun testBlockedWhenScreenTurningOff() { fun testBlockedWhenScreenTurningOff() {
// Let's set it onto QS: // Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer) assertNotNull("lifecycle observer wasn't registered", observer)
observer.onStartedGoingToSleep() observer.onStartedGoingToSleep()
clearInvocations(mediaViewManager) clearInvocations(mediaCarouselController)
mediaHiearchyManager.qsExpansion = 0.0f mediaHiearchyManager.qsExpansion = 0.0f
verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
} }
@@ -133,13 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
fun testAllowedWhenNotTurningOff() { fun testAllowedWhenNotTurningOff() {
// Let's set it onto QS: // Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer) assertNotNull("lifecycle observer wasn't registered", observer)
clearInvocations(mediaViewManager) clearInvocations(mediaCarouselController)
mediaHiearchyManager.qsExpansion = 0.0f mediaHiearchyManager.qsExpansion = 0.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
} }
} }

View File

@@ -53,6 +53,7 @@ import com.android.systemui.ExpandHelper;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.FeatureFlags;
@@ -133,6 +134,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Mock private MetricsLogger mMetricsLogger; @Mock private MetricsLogger mMetricsLogger;
@Mock private NotificationRoundnessManager mNotificationRoundnessManager; @Mock private NotificationRoundnessManager mNotificationRoundnessManager;
@Mock private KeyguardBypassController mKeyguardBypassController; @Mock private KeyguardBypassController mKeyguardBypassController;
@Mock private KeyguardMediaController mKeyguardMediaController;
@Mock private ZenModeController mZenModeController; @Mock private ZenModeController mZenModeController;
@Mock private NotificationSectionsManager mNotificationSectionsManager; @Mock private NotificationSectionsManager mNotificationSectionsManager;
@Mock private NotificationSection mNotificationSection; @Mock private NotificationSection mNotificationSection;
@@ -209,6 +211,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
mock(SysuiStatusBarStateController.class), mock(SysuiStatusBarStateController.class),
mHeadsUpManager, mHeadsUpManager,
mKeyguardBypassController, mKeyguardBypassController,
mKeyguardMediaController,
new FalsingManagerFake(), new FalsingManagerFake(),
mLockscreenUserManager, mLockscreenUserManager,
mock(NotificationGutsManager.class), mock(NotificationGutsManager.class),