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)
public interface FalsingManager {
int VERSION = 3;
int VERSION = 4;
void onSuccessfulUnlock();
@@ -88,11 +88,11 @@ public interface FalsingManager {
void onScreenOff();
void onNotificatonStopDismissing();
void onNotificationStopDismissing();
void onNotificationDismissed();
void onNotificatonStartDismissing();
void onNotificationStartDismissing();
void onNotificationDoubleTap(boolean accepted, float dx, float dy);

View File

@@ -23,7 +23,7 @@
android:clipChildren="false"
android:clipToPadding="false"
>
<com.android.systemui.media.UnboundHorizontalScrollView
<com.android.systemui.media.MediaScrollView
android:id="@+id/media_carousel_scroller"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -41,14 +41,12 @@
>
<!-- QSMediaPlayers will be added here dynamically -->
</LinearLayout>
</com.android.systemui.media.UnboundHorizontalScrollView>
</com.android.systemui.media.MediaScrollView>
<com.android.systemui.qs.PageIndicator
android:id="@+id/media_page_indicator"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginBottom="4dp"
android:layout_gravity="center_horizontal|bottom"
android:gravity="center"
android:tint="@color/media_primary_text"
/>
</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
public void onNotificatonStopDismissing() {
public void onNotificationStopDismissing() {
}
@@ -211,7 +211,7 @@ public class FalsingManagerFake implements FalsingManager {
}
@Override
public void onNotificatonStartDismissing() {
public void onNotificationStartDismissing() {
}

View File

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

View File

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

View File

@@ -380,7 +380,7 @@ public class BrightLineFalsingManager implements FalsingManager {
@Override
public void onNotificatonStopDismissing() {
public void onNotificationStopDismissing() {
}
@Override
@@ -388,7 +388,7 @@ public class BrightLineFalsingManager implements FalsingManager {
}
@Override
public void onNotificatonStartDismissing() {
public void onNotificationStartDismissing() {
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
@@ -57,6 +59,7 @@ class KeyguardMediaController @Inject constructor(
mediaHost.visibleChangedListener = { updateVisibility() }
mediaHost.expansion = 0.0f
mediaHost.showsOnlyActiveMedia = true
mediaHost.falsingProtectionNeeded = true
// Let's now initialize this view, which also creates the host view for us.
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
@@ -70,6 +73,11 @@ class KeyguardMediaController @Inject constructor(
!bypassController.bypassEnabled &&
keyguardOrUserSwitcher &&
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
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.view.LayoutInflater
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.HorizontalScrollView
import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat
import com.android.systemui.R
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor
import javax.inject.Inject
import javax.inject.Provider
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.
* This also handles changes in state and applies them to the media carousel like the expansion.
*/
@Singleton
class MediaViewManager @Inject constructor(
class MediaCarouselController @Inject constructor(
private val context: Context,
private val mediaControlPanelFactory: Provider<MediaControlPanel>,
private val visualStabilityManager: VisualStabilityManager,
private val mediaHostStatesManager: MediaHostStatesManager,
private val activityStarter: ActivityStarter,
@Main executor: DelayableExecutor,
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 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 playerWidthPlusPadding: Int = 0
private var desiredHostState: MediaHostState? = null
private val mediaCarousel: HorizontalScrollView
private val mediaCarousel: MediaScrollView
private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
val mediaFrame: ViewGroup
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private lateinit var settingsButton: View
private val mediaData: MutableMap<String, MediaData> = mutableMapOf()
private val mediaContent: ViewGroup
private val pageIndicator: PageIndicator
private val gestureDetector: GestureDetectorCompat
private val visualStabilityCallback: VisualStabilityManager.Callback
private var activeMediaIndex: Int = 0
private var needsReordering: Boolean = false
private var scrollIntoCurrentMedia: Int = 0
private var currentlyExpanded = true
set(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 {
override fun onDensityOrFontScaleChanged() {
recreatePlayers()
inflateSettingsButton()
}
override fun onOverlayChanged() {
inflateSettingsButton()
}
}
init {
gestureDetector = GestureDetectorCompat(context, gestureListener)
mediaFrame = inflateMediaCarousel()
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
mediaCarousel.setOnTouchListener(touchListener)
mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER)
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
falsingManager)
inflateSettingsButton()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
configurationController.addCallback(configListener)
visualStabilityCallback = VisualStabilityManager.Callback {
@@ -161,6 +159,11 @@ class MediaViewManager @Inject constructor(
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 {
override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
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 {
return LayoutInflater.from(context).inflate(R.layout.media_carousel,
UniqueObjectHostView(context), false) as ViewGroup
@@ -183,68 +200,7 @@ class MediaViewManager @Inject constructor(
mediaContent.addView(view, 0)
}
}
updateMediaPaddings()
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
}
mediaCarouselScrollHandler.onPlayersChanged()
}
private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
@@ -259,6 +215,7 @@ class MediaViewManager @Inject constructor(
existingPlayer = mediaControlPanelFactory.get()
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent))
existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
mediaPlayers[key] = existingPlayer
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
@@ -280,28 +237,18 @@ class MediaViewManager @Inject constructor(
}
}
existingPlayer?.bind(data)
updateMediaPaddings()
updatePageIndicator()
updatePlayerVisibilities()
mediaCarouselScrollHandler.onPlayersChanged()
mediaCarousel.requiresRemeasuring = true
}
private fun removePlayer(key: String) {
val removed = mediaPlayers.remove(key)
removed?.apply {
val beforeActive = mediaContent.indexOfChild(removed.view?.player) <=
activeMediaIndex
mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
mediaContent.removeView(removed.view?.player)
removed.onDestroy()
updateMediaPaddings()
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()
mediaCarouselScrollHandler.onPlayersChanged()
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() {
val numPages = mediaContent.getChildCount()
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
* 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(
@MediaLocation startLocation: Int,
@@ -349,9 +288,6 @@ class MediaViewManager @Inject constructor(
progress: Float,
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 ||
endLocation != currentEndLocation ||
progress != currentTransitionProgress ||
@@ -363,6 +299,51 @@ class MediaViewManager @Inject constructor(
for (mediaPlayer in mediaPlayers.values) {
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)
}
mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
val nowVisible = it.visible
if (nowVisible != playersVisible) {
playersVisible = nowVisible
if (nowVisible) {
mediaCarouselScrollHandler.resetTranslation()
}
}
updateCarouselSize()
}
}
@@ -420,16 +410,7 @@ class MediaViewManager @Inject constructor(
carouselMeasureHeight = height
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
R.dimen.qs_media_padding)
// 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
}
mediaCarousel.scrollX = newScroll
mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
// Let's remeasure the carousel
val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 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 {
/**

View File

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

View File

@@ -113,8 +113,11 @@ class MediaHost @Inject constructor(
} else {
mediaDataManager.hasAnyMedia()
}
hostView.visibility = if (visible) View.VISIBLE else View.GONE
visibleChangedListener?.invoke(visible)
val newVisibility = if (visible) View.VISIBLE else View.GONE
if (newVisibility != hostView.visibility) {
hostView.visibility = newVisibility
visibleChangedListener?.invoke(visible)
}
}
class MediaHostStateHolder @Inject constructor() : MediaHostState {
@@ -153,6 +156,15 @@ class MediaHost @Inject constructor(
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 getPivotY(): Float = gonePivot.y
override fun setGonePivot(x: Float, y: Float) {
@@ -178,6 +190,7 @@ class MediaHost @Inject constructor(
mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.visible = visible
mediaHostState.gonePivot.set(gonePivot)
mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
return mediaHostState
}
@@ -197,6 +210,9 @@ class MediaHost @Inject constructor(
if (visible != other.visible) {
return false
}
if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
return false
}
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
return false
}
@@ -206,6 +222,7 @@ class MediaHost @Inject constructor(
override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode()
result = 31 * result + falsingProtectionNeeded.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
result = 31 * result + if (visible) 1 else 2
result = 31 * result + gonePivot.hashCode()
@@ -238,6 +255,11 @@ interface MediaHostState {
*/
var visible: Boolean
/**
* Does this host need any falsing protection?
*/
var falsingProtectionNeeded: Boolean
/**
* 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,

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
) {
/**
* A listener when the current dimensions of the player change
*/
lateinit var sizeChangedListener: () -> Unit
private var firstRefresh: Boolean = true
private var transitionLayout: TransitionLayout? = null
private val layoutController = TransitionLayoutController()
@@ -75,6 +79,17 @@ class MediaViewController @Inject constructor(
*/
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
*/
@@ -105,6 +120,11 @@ class MediaViewController @Inject constructor(
collapsedLayout.load(context, R.xml.media_collapsed)
expandedLayout.load(context, R.xml.media_expanded)
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)
tmpState
}
currentWidth = result.width
currentHeight = result.height
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
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.widget.FrameLayout;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.systemui.R;
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}
@@ -35,7 +39,22 @@ import com.android.systemui.qs.customize.QSCustomizer;
public class QSContainerImpl extends FrameLayout {
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 QSPanel mQSPanel;
private View mQSDetail;
@@ -53,6 +72,7 @@ public class QSContainerImpl extends FrameLayout {
private boolean mQsDisabled;
private int mContentPaddingStart = -1;
private int mContentPaddingEnd = -1;
private boolean mAnimateBottomOnNextLayout;
public QSContainerImpl(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -71,10 +91,30 @@ public class QSContainerImpl extends FrameLayout {
mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
updateResources();
mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> {
if (mHeader.getHeaderQsPanel().isShown()) {
mAnimateBottomOnNextLayout = true;
}
});
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
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -140,7 +180,8 @@ public class QSContainerImpl extends FrameLayout {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
updateExpansion();
updateExpansion(mAnimateBottomOnNextLayout /* animate */);
mAnimateBottomOnNextLayout = false;
}
public void disable(int state1, int state2, boolean animate) {
@@ -181,13 +222,31 @@ public class QSContainerImpl extends FrameLayout {
}
public void updateExpansion() {
updateExpansion(false /* animate */);
}
public void updateExpansion(boolean animate) {
int height = calculateContainerHeight();
setBottom(getTop() + height);
mQSDetail.setBottom(getTop() + height);
// Pin the drag handle to the bottom of the panel.
mDragHandle.setTranslationY(height - mDragHandle.getHeight());
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() {

View File

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

View File

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

View File

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

View File

@@ -46,6 +46,9 @@ open class TransitionLayoutController {
private var state = TransitionViewState()
private var pivot = PointF()
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 {
animator.apply {
@@ -67,7 +70,16 @@ open class TransitionLayoutController {
progress = animator.animatedFraction,
pivot = pivot,
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()
if (applyImmediately || transitionLayout == null) {
animator.cancel()
transitionLayout?.setState(this.state)
applyStateToLayout(this.state)
currentState = state.copy(reusedState = currentState)
} else if (animated) {
animationStartState = currentState.copy()
@@ -221,7 +233,7 @@ open class TransitionLayoutController {
animator.startDelay = delay
animator.start()
} else if (!animator.isRunning) {
transitionLayout?.setState(this.state)
applyStateToLayout(this.state)
currentState = state.copy(reusedState = currentState)
}
// 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
private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
@Mock
private lateinit var mediaViewManager: MediaViewManager
private lateinit var mediaCarouselController: MediaCarouselController
@Mock
private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Captor
@@ -82,13 +82,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
@Before
fun setup() {
`when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
`when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
mediaHiearchyManager = MediaHierarchyManager(
context,
statusBarStateController,
keyguardStateController,
bypassController,
mediaViewManager,
mediaCarouselController,
notificationLockscreenUserManager,
wakefulnessLifecycle)
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
@@ -97,7 +97,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
// 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() {
// Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer)
observer.onStartedGoingToSleep()
clearInvocations(mediaViewManager)
clearInvocations(mediaCarouselController)
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())
}
@@ -133,13 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
fun testAllowedWhenNotTurningOff() {
// Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer)
clearInvocations(mediaViewManager)
clearInvocations(mediaCarouselController)
mediaHiearchyManager.qsExpansion = 0.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
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.SysuiTestCase;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.media.KeyguardMediaController;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.FeatureFlags;
@@ -133,6 +134,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
@Mock private MetricsLogger mMetricsLogger;
@Mock private NotificationRoundnessManager mNotificationRoundnessManager;
@Mock private KeyguardBypassController mKeyguardBypassController;
@Mock private KeyguardMediaController mKeyguardMediaController;
@Mock private ZenModeController mZenModeController;
@Mock private NotificationSectionsManager mNotificationSectionsManager;
@Mock private NotificationSection mNotificationSection;
@@ -209,6 +211,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
mock(SysuiStatusBarStateController.class),
mHeadsUpManager,
mKeyguardBypassController,
mKeyguardMediaController,
new FalsingManagerFake(),
mLockscreenUserManager,
mock(NotificationGutsManager.class),