summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/res/layout/media_view.xml (renamed from packages/SystemUI/res/layout/qs_media_panel.xml)29
-rw-r--r--packages/SystemUI/res/values/ids.xml2
-rw-r--r--packages/SystemUI/res/xml/media_collapsed.xml184
-rw-r--r--packages/SystemUI/res/xml/media_expanded.xml178
-rw-r--r--packages/SystemUI/res/xml/media_scene.xml447
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt52
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt1
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt115
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java101
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt228
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHost.kt177
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt59
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt280
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt211
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSFragment.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSPanel.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java3
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt87
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt44
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt294
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt233
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt84
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt36
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt145
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt2
33 files changed, 1975 insertions, 1162 deletions
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/media_view.xml
index bf062421ddb4..1a1fddbcfd03 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -16,7 +16,7 @@
-->
<!-- Layout for media controls inside QSPanel carousel -->
-<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.android.systemui.util.animation.TransitionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/qs_media_controls"
android:layout_width="match_parent"
@@ -24,18 +24,7 @@
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_horizontal|fill_vertical"
- app:layoutDescription="@xml/media_scene">
-
- <View
- android:id="@+id/media_background"
- android:layout_width="0dp"
- android:layout_height="0dp"
- android:background="@drawable/qs_media_background"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- />
+ android:background="@drawable/qs_media_background">
<FrameLayout
android:id="@+id/notification_media_progress_time"
@@ -185,15 +174,5 @@
<!-- Buttons to remove this view when no longer needed -->
<include
layout="@layout/qs_media_panel_options"
- android:visibility="gone"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent" />
-
- <androidx.constraintlayout.widget.Guideline
- android:id="@+id/view_width"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- app:layout_constraintGuide_begin="300dp" />
-</androidx.constraintlayout.motion.widget.MotionLayout>
+ android:visibility="gone" />
+</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 09918e764140..d47ad7fcdfbd 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -90,6 +90,8 @@
<item type="id" name="view_width_animator_end_tag"/>
<item type="id" name="view_width_current_value"/>
+ <item type="id" name="requires_remeasuring"/>
+
<!-- Whether the icon is from a notification for which targetSdk < L -->
<item type="id" name="icon_is_pre_L"/>
diff --git a/packages/SystemUI/res/xml/media_collapsed.xml b/packages/SystemUI/res/xml/media_collapsed.xml
new file mode 100644
index 000000000000..57e6f3635785
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_collapsed.xml
@@ -0,0 +1,184 @@
+<?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
+ -->
+<ConstraintSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <Constraint
+ android:id="@+id/icon"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_marginStart="18dp"
+ android:layout_marginTop="22dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+
+ <Constraint
+ android:id="@+id/app_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="10dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginTop="20dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ app:layout_constraintEnd_toStartOf="@id/media_seamless"
+ app:layout_constraintHorizontal_bias="0"
+ />
+
+ <Constraint
+ android:id="@+id/media_seamless"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_min="60dp"
+ android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ />
+
+ <Constraint
+ android:id="@+id/album_art"
+ android:layout_width="@dimen/qs_media_album_size"
+ android:layout_height="@dimen/qs_media_album_size"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginBottom="24dp"
+ app:layout_constraintTop_toBottomOf="@id/icon"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent"
+ />
+
+ <!-- Song name -->
+ <Constraint
+ android:id="@+id/header_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="17dp"
+ android:layout_marginStart="16dp"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintBottom_toTopOf="@id/header_artist"
+ app:layout_constraintStart_toEndOf="@id/album_art"
+ app:layout_constraintEnd_toStartOf="@id/action0"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <!-- Artist name -->
+ <Constraint
+ android:id="@+id/header_artist"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dp"
+ android:layout_marginBottom="24dp"
+ app:layout_constraintTop_toBottomOf="@id/header_title"
+ app:layout_constraintStart_toStartOf="@id/header_title"
+ app:layout_constraintEnd_toStartOf="@id/action0"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <!-- Seek Bar -->
+ <Constraint
+ android:id="@+id/media_progress_bar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:alpha="0.0"
+ app:layout_constraintTop_toBottomOf="@id/album_art"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone"
+ />
+
+ <Constraint
+ android:id="@+id/notification_media_progress_time"
+ android:alpha="0.0"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="35dp"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintTop_toBottomOf="@id/album_art"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:visibility="gone"
+ />
+
+ <Constraint
+ android:id="@+id/action0"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginTop="16dp"
+ android:visibility="gone"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintLeft_toRightOf="@id/header_title"
+ app:layout_constraintRight_toLeftOf="@id/action1"
+ >
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action1"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginTop="18dp"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintLeft_toRightOf="@id/action0"
+ app:layout_constraintRight_toLeftOf="@id/action2"
+ >
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action2"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginTop="18dp"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintLeft_toRightOf="@id/action1"
+ app:layout_constraintRight_toLeftOf="@id/action3"
+ >
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action3"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginTop="18dp"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintLeft_toRightOf="@id/action2"
+ app:layout_constraintRight_toLeftOf="@id/action4"
+ >
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action4"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:visibility="gone"
+ android:layout_marginTop="18dp"
+ app:layout_constraintTop_toBottomOf="@id/app_name"
+ app:layout_constraintLeft_toRightOf="@id/action3"
+ app:layout_constraintRight_toRightOf="parent"
+ >
+ </Constraint>
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_expanded.xml b/packages/SystemUI/res/xml/media_expanded.xml
new file mode 100644
index 000000000000..78973f3207d1
--- /dev/null
+++ b/packages/SystemUI/res/xml/media_expanded.xml
@@ -0,0 +1,178 @@
+<?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
+ -->
+<ConstraintSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <Constraint
+ android:id="@+id/icon"
+ android:layout_width="16dp"
+ android:layout_height="16dp"
+ android:layout_marginStart="18dp"
+ android:layout_marginTop="22dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+
+ <Constraint
+ android:id="@+id/app_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="10dp"
+ android:layout_marginStart="10dp"
+ android:layout_marginTop="20dp"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ app:layout_constraintEnd_toStartOf="@id/media_seamless"
+ app:layout_constraintHorizontal_bias="0"
+ />
+
+ <Constraint
+ android:id="@+id/media_seamless"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintWidth_min="60dp"
+ android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ />
+
+ <Constraint
+ android:id="@+id/album_art"
+ android:layout_width="@dimen/qs_media_album_size"
+ android:layout_height="@dimen/qs_media_album_size"
+ android:layout_marginTop="14dp"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintTop_toBottomOf="@+id/app_name"
+ app:layout_constraintStart_toStartOf="parent"
+ />
+
+ <!-- Song name -->
+ <Constraint
+ android:id="@+id/header_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginTop="17dp"
+ android:layout_marginStart="16dp"
+ app:layout_constraintTop_toBottomOf="@+id/app_name"
+ app:layout_constraintStart_toEndOf="@id/album_art"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <!-- Artist name -->
+ <Constraint
+ android:id="@+id/header_artist"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginTop="3dp"
+ app:layout_constraintTop_toBottomOf="@id/header_title"
+ app:layout_constraintStart_toStartOf="@id/header_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0"/>
+
+ <!-- Seek Bar -->
+ <Constraint
+ android:id="@+id/media_progress_bar"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="3dp"
+ app:layout_constraintTop_toBottomOf="@id/header_artist"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ />
+
+ <Constraint
+ android:id="@+id/notification_media_progress_time"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="38dp"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintTop_toBottomOf="@id/header_artist"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ />
+
+ <Constraint
+ android:id="@+id/action0"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginTop="5dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintHorizontal_chainStyle="packed"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toLeftOf="@id/action1"
+ app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action1"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintLeft_toRightOf="@id/action0"
+ app:layout_constraintRight_toLeftOf="@id/action2"
+ app:layout_constraintTop_toTopOf="@id/action0"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action2"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintLeft_toRightOf="@id/action1"
+ app:layout_constraintRight_toLeftOf="@id/action3"
+ app:layout_constraintTop_toTopOf="@id/action0"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action3"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ app:layout_constraintLeft_toRightOf="@id/action2"
+ app:layout_constraintRight_toLeftOf="@id/action4"
+ app:layout_constraintTop_toTopOf="@id/action0"
+ android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </Constraint>
+
+ <Constraint
+ android:id="@+id/action4"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginEnd="4dp"
+ android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
+ app:layout_constraintLeft_toRightOf="@id/action3"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toTopOf="@id/action0"
+ app:layout_constraintBottom_toBottomOf="parent">
+ </Constraint>
+</ConstraintSet>
diff --git a/packages/SystemUI/res/xml/media_scene.xml b/packages/SystemUI/res/xml/media_scene.xml
deleted file mode 100644
index f61b2b096d3c..000000000000
--- a/packages/SystemUI/res/xml/media_scene.xml
+++ /dev/null
@@ -1,447 +0,0 @@
-<?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
- -->
-<MotionScene
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <Transition
- app:constraintSetStart="@id/collapsed"
- app:constraintSetEnd="@id/expanded"
- app:duration="1000" >
- <KeyFrameSet >
- <KeyPosition
- app:motionTarget="@+id/action0"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyPosition
- app:motionTarget="@+id/action1"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyPosition
- app:motionTarget="@+id/action2"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyPosition
- app:motionTarget="@+id/action3"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyPosition
- app:motionTarget="@+id/action4"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyPosition
- app:motionTarget="@+id/media_progress_bar"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyAttribute
- app:motionTarget="@id/media_progress_bar"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/media_progress_bar"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyPosition
- app:motionTarget="@+id/notification_media_progress_time"
- app:keyPositionType="pathRelative"
- app:framePosition="70"
- app:sizePercent="0.9" />
- <KeyAttribute
- app:motionTarget="@id/notification_media_progress_time"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/notification_media_progress_time"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyAttribute
- app:motionTarget="@id/action0"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/action0"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyAttribute
- app:motionTarget="@id/action1"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/action1"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyAttribute
- app:motionTarget="@id/action2"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/action2"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyAttribute
- app:motionTarget="@id/action3"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/action3"
- app:framePosition="70"
- android:alpha="0.0"/>
- <KeyAttribute
- app:motionTarget="@id/action4"
- app:framePosition="0"
- android:alpha="0.0" />
- <KeyAttribute
- app:motionTarget="@+id/action4"
- app:framePosition="70"
- android:alpha="0.0"/>
- </KeyFrameSet>
- </Transition>
-
- <ConstraintSet android:id="@+id/expanded">
- <Constraint
- android:id="@+id/icon"
- android:layout_width="16dp"
- android:layout_height="16dp"
- android:layout_marginStart="18dp"
- android:layout_marginTop="22dp"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- />
-
- <Constraint
- android:id="@+id/app_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="10dp"
- android:layout_marginStart="10dp"
- android:layout_marginTop="20dp"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toEndOf="@id/icon"
- app:layout_constraintEnd_toStartOf="@id/media_seamless"
- app:layout_constraintHorizontal_bias="0"
- />
-
- <Constraint
- android:id="@+id/media_seamless"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_min="60dp"
- android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- />
-
- <Constraint
- android:id="@+id/album_art"
- android:layout_width="@dimen/qs_media_album_size"
- android:layout_height="@dimen/qs_media_album_size"
- android:layout_marginTop="14dp"
- android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintTop_toBottomOf="@+id/app_name"
- app:layout_constraintStart_toStartOf="parent"
- />
-
- <!-- Song name -->
- <Constraint
- android:id="@+id/header_title"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- android:layout_marginTop="17dp"
- android:layout_marginStart="16dp"
- app:layout_constraintTop_toBottomOf="@+id/app_name"
- app:layout_constraintStart_toEndOf="@id/album_art"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintHorizontal_bias="0"/>
-
- <!-- Artist name -->
- <Constraint
- android:id="@+id/header_artist"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- android:layout_marginTop="3dp"
- app:layout_constraintTop_toBottomOf="@id/header_title"
- app:layout_constraintStart_toStartOf="@id/header_title"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintHorizontal_bias="0"/>
-
- <!-- Seek Bar -->
- <Constraint
- android:id="@+id/media_progress_bar"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="3dp"
- app:layout_constraintTop_toBottomOf="@id/header_artist"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- />
-
- <Constraint
- android:id="@+id/notification_media_progress_time"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="38dp"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintTop_toBottomOf="@id/header_artist"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- />
-
- <Constraint
- android:id="@+id/action0"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginTop="5dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintLeft_toLeftOf="parent"
- app:layout_constraintRight_toLeftOf="@id/action1"
- app:layout_constraintTop_toBottomOf="@id/notification_media_progress_time"
- app:layout_constraintBottom_toBottomOf="parent">
- </Constraint>
-
- <Constraint
- android:id="@+id/action1"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintLeft_toRightOf="@id/action0"
- app:layout_constraintRight_toLeftOf="@id/action2"
- app:layout_constraintTop_toTopOf="@id/action0"
- app:layout_constraintBottom_toBottomOf="parent">
- </Constraint>
-
- <Constraint
- android:id="@+id/action2"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintLeft_toRightOf="@id/action1"
- app:layout_constraintRight_toLeftOf="@id/action3"
- app:layout_constraintTop_toTopOf="@id/action0"
- app:layout_constraintBottom_toBottomOf="parent">
- </Constraint>
-
- <Constraint
- android:id="@+id/action3"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- app:layout_constraintLeft_toRightOf="@id/action2"
- app:layout_constraintRight_toLeftOf="@id/action4"
- app:layout_constraintTop_toTopOf="@id/action0"
- android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintBottom_toBottomOf="parent">
- </Constraint>
-
- <Constraint
- android:id="@+id/action4"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginBottom="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintLeft_toRightOf="@id/action3"
- app:layout_constraintRight_toRightOf="@id/view_width"
- app:layout_constraintTop_toTopOf="@id/action0"
- app:layout_constraintBottom_toBottomOf="parent">
- </Constraint>
- </ConstraintSet>
-
- <ConstraintSet android:id="@+id/collapsed">
- <Constraint
- android:id="@+id/icon"
- android:layout_width="16dp"
- android:layout_height="16dp"
- android:layout_marginStart="18dp"
- android:layout_marginTop="22dp"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"
- />
-
- <Constraint
- android:id="@+id/app_name"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginEnd="10dp"
- android:layout_marginStart="10dp"
- android:layout_marginTop="20dp"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toEndOf="@id/icon"
- app:layout_constraintEnd_toStartOf="@id/media_seamless"
- app:layout_constraintHorizontal_bias="0"
- />
-
- <Constraint
- android:id="@+id/media_seamless"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintWidth_min="60dp"
- android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- />
-
- <Constraint
- android:id="@+id/album_art"
- android:layout_width="@dimen/qs_media_album_size"
- android:layout_height="@dimen/qs_media_album_size"
- android:layout_marginTop="16dp"
- android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
- android:layout_marginBottom="24dp"
- app:layout_constraintTop_toBottomOf="@id/icon"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toBottomOf="parent"
- />
-
- <!-- Song name -->
- <Constraint
- android:id="@+id/header_title"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="17dp"
- android:layout_marginStart="16dp"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintBottom_toTopOf="@id/header_artist"
- app:layout_constraintStart_toEndOf="@id/album_art"
- app:layout_constraintEnd_toStartOf="@id/action0"
- app:layout_constraintHorizontal_bias="0"/>
-
- <!-- Artist name -->
- <Constraint
- android:id="@+id/header_artist"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="3dp"
- android:layout_marginBottom="24dp"
- app:layout_constraintTop_toBottomOf="@id/header_title"
- app:layout_constraintStart_toStartOf="@id/header_title"
- app:layout_constraintEnd_toStartOf="@id/action0"
- app:layout_constraintBottom_toBottomOf="parent"
- app:layout_constraintHorizontal_bias="0"/>
-
- <!-- Seek Bar -->
- <Constraint
- android:id="@+id/media_progress_bar"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:alpha="0.0"
- app:layout_constraintTop_toBottomOf="@id/album_art"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/notification_media_progress_time"
- android:alpha="0.0"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginTop="35dp"
- android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
- android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
- app:layout_constraintTop_toBottomOf="@id/album_art"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="@id/view_width"
- android:visibility="gone"
- />
-
- <Constraint
- android:id="@+id/action0"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginTop="16dp"
- android:visibility="gone"
- app:layout_constraintHorizontal_chainStyle="packed"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintLeft_toRightOf="@id/header_title"
- app:layout_constraintRight_toLeftOf="@id/action1"
- >
- </Constraint>
-
- <Constraint
- android:id="@+id/action1"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginTop="18dp"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintLeft_toRightOf="@id/action0"
- app:layout_constraintRight_toLeftOf="@id/action2"
- >
- </Constraint>
-
- <Constraint
- android:id="@+id/action2"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginTop="18dp"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintLeft_toRightOf="@id/action1"
- app:layout_constraintRight_toLeftOf="@id/action3"
- >
- </Constraint>
-
- <Constraint
- android:id="@+id/action3"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:layout_marginTop="18dp"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintLeft_toRightOf="@id/action2"
- app:layout_constraintRight_toLeftOf="@id/action4"
- >
- </Constraint>
-
- <Constraint
- android:id="@+id/action4"
- android:layout_width="48dp"
- android:layout_height="48dp"
- android:layout_marginStart="4dp"
- android:layout_marginEnd="4dp"
- android:visibility="gone"
- android:layout_marginTop="18dp"
- app:layout_constraintTop_toBottomOf="@id/app_name"
- app:layout_constraintLeft_toRightOf="@id/action3"
- app:layout_constraintRight_toRightOf="@id/view_width"
- >
- </Constraint>
- </ConstraintSet>
-</MotionScene>
diff --git a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt b/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt
deleted file mode 100644
index 2fe0d9f4711f..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/GoneChildrenHideHelper.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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.Rect
-import android.view.View
-import android.view.ViewGroup
-
-private val EMPTY_RECT = Rect(0,0,0,0)
-
-private val LAYOUT_CHANGE_LISTENER = object : View.OnLayoutChangeListener {
-
- override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
- oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
- v?.let {
- if (v.visibility == View.GONE) {
- v.clipBounds = EMPTY_RECT
- } else {
- v.clipBounds = null
- }
- }
- }
-}
-/**
- * A helper class that clips all GONE children. Useful for transitions in motionlayout which
- * don't clip its children.
- */
-class GoneChildrenHideHelper private constructor() {
- companion object {
- @JvmStatic
- fun clipGoneChildrenOnLayout(layout: ViewGroup) {
- val childCount = layout.childCount
- for (i in 0 until childCount) {
- val child = layout.getChildAt(i)
- child.addOnLayoutChangeListener(LAYOUT_CHANGE_LISTENER)
- }
- }
- }
-} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
index 85e1c6b77be4..5f43e43c03c6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt
@@ -45,6 +45,7 @@ class KeyguardMediaController @Inject constructor(
}
})
}
+
private var view: MediaHeaderView? = null
/**
diff --git a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt b/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
deleted file mode 100644
index a366725a4398..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/LayoutAnimationHelper.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.Rect
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewTreeObserver
-import com.android.systemui.statusbar.notification.AnimatableProperty
-import com.android.systemui.statusbar.notification.PropertyAnimator
-import com.android.systemui.statusbar.notification.stack.AnimationProperties
-
-/**
- * A utility class that helps with animations of bound changes designed for motionlayout which
- * doesn't work together with regular changeBounds.
- */
-class LayoutAnimationHelper {
-
- private val layout: ViewGroup
- private var sizeAnimationPending = false
- private val desiredBounds = mutableMapOf<View, Rect>()
- private val animationProperties = AnimationProperties()
- private val layoutListener = object : View.OnLayoutChangeListener {
- override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
- oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
- v?.let {
- if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 ||
- oldTop - oldBottom == 0) {
- return
- }
- if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) {
- val rect = desiredBounds.getOrPut(v, { Rect() })
- rect.set(left, top, right, bottom)
- onDesiredLocationChanged(v, rect)
- }
- }
- }
- }
-
- constructor(layout: ViewGroup) {
- this.layout = layout
- val childCount = this.layout.childCount
- for (i in 0 until childCount) {
- val child = this.layout.getChildAt(i)
- child.addOnLayoutChangeListener(layoutListener)
- }
- }
-
- private fun onDesiredLocationChanged(v: View, rect: Rect) {
- if (!sizeAnimationPending) {
- applyBounds(v, rect, animate = false)
- }
- // We need to reapply the current bounds in every frame since the layout may override
- // the layout bounds making this view jump and not all calls to apply bounds actually
- // reapply them, for example if there's already an animator to the same target
- reapplyProperty(v, AnimatableProperty.ABSOLUTE_X);
- reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y);
- reapplyProperty(v, AnimatableProperty.WIDTH);
- reapplyProperty(v, AnimatableProperty.HEIGHT);
- }
-
- private fun reapplyProperty(v: View, property: AnimatableProperty) {
- property.property.set(v, property.property.get(v))
- }
-
- private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) {
- PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(),
- animationProperties, animate)
- PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(),
- animationProperties, animate)
- PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(),
- animationProperties, animate)
- PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(),
- animationProperties, animate)
- }
-
- private fun startBoundAnimation(v: View) {
- val target = desiredBounds[v] ?: return
- applyBounds(v, target, animate = true)
- }
-
- fun animatePendingSizeChange(duration: Long, delay: Long) {
- animationProperties.duration = duration
- animationProperties.delay = delay
- if (!sizeAnimationPending) {
- sizeAnimationPending = true
- layout.viewTreeObserver.addOnPreDrawListener (
- object : ViewTreeObserver.OnPreDrawListener {
- override fun onPreDraw(): Boolean {
- layout.viewTreeObserver.removeOnPreDrawListener(this)
- sizeAnimationPending = false
- val childCount = layout.childCount
- for (i in 0 until childCount) {
- val child = layout.getChildAt(i)
- startBoundAnimation(child)
- }
- return true
- }
- })
- }
- }
-} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index c7b93262b181..8e1e1b27cadf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -43,12 +43,9 @@ import android.widget.ImageView;
import android.widget.SeekBar;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import androidx.constraintlayout.motion.widget.Key;
-import androidx.constraintlayout.motion.widget.KeyAttributes;
-import androidx.constraintlayout.motion.widget.KeyFrames;
-import androidx.constraintlayout.motion.widget.MotionLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -59,11 +56,11 @@ import com.android.settingslib.widget.AdaptiveIcon;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSMediaBrowser;
+import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.concurrency.DelayableExecutor;
import org.jetbrains.annotations.NotNull;
-import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -87,16 +84,15 @@ public class MediaControlPanel {
private final Executor mForegroundExecutor;
protected final Executor mBackgroundExecutor;
private final ActivityStarter mActivityStarter;
- private LayoutAnimationHelper mLayoutAnimationHelper;
private Context mContext;
private PlayerViewHolder mViewHolder;
+ private MediaViewController mMediaViewController;
private MediaSession.Token mToken;
private MediaController mController;
private int mBackgroundColor;
protected ComponentName mServiceComponent;
private boolean mIsRegistered = false;
- private List<KeyFrames> mKeyFrames;
private String mKey;
private int mAlbumArtSize;
private int mAlbumArtRadius;
@@ -133,12 +129,14 @@ public class MediaControlPanel {
* @param activityStarter activity starter
*/
public MediaControlPanel(Context context, Executor foregroundExecutor,
- DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
+ DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
+ MediaHostStatesManager mediaHostStatesManager) {
mContext = context;
mForegroundExecutor = foregroundExecutor;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
+ mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
loadDimens();
}
@@ -147,6 +145,7 @@ public class MediaControlPanel {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
}
mSeekBarViewModel.onDestroy();
+ mMediaViewController.onDestroy();
}
private void loadDimens() {
@@ -165,6 +164,15 @@ public class MediaControlPanel {
}
/**
+ * Get the view controller used to display media controls
+ * @return the media view controller
+ */
+ @NonNull
+ public MediaViewController getMediaViewController() {
+ return mMediaViewController;
+ }
+
+ /**
* Sets the listening state of the player.
*
* Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
@@ -187,15 +195,13 @@ public class MediaControlPanel {
/** Attaches the player to the view holder. */
public void attach(PlayerViewHolder vh) {
mViewHolder = vh;
- MotionLayout motionView = vh.getPlayer();
- mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
- GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
- mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
+ TransitionLayout player = vh.getPlayer();
mSeekBarObserver = new SeekBarObserver(vh);
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
SeekBar bar = vh.getSeekBar();
bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
+ mMediaViewController.attach(player);
}
/**
@@ -220,8 +226,8 @@ public class MediaControlPanel {
mController = new MediaController(mContext, mToken);
- ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
- ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
+ ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+ ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
// Try to find a browser service component for this app
// TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -247,7 +253,7 @@ public class MediaControlPanel {
mController.registerCallback(mSessionCallback);
- mViewHolder.getBackground().setBackgroundTintList(
+ mViewHolder.getPlayer().setBackgroundTintList(
ColorStateList.valueOf(mBackgroundColor));
// Click action
@@ -356,7 +362,6 @@ public class MediaControlPanel {
}
});
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
- updateKeyFrameVisibility(actionId, visibleInCompat);
setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
}
@@ -374,9 +379,9 @@ public class MediaControlPanel {
// Set up long press menu
// TODO: b/156036025 bring back media guts
- // Update both constraint sets to regenerate the animation.
- mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
- mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
+ // TODO: We don't need to refresh this state constantly, only if the state actually changed
+ // to something which might impact the measurement
+ mMediaViewController.refreshState();
}
@UiThread
@@ -412,30 +417,6 @@ public class MediaControlPanel {
}
/**
- * Updates the keyframe visibility such that only views that are not visible actually go
- * through a transition and fade in.
- *
- * @param actionId the id to change
- * @param visible is the view visible
- */
- private void updateKeyFrameVisibility(int actionId, boolean visible) {
- if (mKeyFrames == null) {
- return;
- }
- for (int i = 0; i < mKeyFrames.size(); i++) {
- KeyFrames keyframe = mKeyFrames.get(i);
- ArrayList<Key> viewKeyFrames = keyframe.getKeyFramesForView(actionId);
- for (int j = 0; j < viewKeyFrames.size(); j++) {
- Key key = viewKeyFrames.get(j);
- if (key instanceof KeyAttributes) {
- KeyAttributes attributes = (KeyAttributes) key;
- attributes.setValue("alpha", visible ? 1.0f : 0.0f);
- }
- }
- }
- }
-
- /**
* Return the token for the current media session
* @return the token
*/
@@ -528,8 +509,8 @@ public class MediaControlPanel {
}
// Hide all the old buttons
- ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
- ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
+ ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
+ ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
for (int i = 1; i < ACTION_IDS.length; i++) {
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
@@ -571,6 +552,7 @@ public class MediaControlPanel {
options.setVisibility(View.VISIBLE);
return true; // consumed click
});
+ mMediaViewController.refreshState();
}
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
@@ -649,33 +631,4 @@ public class MediaControlPanel {
* Called when a player can't be resumed to give it an opportunity to hide or remove itself
*/
protected void removePlayer() { }
-
- public void measure(@Nullable MediaMeasurementInput input) {
- if (mViewHolder == null) {
- return;
- }
- if (input != null) {
- int width = input.getWidth();
- setPlayerWidth(width);
- mViewHolder.getPlayer().measure(input.getWidthMeasureSpec(),
- input.getHeightMeasureSpec());
- }
- }
-
- public void setPlayerWidth(int width) {
- if (mViewHolder == null) {
- return;
- }
- MotionLayout view = mViewHolder.getPlayer();
- ConstraintSet expandedSet = view.getConstraintSet(R.id.expanded);
- ConstraintSet collapsedSet = view.getConstraintSet(R.id.collapsed);
- collapsedSet.setGuidelineBegin(R.id.view_width, width);
- expandedSet.setGuidelineBegin(R.id.view_width, width);
- view.updateState(R.id.collapsed, collapsedSet);
- view.updateState(R.id.expanded, expandedSet);
- }
-
- public void animatePendingSizeChange(long duration, long startDelay) {
- mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index cce1d3efd6e5..775a1649702a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -21,10 +21,13 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.IntDef
import android.content.Context
+import android.graphics.Rect
+import android.util.MathUtils
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
import com.android.systemui.Interpolators
+import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
@@ -47,8 +50,8 @@ class MediaHierarchyManager @Inject constructor(
private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController,
private val mediaViewManager: MediaViewManager,
- private val mediaMeasurementProvider: MediaMeasurementManager,
- private val notifLockscreenUserManager: NotificationLockscreenUserManager
+ private val notifLockscreenUserManager: NotificationLockscreenUserManager,
+ wakefulnessLifecycle: WakefulnessLifecycle
) {
/**
* The root overlay of the hierarchy. This is where the media notification is attached to
@@ -56,23 +59,31 @@ class MediaHierarchyManager @Inject constructor(
* view is always in its final state when it is attached to a view host.
*/
private var rootOverlay: ViewGroupOverlay? = null
- private lateinit var currentState: MediaState
- private val mediaCarousel
+
+ private var rootView: View? = null
+ private var currentBounds = Rect()
+ private var animationStartBounds: Rect = Rect()
+ private var targetBounds: Rect = Rect()
+ private val mediaFrame
get() = mediaViewManager.mediaFrame
- private var animationStartState: MediaState? = null
private var statusbarState: Int = statusBarStateController.state
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
interpolator = Interpolators.FAST_OUT_SLOW_IN
addUpdateListener {
updateTargetState()
- applyState(animationStartState!!.interpolate(targetState!!, animatedFraction))
+ interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
+ result = currentBounds)
+ applyState(currentBounds)
}
addListener(object : AnimatorListenerAdapter() {
private var cancelled: Boolean = false
override fun onAnimationCancel(animation: Animator?) {
cancelled = true
+ animationPending = false
+ rootView?.removeCallbacks(startAnimation)
}
+
override fun onAnimationEnd(animation: Animator?) {
if (!cancelled) {
applyTargetStateIfNotAnimating()
@@ -81,30 +92,41 @@ class MediaHierarchyManager @Inject constructor(
override fun onAnimationStart(animation: Animator?) {
cancelled = false
+ animationPending = false
}
})
}
- private var targetState: MediaState? = null
- private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
+ private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
/**
* The last location where this view was at before going to the desired location. This is
* useful for guided transitions.
*/
- @MediaLocation private var previousLocation = -1
-
+ @MediaLocation
+ private var previousLocation = -1
/**
* The desired location where the view will be at the end of the transition.
*/
- @MediaLocation private var desiredLocation = -1
+ @MediaLocation
+ private var desiredLocation = -1
/**
* The current attachment location where the view is currently attached.
* Usually this matches the desired location except for animations whenever a view moves
* to the new desired location, during which it is in [IN_OVERLAY].
*/
- @MediaLocation private var currentAttachmentLocation = -1
+ @MediaLocation
+ private var currentAttachmentLocation = -1
+ /**
+ * Are we currently waiting on an animation to start?
+ */
+ private var animationPending: Boolean = false
+ private val startAnimation: Runnable = Runnable { animator.start() }
+
+ /**
+ * The expansion of quick settings
+ */
var qsExpansion: Float = 0.0f
set(value) {
if (field != value) {
@@ -117,6 +139,40 @@ class MediaHierarchyManager @Inject constructor(
}
}
+ /**
+ * Are location changes currently blocked?
+ */
+ private val blockLocationChanges: Boolean
+ get() {
+ return goingToSleep || dozeAnimationRunning
+ }
+
+ /**
+ * Are we currently going to sleep
+ */
+ private var goingToSleep: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ if (!value) {
+ updateDesiredLocation()
+ }
+ }
+ }
+
+ /**
+ * Is the doze animation currently Running
+ */
+ private var dozeAnimationRunning: Boolean = false
+ private set(value) {
+ if (field != value) {
+ field = value
+ if (!value) {
+ updateDesiredLocation()
+ }
+ }
+ }
+
init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onStatePreChange(oldState: Int, newState: Int) {
@@ -129,6 +185,34 @@ class MediaHierarchyManager @Inject constructor(
override fun onStateChanged(newState: Int) {
updateTargetState()
}
+
+ override fun onDozeAmountChanged(linear: Float, eased: Float) {
+ dozeAnimationRunning = linear != 0.0f && linear != 1.0f
+ }
+
+ override fun onDozingChanged(isDozing: Boolean) {
+ if (!isDozing) {
+ dozeAnimationRunning = false
+ }
+ }
+ })
+
+ wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
+ override fun onFinishedGoingToSleep() {
+ goingToSleep = false
+ }
+
+ override fun onStartedGoingToSleep() {
+ goingToSleep = true
+ }
+
+ override fun onFinishedWakingUp() {
+ goingToSleep = false
+ }
+
+ override fun onStartedWakingUp() {
+ goingToSleep = false
+ }
})
}
@@ -138,8 +222,8 @@ class MediaHierarchyManager @Inject constructor(
*
* @return the hostView associated with this location
*/
- fun register(mediaObject: MediaHost): ViewGroup {
- val viewHost = createUniqueObjectHost(mediaObject)
+ fun register(mediaObject: MediaHost): UniqueObjectHostView {
+ val viewHost = createUniqueObjectHost()
mediaObject.hostView = viewHost
mediaHosts[mediaObject.location] = mediaObject
if (mediaObject.location == desiredLocation) {
@@ -154,22 +238,13 @@ class MediaHierarchyManager @Inject constructor(
return viewHost
}
- private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView {
+ private fun createUniqueObjectHost(): UniqueObjectHostView {
val viewHost = UniqueObjectHostView(context)
- viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
- viewHost.onMeasureListener = { input ->
- if (host.location == desiredLocation) {
- // Measurement of the currently active player is happening, Let's make
- // sure the player width is up to date
- val measuringInput = host.getMeasuringInput(input)
- mediaViewManager.setPlayerWidth(measuringInput.width)
- }
- }
-
viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View?) {
if (rootOverlay == null) {
- rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay)
+ rootView = viewHost.viewRootImpl.view
+ rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
}
viewHost.removeOnAttachStateChangeListener(this)
}
@@ -195,8 +270,9 @@ class MediaHierarchyManager @Inject constructor(
// Let's perform a transition
val animate = shouldAnimateTransition(desiredLocation, previousLocation)
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
- mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState,
- animate, animDuration, delay)
+ val host = getHost(desiredLocation)
+ mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
+ delay)
performTransitionToNewLocation(isNewView, animate)
}
}
@@ -222,14 +298,18 @@ class MediaHierarchyManager @Inject constructor(
// Let's animate to the new position, starting from the current position
// We also go in here in case the view was detached, since the bounds wouldn't
// be correct anymore
- animationStartState = currentState.copy()
+ animationStartBounds.set(currentBounds)
} else {
// otherwise, let's take the freshest state, since the current one could
// be outdated
- animationStartState = previousHost.currentState.copy()
+ animationStartBounds.set(previousHost.currentBounds)
}
adjustAnimatorForTransition(desiredLocation, previousLocation)
- animator.start()
+ rootView?.let {
+ // Let's delay the animation start until we finished laying out
+ animationPending = true
+ it.postOnAnimation(startAnimation)
+ }
} else {
cancelAnimationAndApplyDesiredState()
}
@@ -239,6 +319,9 @@ class MediaHierarchyManager @Inject constructor(
@MediaLocation currentLocation: Int,
@MediaLocation previousLocation: Int
): Boolean {
+ if (isCurrentlyInGuidedTransformation()) {
+ return false
+ }
if (currentLocation == LOCATION_QQS &&
previousLocation == LOCATION_LOCKSCREEN &&
(statusBarStateController.leaveOpenOnKeyguardHide() ||
@@ -247,7 +330,7 @@ class MediaHierarchyManager @Inject constructor(
// non-trivial reattaching logic happening that will make the view not-shown earlier
return true
}
- return mediaCarousel.isShown || animator.isRunning
+ return mediaFrame.isShown || animator.isRunning || animationPending
}
private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
@@ -279,7 +362,7 @@ class MediaHierarchyManager @Inject constructor(
// Let's immediately apply the target state (which is interpolated) if there is
// no animation running. Otherwise the animation update will already update
// the location
- applyState(targetState!!)
+ applyState(targetBounds)
}
}
@@ -291,14 +374,34 @@ class MediaHierarchyManager @Inject constructor(
val progress = getTransformationProgress()
val currentHost = getHost(desiredLocation)!!
val previousHost = getHost(previousLocation)!!
- val newState = currentHost.currentState
- val previousState = previousHost.currentState
- targetState = previousState.interpolate(newState, progress)
+ val newBounds = currentHost.currentBounds
+ val previousBounds = previousHost.currentBounds
+ targetBounds = interpolateBounds(previousBounds, newBounds, progress)
} else {
- targetState = getHost(desiredLocation)?.currentState
+ val bounds = getHost(desiredLocation)?.currentBounds ?: return
+ targetBounds.set(bounds)
}
}
+ private fun interpolateBounds(
+ startBounds: Rect,
+ endBounds: Rect,
+ progress: Float,
+ result: Rect? = null
+ ): Rect {
+ val left = MathUtils.lerp(startBounds.left.toFloat(),
+ endBounds.left.toFloat(), progress).toInt()
+ val top = MathUtils.lerp(startBounds.top.toFloat(),
+ endBounds.top.toFloat(), progress).toInt()
+ val right = MathUtils.lerp(startBounds.right.toFloat(),
+ endBounds.right.toFloat(), progress).toInt()
+ val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
+ endBounds.bottom.toFloat(), progress).toInt()
+ val resultBounds = result ?: Rect()
+ resultBounds.set(left, top, right, bottom)
+ return resultBounds
+ }
+
/**
* @return true if this transformation is guided by an external progress like a finger
*/
@@ -339,21 +442,27 @@ class MediaHierarchyManager @Inject constructor(
private fun cancelAnimationAndApplyDesiredState() {
animator.cancel()
getHost(desiredLocation)?.let {
- applyState(it.currentState)
+ applyState(it.currentBounds, immediately = true)
}
}
- private fun applyState(state: MediaState) {
- currentState = state.copy()
- mediaViewManager.setCurrentState(currentState)
+ /**
+ * Apply the current state to the view, updating it's bounds and desired state
+ */
+ private fun applyState(bounds: Rect, immediately: Boolean = false) {
+ currentBounds.set(bounds)
+ val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
+ val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
+ val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
+ val endLocation = desiredLocation
+ mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately)
updateHostAttachment()
if (currentAttachmentLocation == IN_OVERLAY) {
- val boundsOnScreen = state.boundsOnScreen
- mediaCarousel.setLeftTopRightBottom(
- boundsOnScreen.left,
- boundsOnScreen.top,
- boundsOnScreen.right,
- boundsOnScreen.bottom)
+ mediaFrame.setLeftTopRightBottom(
+ currentBounds.left,
+ currentBounds.top,
+ currentBounds.right,
+ currentBounds.bottom)
}
}
@@ -364,26 +473,29 @@ class MediaHierarchyManager @Inject constructor(
currentAttachmentLocation = newLocation
// Remove the carousel from the old host
- (mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
+ (mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
// Add it to the new one
val targetHost = getHost(desiredLocation)!!.hostView
if (inOverlay) {
- rootOverlay!!.add(mediaCarousel)
+ rootOverlay!!.add(mediaFrame)
} else {
- targetHost.addView(mediaCarousel)
- mediaViewManager.onViewReattached()
+ targetHost.addView(mediaFrame)
}
}
}
private fun isTransitionRunning(): Boolean {
return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
- animator.isRunning
+ animator.isRunning || animationPending
}
@MediaLocation
private fun calculateLocation(): Int {
+ if (blockLocationChanges) {
+ // Keep the current location until we're allowed to again
+ return desiredLocation
+ }
val onLockscreen = (!bypassController.bypassEnabled &&
(statusbarState == StatusBarState.KEYGUARD ||
statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
@@ -396,13 +508,6 @@ class MediaHierarchyManager @Inject constructor(
}
}
- /**
- * The expansion of quick settings
- */
- @IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
- @Retention(AnnotationRetention.SOURCE)
- annotation class MediaLocation
-
companion object {
/**
* Attached in expanded quick settings
@@ -425,3 +530,8 @@ class MediaHierarchyManager @Inject constructor(
const val IN_OVERLAY = -1000
}
}
+
+@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
+ MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
+@Retention(AnnotationRetention.SOURCE)
+annotation class MediaLocation \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 240e44cb8db4..e904e935b0e0 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -1,20 +1,22 @@
package com.android.systemui.media
import android.graphics.Rect
-import android.util.MathUtils
import android.view.View
import android.view.View.OnAttachStateChangeListener
-import android.view.ViewGroup
-import com.android.systemui.media.MediaHierarchyManager.MediaLocation
import com.android.systemui.util.animation.MeasurementInput
+import com.android.systemui.util.animation.MeasurementOutput
+import com.android.systemui.util.animation.UniqueObjectHostView
+import java.util.Objects
import javax.inject.Inject
class MediaHost @Inject constructor(
- private val state: MediaHostState,
+ private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager,
- private val mediaDataManager: MediaDataManager
-) : MediaState by state {
- lateinit var hostView: ViewGroup
+ private val mediaDataManager: MediaDataManager,
+ private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
+ private val mediaHostStatesManager: MediaHostStatesManager
+) : MediaHostState by state {
+ lateinit var hostView: UniqueObjectHostView
var location: Int = -1
private set
var visibleChangedListener: ((Boolean) -> Unit)? = null
@@ -24,9 +26,9 @@ class MediaHost @Inject constructor(
private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
/**
- * Get the current Media state. This also updates the location on screen
+ * Get the current bounds on the screen. This makes sure the state is fresh and up to date
*/
- val currentState: MediaState
+ val currentBounds: Rect = Rect()
get() {
hostView.getLocationOnScreen(tmpLocationOnScreen)
var left = tmpLocationOnScreen[0] + hostView.paddingLeft
@@ -43,8 +45,8 @@ class MediaHost @Inject constructor(
bottom = 0
top = 0
}
- state.boundsOnScreen.set(left, top, right, bottom)
- return state
+ field.set(left, top, right, bottom)
+ return field
}
private val listener = object : MediaDataManager.Listener {
@@ -59,6 +61,8 @@ class MediaHost @Inject constructor(
/**
* Initialize this MediaObject and create a host view.
+ * All state should already be set on this host before calling this method in order to avoid
+ * unnecessary state changes which lead to remeasurings later on.
*
* @param location the location this host name has. Used to identify the host during
* transitions.
@@ -68,14 +72,39 @@ class MediaHost @Inject constructor(
hostView = mediaHierarchyManager.register(this)
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
- mediaDataManager.addListener(listener)
+ // we should listen to the combined state change, since otherwise there might
+ // be a delay until the views and the controllers are initialized, leaving us
+ // with either a blank view or the controllers not yet initialized and the
+ // measuring wrong
+ mediaDataManagerCombineLatest.addListener(listener)
updateViewVisibility()
}
override fun onViewDetachedFromWindow(v: View?) {
- mediaDataManager.removeListener(listener)
+ mediaDataManagerCombineLatest.removeListener(listener)
}
})
+
+ // Listen to measurement updates and update our state with it
+ hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
+ override fun onMeasure(input: MeasurementInput): MeasurementOutput {
+ // Modify the measurement to exactly match the dimensions
+ if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
+ input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
+ View.MeasureSpec.getSize(input.widthMeasureSpec),
+ View.MeasureSpec.EXACTLY)
+ }
+ // This will trigger a state change that ensures that we now have a state available
+ state.measurementInput = input
+ return mediaHostStatesManager.getPlayerDimensions(state)
+ }
+ }
+
+ // Whenever the state changes, let our state manager know
+ state.changedListener = {
+ mediaHostStatesManager.updateHostState(location, state)
+ }
+
updateViewVisibility()
}
@@ -89,71 +118,93 @@ class MediaHost @Inject constructor(
visibleChangedListener?.invoke(visible)
}
- class MediaHostState @Inject constructor() : MediaState {
- var measurementInput: MediaMeasurementInput? = null
+ class MediaHostStateHolder @Inject constructor() : MediaHostState {
+
+ override var measurementInput: MeasurementInput? = null
+ set(value) {
+ if (value?.equals(field) != true) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
override var expansion: Float = 0.0f
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
override var showsOnlyActiveMedia: Boolean = false
- override val boundsOnScreen: Rect = Rect()
+ set(value) {
+ if (!value.equals(field)) {
+ field = value
+ changedListener?.invoke()
+ }
+ }
+
+ /**
+ * A listener for all changes. This won't be copied over when invoking [copy]
+ */
+ var changedListener: (() -> Unit)? = null
- override fun copy(): MediaState {
- val mediaHostState = MediaHostState()
+ /**
+ * Get a copy of this state. This won't copy any listeners it may have set
+ */
+ override fun copy(): MediaHostState {
+ val mediaHostState = MediaHostStateHolder()
mediaHostState.expansion = expansion
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
- mediaHostState.boundsOnScreen.set(boundsOnScreen)
- mediaHostState.measurementInput = measurementInput
+ mediaHostState.measurementInput = measurementInput?.copy()
return mediaHostState
}
- override fun interpolate(other: MediaState, amount: Float): MediaState {
- val result = MediaHostState()
- result.expansion = MathUtils.lerp(expansion, other.expansion, amount)
- val left = MathUtils.lerp(boundsOnScreen.left.toFloat(),
- other.boundsOnScreen.left.toFloat(), amount).toInt()
- val top = MathUtils.lerp(boundsOnScreen.top.toFloat(),
- other.boundsOnScreen.top.toFloat(), amount).toInt()
- val right = MathUtils.lerp(boundsOnScreen.right.toFloat(),
- other.boundsOnScreen.right.toFloat(), amount).toInt()
- val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(),
- other.boundsOnScreen.bottom.toFloat(), amount).toInt()
- result.boundsOnScreen.set(left, top, right, bottom)
- result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
- if (amount > 0.0f) {
- if (other is MediaHostState) {
- result.measurementInput = other.measurementInput
- }
- } else {
- result.measurementInput
+ override fun equals(other: Any?): Boolean {
+ if (!(other is MediaHostState)) {
+ return false
}
- return result
+ if (!Objects.equals(measurementInput, other.measurementInput)) {
+ return false
+ }
+ if (expansion != other.expansion) {
+ return false
+ }
+ if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
+ return false
+ }
+ return true
}
- override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput {
- measurementInput = MediaMeasurementInput(input, expansion)
- return measurementInput as MediaMeasurementInput
+ override fun hashCode(): Int {
+ var result = measurementInput?.hashCode() ?: 0
+ result = 31 * result + expansion.hashCode()
+ result = 31 * result + showsOnlyActiveMedia.hashCode()
+ return result
}
}
}
-interface MediaState {
+interface MediaHostState {
+
+ /**
+ * The last measurement input that this state was measured with. Infers with and height of
+ * the players.
+ */
+ var measurementInput: MeasurementInput?
+
+ /**
+ * The expansion of the player, 0 for fully collapsed, 1 for fully expanded
+ */
var expansion: Float
+
+ /**
+ * Is this host only showing active media or is it showing all of them including resumption?
+ */
var showsOnlyActiveMedia: Boolean
- val boundsOnScreen: Rect
- fun copy(): MediaState
- fun interpolate(other: MediaState, amount: Float): MediaState
- fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput
-}
-/**
- * The measurement input for a Media View
- */
-data class MediaMeasurementInput(
- private val viewInput: MeasurementInput,
- val expansion: Float
-) : MeasurementInput by viewInput {
-
- override fun sameAs(input: MeasurementInput?): Boolean {
- if (!(input is MediaMeasurementInput)) {
- return false
- }
- return width == input.width && expansion == input.expansion
- }
+
+ /**
+ * Get a copy of this view state, deepcopying all appropriate members
+ */
+ fun copy(): MediaHostState
} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
new file mode 100644
index 000000000000..f90af2a01de0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHostStatesManager.kt
@@ -0,0 +1,116 @@
+/*
+ * 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 com.android.systemui.util.animation.MeasurementOutput
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * A class responsible for managing all media host states of the various host locations and
+ * coordinating the heights among different players. This class can be used to get the most up to
+ * date state for any location.
+ */
+@Singleton
+class MediaHostStatesManager @Inject constructor() {
+
+ private val callbacks: MutableSet<Callback> = mutableSetOf()
+ private val controllers: MutableSet<MediaViewController> = mutableSetOf()
+
+ /**
+ * A map with all media states of all locations.
+ */
+ val mediaHostStates: MutableMap<Int, MediaHostState> = mutableMapOf()
+
+ /**
+ * Notify that a media state for a given location has changed. Should only be called from
+ * Media hosts themselves.
+ */
+ fun updateHostState(@MediaLocation location: Int, hostState: MediaHostState) {
+ val currentState = mediaHostStates.get(location)
+ if (!hostState.equals(currentState)) {
+ val newState = hostState.copy()
+ mediaHostStates.put(location, newState)
+ // First update all the controllers to ensure they get the chance to measure
+ for (controller in controllers) {
+ controller.stateCallback.onHostStateChanged(location, newState)
+ }
+
+ // Then update all other callbacks which may depend on the controllers above
+ for (callback in callbacks) {
+ callback.onHostStateChanged(location, newState)
+ }
+ }
+ }
+
+ /**
+ * Get the dimensions of all players combined, which determines the overall height of the
+ * media carousel and the media hosts.
+ */
+ fun getPlayerDimensions(hostState: MediaHostState): MeasurementOutput {
+ val result = MeasurementOutput(0, 0)
+ for (controller in controllers) {
+ val measurement = controller.getMeasurementsForState(hostState)
+ measurement?.let {
+ if (it.measuredHeight > result.measuredHeight) {
+ result.measuredHeight = it.measuredHeight
+ }
+ if (it.measuredWidth > result.measuredWidth) {
+ result.measuredWidth = it.measuredWidth
+ }
+ }
+ }
+ return result
+ }
+
+ /**
+ * Add a callback to be called when a MediaState has updated
+ */
+ fun addCallback(callback: Callback) {
+ callbacks.add(callback)
+ }
+
+ /**
+ * Remove a callback that listens to media states
+ */
+ fun removeCallback(callback: Callback) {
+ callbacks.remove(callback)
+ }
+
+ /**
+ * Register a controller that listens to media states and is used to determine the size of
+ * the media carousel
+ */
+ fun addController(controller: MediaViewController) {
+ controllers.add(controller)
+ }
+
+ /**
+ * Notify the manager about the removal of a controller.
+ */
+ fun removeController(controller: MediaViewController) {
+ controllers.remove(controller)
+ }
+
+ interface Callback {
+ /**
+ * Notify the callbacks that a media state for a host has changed, and that the
+ * corresponding view states should be updated and applied
+ */
+ fun onHostStateChanged(@MediaLocation location: Int, mediaHostState: MediaHostState)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
deleted file mode 100644
index 4bbf5eb9f0dc..000000000000
--- a/packages/SystemUI/src/com/android/systemui/media/MediaMeasurementManager.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * 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 com.android.systemui.util.animation.BaseMeasurementCache
-import com.android.systemui.util.animation.GuaranteedMeasurementCache
-import com.android.systemui.util.animation.MeasurementCache
-import com.android.systemui.util.animation.MeasurementInput
-import com.android.systemui.util.animation.MeasurementOutput
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/**
- * A class responsible creating measurement caches for media hosts which also coordinates with
- * the view manager to obtain sizes for unknown measurement inputs.
- */
-@Singleton
-class MediaMeasurementManager @Inject constructor(
- private val mediaViewManager: MediaViewManager
-) {
- private val baseCache: MeasurementCache
-
- init {
- baseCache = BaseMeasurementCache()
- }
-
- private fun provideMeasurement(input: MediaMeasurementInput) : MeasurementOutput? {
- return mediaViewManager.obtainMeasurement(input)
- }
-
- /**
- * Obtain a guaranteed measurement cache for a host view. The measurement cache makes sure that
- * requesting any size from the cache will always return the correct value.
- */
- fun obtainCache(host: MediaState): GuaranteedMeasurementCache {
- val remapper = { input: MeasurementInput ->
- host.getMeasuringInput(input)
- }
- val provider = { input: MeasurementInput ->
- provideMeasurement(input as MediaMeasurementInput)
- }
- return GuaranteedMeasurementCache(baseCache, remapper, provider)
- }
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
new file mode 100644
index 000000000000..e82bb402407e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -0,0 +1,280 @@
+/*
+ * 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.content.Context
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.R
+import com.android.systemui.util.animation.TransitionLayout
+import com.android.systemui.util.animation.TransitionLayoutController
+import com.android.systemui.util.animation.TransitionViewState
+import com.android.systemui.util.animation.MeasurementOutput
+
+/**
+ * A class responsible for controlling a single instance of a media player handling interactions
+ * with the view instance and keeping the media view states up to date.
+ */
+class MediaViewController(
+ context: Context,
+ val mediaHostStatesManager: MediaHostStatesManager
+) {
+
+ private var firstRefresh: Boolean = true
+ private var transitionLayout: TransitionLayout? = null
+ private val layoutController = TransitionLayoutController()
+ private var animationDelay: Long = 0
+ private var animationDuration: Long = 0
+ private var animateNextStateChange: Boolean = false
+ private val measurement = MeasurementOutput(0, 0)
+
+ /**
+ * A map containing all viewStates for all locations of this mediaState
+ */
+ private val mViewStates: MutableMap<MediaHostState, TransitionViewState?> = mutableMapOf()
+
+ /**
+ * The ending location of the view where it ends when all animations and transitions have
+ * finished
+ */
+ private var currentEndLocation: Int = -1
+
+ /**
+ * The ending location of the view where it ends when all animations and transitions have
+ * finished
+ */
+ private var currentStartLocation: Int = -1
+
+ /**
+ * The progress of the transition or 1.0 if there is no transition happening
+ */
+ private var currentTransitionProgress: Float = 1.0f
+
+ /**
+ * A temporary state used to store intermediate measurements.
+ */
+ private val tmpState = TransitionViewState()
+
+ /**
+ * A callback for media state changes
+ */
+ val stateCallback = object : MediaHostStatesManager.Callback {
+ override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+ if (location == currentEndLocation || location == currentStartLocation) {
+ setCurrentState(currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = false)
+ }
+ }
+ }
+
+ /**
+ * The expanded constraint set used to render a expanded player. If it is modified, make sure
+ * to call [refreshState]
+ */
+ val collapsedLayout = ConstraintSet()
+
+ /**
+ * The expanded constraint set used to render a collapsed player. If it is modified, make sure
+ * to call [refreshState]
+ */
+ val expandedLayout = ConstraintSet()
+
+ init {
+ collapsedLayout.load(context, R.xml.media_collapsed)
+ expandedLayout.load(context, R.xml.media_expanded)
+ mediaHostStatesManager.addController(this)
+ }
+
+ /**
+ * Notify this controller that the view has been removed and all listeners should be destroyed
+ */
+ fun onDestroy() {
+ mediaHostStatesManager.removeController(this)
+ }
+
+ private fun ensureAllMeasurements() {
+ val mediaStates = mediaHostStatesManager.mediaHostStates
+ for (entry in mediaStates) {
+ obtainViewState(entry.value)
+ }
+ }
+
+ /**
+ * Get the constraintSet for a given expansion
+ */
+ private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
+ if (expansion > 0) expandedLayout else collapsedLayout
+
+ /**
+ * Obtain a new viewState for a given media state. This usually returns a cached state, but if
+ * it's not available, it will recreate one by measuring, which may be expensive.
+ */
+ private fun obtainViewState(state: MediaHostState): TransitionViewState? {
+ val viewState = mViewStates[state]
+ if (viewState != null) {
+ // we already have cached this measurement, let's continue
+ return viewState
+ }
+
+ val result: TransitionViewState?
+ if (transitionLayout != null && state.measurementInput != null) {
+ // Let's create a new measurement
+ if (state.expansion == 0.0f || state.expansion == 1.0f) {
+ result = transitionLayout!!.calculateViewState(
+ state.measurementInput!!,
+ constraintSetForExpansion(state.expansion),
+ TransitionViewState())
+
+ // We don't want to cache interpolated or null states as this could quickly fill up
+ // our cache. We only cache the start and the end states since the interpolation
+ // is cheap
+ mViewStates[state.copy()] = result
+ } else {
+ // This is an interpolated state
+ val startState = state.copy().also { it.expansion = 0.0f }
+
+ // Given that we have a measurement and a view, let's get (guaranteed) viewstates
+ // from the start and end state and interpolate them
+ val startViewState = obtainViewState(startState) as TransitionViewState
+ val endState = state.copy().also { it.expansion = 1.0f }
+ val endViewState = obtainViewState(endState) as TransitionViewState
+ result = TransitionViewState()
+ layoutController.getInterpolatedState(
+ startViewState,
+ endViewState,
+ state.expansion,
+ result)
+ }
+ } else {
+ result = null
+ }
+ return result
+ }
+
+ /**
+ * Attach a view to this controller. This may perform measurements if it's not available yet
+ * and should therefore be done carefully.
+ */
+ fun attach(transitionLayout: TransitionLayout) {
+ this.transitionLayout = transitionLayout
+ layoutController.attach(transitionLayout)
+ ensureAllMeasurements()
+ if (currentEndLocation == -1) {
+ return
+ }
+ // Set the previously set state immediately to the view, now that it's finally attached
+ setCurrentState(
+ startLocation = currentStartLocation,
+ endLocation = currentEndLocation,
+ transitionProgress = currentTransitionProgress,
+ applyImmediately = true)
+ }
+
+ /**
+ * Obtain a measurement for a given location. This makes sure that the state is up to date
+ * and all widgets know their location. Calling this method may create a measurement if we
+ * don't have a cached value available already.
+ */
+ fun getMeasurementsForState(hostState: MediaHostState): MeasurementOutput? {
+ val viewState = obtainViewState(hostState) ?: return null
+ measurement.measuredWidth = viewState.width
+ measurement.measuredHeight = viewState.height
+ return measurement
+ }
+
+ /**
+ * Set a new state for the controlled view which can be an interpolation between multiple
+ * locations.
+ */
+ fun setCurrentState(
+ @MediaLocation startLocation: Int,
+ @MediaLocation endLocation: Int,
+ transitionProgress: Float,
+ applyImmediately: Boolean
+ ) {
+ currentEndLocation = endLocation
+ currentStartLocation = startLocation
+ currentTransitionProgress = transitionProgress
+
+ val shouldAnimate = animateNextStateChange && !applyImmediately
+
+ // Obtain the view state that we'd want to be at the end
+ // The view might not be bound yet or has never been measured and in that case will be
+ // reset once the state is fully available
+ val endState = obtainViewStateForLocation(endLocation) ?: return
+ layoutController.setMeasureState(endState)
+
+ // If the view isn't bound, we can drop the animation, otherwise we'll executute it
+ animateNextStateChange = false
+ if (transitionLayout == null) {
+ return
+ }
+
+ val startState = obtainViewStateForLocation(startLocation)
+ val result: TransitionViewState?
+ if (transitionProgress == 1.0f || startState == null) {
+ result = endState
+ } else if (transitionProgress == 0.0f) {
+ result = startState
+ } else {
+ layoutController.getInterpolatedState(startState, endState, transitionProgress,
+ tmpState)
+ result = tmpState
+ }
+ layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration,
+ animationDelay)
+ }
+
+ private fun obtainViewStateForLocation(location: Int): TransitionViewState? {
+ val mediaState = mediaHostStatesManager.mediaHostStates[location] ?: return null
+ return obtainViewState(mediaState)
+ }
+
+ /**
+ * Notify that the location is changing right now and a [setCurrentState] change is imminent.
+ * This updates the width the view will me measured with.
+ */
+ fun onLocationPreChange(@MediaLocation newLocation: Int) {
+ val viewState = obtainViewStateForLocation(newLocation)
+ viewState?.let {
+ layoutController.setMeasureState(it)
+ }
+ }
+
+ /**
+ * Request that the next state change should be animated with the given parameters.
+ */
+ fun animatePendingStateChange(duration: Long, delay: Long) {
+ animateNextStateChange = true
+ animationDuration = duration
+ animationDelay = delay
+ }
+
+ /**
+ * Clear all existing measurements and refresh the state to match the view.
+ */
+ fun refreshState() {
+ if (!firstRefresh) {
+ // Let's clear all of our measurements and recreate them!
+ mViewStates.clear()
+ setCurrentState(currentStartLocation, currentEndLocation, currentTransitionProgress,
+ applyImmediately = false)
+ }
+ firstRefresh = false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 16302d1632b5..8ab30c75c7eb 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -16,8 +16,8 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager
-import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView
+import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.Executor
import javax.inject.Inject
@@ -36,16 +36,51 @@ class MediaViewManager @Inject constructor(
@Background private val backgroundExecutor: DelayableExecutor,
private val visualStabilityManager: VisualStabilityManager,
private val activityStarter: ActivityStarter,
+ private val mediaHostStatesManager: MediaHostStatesManager,
mediaManager: MediaDataCombineLatest
) {
- private var playerWidth: Int = 0
+
+ /**
+ * 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.
+ */
+ @MediaLocation
+ private var desiredLocation: Int = -1
+
+ /**
+ * The ending location of the view where it ends when all animations and transitions have
+ * finished
+ */
+ @MediaLocation
+ private var currentEndLocation: Int = -1
+
+ /**
+ * The ending location of the view where it ends when all animations and transitions have
+ * finished
+ */
+ @MediaLocation
+ private var currentStartLocation: Int = -1
+
+ /**
+ * The progress of the transition or 1.0 if there is no transition happening
+ */
+ private var currentTransitionProgress: Float = 1.0f
+
+ /**
+ * The measured width of the carousel
+ */
+ private var carouselMeasureWidth: Int = 0
+
+ /**
+ * The measured height of the carousel
+ */
+ private var carouselMeasureHeight: Int = 0
private var playerWidthPlusPadding: Int = 0
- private var desiredState: MediaHost.MediaHostState? = null
- private var currentState: MediaState? = null
+ private var desiredHostState: MediaHostState? = null
private val mediaCarousel: HorizontalScrollView
val mediaFrame: ViewGroup
+ val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val mediaContent: ViewGroup
- private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val pageIndicator: PageIndicator
private val gestureDetector: GestureDetectorCompat
private val visualStabilityCallback: VisualStabilityManager.Callback
@@ -115,6 +150,7 @@ class MediaViewManager @Inject constructor(
override fun onMediaDataLoaded(key: String, data: MediaData) {
updateView(key, data)
updatePlayerVisibilities()
+ mediaCarousel.requiresRemeasuring = true
}
override fun onMediaDataRemoved(key: String) {
@@ -137,6 +173,13 @@ class MediaViewManager @Inject constructor(
}
}
})
+ mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
+ override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
+ if (location == desiredLocation) {
+ onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
+ }
+ }
+ })
}
private fun inflateMediaCarousel(): ViewGroup {
@@ -220,7 +263,7 @@ class MediaViewManager @Inject constructor(
var existingPlayer = mediaPlayers[key]
if (existingPlayer == null) {
existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
- activityStarter)
+ activityStarter, mediaHostStatesManager)
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent))
mediaPlayers[key] = existingPlayer
@@ -228,14 +271,14 @@ class MediaViewManager @Inject constructor(
ViewGroup.LayoutParams.WRAP_CONTENT)
existingPlayer.view?.player?.setLayoutParams(lp)
existingPlayer.setListening(currentlyExpanded)
+ updatePlayerToState(existingPlayer, noAnimation = true)
if (existingPlayer.isPlaying) {
mediaContent.addView(existingPlayer.view?.player, 0)
} else {
mediaContent.addView(existingPlayer.view?.player)
}
- updatePlayerToCurrentState(existingPlayer)
} else if (existingPlayer.isPlaying &&
- mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
+ mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
if (visualStabilityManager.isReorderingAllowed) {
mediaContent.removeView(existingPlayer.view?.player)
mediaContent.addView(existingPlayer.view?.player, 0)
@@ -244,20 +287,10 @@ class MediaViewManager @Inject constructor(
}
}
existingPlayer.bind(data)
- // Resetting the progress to make sure it's taken into account for the latest
- // motion model
- existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f
updateMediaPaddings()
updatePageIndicator()
}
- private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
- if (desiredState != null && desiredState!!.measurementInput != null) {
- // make sure the player width is set to the current state
- existingPlayer.setPlayerWidth(playerWidth)
- }
- }
-
private fun updateMediaPaddings() {
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
val childCount = mediaContent.childCount
@@ -281,117 +314,101 @@ class MediaViewManager @Inject constructor(
}
/**
- * Set the current state of a view. This is updated often during animations and we shouldn't
- * do anything expensive.
+ * 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.
*/
- fun setCurrentState(state: MediaState) {
- currentState = state
- currentlyExpanded = state.expansion > 0
+ fun setCurrentState(
+ @MediaLocation startLocation: Int,
+ @MediaLocation endLocation: Int,
+ progress: Float,
+ immediately: Boolean
+ ) {
// Hack: Since the indicator doesn't move with the player expansion, just make it disappear
// and then reappear at the end.
- pageIndicator.alpha = if (state.expansion == 1f || state.expansion == 0f) 1f else 0f
- for (mediaPlayer in mediaPlayers.values) {
- val view = mediaPlayer.view?.player
- view?.progress = state.expansion
+ pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
+ if (startLocation != currentStartLocation ||
+ endLocation != currentEndLocation ||
+ progress != currentTransitionProgress ||
+ immediately
+ ) {
+ currentStartLocation = startLocation
+ currentEndLocation = endLocation
+ currentTransitionProgress = progress
+ for (mediaPlayer in mediaPlayers.values) {
+ updatePlayerToState(mediaPlayer, immediately)
+ }
}
}
+ private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
+ mediaPlayer.mediaViewController.setCurrentState(
+ startLocation = currentStartLocation,
+ endLocation = currentEndLocation,
+ transitionProgress = currentTransitionProgress,
+ applyImmediately = noAnimation)
+ }
+
/**
* The desired location of this view has changed. We should remeasure the view to match
* the new bounds and kick off bounds animations if necessary.
* If an animation is happening, an animation is kicked of externally, which sets a new
* current state until we reach the targetState.
*
- * @param desiredState the target state we're transitioning to
+ * @param desiredLocation the location we're going to
+ * @param desiredHostState the target state we're transitioning to
* @param animate should this be animated
*/
fun onDesiredLocationChanged(
- desiredState: MediaState?,
+ desiredLocation: Int,
+ desiredHostState: MediaHostState?,
animate: Boolean,
- duration: Long,
- startDelay: Long
+ duration: Long = 200,
+ startDelay: Long = 0
) {
- if (desiredState is MediaHost.MediaHostState) {
+ desiredHostState?.let {
// This is a hosting view, let's remeasure our players
- this.desiredState = desiredState
- val width = desiredState.boundsOnScreen.width()
- if (playerWidth != width) {
- setPlayerWidth(width)
- for (mediaPlayer in mediaPlayers.values) {
- if (animate && mediaPlayer.view?.player?.visibility == View.VISIBLE) {
- mediaPlayer.animatePendingSizeChange(duration, startDelay)
- }
- }
- val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
- val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
- var left = 0
- for (i in 0 until mediaContent.childCount) {
- val view = mediaContent.getChildAt(i)
- view.measure(widthSpec, heightSpec)
- view.layout(left, 0, left + width, view.measuredHeight)
- left = left + playerWidthPlusPadding
+ this.desiredLocation = desiredLocation
+ this.desiredHostState = it
+ currentlyExpanded = it.expansion > 0
+ for (mediaPlayer in mediaPlayers.values) {
+ if (animate) {
+ mediaPlayer.mediaViewController.animatePendingStateChange(
+ duration = duration,
+ delay = startDelay)
}
+ mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
}
+ updateCarouselSize()
}
}
- fun setPlayerWidth(width: Int) {
- if (width != playerWidth) {
- playerWidth = width
- playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
+ /**
+ * Update the size of the carousel, remeasuring it if necessary.
+ */
+ private fun updateCarouselSize() {
+ val width = desiredHostState?.measurementInput?.width ?: 0
+ val height = desiredHostState?.measurementInput?.height ?: 0
+ if (width != carouselMeasureWidth && width != 0 ||
+ height != carouselMeasureWidth && height != 0) {
+ carouselMeasureWidth = width
+ carouselMeasureHeight = height
+ playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
R.dimen.qs_media_padding)
- for (mediaPlayer in mediaPlayers.values) {
- mediaPlayer.setPlayerWidth(width)
- }
// 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)
+ newScroll += playerWidthPlusPadding -
+ (scrollIntoCurrentMedia - playerWidthPlusPadding)
} else {
newScroll += scrollIntoCurrentMedia
}
mediaCarousel.scrollX = newScroll
- }
- }
-
- /**
- * Get a measurement for the given input state. This measures the first player and returns
- * its bounds as if it were measured with the given measurement dimensions
- */
- fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? {
- val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
- var result: MeasurementOutput? = null
- firstPlayer.view?.player?.let {
- // Let's measure the size of the first player and return its height
- val previousProgress = it.progress
- val previousRight = it.right
- val previousBottom = it.bottom
- it.progress = input.expansion
- firstPlayer.measure(input)
- // Relayouting is necessary in motionlayout to obtain its size properly ....
- it.layout(0, 0, it.measuredWidth, it.measuredHeight)
- result = MeasurementOutput(it.measuredWidth, it.measuredHeight)
- it.progress = previousProgress
- if (desiredState != null) {
- // remeasure it to the old size again!
- firstPlayer.measure(desiredState!!.measurementInput)
- it.layout(0, 0, previousRight, previousBottom)
- }
- }
- return result
- }
-
- fun onViewReattached() {
- if (desiredState is MediaHost.MediaHostState) {
- // HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
- // a measure to force it.
- val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
- val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
- for (mediaPlayer in mediaPlayers.values) {
- mediaPlayer.view?.player?.measure(widthSpec, heightSpec)
- }
+ // Let's remeasure the carousel
+ val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0
+ val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0
+ mediaCarousel.measure(widthSpec, heightSpec)
+ mediaCarousel.layout(0, 0, width, mediaCarousel.measuredHeight)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
index 764dbe6d428f..60c576bd6c34 100644
--- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -23,18 +23,15 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
-
-import androidx.constraintlayout.motion.widget.MotionLayout
-
import com.android.systemui.R
+import com.android.systemui.util.animation.TransitionLayout
/**
* ViewHolder for a media player.
*/
class PlayerViewHolder private constructor(itemView: View) {
- val player = itemView as MotionLayout
- val background = itemView.requireViewById<View>(R.id.media_background)
+ val player = itemView as TransitionLayout
// Player information
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
@@ -61,7 +58,7 @@ class PlayerViewHolder private constructor(itemView: View) {
val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
init {
- (background.background as IlluminationDrawable).let {
+ (player.background as IlluminationDrawable).let {
it.setupTouch(seamless, player)
it.setupTouch(action0, player)
it.setupTouch(action1, player)
@@ -95,7 +92,7 @@ class PlayerViewHolder private constructor(itemView: View) {
* @param parent Parent of inflated view.
*/
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder {
- val v = inflater.inflate(R.layout.qs_media_panel, parent, false)
+ val v = inflater.inflate(R.layout.media_view, parent, false)
return PlayerViewHolder(v)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index b877e8745769..8f9e9e2eacd5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -427,7 +427,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
(ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
float targetPosition = absoluteBottomPosition - params.bottomMargin
- hostView.getHeight();
- float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top
+ float currentPosition = mediaHost.getCurrentBounds().top
- hostView.getTranslationY();
hostView.setTranslationY(targetPosition - currentPosition);
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index cdde06b0b143..4f0b56e705de 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -187,9 +187,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
}
protected void addMediaHostView() {
- mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
mMediaHost.setExpansion(1.0f);
mMediaHost.setShowsOnlyActiveMedia(false);
+ mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
ViewGroup hostView = mMediaHost.getHostView();
addView(hostView);
int sidePaddings = getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 2f06c4b1fed0..75507beba7ae 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -190,9 +190,9 @@ public class QuickQSPanel extends QSPanel {
switchTileLayout();
return null;
});
- mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
mMediaHost.setExpansion(0.0f);
mMediaHost.setShowsOnlyActiveMedia(true);
+ mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
reAttachMediaHost();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index b57b22fed853..8e6398f3630b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -133,7 +133,7 @@ public class CrossFadeHelper {
}
public static void fadeIn(View view, float fadeInAmount) {
- fadeIn(view, fadeInAmount, true /* remap */);
+ fadeIn(view, fadeInAmount, false /* remap */);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
index dea1a07c0268..cb7da4fb9b56 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/CustomInterpolatorTransformation.java
@@ -66,7 +66,7 @@ public abstract class CustomInterpolatorTransformation
return false;
}
View view = ownState.getTransformedView();
- CrossFadeHelper.fadeIn(view, transformationAmount);
+ CrossFadeHelper.fadeIn(view, transformationAmount, true /* remap */);
ownState.transformViewFullyFrom(otherState, this, transformationAmount);
otherState.recycle();
return true;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
index 27109d2acfa2..9a8cff0f8dc1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/TransformState.java
@@ -95,7 +95,7 @@ public class TransformState {
if (sameAs(otherState)) {
ensureVisible();
} else {
- CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+ CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
}
transformViewFullyFrom(otherState, transformationAmount);
}
@@ -424,7 +424,7 @@ public class TransformState {
if (transformationAmount == 0.0f) {
prepareFadeIn();
}
- CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
+ CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
}
public void disappear(float transformationAmount, TransformableView otherView) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
index 207144931c3b..bc2adac31d07 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridNotificationView.java
@@ -92,7 +92,7 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout
// We want to transform from the same y location as the title
TransformState otherState = notification.getCurrentState(
TRANSFORMING_VIEW_TITLE);
- CrossFadeHelper.fadeIn(mTextView, transformationAmount);
+ CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */);
if (otherState != null) {
ownState.transformViewVerticalFrom(otherState, transformationAmount);
otherState.recycle();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
index 2d99ab1e999f..14aab9d62dfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
@@ -23,7 +23,6 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
@@ -107,7 +106,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
TransformState otherState = notification.getCurrentState(
TRANSFORMING_VIEW_TITLE);
final View text = ownState.getTransformedView();
- CrossFadeHelper.fadeIn(text, transformationAmount);
+ CrossFadeHelper.fadeIn(text, transformationAmount, true /* remap */);
if (otherState != null) {
ownState.transformViewVerticalFrom(otherState, this,
transformationAmount);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index fa7f282be74a..02e537d2879f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -284,7 +284,7 @@ public abstract class NotificationViewWrapper implements TransformableView {
@Override
public void transformFrom(TransformableView notification, float transformationAmount) {
- CrossFadeHelper.fadeIn(mView, transformationAmount);
+ CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt
deleted file mode 100644
index 2be698b4e796..000000000000
--- a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementCache.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.util.animation
-
-/**
- * A class responsible for caching view Measurements which guarantees that we always obtain a value
- */
-class GuaranteedMeasurementCache constructor(
- private val baseCache : MeasurementCache,
- private val inputMapper: (MeasurementInput) -> MeasurementInput,
- private val measurementProvider: (MeasurementInput) -> MeasurementOutput?
-) : MeasurementCache {
-
- override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput {
- val mappedInput = inputMapper.invoke(input)
- if (!baseCache.contains(mappedInput)) {
- var measurement = measurementProvider.invoke(mappedInput)
- if (measurement != null) {
- // Only cache measurings that actually have a size
- baseCache.putMeasurement(mappedInput, measurement)
- } else {
- measurement = MeasurementOutput(0, 0)
- }
- return measurement
- } else {
- return baseCache.obtainMeasurement(mappedInput)
- }
- }
-
- override fun contains(input: MeasurementInput): Boolean {
- return baseCache.contains(inputMapper.invoke(input))
- }
-
- override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) {
- if (output.measuredWidth == 0 || output.measuredHeight == 0) {
- // Only cache measurings that actually have a size
- return;
- }
- val remappedInput = inputMapper.invoke(input)
- baseCache.putMeasurement(remappedInput, output)
- }
-}
-
-/**
- * A base implementation class responsible for caching view Measurements
- */
-class BaseMeasurementCache : MeasurementCache {
- private val dataCache: MutableMap<MeasurementInput, MeasurementOutput> = mutableMapOf()
-
- override fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput {
- val measurementOutput = dataCache[input]
- if (measurementOutput == null) {
- return MeasurementOutput(0, 0)
- } else {
- return measurementOutput
- }
- }
-
- override fun contains(input: MeasurementInput) : Boolean {
- return dataCache[input] != null
- }
-
- override fun putMeasurement(input: MeasurementInput, output: MeasurementOutput) {
- dataCache[input] = output
- }
-}
-
-interface MeasurementCache {
- fun obtainMeasurement(input: MeasurementInput) : MeasurementOutput
- fun contains(input: MeasurementInput) : Boolean
- fun putMeasurement(input: MeasurementInput, output: MeasurementOutput)
-}
-
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt
new file mode 100644
index 000000000000..c7edd51e220a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/MeasurementInput.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.util.animation
+
+import android.view.View
+
+/**
+ * The output of a view measurement
+ */
+data class MeasurementOutput(
+ var measuredWidth: Int,
+ var measuredHeight: Int
+)
+
+/**
+ * The data object holding a basic view measurement input
+ */
+data class MeasurementInput(
+ var widthMeasureSpec: Int,
+ var heightMeasureSpec: Int
+) {
+ val width: Int
+ get() {
+ return View.MeasureSpec.getSize(widthMeasureSpec)
+ }
+ val height: Int
+ get() {
+ return View.MeasureSpec.getSize(heightMeasureSpec)
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
new file mode 100644
index 000000000000..701ff5ecf8a1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayout.kt
@@ -0,0 +1,294 @@
+/*
+ * 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.util.animation
+
+import android.content.Context
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.android.systemui.statusbar.CrossFadeHelper
+
+/**
+ * A view that handles displaying of children and transitions of them in an optimized way,
+ * minimizing the number of measure passes, while allowing for maximum flexibility
+ * and interruptibility.
+ */
+class TransitionLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val originalGoneChildrenSet: MutableSet<Int> = mutableSetOf()
+ private var measureAsConstraint: Boolean = false
+ private var currentState: TransitionViewState = TransitionViewState()
+ private var updateScheduled = false
+
+ /**
+ * The measured state of this view which is the one we will lay ourselves out with. This
+ * may differ from the currentState if there is an external animation or transition running.
+ * This state will not be used to measure the widgets, where the current state is preferred.
+ */
+ var measureState: TransitionViewState = TransitionViewState()
+ private val preDrawApplicator = object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ updateScheduled = false
+ viewTreeObserver.removeOnPreDrawListener(this)
+ applyCurrentState()
+ return true
+ }
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ val childCount = childCount
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ if (child.id == View.NO_ID) {
+ child.id = i
+ }
+ if (child.visibility == GONE) {
+ originalGoneChildrenSet.add(child.id)
+ }
+ }
+ }
+
+ /**
+ * Apply the current state to the view and its widgets
+ */
+ private fun applyCurrentState() {
+ val childCount = childCount
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ val widgetState = currentState.widgetStates.get(child.id) ?: continue
+ if (child.measuredWidth != widgetState.measureWidth ||
+ child.measuredHeight != widgetState.measureHeight) {
+ val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
+ MeasureSpec.EXACTLY)
+ val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
+ MeasureSpec.EXACTLY)
+ child.measure(measureWidthSpec, measureHeightSpec)
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+ }
+ val left = widgetState.x.toInt()
+ val top = widgetState.y.toInt()
+ child.setLeftTopRightBottom(left, top, left + widgetState.width,
+ top + widgetState.height)
+ child.scaleX = widgetState.scale
+ child.scaleY = widgetState.scale
+ val clipBounds = child.clipBounds ?: Rect()
+ clipBounds.set(0, 0, widgetState.width, widgetState.height)
+ child.clipBounds = clipBounds
+ CrossFadeHelper.fadeIn(child, widgetState.alpha)
+ child.visibility = if (widgetState.gone || widgetState.alpha == 0.0f) {
+ View.INVISIBLE
+ } else {
+ View.VISIBLE
+ }
+ }
+ updateBounds()
+ }
+
+ private fun applyCurrentStateOnPredraw() {
+ if (!updateScheduled) {
+ updateScheduled = true
+ viewTreeObserver.addOnPreDrawListener(preDrawApplicator)
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ if (measureAsConstraint) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ } else {
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ val widgetState = currentState.widgetStates.get(child.id) ?: continue
+ val measureWidthSpec = MeasureSpec.makeMeasureSpec(widgetState.measureWidth,
+ MeasureSpec.EXACTLY)
+ val measureHeightSpec = MeasureSpec.makeMeasureSpec(widgetState.measureHeight,
+ MeasureSpec.EXACTLY)
+ child.measure(measureWidthSpec, measureHeightSpec)
+ }
+ setMeasuredDimension(measureState.width, measureState.height)
+ }
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ if (measureAsConstraint) {
+ super.onLayout(changed, left, top, right, bottom)
+ } else {
+ val childCount = childCount
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ child.layout(0, 0, child.measuredWidth, child.measuredHeight)
+ }
+ // Reapply the bounds to update the background
+ applyCurrentState()
+ }
+ }
+
+ private fun updateBounds() {
+ val layoutLeft = left
+ val layoutTop = top
+ setLeftTopRightBottom(layoutLeft, layoutTop, layoutLeft + currentState.width,
+ layoutTop + currentState.height)
+ }
+
+ /**
+ * Calculates a view state for a given ConstraintSet and measurement, saving all positions
+ * of all widgets.
+ *
+ * @param input the measurement input this should be done with
+ * @param constraintSet the constraint set to apply
+ * @param resusableState the result that we can reuse to minimize memory impact
+ */
+ fun calculateViewState(
+ input: MeasurementInput,
+ constraintSet: ConstraintSet,
+ existing: TransitionViewState? = null
+ ): TransitionViewState {
+
+ val result = existing ?: TransitionViewState()
+ // Reset gone children to the original state
+ applySetToFullLayout(constraintSet)
+ val previousHeight = measuredHeight
+ val previousWidth = measuredWidth
+
+ // Let's measure outselves as a ConstraintLayout
+ measureAsConstraint = true
+ measure(input.widthMeasureSpec, input.heightMeasureSpec)
+ val layoutLeft = left
+ val layoutTop = top
+ layout(layoutLeft, layoutTop, layoutLeft + measuredWidth, layoutTop + measuredHeight)
+ measureAsConstraint = false
+ result.initFromLayout(this)
+ ensureViewsNotGone()
+
+ // Let's reset our layout to have the right size again
+ setMeasuredDimension(previousWidth, previousHeight)
+ applyCurrentStateOnPredraw()
+ return result
+ }
+
+ private fun applySetToFullLayout(constraintSet: ConstraintSet) {
+ // Let's reset our views to the initial gone state of the layout, since the constraintset
+ // might only be a subset of the views. Otherwise the gone state would be calculated
+ // wrongly later if we made this invisible in the layout (during apply we make sure they
+ // are invisible instead
+ val childCount = childCount
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ if (originalGoneChildrenSet.contains(child.id)) {
+ child.visibility = View.GONE
+ }
+ }
+ // Let's now apply the constraintSet to get the full state
+ constraintSet.applyTo(this)
+ }
+
+ /**
+ * Ensures that our views are never gone but invisible instead, this allows us to animate them
+ * without remeasuring.
+ */
+ private fun ensureViewsNotGone() {
+ val childCount = childCount
+ for (i in 0 until childCount) {
+ val child = getChildAt(i)
+ val widgetState = currentState.widgetStates.get(child.id)
+ child.visibility = if (widgetState?.gone != false) View.INVISIBLE else View.VISIBLE
+ }
+ }
+
+ /**
+ * Set the state that should be applied to this View
+ *
+ */
+ fun setState(state: TransitionViewState) {
+ currentState = state
+ applyCurrentState()
+ }
+}
+
+class TransitionViewState {
+ var widgetStates: MutableMap<Int, WidgetState> = mutableMapOf()
+ var width: Int = 0
+ var height: Int = 0
+ fun copy(reusedState: TransitionViewState? = null): TransitionViewState {
+ // we need a deep copy of this, so we can't use a data class
+ val copy = reusedState ?: TransitionViewState()
+ copy.width = width
+ copy.height = height
+ for (entry in widgetStates) {
+ copy.widgetStates[entry.key] = entry.value.copy()
+ }
+ return copy
+ }
+
+ fun initFromLayout(transitionLayout: TransitionLayout) {
+ val childCount = transitionLayout.childCount
+ for (i in 0 until childCount) {
+ val child = transitionLayout.getChildAt(i)
+ val widgetState = widgetStates.getOrPut(child.id, {
+ WidgetState(0.0f, 0.0f, 0, 0, 0, 0, 0.0f)
+ })
+ widgetState.initFromLayout(child)
+ }
+ width = transitionLayout.measuredWidth
+ height = transitionLayout.measuredHeight
+ }
+}
+
+data class WidgetState(
+ var x: Float = 0.0f,
+ var y: Float = 0.0f,
+ var width: Int = 0,
+ var height: Int = 0,
+ var measureWidth: Int = 0,
+ var measureHeight: Int = 0,
+ var alpha: Float = 1.0f,
+ var scale: Float = 1.0f,
+ var gone: Boolean = false
+) {
+ fun initFromLayout(view: View) {
+ gone = view.visibility == View.GONE
+ if (gone) {
+ val layoutParams = view.layoutParams as ConstraintLayout.LayoutParams
+ x = layoutParams.constraintWidget.left.toFloat()
+ y = layoutParams.constraintWidget.top.toFloat()
+ width = layoutParams.constraintWidget.width
+ height = layoutParams.constraintWidget.height
+ measureHeight = height
+ measureWidth = width
+ alpha = 0.0f
+ scale = 0.0f
+ } else {
+ x = view.left.toFloat()
+ y = view.top.toFloat()
+ width = view.width
+ height = view.height
+ measureWidth = width
+ measureHeight = height
+ gone = view.visibility == View.GONE
+ alpha = view.alpha
+ // No scale by default. Only during transitions!
+ scale = 1.0f
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
new file mode 100644
index 000000000000..9ee141053861
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.util.animation
+
+import android.animation.ValueAnimator
+import android.util.MathUtils
+import com.android.systemui.Interpolators
+
+/**
+ * The fraction after which we start fading in when going from a gone widget to a visible one
+ */
+private const val GONE_FADE_FRACTION = 0.8f
+
+/**
+ * The amont we're scaling appearing views
+ */
+private const val GONE_SCALE_AMOUNT = 0.8f
+
+/**
+ * A controller for a [TransitionLayout] which handles state transitions and keeps the transition
+ * layout up to date with the desired state.
+ */
+open class TransitionLayoutController {
+
+ /**
+ * The layout that this controller controls
+ */
+ private var transitionLayout: TransitionLayout? = null
+ private var currentState = TransitionViewState()
+ private var animationStartState: TransitionViewState? = null
+ private var state = TransitionViewState()
+ private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f)
+
+ init {
+ animator.apply {
+ addUpdateListener {
+ updateStateFromAnimation()
+ }
+ interpolator = Interpolators.FAST_OUT_SLOW_IN
+ }
+ }
+
+ private fun updateStateFromAnimation() {
+ if (animationStartState == null || !animator.isRunning) {
+ return
+ }
+ val view = transitionLayout ?: return
+ getInterpolatedState(
+ startState = animationStartState!!,
+ endState = state,
+ progress = animator.animatedFraction,
+ resultState = currentState)
+ view.setState(currentState)
+ }
+
+ /**
+ * Get an interpolated state between two viewstates. This interpolates all positions for all
+ * widgets as well as it's bounds based on the given input.
+ */
+ fun getInterpolatedState(
+ startState: TransitionViewState,
+ endState: TransitionViewState,
+ progress: Float,
+ resultState: TransitionViewState
+ ) {
+ val view = transitionLayout ?: return
+ val childCount = view.childCount
+ for (i in 0 until childCount) {
+ val id = view.getChildAt(i).id
+ val resultWidgetState = resultState.widgetStates[id] ?: WidgetState()
+ val widgetStart = startState.widgetStates[id] ?: continue
+ val widgetEnd = endState.widgetStates[id] ?: continue
+ var alphaProgress = progress
+ var widthProgress = progress
+ val resultMeasureWidth: Int
+ val resultMeasureHeight: Int
+ val newScale: Float
+ val resultX: Float
+ val resultY: Float
+ if (widgetStart.gone != widgetEnd.gone) {
+ // A view is appearing or disappearing. Let's not just interpolate between them as
+ // this looks quite ugly
+ val nowGone: Boolean
+ if (widgetStart.gone) {
+
+ // Only fade it in at the very end
+ alphaProgress = MathUtils.map(GONE_FADE_FRACTION, 1.0f, 0.0f, 1.0f, progress)
+ nowGone = progress < GONE_FADE_FRACTION
+
+ // Scale it just a little, not all the way
+ val endScale = widgetEnd.scale
+ newScale = MathUtils.lerp(GONE_SCALE_AMOUNT * endScale, endScale, progress)
+
+ // don't clip
+ widthProgress = 1.0f
+
+ // Let's directly measure it with the end state
+ resultMeasureWidth = widgetEnd.measureWidth
+ resultMeasureHeight = widgetEnd.measureHeight
+
+ // Let's make sure we're centering the view in the gone view instead of having
+ // the left at 0
+ resultX = MathUtils.lerp(widgetStart.x - resultMeasureWidth / 2.0f,
+ widgetEnd.x,
+ progress)
+ resultY = MathUtils.lerp(widgetStart.y - resultMeasureHeight / 2.0f,
+ widgetEnd.y,
+ progress)
+ } else {
+
+ // Fadeout in the very beginning
+ alphaProgress = MathUtils.map(0.0f, 1.0f - GONE_FADE_FRACTION, 0.0f, 1.0f,
+ progress)
+ nowGone = progress > 1.0f - GONE_FADE_FRACTION
+
+ // Scale it just a little, not all the way
+ val startScale = widgetStart.scale
+ newScale = MathUtils.lerp(startScale, startScale * GONE_SCALE_AMOUNT, progress)
+
+ // Don't clip
+ widthProgress = 0.0f
+
+ // Let's directly measure it with the start state
+ resultMeasureWidth = widgetStart.measureWidth
+ resultMeasureHeight = widgetStart.measureHeight
+
+ // Let's make sure we're centering the view in the gone view instead of having
+ // the left at 0
+ resultX = MathUtils.lerp(widgetStart.x,
+ widgetEnd.x - resultMeasureWidth / 2.0f,
+ progress)
+ resultY = MathUtils.lerp(widgetStart.y,
+ widgetEnd.y - resultMeasureHeight / 2.0f,
+ progress)
+ }
+ resultWidgetState.gone = nowGone
+ } else {
+ resultWidgetState.gone = widgetStart.gone
+ // Let's directly measure it with the end state
+ resultMeasureWidth = widgetEnd.measureWidth
+ resultMeasureHeight = widgetEnd.measureHeight
+ newScale = MathUtils.lerp(widgetStart.scale, widgetEnd.scale, progress)
+ resultX = MathUtils.lerp(widgetStart.x, widgetEnd.x, progress)
+ resultY = MathUtils.lerp(widgetStart.y, widgetEnd.y, progress)
+ }
+ resultWidgetState.apply {
+ x = resultX
+ y = resultY
+ alpha = MathUtils.lerp(widgetStart.alpha, widgetEnd.alpha, alphaProgress)
+ width = MathUtils.lerp(widgetStart.width.toFloat(), widgetEnd.width.toFloat(),
+ widthProgress).toInt()
+ height = MathUtils.lerp(widgetStart.height.toFloat(), widgetEnd.height.toFloat(),
+ widthProgress).toInt()
+ scale = newScale
+
+ // Let's directly measure it with the end state
+ measureWidth = resultMeasureWidth
+ measureHeight = resultMeasureHeight
+ }
+ resultState.widgetStates[id] = resultWidgetState
+ }
+ resultState.apply {
+ width = MathUtils.lerp(startState.width.toFloat(), endState.width.toFloat(),
+ progress).toInt()
+ height = MathUtils.lerp(startState.height.toFloat(), endState.height.toFloat(),
+ progress).toInt()
+ }
+ }
+
+ fun attach(transitionLayout: TransitionLayout) {
+ this.transitionLayout = transitionLayout
+ }
+
+ /**
+ * Set a new state to be applied to the dynamic view.
+ *
+ * @param state the state to be applied
+ * @param animate should this change be animated. If [false] the we will either apply the
+ * state immediately if no animation is running, and if one is running, we will update the end
+ * value to match the new state.
+ * @param applyImmediately should this change be applied immediately, canceling all running
+ * animations
+ */
+ fun setState(
+ state: TransitionViewState,
+ applyImmediately: Boolean,
+ animate: Boolean,
+ duration: Long = 0,
+ delay: Long = 0
+ ) {
+ val animated = animate && currentState.width != 0
+ this.state = state.copy()
+ if (applyImmediately || transitionLayout == null) {
+ animator.cancel()
+ transitionLayout?.setState(this.state)
+ currentState = state.copy(reusedState = currentState)
+ } else if (animated) {
+ animationStartState = currentState.copy()
+ animator.duration = duration
+ animator.startDelay = delay
+ animator.start()
+ } else if (!animator.isRunning) {
+ transitionLayout?.setState(this.state)
+ currentState = state.copy(reusedState = currentState)
+ }
+ // otherwise the desired state was updated and the animation will go to the new target
+ }
+
+ /**
+ * Set a new state that will be used to measure the view itself and is useful during
+ * transitions, where the state set via [setState] may differ from how the view
+ * should be measured.
+ */
+ fun setMeasureState(
+ state: TransitionViewState
+ ) {
+ transitionLayout?.measureState = state
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
index bf94c5d36ff7..5b6444d8feaf 100644
--- a/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/animation/UniqueObjectHostView.kt
@@ -19,7 +19,9 @@ package com.android.systemui.util.animation
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
+import android.view.ViewGroup
import android.widget.FrameLayout
+import com.android.systemui.R
/**
* A special view that is designed to host a single "unique object". The unique object is
@@ -34,8 +36,7 @@ import android.widget.FrameLayout
class UniqueObjectHostView(
context: Context
) : FrameLayout(context) {
- lateinit var measurementCache : GuaranteedMeasurementCache
- var onMeasureListener: ((MeasurementInput) -> Unit)? = null
+ lateinit var measurementManager: MeasurementManager
@SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -45,64 +46,63 @@ class UniqueObjectHostView(
val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical
val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
- val measurementInput = MeasurementInputData(widthSpec, heightSpec)
- onMeasureListener?.apply {
- invoke(measurementInput)
- }
+ val measurementInput = MeasurementInput(widthSpec, heightSpec)
+
+ // Let's make sure the measurementManager knows about our size, to ensure that we have
+ // a value available. This might perform a measure internally if we don't have a cached
+ // size.
+ val (cachedWidth, cachedHeight) = measurementManager.onMeasure(measurementInput)
+
if (!isCurrentHost()) {
- // We're not currently the host, let's get the dimension from our cache (this might
- // perform a measuring if the cache doesn't have it yet)
+ // We're not currently the host, let's use the dimension from our cache
// The goal here is that the view will always have a consistent measuring, regardless
// if it's attached or not.
// The behavior is therefore very similar to the view being persistently attached to
// this host, which can prevent flickers. It also makes sure that we always know
// the size of the view during transitions even if it has never been attached here
// before.
- val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput)
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Let's update our cache
- val child = getChildAt(0)!!
- val output = MeasurementOutput(child.measuredWidth, child.measuredHeight)
- measurementCache.putMeasurement(measurementInput, output)
+ getChildAt(0)?.requiresRemeasuring = false
}
}
+ override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
+ if (child?.measuredWidth == 0 || measuredWidth == 0 || child?.requiresRemeasuring == true) {
+ super.addView(child, index, params)
+ return
+ }
+ // Suppress layouts when adding a view. The view should already be laid out with the
+ // right size when being attached to this view
+ invalidate()
+ addViewInLayout(child, index, params, true /* preventRequestLayout */)
+ val left = paddingLeft
+ val top = paddingTop
+ val paddingHorizontal = paddingStart + paddingEnd
+ val paddingVertical = paddingTop + paddingBottom
+ child!!.layout(left,
+ top,
+ left + measuredWidth - paddingHorizontal,
+ top + measuredHeight - paddingVertical)
+ }
+
private fun isCurrentHost() = childCount != 0
-}
-/**
- * A basic view measurement input
- */
-interface MeasurementInput {
- fun sameAs(input: MeasurementInput?): Boolean {
- return equals(input)
+ interface MeasurementManager {
+ fun onMeasure(input: MeasurementInput): MeasurementOutput
}
- val width : Int
- get() {
- return View.MeasureSpec.getSize(widthMeasureSpec)
- }
- val height : Int
- get() {
- return View.MeasureSpec.getSize(heightMeasureSpec)
- }
- var widthMeasureSpec: Int
- var heightMeasureSpec: Int
}
/**
- * The output of a view measurement
+ * Does this view require remeasuring currently outside of the regular measure flow?
*/
-data class MeasurementOutput(
- val measuredWidth: Int,
- val measuredHeight: Int
-)
-
-/**
- * The data object holding a basic view measurement input
- */
-data class MeasurementInputData(
- override var widthMeasureSpec: Int,
- override var heightMeasureSpec: Int
-) : MeasurementInput
+var View.requiresRemeasuring: Boolean
+ get() {
+ val required = getTag(R.id.requires_remeasuring)
+ return required?.equals(true) ?: false
+ }
+ set(value) {
+ setTag(R.id.requires_remeasuring, value)
+ }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 4d30500bad45..b71a62c9d1a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -31,30 +31,24 @@ import android.widget.ImageButton
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
-
-import androidx.constraintlayout.motion.widget.MotionLayout
-import androidx.constraintlayout.motion.widget.MotionScene
-import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
-
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock
-
import com.google.common.truth.Truth.assertThat
-
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
-import java.util.ArrayList
-
private const val KEY = "TEST_KEY"
private const val APP = "APP"
private const val BG_COLOR = Color.RED
@@ -78,8 +72,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
@Mock private lateinit var activityStarter: ActivityStarter
@Mock private lateinit var holder: PlayerViewHolder
- @Mock private lateinit var motion: MotionLayout
- private lateinit var background: TextView
+ @Mock private lateinit var view: TransitionLayout
+ @Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
private lateinit var appIcon: ImageView
private lateinit var appName: TextView
private lateinit var albumView: ImageView
@@ -107,21 +101,15 @@ public class MediaControlPanelTest : SysuiTestCase() {
bgExecutor = FakeExecutor(FakeSystemClock())
activityStarter = mock(ActivityStarter::class.java)
+ mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
- player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter)
+ player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
+ mediaHostStatesManager)
// Mock out a view holder for the player to attach to.
holder = mock(PlayerViewHolder::class.java)
- motion = mock(MotionLayout::class.java)
- val trans: ArrayList<MotionScene.Transition> = ArrayList()
- trans.add(mock(MotionScene.Transition::class.java))
- whenever(motion.definedTransitions).thenReturn(trans)
- val constraintSet = mock(ConstraintSet::class.java)
- whenever(motion.getConstraintSet(R.id.expanded)).thenReturn(constraintSet)
- whenever(motion.getConstraintSet(R.id.collapsed)).thenReturn(constraintSet)
- whenever(holder.player).thenReturn(motion)
- background = TextView(context)
- whenever(holder.background).thenReturn(background)
+ view = mock(TransitionLayout::class.java)
+ whenever(holder.player).thenReturn(view)
appIcon = ImageView(context)
whenever(holder.appIcon).thenReturn(appIcon)
appName = TextView(context)
@@ -205,7 +193,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device)
player.bind(state)
- assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
+ val list = ArgumentCaptor.forClass(ColorStateList::class.java)
+ verify(view).setBackgroundTintList(list.capture())
+ assertThat(list.value).isEqualTo(ColorStateList.valueOf(BG_COLOR))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
new file mode 100644
index 000000000000..c9e6f55ff59a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.Rect
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
+import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.statusbar.NotificationLockscreenUserManager
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.SysuiStatusBarStateController
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.animation.UniqueObjectHostView
+import org.junit.Assert.assertNotNull
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyLong
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class MediaHierarchyManagerTest : SysuiTestCase() {
+
+ @Mock
+ private lateinit var lockHost: MediaHost
+ @Mock
+ private lateinit var qsHost: MediaHost
+ @Mock
+ private lateinit var qqsHost: MediaHost
+ @Mock
+ private lateinit var bypassController: KeyguardBypassController
+ @Mock
+ private lateinit var mediaFrame: ViewGroup
+ @Mock
+ private lateinit var keyguardStateController: KeyguardStateController
+ @Mock
+ private lateinit var statusBarStateController: SysuiStatusBarStateController
+ @Mock
+ private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
+ @Mock
+ private lateinit var mediaViewManager: MediaViewManager
+ @Mock
+ private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+ @Captor
+ private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
+ @JvmField
+ @Rule
+ val mockito = MockitoJUnit.rule()
+ private lateinit var mediaHiearchyManager: MediaHierarchyManager
+
+ @Before
+ fun setup() {
+ `when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
+ mediaHiearchyManager = MediaHierarchyManager(
+ context,
+ statusBarStateController,
+ keyguardStateController,
+ bypassController,
+ mediaViewManager,
+ notificationLockscreenUserManager,
+ wakefulnessLifecycle)
+ verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
+ setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN)
+ setupHost(qsHost, MediaHierarchyManager.LOCATION_QS)
+ setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
+ `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+ // We'll use the viewmanager to verify a few calls below, let's reset this.
+ clearInvocations(mediaViewManager)
+
+ }
+
+ private fun setupHost(host: MediaHost, location: Int) {
+ `when`(host.location).thenReturn(location)
+ `when`(host.currentBounds).thenReturn(Rect())
+ `when`(host.hostView).thenReturn(UniqueObjectHostView(context))
+ mediaHiearchyManager.register(host)
+ }
+
+ @Test
+ fun testHostViewSetOnRegister() {
+ val host = mediaHiearchyManager.register(lockHost)
+ verify(lockHost).hostView = eq(host)
+ }
+
+ @Test
+ fun testBlockedWhenScreenTurningOff() {
+ // Let's set it onto QS:
+ mediaHiearchyManager.qsExpansion = 1.0f
+ verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+ any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+ val observer = wakefullnessObserver.value
+ assertNotNull("lifecycle observer wasn't registered", observer)
+ observer.onStartedGoingToSleep()
+ clearInvocations(mediaViewManager)
+ mediaHiearchyManager.qsExpansion = 0.0f
+ verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+ any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+ }
+
+ @Test
+ fun testAllowedWhenNotTurningOff() {
+ // Let's set it onto QS:
+ mediaHiearchyManager.qsExpansion = 1.0f
+ verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+ any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+ val observer = wakefullnessObserver.value
+ assertNotNull("lifecycle observer wasn't registered", observer)
+ clearInvocations(mediaViewManager)
+ mediaHiearchyManager.qsExpansion = 0.0f
+ verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
+ any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
index 767852582dc3..d6849bf2aa39 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/PlayerViewHolderTest.kt
@@ -57,6 +57,6 @@ class PlayerViewHolderTest : SysuiTestCase() {
@Test
fun backgroundIsIlluminationDrawable() {
val holder = PlayerViewHolder.create(inflater, parent)
- assertThat(holder.background.background as IlluminationDrawable).isNotNull()
+ assertThat(holder.player.background as IlluminationDrawable).isNotNull()
}
}