Merge "Making the media carousel dismissable" into rvc-dev am: 82359e319e
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/11885872 Change-Id: I9fb068c29db1a3767d8c1a7653e220f8e29326d3
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -381,7 +381,7 @@ public class BrightLineFalsingManager implements FalsingManager {
|
||||
|
||||
|
||||
@Override
|
||||
public void onNotificatonStopDismissing() {
|
||||
public void onNotificationStopDismissing() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -389,7 +389,7 @@ public class BrightLineFalsingManager implements FalsingManager {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificatonStartDismissing() {
|
||||
public void onNotificationStartDismissing() {
|
||||
updateInteractionType(Classifier.NOTIFICATION_DISMISS);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user