Merge "Making the media carousel dismissable" into rvc-dev
This commit is contained in:
@@ -30,7 +30,7 @@ import java.io.PrintWriter;
|
|||||||
*/
|
*/
|
||||||
@ProvidesInterface(version = FalsingManager.VERSION)
|
@ProvidesInterface(version = FalsingManager.VERSION)
|
||||||
public interface FalsingManager {
|
public interface FalsingManager {
|
||||||
int VERSION = 3;
|
int VERSION = 4;
|
||||||
|
|
||||||
void onSuccessfulUnlock();
|
void onSuccessfulUnlock();
|
||||||
|
|
||||||
@@ -88,11 +88,11 @@ public interface FalsingManager {
|
|||||||
|
|
||||||
void onScreenOff();
|
void onScreenOff();
|
||||||
|
|
||||||
void onNotificatonStopDismissing();
|
void onNotificationStopDismissing();
|
||||||
|
|
||||||
void onNotificationDismissed();
|
void onNotificationDismissed();
|
||||||
|
|
||||||
void onNotificatonStartDismissing();
|
void onNotificationStartDismissing();
|
||||||
|
|
||||||
void onNotificationDoubleTap(boolean accepted, float dx, float dy);
|
void onNotificationDoubleTap(boolean accepted, float dx, float dy);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
>
|
>
|
||||||
<com.android.systemui.media.UnboundHorizontalScrollView
|
<com.android.systemui.media.MediaScrollView
|
||||||
android:id="@+id/media_carousel_scroller"
|
android:id="@+id/media_carousel_scroller"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -41,14 +41,12 @@
|
|||||||
>
|
>
|
||||||
<!-- QSMediaPlayers will be added here dynamically -->
|
<!-- QSMediaPlayers will be added here dynamically -->
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.android.systemui.media.UnboundHorizontalScrollView>
|
</com.android.systemui.media.MediaScrollView>
|
||||||
<com.android.systemui.qs.PageIndicator
|
<com.android.systemui.qs.PageIndicator
|
||||||
android:id="@+id/media_page_indicator"
|
android:id="@+id/media_page_indicator"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:layout_gravity="center_horizontal|bottom"
|
|
||||||
android:gravity="center"
|
|
||||||
android:tint="@color/media_primary_text"
|
android:tint="@color/media_primary_text"
|
||||||
/>
|
/>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -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
|
@Override
|
||||||
public void onNotificatonStopDismissing() {
|
public void onNotificationStopDismissing() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ public class FalsingManagerFake implements FalsingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNotificatonStartDismissing() {
|
public void onNotificationStartDismissing() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -481,15 +481,15 @@ public class FalsingManagerImpl implements FalsingManager {
|
|||||||
mDataCollector.onNotificationDismissed();
|
mDataCollector.onNotificationDismissed();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onNotificatonStartDismissing() {
|
public void onNotificationStartDismissing() {
|
||||||
if (FalsingLog.ENABLED) {
|
if (FalsingLog.ENABLED) {
|
||||||
FalsingLog.i("onNotificatonStartDismissing", "");
|
FalsingLog.i("onNotificationStartDismissing", "");
|
||||||
}
|
}
|
||||||
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
|
mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS);
|
||||||
mDataCollector.onNotificatonStartDismissing();
|
mDataCollector.onNotificatonStartDismissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onNotificatonStopDismissing() {
|
public void onNotificationStopDismissing() {
|
||||||
mDataCollector.onNotificatonStopDismissing();
|
mDataCollector.onNotificatonStopDismissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,8 +302,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNotificatonStopDismissing() {
|
public void onNotificationStopDismissing() {
|
||||||
mInternalFalsingManager.onNotificatonStopDismissing();
|
mInternalFalsingManager.onNotificationStopDismissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -312,8 +312,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNotificatonStartDismissing() {
|
public void onNotificationStartDismissing() {
|
||||||
mInternalFalsingManager.onNotificatonStartDismissing();
|
mInternalFalsingManager.onNotificationStartDismissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ public class BrightLineFalsingManager implements FalsingManager {
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNotificatonStopDismissing() {
|
public void onNotificationStopDismissing() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -388,7 +388,7 @@ public class BrightLineFalsingManager implements FalsingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNotificatonStartDismissing() {
|
public void onNotificationStartDismissing() {
|
||||||
updateInteractionType(Classifier.NOTIFICATION_DISMISS);
|
updateInteractionType(Classifier.NOTIFICATION_DISMISS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ class KeyguardMediaController @Inject constructor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private var view: MediaHeaderView? = null
|
var visibilityChangedListener: ((Boolean) -> Unit)? = null
|
||||||
|
var view: MediaHeaderView? = null
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach this controller to a media view, initializing its state
|
* Attach this controller to a media view, initializing its state
|
||||||
@@ -57,6 +59,7 @@ class KeyguardMediaController @Inject constructor(
|
|||||||
mediaHost.visibleChangedListener = { updateVisibility() }
|
mediaHost.visibleChangedListener = { updateVisibility() }
|
||||||
mediaHost.expansion = 0.0f
|
mediaHost.expansion = 0.0f
|
||||||
mediaHost.showsOnlyActiveMedia = true
|
mediaHost.showsOnlyActiveMedia = true
|
||||||
|
mediaHost.falsingProtectionNeeded = true
|
||||||
|
|
||||||
// Let's now initialize this view, which also creates the host view for us.
|
// Let's now initialize this view, which also creates the host view for us.
|
||||||
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
|
mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN)
|
||||||
@@ -70,6 +73,11 @@ class KeyguardMediaController @Inject constructor(
|
|||||||
!bypassController.bypassEnabled &&
|
!bypassController.bypassEnabled &&
|
||||||
keyguardOrUserSwitcher &&
|
keyguardOrUserSwitcher &&
|
||||||
notifLockscreenUserManager.shouldShowLockscreenNotifications()
|
notifLockscreenUserManager.shouldShowLockscreenNotifications()
|
||||||
view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE
|
val previousVisibility = view?.visibility ?: View.GONE
|
||||||
|
val newVisibility = if (shouldBeVisible) View.VISIBLE else View.GONE
|
||||||
|
view?.visibility = newVisibility
|
||||||
|
if (previousVisibility != newVisibility) {
|
||||||
|
visibilityChangedListener?.invoke(shouldBeVisible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,41 +1,65 @@
|
|||||||
package com.android.systemui.media
|
package com.android.systemui.media
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.GestureDetector
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.HorizontalScrollView
|
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.core.view.GestureDetectorCompat
|
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
|
import com.android.systemui.dagger.qualifiers.Main
|
||||||
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
|
import com.android.systemui.plugins.FalsingManager
|
||||||
import com.android.systemui.qs.PageIndicator
|
import com.android.systemui.qs.PageIndicator
|
||||||
import com.android.systemui.statusbar.notification.VisualStabilityManager
|
import com.android.systemui.statusbar.notification.VisualStabilityManager
|
||||||
import com.android.systemui.statusbar.policy.ConfigurationController
|
import com.android.systemui.statusbar.policy.ConfigurationController
|
||||||
import com.android.systemui.util.animation.UniqueObjectHostView
|
import com.android.systemui.util.animation.UniqueObjectHostView
|
||||||
import com.android.systemui.util.animation.requiresRemeasuring
|
import com.android.systemui.util.animation.requiresRemeasuring
|
||||||
|
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
private const val FLING_SLOP = 1000000
|
private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that is responsible for keeping the view carousel up to date.
|
* Class that is responsible for keeping the view carousel up to date.
|
||||||
* This also handles changes in state and applies them to the media carousel like the expansion.
|
* This also handles changes in state and applies them to the media carousel like the expansion.
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
class MediaViewManager @Inject constructor(
|
class MediaCarouselController @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mediaControlPanelFactory: Provider<MediaControlPanel>,
|
private val mediaControlPanelFactory: Provider<MediaControlPanel>,
|
||||||
private val visualStabilityManager: VisualStabilityManager,
|
private val visualStabilityManager: VisualStabilityManager,
|
||||||
private val mediaHostStatesManager: MediaHostStatesManager,
|
private val mediaHostStatesManager: MediaHostStatesManager,
|
||||||
|
private val activityStarter: ActivityStarter,
|
||||||
|
@Main executor: DelayableExecutor,
|
||||||
mediaManager: MediaDataCombineLatest,
|
mediaManager: MediaDataCombineLatest,
|
||||||
configurationController: ConfigurationController
|
configurationController: ConfigurationController,
|
||||||
|
mediaDataManager: MediaDataManager,
|
||||||
|
falsingManager: FalsingManager
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* The current width of the carousel
|
||||||
|
*/
|
||||||
|
private var currentCarouselWidth: Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current height of the carousel
|
||||||
|
*/
|
||||||
|
private var currentCarouselHeight: Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Are we currently showing only active players
|
||||||
|
*/
|
||||||
|
private var currentlyShowingOnlyActive: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the player currently visible (at the end of the transformation
|
||||||
|
*/
|
||||||
|
private var playersVisible: Boolean = false
|
||||||
/**
|
/**
|
||||||
* The desired location where we'll be at the end of the transformation. Usually this matches
|
* The desired location where we'll be at the end of the transformation. Usually this matches
|
||||||
* the end location, except when we're still waiting on a state update call.
|
* the end location, except when we're still waiting on a state update call.
|
||||||
@@ -73,17 +97,16 @@ class MediaViewManager @Inject constructor(
|
|||||||
private var carouselMeasureHeight: Int = 0
|
private var carouselMeasureHeight: Int = 0
|
||||||
private var playerWidthPlusPadding: Int = 0
|
private var playerWidthPlusPadding: Int = 0
|
||||||
private var desiredHostState: MediaHostState? = null
|
private var desiredHostState: MediaHostState? = null
|
||||||
private val mediaCarousel: HorizontalScrollView
|
private val mediaCarousel: MediaScrollView
|
||||||
|
private val mediaCarouselScrollHandler: MediaCarouselScrollHandler
|
||||||
val mediaFrame: ViewGroup
|
val mediaFrame: ViewGroup
|
||||||
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
|
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
|
||||||
|
private lateinit var settingsButton: View
|
||||||
private val mediaData: MutableMap<String, MediaData> = mutableMapOf()
|
private val mediaData: MutableMap<String, MediaData> = mutableMapOf()
|
||||||
private val mediaContent: ViewGroup
|
private val mediaContent: ViewGroup
|
||||||
private val pageIndicator: PageIndicator
|
private val pageIndicator: PageIndicator
|
||||||
private val gestureDetector: GestureDetectorCompat
|
|
||||||
private val visualStabilityCallback: VisualStabilityManager.Callback
|
private val visualStabilityCallback: VisualStabilityManager.Callback
|
||||||
private var activeMediaIndex: Int = 0
|
|
||||||
private var needsReordering: Boolean = false
|
private var needsReordering: Boolean = false
|
||||||
private var scrollIntoCurrentMedia: Int = 0
|
|
||||||
private var currentlyExpanded = true
|
private var currentlyExpanded = true
|
||||||
set(value) {
|
set(value) {
|
||||||
if (field != value) {
|
if (field != value) {
|
||||||
@@ -93,50 +116,25 @@ class MediaViewManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val scrollChangedListener = object : View.OnScrollChangeListener {
|
|
||||||
override fun onScrollChange(
|
|
||||||
v: View?,
|
|
||||||
scrollX: Int,
|
|
||||||
scrollY: Int,
|
|
||||||
oldScrollX: Int,
|
|
||||||
oldScrollY: Int
|
|
||||||
) {
|
|
||||||
if (playerWidthPlusPadding == 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
onMediaScrollingChanged(scrollX / playerWidthPlusPadding,
|
|
||||||
scrollX % playerWidthPlusPadding)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
|
|
||||||
override fun onFling(
|
|
||||||
eStart: MotionEvent?,
|
|
||||||
eCurrent: MotionEvent?,
|
|
||||||
vX: Float,
|
|
||||||
vY: Float
|
|
||||||
): Boolean {
|
|
||||||
return this@MediaViewManager.onFling(eStart, eCurrent, vX, vY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val touchListener = object : View.OnTouchListener {
|
|
||||||
override fun onTouch(view: View, motionEvent: MotionEvent?): Boolean {
|
|
||||||
return this@MediaViewManager.onTouch(view, motionEvent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val configListener = object : ConfigurationController.ConfigurationListener {
|
private val configListener = object : ConfigurationController.ConfigurationListener {
|
||||||
override fun onDensityOrFontScaleChanged() {
|
override fun onDensityOrFontScaleChanged() {
|
||||||
recreatePlayers()
|
recreatePlayers()
|
||||||
|
inflateSettingsButton()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOverlayChanged() {
|
||||||
|
inflateSettingsButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
gestureDetector = GestureDetectorCompat(context, gestureListener)
|
|
||||||
mediaFrame = inflateMediaCarousel()
|
mediaFrame = inflateMediaCarousel()
|
||||||
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
|
mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller)
|
||||||
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
|
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
|
||||||
mediaCarousel.setOnScrollChangeListener(scrollChangedListener)
|
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
|
||||||
mediaCarousel.setOnTouchListener(touchListener)
|
executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
|
||||||
mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER)
|
falsingManager)
|
||||||
|
inflateSettingsButton()
|
||||||
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
|
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
|
||||||
configurationController.addCallback(configListener)
|
configurationController.addCallback(configListener)
|
||||||
visualStabilityCallback = VisualStabilityManager.Callback {
|
visualStabilityCallback = VisualStabilityManager.Callback {
|
||||||
@@ -161,6 +159,11 @@ class MediaViewManager @Inject constructor(
|
|||||||
removePlayer(key)
|
removePlayer(key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||||
|
// The pageIndicator is not laid out yet when we get the current state update,
|
||||||
|
// Lets make sure we have the right dimensions
|
||||||
|
updatePageIndicatorLocation()
|
||||||
|
}
|
||||||
mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
|
mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
|
||||||
override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
|
override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
|
||||||
if (location == desiredLocation) {
|
if (location == desiredLocation) {
|
||||||
@@ -170,6 +173,20 @@ class MediaViewManager @Inject constructor(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun inflateSettingsButton() {
|
||||||
|
val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button,
|
||||||
|
mediaFrame, false) as View
|
||||||
|
if (this::settingsButton.isInitialized) {
|
||||||
|
mediaFrame.removeView(settingsButton)
|
||||||
|
}
|
||||||
|
settingsButton = settings
|
||||||
|
mediaFrame.addView(settingsButton)
|
||||||
|
mediaCarouselScrollHandler.onSettingsButtonUpdated(settings)
|
||||||
|
settingsButton.setOnClickListener {
|
||||||
|
activityStarter.startActivity(settingsIntent, true /* dismissShade */)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun inflateMediaCarousel(): ViewGroup {
|
private fun inflateMediaCarousel(): ViewGroup {
|
||||||
return LayoutInflater.from(context).inflate(R.layout.media_carousel,
|
return LayoutInflater.from(context).inflate(R.layout.media_carousel,
|
||||||
UniqueObjectHostView(context), false) as ViewGroup
|
UniqueObjectHostView(context), false) as ViewGroup
|
||||||
@@ -183,68 +200,7 @@ class MediaViewManager @Inject constructor(
|
|||||||
mediaContent.addView(view, 0)
|
mediaContent.addView(view, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateMediaPaddings()
|
mediaCarouselScrollHandler.onPlayersChanged()
|
||||||
updatePlayerVisibilities()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) {
|
|
||||||
val wasScrolledIn = scrollIntoCurrentMedia != 0
|
|
||||||
scrollIntoCurrentMedia = scrollInAmount
|
|
||||||
val nowScrolledIn = scrollIntoCurrentMedia != 0
|
|
||||||
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
|
|
||||||
activeMediaIndex = newIndex
|
|
||||||
updatePlayerVisibilities()
|
|
||||||
}
|
|
||||||
val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
|
|
||||||
scrollInAmount.toFloat() / playerWidthPlusPadding else 0f
|
|
||||||
pageIndicator.setLocation(location)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onTouch(view: View, motionEvent: MotionEvent?): Boolean {
|
|
||||||
if (gestureDetector.onTouchEvent(motionEvent)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (motionEvent?.getAction() == MotionEvent.ACTION_UP) {
|
|
||||||
val pos = mediaCarousel.scrollX % playerWidthPlusPadding
|
|
||||||
if (pos > playerWidthPlusPadding / 2) {
|
|
||||||
mediaCarousel.smoothScrollBy(playerWidthPlusPadding - pos, 0)
|
|
||||||
} else {
|
|
||||||
mediaCarousel.smoothScrollBy(-1 * pos, 0)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return view.onTouchEvent(motionEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFling(
|
|
||||||
eStart: MotionEvent?,
|
|
||||||
eCurrent: MotionEvent?,
|
|
||||||
vX: Float,
|
|
||||||
vY: Float
|
|
||||||
): Boolean {
|
|
||||||
if (vX * vX < 0.5 * vY * vY) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (vX * vX < FLING_SLOP) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val pos = mediaCarousel.scrollX
|
|
||||||
val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0
|
|
||||||
var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex
|
|
||||||
destIndex = Math.max(0, destIndex)
|
|
||||||
destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex)
|
|
||||||
val view = mediaContent.getChildAt(destIndex)
|
|
||||||
mediaCarousel.smoothScrollTo(view.left, mediaCarousel.scrollY)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePlayerVisibilities() {
|
|
||||||
val scrolledIn = scrollIntoCurrentMedia != 0
|
|
||||||
for (i in 0 until mediaContent.childCount) {
|
|
||||||
val view = mediaContent.getChildAt(i)
|
|
||||||
val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn)
|
|
||||||
view.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
|
private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) {
|
||||||
@@ -259,6 +215,7 @@ class MediaViewManager @Inject constructor(
|
|||||||
existingPlayer = mediaControlPanelFactory.get()
|
existingPlayer = mediaControlPanelFactory.get()
|
||||||
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
||||||
mediaContent))
|
mediaContent))
|
||||||
|
existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
|
||||||
mediaPlayers[key] = existingPlayer
|
mediaPlayers[key] = existingPlayer
|
||||||
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
@@ -280,28 +237,18 @@ class MediaViewManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
existingPlayer?.bind(data)
|
existingPlayer?.bind(data)
|
||||||
updateMediaPaddings()
|
|
||||||
updatePageIndicator()
|
updatePageIndicator()
|
||||||
updatePlayerVisibilities()
|
mediaCarouselScrollHandler.onPlayersChanged()
|
||||||
mediaCarousel.requiresRemeasuring = true
|
mediaCarousel.requiresRemeasuring = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removePlayer(key: String) {
|
private fun removePlayer(key: String) {
|
||||||
val removed = mediaPlayers.remove(key)
|
val removed = mediaPlayers.remove(key)
|
||||||
removed?.apply {
|
removed?.apply {
|
||||||
val beforeActive = mediaContent.indexOfChild(removed.view?.player) <=
|
mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
|
||||||
activeMediaIndex
|
|
||||||
mediaContent.removeView(removed.view?.player)
|
mediaContent.removeView(removed.view?.player)
|
||||||
removed.onDestroy()
|
removed.onDestroy()
|
||||||
updateMediaPaddings()
|
mediaCarouselScrollHandler.onPlayersChanged()
|
||||||
if (beforeActive) {
|
|
||||||
// also update the index here since the scroll below might not always lead
|
|
||||||
// to a scrolling changed
|
|
||||||
activeMediaIndex = Math.max(0, activeMediaIndex - 1)
|
|
||||||
mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX -
|
|
||||||
playerWidthPlusPadding, 0)
|
|
||||||
}
|
|
||||||
updatePlayerVisibilities()
|
|
||||||
updatePageIndicator()
|
updatePageIndicator()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,20 +264,6 @@ class MediaViewManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateMediaPaddings() {
|
|
||||||
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
|
|
||||||
val childCount = mediaContent.childCount
|
|
||||||
for (i in 0 until childCount) {
|
|
||||||
val mediaView = mediaContent.getChildAt(i)
|
|
||||||
val desiredPaddingEnd = if (i == childCount - 1) 0 else padding
|
|
||||||
val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
if (layoutParams.marginEnd != desiredPaddingEnd) {
|
|
||||||
layoutParams.marginEnd = desiredPaddingEnd
|
|
||||||
mediaView.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePageIndicator() {
|
private fun updatePageIndicator() {
|
||||||
val numPages = mediaContent.getChildCount()
|
val numPages = mediaContent.getChildCount()
|
||||||
pageIndicator.setNumPages(numPages, Color.WHITE)
|
pageIndicator.setNumPages(numPages, Color.WHITE)
|
||||||
@@ -342,6 +275,12 @@ class MediaViewManager @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Set a new interpolated state for all players. This is a state that is usually controlled
|
* Set a new interpolated state for all players. This is a state that is usually controlled
|
||||||
* by a finger movement where the user drags from one state to the next.
|
* by a finger movement where the user drags from one state to the next.
|
||||||
|
*
|
||||||
|
* @param startLocation the start location of our state or -1 if this is directly set
|
||||||
|
* @param endLocation the ending location of our state.
|
||||||
|
* @param progress the progress of the transition between startLocation and endlocation. If
|
||||||
|
* this is not a guided transformation, this will be 1.0f
|
||||||
|
* @param immediately should this state be applied immediately, canceling all animations?
|
||||||
*/
|
*/
|
||||||
fun setCurrentState(
|
fun setCurrentState(
|
||||||
@MediaLocation startLocation: Int,
|
@MediaLocation startLocation: Int,
|
||||||
@@ -349,9 +288,6 @@ class MediaViewManager @Inject constructor(
|
|||||||
progress: Float,
|
progress: Float,
|
||||||
immediately: Boolean
|
immediately: Boolean
|
||||||
) {
|
) {
|
||||||
// Hack: Since the indicator doesn't move with the player expansion, just make it disappear
|
|
||||||
// and then reappear at the end.
|
|
||||||
pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
|
|
||||||
if (startLocation != currentStartLocation ||
|
if (startLocation != currentStartLocation ||
|
||||||
endLocation != currentEndLocation ||
|
endLocation != currentEndLocation ||
|
||||||
progress != currentTransitionProgress ||
|
progress != currentTransitionProgress ||
|
||||||
@@ -363,6 +299,51 @@ class MediaViewManager @Inject constructor(
|
|||||||
for (mediaPlayer in mediaPlayers.values) {
|
for (mediaPlayer in mediaPlayers.values) {
|
||||||
updatePlayerToState(mediaPlayer, immediately)
|
updatePlayerToState(mediaPlayer, immediately)
|
||||||
}
|
}
|
||||||
|
maybeResetSettingsCog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePageIndicatorLocation() {
|
||||||
|
// Update the location of the page indicator, carousel clipping
|
||||||
|
pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f +
|
||||||
|
mediaCarouselScrollHandler.contentTranslation
|
||||||
|
val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height -
|
||||||
|
layoutParams.bottomMargin).toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the dimension of this carousel.
|
||||||
|
*/
|
||||||
|
private fun updateCarouselDimensions() {
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
for (mediaPlayer in mediaPlayers.values) {
|
||||||
|
val controller = mediaPlayer.mediaViewController
|
||||||
|
width = Math.max(width, controller.currentWidth)
|
||||||
|
height = Math.max(height, controller.currentHeight)
|
||||||
|
}
|
||||||
|
if (width != currentCarouselWidth || height != currentCarouselHeight) {
|
||||||
|
currentCarouselWidth = width
|
||||||
|
currentCarouselHeight = height
|
||||||
|
mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight)
|
||||||
|
updatePageIndicatorLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeResetSettingsCog() {
|
||||||
|
val hostStates = mediaHostStatesManager.mediaHostStates
|
||||||
|
val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia
|
||||||
|
?: true
|
||||||
|
val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia
|
||||||
|
?: endShowsActive
|
||||||
|
if (currentlyShowingOnlyActive != endShowsActive ||
|
||||||
|
((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) &&
|
||||||
|
startShowsActive != endShowsActive)) {
|
||||||
|
/// Whenever we're transitioning from between differing states or the endstate differs
|
||||||
|
// we reset the translation
|
||||||
|
currentlyShowingOnlyActive = endShowsActive
|
||||||
|
mediaCarouselScrollHandler.resetTranslation(animate = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +385,15 @@ class MediaViewManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
|
mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
|
||||||
}
|
}
|
||||||
|
mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia
|
||||||
|
mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded
|
||||||
|
val nowVisible = it.visible
|
||||||
|
if (nowVisible != playersVisible) {
|
||||||
|
playersVisible = nowVisible
|
||||||
|
if (nowVisible) {
|
||||||
|
mediaCarouselScrollHandler.resetTranslation()
|
||||||
|
}
|
||||||
|
}
|
||||||
updateCarouselSize()
|
updateCarouselSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,16 +410,7 @@ class MediaViewManager @Inject constructor(
|
|||||||
carouselMeasureHeight = height
|
carouselMeasureHeight = height
|
||||||
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
|
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
|
||||||
R.dimen.qs_media_padding)
|
R.dimen.qs_media_padding)
|
||||||
// The player width has changed, let's update the scroll position to make sure
|
mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding
|
||||||
// it's still at the same place
|
|
||||||
var newScroll = activeMediaIndex * playerWidthPlusPadding
|
|
||||||
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
|
|
||||||
newScroll += playerWidthPlusPadding -
|
|
||||||
(scrollIntoCurrentMedia - playerWidthPlusPadding)
|
|
||||||
} else {
|
|
||||||
newScroll += scrollIntoCurrentMedia
|
|
||||||
}
|
|
||||||
mediaCarousel.scrollX = newScroll
|
|
||||||
// Let's remeasure the carousel
|
// Let's remeasure the carousel
|
||||||
val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
|
val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
|
||||||
val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
|
val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
|
||||||
@@ -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 {
|
interface Listener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class MediaHierarchyManager @Inject constructor(
|
|||||||
private val statusBarStateController: SysuiStatusBarStateController,
|
private val statusBarStateController: SysuiStatusBarStateController,
|
||||||
private val keyguardStateController: KeyguardStateController,
|
private val keyguardStateController: KeyguardStateController,
|
||||||
private val bypassController: KeyguardBypassController,
|
private val bypassController: KeyguardBypassController,
|
||||||
private val mediaViewManager: MediaViewManager,
|
private val mediaCarouselController: MediaCarouselController,
|
||||||
private val notifLockscreenUserManager: NotificationLockscreenUserManager,
|
private val notifLockscreenUserManager: NotificationLockscreenUserManager,
|
||||||
wakefulnessLifecycle: WakefulnessLifecycle
|
wakefulnessLifecycle: WakefulnessLifecycle
|
||||||
) {
|
) {
|
||||||
@@ -65,7 +65,7 @@ class MediaHierarchyManager @Inject constructor(
|
|||||||
private var animationStartBounds: Rect = Rect()
|
private var animationStartBounds: Rect = Rect()
|
||||||
private var targetBounds: Rect = Rect()
|
private var targetBounds: Rect = Rect()
|
||||||
private val mediaFrame
|
private val mediaFrame
|
||||||
get() = mediaViewManager.mediaFrame
|
get() = mediaCarouselController.mediaFrame
|
||||||
private var statusbarState: Int = statusBarStateController.state
|
private var statusbarState: Int = statusBarStateController.state
|
||||||
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
|
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
|
||||||
interpolator = Interpolators.FAST_OUT_SLOW_IN
|
interpolator = Interpolators.FAST_OUT_SLOW_IN
|
||||||
@@ -273,8 +273,8 @@ class MediaHierarchyManager @Inject constructor(
|
|||||||
val animate = shouldAnimateTransition(desiredLocation, previousLocation)
|
val animate = shouldAnimateTransition(desiredLocation, previousLocation)
|
||||||
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
|
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
|
||||||
val host = getHost(desiredLocation)
|
val host = getHost(desiredLocation)
|
||||||
mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
|
mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate,
|
||||||
delay)
|
animDuration, delay)
|
||||||
performTransitionToNewLocation(isNewView, animate)
|
performTransitionToNewLocation(isNewView, animate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,7 +457,7 @@ class MediaHierarchyManager @Inject constructor(
|
|||||||
val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
|
val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
|
||||||
val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
|
val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
|
||||||
val endLocation = desiredLocation
|
val endLocation = desiredLocation
|
||||||
mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately)
|
mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately)
|
||||||
updateHostAttachment()
|
updateHostAttachment()
|
||||||
if (currentAttachmentLocation == IN_OVERLAY) {
|
if (currentAttachmentLocation == IN_OVERLAY) {
|
||||||
mediaFrame.setLeftTopRightBottom(
|
mediaFrame.setLeftTopRightBottom(
|
||||||
|
|||||||
@@ -113,8 +113,11 @@ class MediaHost @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
mediaDataManager.hasAnyMedia()
|
mediaDataManager.hasAnyMedia()
|
||||||
}
|
}
|
||||||
hostView.visibility = if (visible) View.VISIBLE else View.GONE
|
val newVisibility = if (visible) View.VISIBLE else View.GONE
|
||||||
visibleChangedListener?.invoke(visible)
|
if (newVisibility != hostView.visibility) {
|
||||||
|
hostView.visibility = newVisibility
|
||||||
|
visibleChangedListener?.invoke(visible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaHostStateHolder @Inject constructor() : MediaHostState {
|
class MediaHostStateHolder @Inject constructor() : MediaHostState {
|
||||||
@@ -153,6 +156,15 @@ class MediaHost @Inject constructor(
|
|||||||
changedListener?.invoke()
|
changedListener?.invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var falsingProtectionNeeded: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
if (field == value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
changedListener?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getPivotX(): Float = gonePivot.x
|
override fun getPivotX(): Float = gonePivot.x
|
||||||
override fun getPivotY(): Float = gonePivot.y
|
override fun getPivotY(): Float = gonePivot.y
|
||||||
override fun setGonePivot(x: Float, y: Float) {
|
override fun setGonePivot(x: Float, y: Float) {
|
||||||
@@ -178,6 +190,7 @@ class MediaHost @Inject constructor(
|
|||||||
mediaHostState.measurementInput = measurementInput?.copy()
|
mediaHostState.measurementInput = measurementInput?.copy()
|
||||||
mediaHostState.visible = visible
|
mediaHostState.visible = visible
|
||||||
mediaHostState.gonePivot.set(gonePivot)
|
mediaHostState.gonePivot.set(gonePivot)
|
||||||
|
mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded
|
||||||
return mediaHostState
|
return mediaHostState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,6 +210,9 @@ class MediaHost @Inject constructor(
|
|||||||
if (visible != other.visible) {
|
if (visible != other.visible) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (falsingProtectionNeeded != other.falsingProtectionNeeded) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
|
if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -206,6 +222,7 @@ class MediaHost @Inject constructor(
|
|||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = measurementInput?.hashCode() ?: 0
|
var result = measurementInput?.hashCode() ?: 0
|
||||||
result = 31 * result + expansion.hashCode()
|
result = 31 * result + expansion.hashCode()
|
||||||
|
result = 31 * result + falsingProtectionNeeded.hashCode()
|
||||||
result = 31 * result + showsOnlyActiveMedia.hashCode()
|
result = 31 * result + showsOnlyActiveMedia.hashCode()
|
||||||
result = 31 * result + if (visible) 1 else 2
|
result = 31 * result + if (visible) 1 else 2
|
||||||
result = 31 * result + gonePivot.hashCode()
|
result = 31 * result + gonePivot.hashCode()
|
||||||
@@ -238,6 +255,11 @@ interface MediaHostState {
|
|||||||
*/
|
*/
|
||||||
var visible: Boolean
|
var visible: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this host need any falsing protection?
|
||||||
|
*/
|
||||||
|
var falsingProtectionNeeded: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the pivot point when clipping the height or width.
|
* Sets the pivot point when clipping the height or width.
|
||||||
* Clipping happens when animating visibility when we're visible in QS but not on QQS,
|
* Clipping happens when animating visibility when we're visible in QS but not on QQS,
|
||||||
|
|||||||
@@ -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
|
private val mediaHostStatesManager: MediaHostStatesManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A listener when the current dimensions of the player change
|
||||||
|
*/
|
||||||
|
lateinit var sizeChangedListener: () -> Unit
|
||||||
private var firstRefresh: Boolean = true
|
private var firstRefresh: Boolean = true
|
||||||
private var transitionLayout: TransitionLayout? = null
|
private var transitionLayout: TransitionLayout? = null
|
||||||
private val layoutController = TransitionLayoutController()
|
private val layoutController = TransitionLayoutController()
|
||||||
@@ -75,6 +79,17 @@ class MediaViewController @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
private val tmpPoint = PointF()
|
private val tmpPoint = PointF()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current width of the player. This might not factor in case the player is animating
|
||||||
|
* to the current state, but represents the end state
|
||||||
|
*/
|
||||||
|
var currentWidth: Int = 0
|
||||||
|
/**
|
||||||
|
* The current height of the player. This might not factor in case the player is animating
|
||||||
|
* to the current state, but represents the end state
|
||||||
|
*/
|
||||||
|
var currentHeight: Int = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A callback for media state changes
|
* A callback for media state changes
|
||||||
*/
|
*/
|
||||||
@@ -105,6 +120,11 @@ class MediaViewController @Inject constructor(
|
|||||||
collapsedLayout.load(context, R.xml.media_collapsed)
|
collapsedLayout.load(context, R.xml.media_collapsed)
|
||||||
expandedLayout.load(context, R.xml.media_expanded)
|
expandedLayout.load(context, R.xml.media_expanded)
|
||||||
mediaHostStatesManager.addController(this)
|
mediaHostStatesManager.addController(this)
|
||||||
|
layoutController.sizeChangedListener = { width: Int, height: Int ->
|
||||||
|
currentWidth = width
|
||||||
|
currentHeight = height
|
||||||
|
sizeChangedListener.invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -279,6 +299,8 @@ class MediaViewController @Inject constructor(
|
|||||||
tmpPoint, tmpState)
|
tmpPoint, tmpState)
|
||||||
tmpState
|
tmpState
|
||||||
}
|
}
|
||||||
|
currentWidth = result.width
|
||||||
|
currentHeight = result.height
|
||||||
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
|
layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
|
||||||
animationDelay)
|
animationDelay)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.dynamicanimation.animation.FloatPropertyCompat;
|
||||||
|
import androidx.dynamicanimation.animation.SpringForce;
|
||||||
|
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.qs.customize.QSCustomizer;
|
import com.android.systemui.qs.customize.QSCustomizer;
|
||||||
|
import com.android.systemui.util.animation.PhysicsAnimator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
|
* Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
|
||||||
@@ -35,7 +39,22 @@ import com.android.systemui.qs.customize.QSCustomizer;
|
|||||||
public class QSContainerImpl extends FrameLayout {
|
public class QSContainerImpl extends FrameLayout {
|
||||||
|
|
||||||
private final Point mSizePoint = new Point();
|
private final Point mSizePoint = new Point();
|
||||||
|
private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM =
|
||||||
|
new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") {
|
||||||
|
@Override
|
||||||
|
public float getValue(QSContainerImpl qsImpl) {
|
||||||
|
return qsImpl.getBackgroundBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(QSContainerImpl background, float value) {
|
||||||
|
background.setBackgroundBottom((int) value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING
|
||||||
|
= new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM,
|
||||||
|
SpringForce.DAMPING_RATIO_LOW_BOUNCY);
|
||||||
|
private int mBackgroundBottom = -1;
|
||||||
private int mHeightOverride = -1;
|
private int mHeightOverride = -1;
|
||||||
private QSPanel mQSPanel;
|
private QSPanel mQSPanel;
|
||||||
private View mQSDetail;
|
private View mQSDetail;
|
||||||
@@ -53,6 +72,7 @@ public class QSContainerImpl extends FrameLayout {
|
|||||||
private boolean mQsDisabled;
|
private boolean mQsDisabled;
|
||||||
private int mContentPaddingStart = -1;
|
private int mContentPaddingStart = -1;
|
||||||
private int mContentPaddingEnd = -1;
|
private int mContentPaddingEnd = -1;
|
||||||
|
private boolean mAnimateBottomOnNextLayout;
|
||||||
|
|
||||||
public QSContainerImpl(Context context, AttributeSet attrs) {
|
public QSContainerImpl(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
@@ -71,10 +91,30 @@ public class QSContainerImpl extends FrameLayout {
|
|||||||
mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
|
mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background);
|
||||||
mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
|
mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view);
|
||||||
updateResources();
|
updateResources();
|
||||||
|
mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> {
|
||||||
|
if (mHeader.getHeaderQsPanel().isShown()) {
|
||||||
|
mAnimateBottomOnNextLayout = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setBackgroundBottom(int value) {
|
||||||
|
// We're saving the bottom separately since otherwise the bottom would be overridden in
|
||||||
|
// the layout and the animation wouldn't properly start at the old position.
|
||||||
|
mBackgroundBottom = value;
|
||||||
|
mBackground.setBottom(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float getBackgroundBottom() {
|
||||||
|
if (mBackgroundBottom == -1) {
|
||||||
|
return mBackground.getBottom();
|
||||||
|
}
|
||||||
|
return mBackgroundBottom;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onConfigurationChanged(Configuration newConfig) {
|
protected void onConfigurationChanged(Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
@@ -140,7 +180,8 @@ public class QSContainerImpl extends FrameLayout {
|
|||||||
@Override
|
@Override
|
||||||
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
super.onLayout(changed, left, top, right, bottom);
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
updateExpansion();
|
updateExpansion(mAnimateBottomOnNextLayout /* animate */);
|
||||||
|
mAnimateBottomOnNextLayout = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void disable(int state1, int state2, boolean animate) {
|
public void disable(int state1, int state2, boolean animate) {
|
||||||
@@ -181,13 +222,31 @@ public class QSContainerImpl extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateExpansion() {
|
public void updateExpansion() {
|
||||||
|
updateExpansion(false /* animate */);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateExpansion(boolean animate) {
|
||||||
int height = calculateContainerHeight();
|
int height = calculateContainerHeight();
|
||||||
setBottom(getTop() + height);
|
setBottom(getTop() + height);
|
||||||
mQSDetail.setBottom(getTop() + height);
|
mQSDetail.setBottom(getTop() + height);
|
||||||
// Pin the drag handle to the bottom of the panel.
|
// Pin the drag handle to the bottom of the panel.
|
||||||
mDragHandle.setTranslationY(height - mDragHandle.getHeight());
|
mDragHandle.setTranslationY(height - mDragHandle.getHeight());
|
||||||
mBackground.setTop(mQSPanelContainer.getTop());
|
mBackground.setTop(mQSPanelContainer.getTop());
|
||||||
mBackground.setBottom(height);
|
updateBackgroundBottom(height, animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBackgroundBottom(int height, boolean animated) {
|
||||||
|
PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this);
|
||||||
|
if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) {
|
||||||
|
// An animation is running or we want to animate
|
||||||
|
// Let's make sure to set the currentValue again, since the call below might only
|
||||||
|
// start in the next frame and otherwise we'd flicker
|
||||||
|
BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this));
|
||||||
|
physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start();
|
||||||
|
} else {
|
||||||
|
BACKGROUND_BOTTOM.setValue(this, height);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int calculateContainerHeight() {
|
protected int calculateContainerHeight() {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import java.io.FileDescriptor;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@@ -141,6 +142,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
private int mLastOrientation = -1;
|
private int mLastOrientation = -1;
|
||||||
private int mMediaTotalBottomMargin;
|
private int mMediaTotalBottomMargin;
|
||||||
private int mFooterMarginStartHorizontal;
|
private int mFooterMarginStartHorizontal;
|
||||||
|
private Consumer<Boolean> mMediaVisibilityChangedListener;
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@@ -159,7 +161,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
R.dimen.quick_settings_bottom_margin_media);
|
R.dimen.quick_settings_bottom_margin_media);
|
||||||
mMediaHost = mediaHost;
|
mMediaHost = mediaHost;
|
||||||
mMediaHost.setVisibleChangedListener((visible) -> {
|
mMediaHost.setVisibleChangedListener((visible) -> {
|
||||||
switchTileLayout();
|
onMediaVisibilityChanged(visible);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
mContext = context;
|
mContext = context;
|
||||||
@@ -207,6 +209,13 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
updateResources();
|
updateResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void onMediaVisibilityChanged(Boolean visible) {
|
||||||
|
switchTileLayout();
|
||||||
|
if (mMediaVisibilityChangedListener != null) {
|
||||||
|
mMediaVisibilityChangedListener.accept(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void addSecurityFooter() {
|
protected void addSecurityFooter() {
|
||||||
mSecurityFooter = new QSSecurityFooter(this, mContext);
|
mSecurityFooter = new QSSecurityFooter(this, mContext);
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1074,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
|||||||
mHeaderContainer = headerContainer;
|
mHeaderContainer = headerContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) {
|
||||||
|
mMediaVisibilityChangedListener = visibilityChangedListener;
|
||||||
|
}
|
||||||
|
|
||||||
private class H extends Handler {
|
private class H extends Handler {
|
||||||
private static final int SHOW_DETAIL = 1;
|
private static final int SHOW_DETAIL = 1;
|
||||||
private static final int SET_TILE_VISIBILITY = 2;
|
private static final int SET_TILE_VISIBILITY = 2;
|
||||||
|
|||||||
@@ -769,10 +769,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable {
|
|||||||
return mContentTranslation;
|
return mContentTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean wantsAddAndRemoveAnimations() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets whether this view is the first notification in a section. */
|
/** Sets whether this view is the first notification in a section. */
|
||||||
public void setFirstInSection(boolean firstInSection) {
|
public void setFirstInSection(boolean firstInSection) {
|
||||||
mFirstInSection = firstInSection;
|
mFirstInSection = firstInSection;
|
||||||
|
|||||||
@@ -50,9 +50,4 @@ public class MediaHeaderView extends ExpandableView {
|
|||||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean wantsAddAndRemoveAnimations() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ import com.android.systemui.Interpolators;
|
|||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SwipeHelper;
|
import com.android.systemui.SwipeHelper;
|
||||||
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
import com.android.systemui.colorextraction.SysuiColorExtractor;
|
||||||
|
import com.android.systemui.media.KeyguardMediaController;
|
||||||
import com.android.systemui.plugins.FalsingManager;
|
import com.android.systemui.plugins.FalsingManager;
|
||||||
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
||||||
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
|
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
|
||||||
@@ -552,6 +553,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
SysuiStatusBarStateController statusBarStateController,
|
SysuiStatusBarStateController statusBarStateController,
|
||||||
HeadsUpManagerPhone headsUpManager,
|
HeadsUpManagerPhone headsUpManager,
|
||||||
KeyguardBypassController keyguardBypassController,
|
KeyguardBypassController keyguardBypassController,
|
||||||
|
KeyguardMediaController keyguardMediaController,
|
||||||
FalsingManager falsingManager,
|
FalsingManager falsingManager,
|
||||||
NotificationLockscreenUserManager notificationLockscreenUserManager,
|
NotificationLockscreenUserManager notificationLockscreenUserManager,
|
||||||
NotificationGutsManager notificationGutsManager,
|
NotificationGutsManager notificationGutsManager,
|
||||||
@@ -670,6 +672,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
initializeForegroundServiceSection(fgsFeatureController);
|
initializeForegroundServiceSection(fgsFeatureController);
|
||||||
mUiEventLogger = uiEventLogger;
|
mUiEventLogger = uiEventLogger;
|
||||||
mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
|
mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener);
|
||||||
|
keyguardMediaController.setVisibilityChangedListener((visible) -> {
|
||||||
|
if (visible) {
|
||||||
|
generateAddAnimation(keyguardMediaController.getView(), false /*fromMoreCard */);
|
||||||
|
} else {
|
||||||
|
generateRemoveAnimation(keyguardMediaController.getView());
|
||||||
|
}
|
||||||
|
requestChildrenUpdate();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeForegroundServiceSection(
|
private void initializeForegroundServiceSection(
|
||||||
@@ -3101,9 +3112,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
*/
|
*/
|
||||||
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
|
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
|
||||||
private boolean generateRemoveAnimation(ExpandableView child) {
|
private boolean generateRemoveAnimation(ExpandableView child) {
|
||||||
if (!child.wantsAddAndRemoveAnimations()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
|
if (removeRemovedChildFromHeadsUpChangeAnimations(child)) {
|
||||||
mAddedHeadsUpChildren.remove(child);
|
mAddedHeadsUpChildren.remove(child);
|
||||||
return false;
|
return false;
|
||||||
@@ -3458,8 +3466,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
@Override
|
@Override
|
||||||
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
|
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
|
||||||
public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
|
public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {
|
||||||
if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()
|
if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) {
|
||||||
&& child.wantsAddAndRemoveAnimations()) {
|
|
||||||
// Generate Animations
|
// Generate Animations
|
||||||
mChildrenToAddAnimated.add(child);
|
mChildrenToAddAnimated.add(child);
|
||||||
if (fromMoreCard) {
|
if (fromMoreCard) {
|
||||||
@@ -3654,6 +3661,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
ignoreChildren = false;
|
ignoreChildren = false;
|
||||||
}
|
}
|
||||||
childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
|
childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth();
|
||||||
|
} else if (child instanceof MediaHeaderView) {
|
||||||
|
childWasSwipedOut = true;
|
||||||
}
|
}
|
||||||
if (!childWasSwipedOut) {
|
if (!childWasSwipedOut) {
|
||||||
Rect clipBounds = child.getClipBounds();
|
Rect clipBounds = child.getClipBounds();
|
||||||
@@ -6370,7 +6379,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
@Override
|
@Override
|
||||||
public void onDragCancelled(View v) {
|
public void onDragCancelled(View v) {
|
||||||
setSwipingInProgress(false);
|
setSwipingInProgress(false);
|
||||||
mFalsingManager.onNotificatonStopDismissing();
|
mFalsingManager.onNotificationStopDismissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6470,7 +6479,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBeginDrag(View v) {
|
public void onBeginDrag(View v) {
|
||||||
mFalsingManager.onNotificatonStartDismissing();
|
mFalsingManager.onNotificationStartDismissing();
|
||||||
setSwipingInProgress(true);
|
setSwipingInProgress(true);
|
||||||
mAmbientState.onBeginDrag((ExpandableView) v);
|
mAmbientState.onBeginDrag((ExpandableView) v);
|
||||||
updateContinuousShadowDrawing();
|
updateContinuousShadowDrawing();
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ open class TransitionLayoutController {
|
|||||||
private var state = TransitionViewState()
|
private var state = TransitionViewState()
|
||||||
private var pivot = PointF()
|
private var pivot = PointF()
|
||||||
private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
|
private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
|
||||||
|
private var currentHeight: Int = 0
|
||||||
|
private var currentWidth: Int = 0
|
||||||
|
var sizeChangedListener: ((Int, Int) -> Unit)? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
animator.apply {
|
animator.apply {
|
||||||
@@ -67,7 +70,16 @@ open class TransitionLayoutController {
|
|||||||
progress = animator.animatedFraction,
|
progress = animator.animatedFraction,
|
||||||
pivot = pivot,
|
pivot = pivot,
|
||||||
resultState = currentState)
|
resultState = currentState)
|
||||||
view.setState(currentState)
|
applyStateToLayout(currentState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyStateToLayout(state: TransitionViewState) {
|
||||||
|
transitionLayout?.setState(state)
|
||||||
|
if (currentHeight != state.height || currentWidth != state.width) {
|
||||||
|
currentHeight = state.height
|
||||||
|
currentWidth = state.width
|
||||||
|
sizeChangedListener?.invoke(currentWidth, currentHeight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +225,7 @@ open class TransitionLayoutController {
|
|||||||
this.state = state.copy()
|
this.state = state.copy()
|
||||||
if (applyImmediately || transitionLayout == null) {
|
if (applyImmediately || transitionLayout == null) {
|
||||||
animator.cancel()
|
animator.cancel()
|
||||||
transitionLayout?.setState(this.state)
|
applyStateToLayout(this.state)
|
||||||
currentState = state.copy(reusedState = currentState)
|
currentState = state.copy(reusedState = currentState)
|
||||||
} else if (animated) {
|
} else if (animated) {
|
||||||
animationStartState = currentState.copy()
|
animationStartState = currentState.copy()
|
||||||
@@ -221,7 +233,7 @@ open class TransitionLayoutController {
|
|||||||
animator.startDelay = delay
|
animator.startDelay = delay
|
||||||
animator.start()
|
animator.start()
|
||||||
} else if (!animator.isRunning) {
|
} else if (!animator.isRunning) {
|
||||||
transitionLayout?.setState(this.state)
|
applyStateToLayout(this.state)
|
||||||
currentState = state.copy(reusedState = currentState)
|
currentState = state.copy(reusedState = currentState)
|
||||||
}
|
}
|
||||||
// otherwise the desired state was updated and the animation will go to the new target
|
// otherwise the desired state was updated and the animation will go to the new target
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
|
private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var mediaViewManager: MediaViewManager
|
private lateinit var mediaCarouselController: MediaCarouselController
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
|
private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
|
||||||
@Captor
|
@Captor
|
||||||
@@ -82,13 +82,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
`when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
|
`when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame)
|
||||||
mediaHiearchyManager = MediaHierarchyManager(
|
mediaHiearchyManager = MediaHierarchyManager(
|
||||||
context,
|
context,
|
||||||
statusBarStateController,
|
statusBarStateController,
|
||||||
keyguardStateController,
|
keyguardStateController,
|
||||||
bypassController,
|
bypassController,
|
||||||
mediaViewManager,
|
mediaCarouselController,
|
||||||
notificationLockscreenUserManager,
|
notificationLockscreenUserManager,
|
||||||
wakefulnessLifecycle)
|
wakefulnessLifecycle)
|
||||||
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
|
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
|
||||||
@@ -97,7 +97,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
|||||||
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
|
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
|
||||||
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
|
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
|
||||||
// We'll use the viewmanager to verify a few calls below, let's reset this.
|
// We'll use the viewmanager to verify a few calls below, let's reset this.
|
||||||
clearInvocations(mediaViewManager)
|
clearInvocations(mediaCarouselController)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,14 +118,14 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
|||||||
fun testBlockedWhenScreenTurningOff() {
|
fun testBlockedWhenScreenTurningOff() {
|
||||||
// Let's set it onto QS:
|
// Let's set it onto QS:
|
||||||
mediaHiearchyManager.qsExpansion = 1.0f
|
mediaHiearchyManager.qsExpansion = 1.0f
|
||||||
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
||||||
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
||||||
val observer = wakefullnessObserver.value
|
val observer = wakefullnessObserver.value
|
||||||
assertNotNull("lifecycle observer wasn't registered", observer)
|
assertNotNull("lifecycle observer wasn't registered", observer)
|
||||||
observer.onStartedGoingToSleep()
|
observer.onStartedGoingToSleep()
|
||||||
clearInvocations(mediaViewManager)
|
clearInvocations(mediaCarouselController)
|
||||||
mediaHiearchyManager.qsExpansion = 0.0f
|
mediaHiearchyManager.qsExpansion = 0.0f
|
||||||
verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
verify(mediaCarouselController, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
||||||
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,13 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
|||||||
fun testAllowedWhenNotTurningOff() {
|
fun testAllowedWhenNotTurningOff() {
|
||||||
// Let's set it onto QS:
|
// Let's set it onto QS:
|
||||||
mediaHiearchyManager.qsExpansion = 1.0f
|
mediaHiearchyManager.qsExpansion = 1.0f
|
||||||
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
||||||
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
||||||
val observer = wakefullnessObserver.value
|
val observer = wakefullnessObserver.value
|
||||||
assertNotNull("lifecycle observer wasn't registered", observer)
|
assertNotNull("lifecycle observer wasn't registered", observer)
|
||||||
clearInvocations(mediaViewManager)
|
clearInvocations(mediaCarouselController)
|
||||||
mediaHiearchyManager.qsExpansion = 0.0f
|
mediaHiearchyManager.qsExpansion = 0.0f
|
||||||
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
||||||
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,7 @@ import com.android.systemui.ExpandHelper;
|
|||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.SysuiTestCase;
|
import com.android.systemui.SysuiTestCase;
|
||||||
import com.android.systemui.classifier.FalsingManagerFake;
|
import com.android.systemui.classifier.FalsingManagerFake;
|
||||||
|
import com.android.systemui.media.KeyguardMediaController;
|
||||||
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
|
||||||
import com.android.systemui.statusbar.EmptyShadeView;
|
import com.android.systemui.statusbar.EmptyShadeView;
|
||||||
import com.android.systemui.statusbar.FeatureFlags;
|
import com.android.systemui.statusbar.FeatureFlags;
|
||||||
@@ -133,6 +134,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
|
|||||||
@Mock private MetricsLogger mMetricsLogger;
|
@Mock private MetricsLogger mMetricsLogger;
|
||||||
@Mock private NotificationRoundnessManager mNotificationRoundnessManager;
|
@Mock private NotificationRoundnessManager mNotificationRoundnessManager;
|
||||||
@Mock private KeyguardBypassController mKeyguardBypassController;
|
@Mock private KeyguardBypassController mKeyguardBypassController;
|
||||||
|
@Mock private KeyguardMediaController mKeyguardMediaController;
|
||||||
@Mock private ZenModeController mZenModeController;
|
@Mock private ZenModeController mZenModeController;
|
||||||
@Mock private NotificationSectionsManager mNotificationSectionsManager;
|
@Mock private NotificationSectionsManager mNotificationSectionsManager;
|
||||||
@Mock private NotificationSection mNotificationSection;
|
@Mock private NotificationSection mNotificationSection;
|
||||||
@@ -209,6 +211,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase {
|
|||||||
mock(SysuiStatusBarStateController.class),
|
mock(SysuiStatusBarStateController.class),
|
||||||
mHeadsUpManager,
|
mHeadsUpManager,
|
||||||
mKeyguardBypassController,
|
mKeyguardBypassController,
|
||||||
|
mKeyguardMediaController,
|
||||||
new FalsingManagerFake(),
|
new FalsingManagerFake(),
|
||||||
mLockscreenUserManager,
|
mLockscreenUserManager,
|
||||||
mock(NotificationGutsManager.class),
|
mock(NotificationGutsManager.class),
|
||||||
|
|||||||
Reference in New Issue
Block a user