summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Selim Cinek <cinek@google.com> 2020-05-20 15:39:03 -0700
committer Selim Cinek <cinek@google.com> 2020-05-26 16:30:40 -0700
commit2de5ebb44a5ae512f3a676af7c299d4fb4f21f0d (patch)
tree3fa06b924fd769b4837582c90c57a41373b78aa7
parentacf1151d11bb7fbf4d7d95e9cd56cd904bb05825 (diff)
Replacing MotionLayout with DynamicLayout
Motionlayout had a few limitations because we didn't have any control over the measure cache and the resizing behavior wasn't very performant for our usecase. We therefore implement a new Layout called DyanmicLayout that dynamically allows you to move between different ViewStates in a performance optimized way. A new controller is introduced that is responsible for caching the individual viewstates and react to possible changes in the view hosts. This will make sure that our layout is more stable than ever while beeing fully optimized for multiple players with various different viewstates, allowing us to implement usecases much easier where players are conditionally hidden or replaced when long-pressing. MediaLocationStates are now fully owned by the MediaStatesManager and the hiearchyManager only operates on the bounds of the views, making it more generic and resuable for future usecases. Fixes: 156465387 Test: atest SystemUITests Change-Id: Ica8c2b1404df52d3cc3bf3d0ee5dabbc886557cf
-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.kt158
-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/PlayerViewHolderTest.kt2
32 files changed, 1761 insertions, 1161 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..24bcc2f586ca 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -21,6 +21,8 @@ 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
@@ -47,7 +49,6 @@ class MediaHierarchyManager @Inject constructor(
private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController,
private val mediaViewManager: MediaViewManager,
- private val mediaMeasurementProvider: MediaMeasurementManager,
private val notifLockscreenUserManager: NotificationLockscreenUserManager
) {
/**
@@ -56,23 +57,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 +90,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) {
@@ -138,8 +158,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 +174,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 +206,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 +234,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 +255,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 +266,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 +298,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 +310,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 +378,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,22 +409,21 @@ 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
@@ -396,13 +440,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 +462,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/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()
}
}