Merge changes I75a386d7,Ica8c2b14 into rvc-dev am: 02b5b19256

Change-Id: I7a6ab04c0ba4aba4f4412f584642a4c93ac8cee2
This commit is contained in:
Selim Cinek
2020-05-27 02:03:34 +00:00
committed by Automerger Merge Worker
33 changed files with 1984 additions and 1171 deletions

View File

@@ -16,7 +16,7 @@
--> -->
<!-- Layout for media controls inside QSPanel carousel --> <!-- 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" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/qs_media_controls" android:id="@+id/qs_media_controls"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,18 +24,7 @@
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:gravity="center_horizontal|fill_vertical" android:gravity="center_horizontal|fill_vertical"
app:layoutDescription="@xml/media_scene"> android:background="@drawable/qs_media_background">
<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"
/>
<FrameLayout <FrameLayout
android:id="@+id/notification_media_progress_time" android:id="@+id/notification_media_progress_time"
@@ -185,15 +174,5 @@
<!-- Buttons to remove this view when no longer needed --> <!-- Buttons to remove this view when no longer needed -->
<include <include
layout="@layout/qs_media_panel_options" layout="@layout/qs_media_panel_options"
android:visibility="gone" android:visibility="gone" />
app:layout_constraintEnd_toEndOf="@id/view_width" </com.android.systemui.util.animation.TransitionLayout>
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>

View File

@@ -90,6 +90,8 @@
<item type="id" name="view_width_animator_end_tag"/> <item type="id" name="view_width_animator_end_tag"/>
<item type="id" name="view_width_current_value"/> <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 --> <!-- Whether the icon is from a notification for which targetSdk < L -->
<item type="id" name="icon_is_pre_L"/> <item type="id" name="icon_is_pre_L"/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
}
}
}
}

View File

@@ -45,6 +45,7 @@ class KeyguardMediaController @Inject constructor(
} }
}) })
} }
private var view: MediaHeaderView? = null private var view: MediaHeaderView? = null
/** /**

View File

@@ -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
}
})
}
}
}

View File

@@ -43,12 +43,9 @@ import android.widget.ImageView;
import android.widget.SeekBar; import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.UiThread; 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.constraintlayout.widget.ConstraintSet;
import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 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.R;
import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSMediaBrowser; import com.android.systemui.qs.QSMediaBrowser;
import com.android.systemui.util.animation.TransitionLayout;
import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.DelayableExecutor;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -87,16 +84,15 @@ public class MediaControlPanel {
private final Executor mForegroundExecutor; private final Executor mForegroundExecutor;
protected final Executor mBackgroundExecutor; protected final Executor mBackgroundExecutor;
private final ActivityStarter mActivityStarter; private final ActivityStarter mActivityStarter;
private LayoutAnimationHelper mLayoutAnimationHelper;
private Context mContext; private Context mContext;
private PlayerViewHolder mViewHolder; private PlayerViewHolder mViewHolder;
private MediaViewController mMediaViewController;
private MediaSession.Token mToken; private MediaSession.Token mToken;
private MediaController mController; private MediaController mController;
private int mBackgroundColor; private int mBackgroundColor;
protected ComponentName mServiceComponent; protected ComponentName mServiceComponent;
private boolean mIsRegistered = false; private boolean mIsRegistered = false;
private List<KeyFrames> mKeyFrames;
private String mKey; private String mKey;
private int mAlbumArtSize; private int mAlbumArtSize;
private int mAlbumArtRadius; private int mAlbumArtRadius;
@@ -133,12 +129,14 @@ public class MediaControlPanel {
* @param activityStarter activity starter * @param activityStarter activity starter
*/ */
public MediaControlPanel(Context context, Executor foregroundExecutor, public MediaControlPanel(Context context, Executor foregroundExecutor,
DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) { DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
MediaHostStatesManager mediaHostStatesManager) {
mContext = context; mContext = context;
mForegroundExecutor = foregroundExecutor; mForegroundExecutor = foregroundExecutor;
mBackgroundExecutor = backgroundExecutor; mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter; mActivityStarter = activityStarter;
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor); mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
loadDimens(); loadDimens();
} }
@@ -147,6 +145,7 @@ public class MediaControlPanel {
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
} }
mSeekBarViewModel.onDestroy(); mSeekBarViewModel.onDestroy();
mMediaViewController.onDestroy();
} }
private void loadDimens() { private void loadDimens() {
@@ -164,6 +163,15 @@ public class MediaControlPanel {
return mViewHolder; return mViewHolder;
} }
/**
* 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. * Sets the listening state of the player.
* *
@@ -187,15 +195,13 @@ public class MediaControlPanel {
/** Attaches the player to the view holder. */ /** Attaches the player to the view holder. */
public void attach(PlayerViewHolder vh) { public void attach(PlayerViewHolder vh) {
mViewHolder = vh; mViewHolder = vh;
MotionLayout motionView = vh.getPlayer(); TransitionLayout player = vh.getPlayer();
mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
mSeekBarObserver = new SeekBarObserver(vh); mSeekBarObserver = new SeekBarObserver(vh);
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
SeekBar bar = vh.getSeekBar(); SeekBar bar = vh.getSeekBar();
bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener()); bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener()); bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
mMediaViewController.attach(player);
} }
/** /**
@@ -220,8 +226,8 @@ public class MediaControlPanel {
mController = new MediaController(mContext, mToken); mController = new MediaController(mContext, mToken);
ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded); ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed); ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
// Try to find a browser service component for this app // Try to find a browser service component for this app
// TODO also check for a media button receiver intended for restarting (b/154127084) // TODO also check for a media button receiver intended for restarting (b/154127084)
@@ -247,7 +253,7 @@ public class MediaControlPanel {
mController.registerCallback(mSessionCallback); mController.registerCallback(mSessionCallback);
mViewHolder.getBackground().setBackgroundTintList( mViewHolder.getPlayer().setBackgroundTintList(
ColorStateList.valueOf(mBackgroundColor)); ColorStateList.valueOf(mBackgroundColor));
// Click action // Click action
@@ -356,7 +362,6 @@ public class MediaControlPanel {
} }
}); });
boolean visibleInCompat = actionsWhenCollapsed.contains(i); boolean visibleInCompat = actionsWhenCollapsed.contains(i);
updateKeyFrameVisibility(actionId, visibleInCompat);
setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
setVisibleAndAlpha(expandedSet, actionId, true /*visible */); setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
} }
@@ -374,9 +379,9 @@ public class MediaControlPanel {
// Set up long press menu // Set up long press menu
// TODO: b/156036025 bring back media guts // TODO: b/156036025 bring back media guts
// Update both constraint sets to regenerate the animation. // TODO: We don't need to refresh this state constantly, only if the state actually changed
mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet); // to something which might impact the measurement
mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet); mMediaViewController.refreshState();
} }
@UiThread @UiThread
@@ -411,30 +416,6 @@ public class MediaControlPanel {
return artwork; return artwork;
} }
/**
* 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 for the current media session
* @return the token * @return the token
@@ -528,8 +509,8 @@ public class MediaControlPanel {
} }
// Hide all the old buttons // Hide all the old buttons
ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded); ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed); ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
for (int i = 1; i < ACTION_IDS.length; i++) { for (int i = 1; i < ACTION_IDS.length; i++) {
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */); setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
@@ -571,6 +552,7 @@ public class MediaControlPanel {
options.setVisibility(View.VISIBLE); options.setVisibility(View.VISIBLE);
return true; // consumed click return true; // consumed click
}); });
mMediaViewController.refreshState();
} }
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { 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 * Called when a player can't be resumed to give it an opportunity to hide or remove itself
*/ */
protected void removePlayer() { } 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);
}
} }

View File

@@ -21,10 +21,13 @@ import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.IntDef import android.annotation.IntDef
import android.content.Context import android.content.Context
import android.graphics.Rect
import android.util.MathUtils
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroupOverlay import android.view.ViewGroupOverlay
import com.android.systemui.Interpolators import com.android.systemui.Interpolators
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarState
@@ -47,8 +50,8 @@ class MediaHierarchyManager @Inject constructor(
private val keyguardStateController: KeyguardStateController, private val keyguardStateController: KeyguardStateController,
private val bypassController: KeyguardBypassController, private val bypassController: KeyguardBypassController,
private val mediaViewManager: MediaViewManager, private val mediaViewManager: MediaViewManager,
private val mediaMeasurementProvider: MediaMeasurementManager, private val notifLockscreenUserManager: NotificationLockscreenUserManager,
private val notifLockscreenUserManager: NotificationLockscreenUserManager wakefulnessLifecycle: WakefulnessLifecycle
) { ) {
/** /**
* The root overlay of the hierarchy. This is where the media notification is attached to * The root overlay of the hierarchy. This is where the media notification is attached to
@@ -56,23 +59,31 @@ class MediaHierarchyManager @Inject constructor(
* view is always in its final state when it is attached to a view host. * view is always in its final state when it is attached to a view host.
*/ */
private var rootOverlay: ViewGroupOverlay? = null 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 get() = mediaViewManager.mediaFrame
private var animationStartState: MediaState? = null
private var statusbarState: Int = statusBarStateController.state private var statusbarState: Int = statusBarStateController.state
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
interpolator = Interpolators.FAST_OUT_SLOW_IN interpolator = Interpolators.FAST_OUT_SLOW_IN
addUpdateListener { addUpdateListener {
updateTargetState() updateTargetState()
applyState(animationStartState!!.interpolate(targetState!!, animatedFraction)) interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
result = currentBounds)
applyState(currentBounds)
} }
addListener(object : AnimatorListenerAdapter() { addListener(object : AnimatorListenerAdapter() {
private var cancelled: Boolean = false private var cancelled: Boolean = false
override fun onAnimationCancel(animation: Animator?) { override fun onAnimationCancel(animation: Animator?) {
cancelled = true cancelled = true
animationPending = false
rootView?.removeCallbacks(startAnimation)
} }
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator?) {
if (!cancelled) { if (!cancelled) {
applyTargetStateIfNotAnimating() applyTargetStateIfNotAnimating()
@@ -81,30 +92,41 @@ class MediaHierarchyManager @Inject constructor(
override fun onAnimationStart(animation: Animator?) { override fun onAnimationStart(animation: Animator?) {
cancelled = false 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 * The last location where this view was at before going to the desired location. This is
* useful for guided transitions. * 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. * 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. * The current attachment location where the view is currently attached.
* Usually this matches the desired location except for animations whenever a view moves * 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]. * 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 var qsExpansion: Float = 0.0f
set(value) { set(value) {
if (field != value) { if (field != value) {
@@ -117,6 +139,40 @@ class MediaHierarchyManager @Inject constructor(
} }
} }
/**
* Are location changes currently blocked?
*/
private val blockLocationChanges: Boolean
get() {
return goingToSleep || dozeAnimationRunning
}
/**
* Are we currently going to sleep
*/
private var goingToSleep: Boolean = false
set(value) {
if (field != value) {
field = value
if (!value) {
updateDesiredLocation()
}
}
}
/**
* Is the doze animation currently Running
*/
private var dozeAnimationRunning: Boolean = false
private set(value) {
if (field != value) {
field = value
if (!value) {
updateDesiredLocation()
}
}
}
init { init {
statusBarStateController.addCallback(object : StatusBarStateController.StateListener { statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
override fun onStatePreChange(oldState: Int, newState: Int) { override fun onStatePreChange(oldState: Int, newState: Int) {
@@ -129,6 +185,34 @@ class MediaHierarchyManager @Inject constructor(
override fun onStateChanged(newState: Int) { override fun onStateChanged(newState: Int) {
updateTargetState() updateTargetState()
} }
override fun onDozeAmountChanged(linear: Float, eased: Float) {
dozeAnimationRunning = linear != 0.0f && linear != 1.0f
}
override fun onDozingChanged(isDozing: Boolean) {
if (!isDozing) {
dozeAnimationRunning = false
}
}
})
wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
override fun onFinishedGoingToSleep() {
goingToSleep = false
}
override fun onStartedGoingToSleep() {
goingToSleep = true
}
override fun onFinishedWakingUp() {
goingToSleep = false
}
override fun onStartedWakingUp() {
goingToSleep = false
}
}) })
} }
@@ -138,8 +222,8 @@ class MediaHierarchyManager @Inject constructor(
* *
* @return the hostView associated with this location * @return the hostView associated with this location
*/ */
fun register(mediaObject: MediaHost): ViewGroup { fun register(mediaObject: MediaHost): UniqueObjectHostView {
val viewHost = createUniqueObjectHost(mediaObject) val viewHost = createUniqueObjectHost()
mediaObject.hostView = viewHost mediaObject.hostView = viewHost
mediaHosts[mediaObject.location] = mediaObject mediaHosts[mediaObject.location] = mediaObject
if (mediaObject.location == desiredLocation) { if (mediaObject.location == desiredLocation) {
@@ -154,22 +238,13 @@ class MediaHierarchyManager @Inject constructor(
return viewHost return viewHost
} }
private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView { private fun createUniqueObjectHost(): UniqueObjectHostView {
val viewHost = UniqueObjectHostView(context) 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 { viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(p0: View?) { override fun onViewAttachedToWindow(p0: View?) {
if (rootOverlay == null) { if (rootOverlay == null) {
rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay) rootView = viewHost.viewRootImpl.view
rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
} }
viewHost.removeOnAttachStateChangeListener(this) viewHost.removeOnAttachStateChangeListener(this)
} }
@@ -195,8 +270,9 @@ class MediaHierarchyManager @Inject constructor(
// Let's perform a transition // Let's perform a transition
val animate = shouldAnimateTransition(desiredLocation, previousLocation) val animate = shouldAnimateTransition(desiredLocation, previousLocation)
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState, val host = getHost(desiredLocation)
animate, animDuration, delay) mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
delay)
performTransitionToNewLocation(isNewView, animate) performTransitionToNewLocation(isNewView, animate)
} }
} }
@@ -222,14 +298,18 @@ class MediaHierarchyManager @Inject constructor(
// Let's animate to the new position, starting from the current position // 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 // We also go in here in case the view was detached, since the bounds wouldn't
// be correct anymore // be correct anymore
animationStartState = currentState.copy() animationStartBounds.set(currentBounds)
} else { } else {
// otherwise, let's take the freshest state, since the current one could // otherwise, let's take the freshest state, since the current one could
// be outdated // be outdated
animationStartState = previousHost.currentState.copy() animationStartBounds.set(previousHost.currentBounds)
} }
adjustAnimatorForTransition(desiredLocation, previousLocation) adjustAnimatorForTransition(desiredLocation, previousLocation)
animator.start() rootView?.let {
// Let's delay the animation start until we finished laying out
animationPending = true
it.postOnAnimation(startAnimation)
}
} else { } else {
cancelAnimationAndApplyDesiredState() cancelAnimationAndApplyDesiredState()
} }
@@ -239,6 +319,9 @@ class MediaHierarchyManager @Inject constructor(
@MediaLocation currentLocation: Int, @MediaLocation currentLocation: Int,
@MediaLocation previousLocation: Int @MediaLocation previousLocation: Int
): Boolean { ): Boolean {
if (isCurrentlyInGuidedTransformation()) {
return false
}
if (currentLocation == LOCATION_QQS && if (currentLocation == LOCATION_QQS &&
previousLocation == LOCATION_LOCKSCREEN && previousLocation == LOCATION_LOCKSCREEN &&
(statusBarStateController.leaveOpenOnKeyguardHide() || (statusBarStateController.leaveOpenOnKeyguardHide() ||
@@ -247,7 +330,7 @@ class MediaHierarchyManager @Inject constructor(
// non-trivial reattaching logic happening that will make the view not-shown earlier // non-trivial reattaching logic happening that will make the view not-shown earlier
return true return true
} }
return mediaCarousel.isShown || animator.isRunning return mediaFrame.isShown || animator.isRunning || animationPending
} }
private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) { private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
@@ -279,7 +362,7 @@ class MediaHierarchyManager @Inject constructor(
// Let's immediately apply the target state (which is interpolated) if there is // Let's immediately apply the target state (which is interpolated) if there is
// no animation running. Otherwise the animation update will already update // no animation running. Otherwise the animation update will already update
// the location // the location
applyState(targetState!!) applyState(targetBounds)
} }
} }
@@ -291,14 +374,34 @@ class MediaHierarchyManager @Inject constructor(
val progress = getTransformationProgress() val progress = getTransformationProgress()
val currentHost = getHost(desiredLocation)!! val currentHost = getHost(desiredLocation)!!
val previousHost = getHost(previousLocation)!! val previousHost = getHost(previousLocation)!!
val newState = currentHost.currentState val newBounds = currentHost.currentBounds
val previousState = previousHost.currentState val previousBounds = previousHost.currentBounds
targetState = previousState.interpolate(newState, progress) targetBounds = interpolateBounds(previousBounds, newBounds, progress)
} else { } 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 * @return true if this transformation is guided by an external progress like a finger
*/ */
@@ -339,21 +442,27 @@ class MediaHierarchyManager @Inject constructor(
private fun cancelAnimationAndApplyDesiredState() { private fun cancelAnimationAndApplyDesiredState() {
animator.cancel() animator.cancel()
getHost(desiredLocation)?.let { getHost(desiredLocation)?.let {
applyState(it.currentState) applyState(it.currentBounds, immediately = true)
} }
} }
private fun applyState(state: MediaState) { /**
currentState = state.copy() * Apply the current state to the view, updating it's bounds and desired state
mediaViewManager.setCurrentState(currentState) */
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() updateHostAttachment()
if (currentAttachmentLocation == IN_OVERLAY) { if (currentAttachmentLocation == IN_OVERLAY) {
val boundsOnScreen = state.boundsOnScreen mediaFrame.setLeftTopRightBottom(
mediaCarousel.setLeftTopRightBottom( currentBounds.left,
boundsOnScreen.left, currentBounds.top,
boundsOnScreen.top, currentBounds.right,
boundsOnScreen.right, currentBounds.bottom)
boundsOnScreen.bottom)
} }
} }
@@ -364,26 +473,29 @@ class MediaHierarchyManager @Inject constructor(
currentAttachmentLocation = newLocation currentAttachmentLocation = newLocation
// Remove the carousel from the old host // 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 // Add it to the new one
val targetHost = getHost(desiredLocation)!!.hostView val targetHost = getHost(desiredLocation)!!.hostView
if (inOverlay) { if (inOverlay) {
rootOverlay!!.add(mediaCarousel) rootOverlay!!.add(mediaFrame)
} else { } else {
targetHost.addView(mediaCarousel) targetHost.addView(mediaFrame)
mediaViewManager.onViewReattached()
} }
} }
} }
private fun isTransitionRunning(): Boolean { private fun isTransitionRunning(): Boolean {
return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f || return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
animator.isRunning animator.isRunning || animationPending
} }
@MediaLocation @MediaLocation
private fun calculateLocation(): Int { private fun calculateLocation(): Int {
if (blockLocationChanges) {
// Keep the current location until we're allowed to again
return desiredLocation
}
val onLockscreen = (!bypassController.bypassEnabled && val onLockscreen = (!bypassController.bypassEnabled &&
(statusbarState == StatusBarState.KEYGUARD || (statusbarState == StatusBarState.KEYGUARD ||
statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER)) statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
@@ -396,13 +508,6 @@ class MediaHierarchyManager @Inject constructor(
} }
} }
/**
* The expansion of quick settings
*/
@IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
@Retention(AnnotationRetention.SOURCE)
annotation class MediaLocation
companion object { companion object {
/** /**
* Attached in expanded quick settings * Attached in expanded quick settings
@@ -425,3 +530,8 @@ class MediaHierarchyManager @Inject constructor(
const val IN_OVERLAY = -1000 const val IN_OVERLAY = -1000
} }
} }
@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
@Retention(AnnotationRetention.SOURCE)
annotation class MediaLocation

View File

@@ -1,20 +1,22 @@
package com.android.systemui.media package com.android.systemui.media
import android.graphics.Rect import android.graphics.Rect
import android.util.MathUtils
import android.view.View import android.view.View
import android.view.View.OnAttachStateChangeListener 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.MeasurementInput
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView
import java.util.Objects
import javax.inject.Inject import javax.inject.Inject
class MediaHost @Inject constructor( class MediaHost @Inject constructor(
private val state: MediaHostState, private val state: MediaHostStateHolder,
private val mediaHierarchyManager: MediaHierarchyManager, private val mediaHierarchyManager: MediaHierarchyManager,
private val mediaDataManager: MediaDataManager private val mediaDataManager: MediaDataManager,
) : MediaState by state { private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
lateinit var hostView: ViewGroup private val mediaHostStatesManager: MediaHostStatesManager
) : MediaHostState by state {
lateinit var hostView: UniqueObjectHostView
var location: Int = -1 var location: Int = -1
private set private set
var visibleChangedListener: ((Boolean) -> Unit)? = null var visibleChangedListener: ((Boolean) -> Unit)? = null
@@ -24,9 +26,9 @@ class MediaHost @Inject constructor(
private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0) 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() { get() {
hostView.getLocationOnScreen(tmpLocationOnScreen) hostView.getLocationOnScreen(tmpLocationOnScreen)
var left = tmpLocationOnScreen[0] + hostView.paddingLeft var left = tmpLocationOnScreen[0] + hostView.paddingLeft
@@ -43,8 +45,8 @@ class MediaHost @Inject constructor(
bottom = 0 bottom = 0
top = 0 top = 0
} }
state.boundsOnScreen.set(left, top, right, bottom) field.set(left, top, right, bottom)
return state return field
} }
private val listener = object : MediaDataManager.Listener { private val listener = object : MediaDataManager.Listener {
@@ -59,6 +61,8 @@ class MediaHost @Inject constructor(
/** /**
* Initialize this MediaObject and create a host view. * 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 * @param location the location this host name has. Used to identify the host during
* transitions. * transitions.
@@ -68,14 +72,39 @@ class MediaHost @Inject constructor(
hostView = mediaHierarchyManager.register(this) hostView = mediaHierarchyManager.register(this)
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener { hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) { 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() updateViewVisibility()
} }
override fun onViewDetachedFromWindow(v: View?) { 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() updateViewVisibility()
} }
@@ -89,71 +118,93 @@ class MediaHost @Inject constructor(
visibleChangedListener?.invoke(visible) visibleChangedListener?.invoke(visible)
} }
class MediaHostState @Inject constructor() : MediaState { class MediaHostStateHolder @Inject constructor() : MediaHostState {
var measurementInput: MediaMeasurementInput? = null
override var expansion: Float = 0.0f
override var showsOnlyActiveMedia: Boolean = false
override val boundsOnScreen: Rect = Rect()
override fun copy(): MediaState { override var measurementInput: MeasurementInput? = null
val mediaHostState = MediaHostState() 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
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
/**
* 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.expansion = expansion
mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia mediaHostState.showsOnlyActiveMedia = showsOnlyActiveMedia
mediaHostState.boundsOnScreen.set(boundsOnScreen) mediaHostState.measurementInput = measurementInput?.copy()
mediaHostState.measurementInput = measurementInput
return mediaHostState return mediaHostState
} }
override fun interpolate(other: MediaState, amount: Float): MediaState { override fun equals(other: Any?): Boolean {
val result = MediaHostState() if (!(other is MediaHostState)) {
result.expansion = MathUtils.lerp(expansion, other.expansion, amount) return false
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
} }
if (!Objects.equals(measurementInput, other.measurementInput)) {
return false
}
if (expansion != other.expansion) {
return false
}
if (showsOnlyActiveMedia != other.showsOnlyActiveMedia) {
return false
}
return true
}
override fun hashCode(): Int {
var result = measurementInput?.hashCode() ?: 0
result = 31 * result + expansion.hashCode()
result = 31 * result + showsOnlyActiveMedia.hashCode()
return result return result
} }
override fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput {
measurementInput = MediaMeasurementInput(input, expansion)
return measurementInput as MediaMeasurementInput
}
} }
} }
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 var expansion: Float
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)) { * Is this host only showing active media or is it showing all of them including resumption?
return false */
} var showsOnlyActiveMedia: Boolean
return width == input.width && expansion == input.expansion
} /**
* Get a copy of this view state, deepcopying all appropriate members
*/
fun copy(): MediaHostState
} }

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -16,8 +16,8 @@ import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.PageIndicator import com.android.systemui.qs.PageIndicator
import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.statusbar.notification.VisualStabilityManager
import com.android.systemui.util.animation.MeasurementOutput
import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.UniqueObjectHostView
import com.android.systemui.util.animation.requiresRemeasuring
import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.DelayableExecutor
import java.util.concurrent.Executor import java.util.concurrent.Executor
import javax.inject.Inject import javax.inject.Inject
@@ -36,16 +36,51 @@ class MediaViewManager @Inject constructor(
@Background private val backgroundExecutor: DelayableExecutor, @Background private val backgroundExecutor: DelayableExecutor,
private val visualStabilityManager: VisualStabilityManager, private val visualStabilityManager: VisualStabilityManager,
private val activityStarter: ActivityStarter, private val activityStarter: ActivityStarter,
private val mediaHostStatesManager: MediaHostStatesManager,
mediaManager: MediaDataCombineLatest 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 playerWidthPlusPadding: Int = 0
private var desiredState: MediaHost.MediaHostState? = null private var desiredHostState: MediaHostState? = null
private var currentState: MediaState? = null
private val mediaCarousel: HorizontalScrollView private val mediaCarousel: HorizontalScrollView
val mediaFrame: ViewGroup val mediaFrame: ViewGroup
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val mediaContent: ViewGroup private val mediaContent: ViewGroup
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
private val pageIndicator: PageIndicator private val pageIndicator: PageIndicator
private val gestureDetector: GestureDetectorCompat private val gestureDetector: GestureDetectorCompat
private val visualStabilityCallback: VisualStabilityManager.Callback private val visualStabilityCallback: VisualStabilityManager.Callback
@@ -115,6 +150,7 @@ class MediaViewManager @Inject constructor(
override fun onMediaDataLoaded(key: String, data: MediaData) { override fun onMediaDataLoaded(key: String, data: MediaData) {
updateView(key, data) updateView(key, data)
updatePlayerVisibilities() updatePlayerVisibilities()
mediaCarousel.requiresRemeasuring = true
} }
override fun onMediaDataRemoved(key: String) { 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 { private fun inflateMediaCarousel(): ViewGroup {
@@ -220,7 +263,7 @@ class MediaViewManager @Inject constructor(
var existingPlayer = mediaPlayers[key] var existingPlayer = mediaPlayers[key]
if (existingPlayer == null) { if (existingPlayer == null) {
existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor, existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
activityStarter) activityStarter, mediaHostStatesManager)
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
mediaContent)) mediaContent))
mediaPlayers[key] = existingPlayer mediaPlayers[key] = existingPlayer
@@ -228,14 +271,14 @@ class MediaViewManager @Inject constructor(
ViewGroup.LayoutParams.WRAP_CONTENT) ViewGroup.LayoutParams.WRAP_CONTENT)
existingPlayer.view?.player?.setLayoutParams(lp) existingPlayer.view?.player?.setLayoutParams(lp)
existingPlayer.setListening(currentlyExpanded) existingPlayer.setListening(currentlyExpanded)
updatePlayerToState(existingPlayer, noAnimation = true)
if (existingPlayer.isPlaying) { if (existingPlayer.isPlaying) {
mediaContent.addView(existingPlayer.view?.player, 0) mediaContent.addView(existingPlayer.view?.player, 0)
} else { } else {
mediaContent.addView(existingPlayer.view?.player) mediaContent.addView(existingPlayer.view?.player)
} }
updatePlayerToCurrentState(existingPlayer)
} else if (existingPlayer.isPlaying && } else if (existingPlayer.isPlaying &&
mediaContent.indexOfChild(existingPlayer.view?.player) != 0) { mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
if (visualStabilityManager.isReorderingAllowed) { if (visualStabilityManager.isReorderingAllowed) {
mediaContent.removeView(existingPlayer.view?.player) mediaContent.removeView(existingPlayer.view?.player)
mediaContent.addView(existingPlayer.view?.player, 0) mediaContent.addView(existingPlayer.view?.player, 0)
@@ -244,20 +287,10 @@ class MediaViewManager @Inject constructor(
} }
} }
existingPlayer.bind(data) 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() updateMediaPaddings()
updatePageIndicator() 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() { private fun updateMediaPaddings() {
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
val childCount = mediaContent.childCount 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 * Set a new interpolated state for all players. This is a state that is usually controlled
* do anything expensive. * by a finger movement where the user drags from one state to the next.
*/ */
fun setCurrentState(state: MediaState) { fun setCurrentState(
currentState = state @MediaLocation startLocation: Int,
currentlyExpanded = state.expansion > 0 @MediaLocation endLocation: Int,
progress: Float,
immediately: Boolean
) {
// Hack: Since the indicator doesn't move with the player expansion, just make it disappear // Hack: Since the indicator doesn't move with the player expansion, just make it disappear
// and then reappear at the end. // and then reappear at the end.
pageIndicator.alpha = if (state.expansion == 1f || state.expansion == 0f) 1f else 0f pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
for (mediaPlayer in mediaPlayers.values) { if (startLocation != currentStartLocation ||
val view = mediaPlayer.view?.player endLocation != currentEndLocation ||
view?.progress = state.expansion 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 desired location of this view has changed. We should remeasure the view to match
* the new bounds and kick off bounds animations if necessary. * 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 * If an animation is happening, an animation is kicked of externally, which sets a new
* current state until we reach the targetState. * 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 * @param animate should this be animated
*/ */
fun onDesiredLocationChanged( fun onDesiredLocationChanged(
desiredState: MediaState?, desiredLocation: Int,
desiredHostState: MediaHostState?,
animate: Boolean, animate: Boolean,
duration: Long, duration: Long = 200,
startDelay: Long startDelay: Long = 0
) { ) {
if (desiredState is MediaHost.MediaHostState) { desiredHostState?.let {
// This is a hosting view, let's remeasure our players // This is a hosting view, let's remeasure our players
this.desiredState = desiredState this.desiredLocation = desiredLocation
val width = desiredState.boundsOnScreen.width() this.desiredHostState = it
if (playerWidth != width) { currentlyExpanded = it.expansion > 0
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
}
}
}
}
fun setPlayerWidth(width: Int) {
if (width != playerWidth) {
playerWidth = width
playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
R.dimen.qs_media_padding)
for (mediaPlayer in mediaPlayers.values) { for (mediaPlayer in mediaPlayers.values) {
mediaPlayer.setPlayerWidth(width) if (animate) {
mediaPlayer.mediaViewController.animatePendingStateChange(
duration = duration,
delay = startDelay)
}
mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation)
} }
// The player width has changed, let's update the scroll position to make sure updateCarouselSize()
// it's still at the same place
var newScroll = activeMediaIndex * playerWidthPlusPadding
if (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 * Update the size of the carousel, remeasuring it if necessary.
* its bounds as if it were measured with the given measurement dimensions
*/ */
fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? { private fun updateCarouselSize() {
val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null val width = desiredHostState?.measurementInput?.width ?: 0
var result: MeasurementOutput? = null val height = desiredHostState?.measurementInput?.height ?: 0
firstPlayer.view?.player?.let { if (width != carouselMeasureWidth && width != 0 ||
// Let's measure the size of the first player and return its height height != carouselMeasureWidth && height != 0) {
val previousProgress = it.progress carouselMeasureWidth = width
val previousRight = it.right carouselMeasureHeight = height
val previousBottom = it.bottom playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
it.progress = input.expansion R.dimen.qs_media_padding)
firstPlayer.measure(input) // The player width has changed, let's update the scroll position to make sure
// Relayouting is necessary in motionlayout to obtain its size properly .... // it's still at the same place
it.layout(0, 0, it.measuredWidth, it.measuredHeight) var newScroll = activeMediaIndex * playerWidthPlusPadding
result = MeasurementOutput(it.measuredWidth, it.measuredHeight) if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
it.progress = previousProgress newScroll += playerWidthPlusPadding -
if (desiredState != null) { (scrollIntoCurrentMedia - playerWidthPlusPadding)
// remeasure it to the old size again! } else {
firstPlayer.measure(desiredState!!.measurementInput) newScroll += scrollIntoCurrentMedia
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)
} }
mediaCarousel.scrollX = newScroll
// 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)
} }
} }
} }

View File

@@ -23,18 +23,15 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.motion.widget.MotionLayout
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.util.animation.TransitionLayout
/** /**
* ViewHolder for a media player. * ViewHolder for a media player.
*/ */
class PlayerViewHolder private constructor(itemView: View) { class PlayerViewHolder private constructor(itemView: View) {
val player = itemView as MotionLayout val player = itemView as TransitionLayout
val background = itemView.requireViewById<View>(R.id.media_background)
// Player information // Player information
val appIcon = itemView.requireViewById<ImageView>(R.id.icon) 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) val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
init { init {
(background.background as IlluminationDrawable).let { (player.background as IlluminationDrawable).let {
it.setupTouch(seamless, player) it.setupTouch(seamless, player)
it.setupTouch(action0, player) it.setupTouch(action0, player)
it.setupTouch(action1, player) it.setupTouch(action1, player)
@@ -95,7 +92,7 @@ class PlayerViewHolder private constructor(itemView: View) {
* @param parent Parent of inflated view. * @param parent Parent of inflated view.
*/ */
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder { @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) return PlayerViewHolder(v)
} }
} }

View File

@@ -427,7 +427,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
(ViewGroup.MarginLayoutParams) hostView.getLayoutParams(); (ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
float targetPosition = absoluteBottomPosition - params.bottomMargin float targetPosition = absoluteBottomPosition - params.bottomMargin
- hostView.getHeight(); - hostView.getHeight();
float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top float currentPosition = mediaHost.getCurrentBounds().top
- hostView.getTranslationY(); - hostView.getTranslationY();
hostView.setTranslationY(targetPosition - currentPosition); hostView.setTranslationY(targetPosition - currentPosition);
} else { } else {

View File

@@ -187,9 +187,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
} }
protected void addMediaHostView() { protected void addMediaHostView() {
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
mMediaHost.setExpansion(1.0f); mMediaHost.setExpansion(1.0f);
mMediaHost.setShowsOnlyActiveMedia(false); mMediaHost.setShowsOnlyActiveMedia(false);
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
ViewGroup hostView = mMediaHost.getHostView(); ViewGroup hostView = mMediaHost.getHostView();
addView(hostView); addView(hostView);
int sidePaddings = getResources().getDimensionPixelSize( int sidePaddings = getResources().getDimensionPixelSize(

View File

@@ -190,9 +190,9 @@ public class QuickQSPanel extends QSPanel {
switchTileLayout(); switchTileLayout();
return null; return null;
}); });
mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
mMediaHost.setExpansion(0.0f); mMediaHost.setExpansion(0.0f);
mMediaHost.setShowsOnlyActiveMedia(true); mMediaHost.setShowsOnlyActiveMedia(true);
mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
reAttachMediaHost(); reAttachMediaHost();
} }

View File

@@ -133,7 +133,7 @@ public class CrossFadeHelper {
} }
public static void fadeIn(View view, float fadeInAmount) { public static void fadeIn(View view, float fadeInAmount) {
fadeIn(view, fadeInAmount, true /* remap */); fadeIn(view, fadeInAmount, false /* remap */);
} }
/** /**

View File

@@ -66,7 +66,7 @@ public abstract class CustomInterpolatorTransformation
return false; return false;
} }
View view = ownState.getTransformedView(); View view = ownState.getTransformedView();
CrossFadeHelper.fadeIn(view, transformationAmount); CrossFadeHelper.fadeIn(view, transformationAmount, true /* remap */);
ownState.transformViewFullyFrom(otherState, this, transformationAmount); ownState.transformViewFullyFrom(otherState, this, transformationAmount);
otherState.recycle(); otherState.recycle();
return true; return true;

View File

@@ -95,7 +95,7 @@ public class TransformState {
if (sameAs(otherState)) { if (sameAs(otherState)) {
ensureVisible(); ensureVisible();
} else { } else {
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount); CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
} }
transformViewFullyFrom(otherState, transformationAmount); transformViewFullyFrom(otherState, transformationAmount);
} }
@@ -424,7 +424,7 @@ public class TransformState {
if (transformationAmount == 0.0f) { if (transformationAmount == 0.0f) {
prepareFadeIn(); prepareFadeIn();
} }
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount); CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
} }
public void disappear(float transformationAmount, TransformableView otherView) { public void disappear(float transformationAmount, TransformableView otherView) {

View File

@@ -92,7 +92,7 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout
// We want to transform from the same y location as the title // We want to transform from the same y location as the title
TransformState otherState = notification.getCurrentState( TransformState otherState = notification.getCurrentState(
TRANSFORMING_VIEW_TITLE); TRANSFORMING_VIEW_TITLE);
CrossFadeHelper.fadeIn(mTextView, transformationAmount); CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */);
if (otherState != null) { if (otherState != null) {
ownState.transformViewVerticalFrom(otherState, transformationAmount); ownState.transformViewVerticalFrom(otherState, transformationAmount);
otherState.recycle(); otherState.recycle();

View File

@@ -23,7 +23,6 @@ import android.content.Context;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.util.ArraySet; import android.util.ArraySet;
@@ -107,7 +106,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
TransformState otherState = notification.getCurrentState( TransformState otherState = notification.getCurrentState(
TRANSFORMING_VIEW_TITLE); TRANSFORMING_VIEW_TITLE);
final View text = ownState.getTransformedView(); final View text = ownState.getTransformedView();
CrossFadeHelper.fadeIn(text, transformationAmount); CrossFadeHelper.fadeIn(text, transformationAmount, true /* remap */);
if (otherState != null) { if (otherState != null) {
ownState.transformViewVerticalFrom(otherState, this, ownState.transformViewVerticalFrom(otherState, this,
transformationAmount); transformationAmount);

View File

@@ -284,7 +284,7 @@ public abstract class NotificationViewWrapper implements TransformableView {
@Override @Override
public void transformFrom(TransformableView notification, float transformationAmount) { public void transformFrom(TransformableView notification, float transformationAmount) {
CrossFadeHelper.fadeIn(mView, transformationAmount); CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */);
} }
@Override @Override

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -19,7 +19,9 @@ package com.android.systemui.util.animation
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout 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 * 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( class UniqueObjectHostView(
context: Context context: Context
) : FrameLayout(context) { ) : FrameLayout(context) {
lateinit var measurementCache : GuaranteedMeasurementCache lateinit var measurementManager: MeasurementManager
var onMeasureListener: ((MeasurementInput) -> Unit)? = null
@SuppressLint("DrawAllocation") @SuppressLint("DrawAllocation")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@@ -45,64 +46,63 @@ class UniqueObjectHostView(
val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec)) val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical
val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec)) val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
val measurementInput = MeasurementInputData(widthSpec, heightSpec) val measurementInput = MeasurementInput(widthSpec, heightSpec)
onMeasureListener?.apply {
invoke(measurementInput) // 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()) { if (!isCurrentHost()) {
// We're not currently the host, let's get the dimension from our cache (this might // We're not currently the host, let's use the dimension from our cache
// perform a measuring if the cache doesn't have it yet)
// The goal here is that the view will always have a consistent measuring, regardless // The goal here is that the view will always have a consistent measuring, regardless
// if it's attached or not. // if it's attached or not.
// The behavior is therefore very similar to the view being persistently attached to // 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 // 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 // the size of the view during transitions even if it has never been attached here
// before. // before.
val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput)
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical) setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
} else { } else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// Let's update our cache // Let's update our cache
val child = getChildAt(0)!! getChildAt(0)?.requiresRemeasuring = false
val output = MeasurementOutput(child.measuredWidth, child.measuredHeight)
measurementCache.putMeasurement(measurementInput, output)
} }
} }
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 private fun isCurrentHost() = childCount != 0
}
/** interface MeasurementManager {
* A basic view measurement input fun onMeasure(input: MeasurementInput): MeasurementOutput
*/
interface MeasurementInput {
fun sameAs(input: MeasurementInput?): Boolean {
return equals(input)
} }
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( var View.requiresRemeasuring: Boolean
val measuredWidth: Int, get() {
val measuredHeight: Int val required = getTag(R.id.requires_remeasuring)
) return required?.equals(true) ?: false
}
/** set(value) {
* The data object holding a basic view measurement input setTag(R.id.requires_remeasuring, value)
*/ }
data class MeasurementInputData(
override var widthMeasureSpec: Int,
override var heightMeasureSpec: Int
) : MeasurementInput

View File

@@ -31,30 +31,24 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.TextView 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 androidx.test.filters.SmallTest
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter 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.concurrency.FakeExecutor
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import java.util.ArrayList
private const val KEY = "TEST_KEY" private const val KEY = "TEST_KEY"
private const val APP = "APP" private const val APP = "APP"
private const val BG_COLOR = Color.RED 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 activityStarter: ActivityStarter
@Mock private lateinit var holder: PlayerViewHolder @Mock private lateinit var holder: PlayerViewHolder
@Mock private lateinit var motion: MotionLayout @Mock private lateinit var view: TransitionLayout
private lateinit var background: TextView @Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
private lateinit var appIcon: ImageView private lateinit var appIcon: ImageView
private lateinit var appName: TextView private lateinit var appName: TextView
private lateinit var albumView: ImageView private lateinit var albumView: ImageView
@@ -107,21 +101,15 @@ public class MediaControlPanelTest : SysuiTestCase() {
bgExecutor = FakeExecutor(FakeSystemClock()) bgExecutor = FakeExecutor(FakeSystemClock())
activityStarter = mock(ActivityStarter::class.java) 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. // Mock out a view holder for the player to attach to.
holder = mock(PlayerViewHolder::class.java) holder = mock(PlayerViewHolder::class.java)
motion = mock(MotionLayout::class.java) view = mock(TransitionLayout::class.java)
val trans: ArrayList<MotionScene.Transition> = ArrayList() whenever(holder.player).thenReturn(view)
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)
appIcon = ImageView(context) appIcon = ImageView(context)
whenever(holder.appIcon).thenReturn(appIcon) whenever(holder.appIcon).thenReturn(appIcon)
appName = TextView(context) appName = TextView(context)
@@ -205,7 +193,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(), val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
emptyList(), PACKAGE, session.getSessionToken(), null, device) emptyList(), PACKAGE, session.getSessionToken(), null, device)
player.bind(state) 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 @Test

View File

@@ -0,0 +1,145 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.media
import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.ViewGroup
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.animation.UniqueObjectHostView
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.anyLong
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class MediaHierarchyManagerTest : SysuiTestCase() {
@Mock
private lateinit var lockHost: MediaHost
@Mock
private lateinit var qsHost: MediaHost
@Mock
private lateinit var qqsHost: MediaHost
@Mock
private lateinit var bypassController: KeyguardBypassController
@Mock
private lateinit var mediaFrame: ViewGroup
@Mock
private lateinit var keyguardStateController: KeyguardStateController
@Mock
private lateinit var statusBarStateController: SysuiStatusBarStateController
@Mock
private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager
@Mock
private lateinit var mediaViewManager: MediaViewManager
@Mock
private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
@Captor
private lateinit var wakefullnessObserver: ArgumentCaptor<(WakefulnessLifecycle.Observer)>
@JvmField
@Rule
val mockito = MockitoJUnit.rule()
private lateinit var mediaHiearchyManager: MediaHierarchyManager
@Before
fun setup() {
`when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame)
mediaHiearchyManager = MediaHierarchyManager(
context,
statusBarStateController,
keyguardStateController,
bypassController,
mediaViewManager,
notificationLockscreenUserManager,
wakefulnessLifecycle)
verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture())
setupHost(lockHost, MediaHierarchyManager.LOCATION_LOCKSCREEN)
setupHost(qsHost, MediaHierarchyManager.LOCATION_QS)
setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS)
`when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
// We'll use the viewmanager to verify a few calls below, let's reset this.
clearInvocations(mediaViewManager)
}
private fun setupHost(host: MediaHost, location: Int) {
`when`(host.location).thenReturn(location)
`when`(host.currentBounds).thenReturn(Rect())
`when`(host.hostView).thenReturn(UniqueObjectHostView(context))
mediaHiearchyManager.register(host)
}
@Test
fun testHostViewSetOnRegister() {
val host = mediaHiearchyManager.register(lockHost)
verify(lockHost).hostView = eq(host)
}
@Test
fun testBlockedWhenScreenTurningOff() {
// Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer)
observer.onStartedGoingToSleep()
clearInvocations(mediaViewManager)
mediaHiearchyManager.qsExpansion = 0.0f
verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
}
@Test
fun testAllowedWhenNotTurningOff() {
// Let's set it onto QS:
mediaHiearchyManager.qsExpansion = 1.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
val observer = wakefullnessObserver.value
assertNotNull("lifecycle observer wasn't registered", observer)
clearInvocations(mediaViewManager)
mediaHiearchyManager.qsExpansion = 0.0f
verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
}
}

View File

@@ -57,6 +57,6 @@ class PlayerViewHolderTest : SysuiTestCase() {
@Test @Test
fun backgroundIsIlluminationDrawable() { fun backgroundIsIlluminationDrawable() {
val holder = PlayerViewHolder.create(inflater, parent) val holder = PlayerViewHolder.create(inflater, parent)
assertThat(holder.background.background as IlluminationDrawable).isNotNull() assertThat(holder.player.background as IlluminationDrawable).isNotNull()
} }
} }