Merge changes I75a386d7,Ica8c2b14 into rvc-dev am: 02b5b19256 am: 085add6e68
Change-Id: I31c99389cb5ea835f6ff4ad74a2042e2971e41eb
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
-->
|
||||
|
||||
<!-- Layout for media controls inside QSPanel carousel -->
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.android.systemui.util.animation.TransitionLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/qs_media_controls"
|
||||
android:layout_width="match_parent"
|
||||
@@ -24,18 +24,7 @@
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="center_horizontal|fill_vertical"
|
||||
app:layoutDescription="@xml/media_scene">
|
||||
|
||||
<View
|
||||
android:id="@+id/media_background"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/qs_media_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/view_width"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
/>
|
||||
android:background="@drawable/qs_media_background">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/notification_media_progress_time"
|
||||
@@ -185,15 +174,5 @@
|
||||
<!-- Buttons to remove this view when no longer needed -->
|
||||
<include
|
||||
layout="@layout/qs_media_panel_options"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="@id/view_width"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/view_width"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="300dp" />
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
android:visibility="gone" />
|
||||
</com.android.systemui.util.animation.TransitionLayout>
|
||||
@@ -90,6 +90,8 @@
|
||||
<item type="id" name="view_width_animator_end_tag"/>
|
||||
<item type="id" name="view_width_current_value"/>
|
||||
|
||||
<item type="id" name="requires_remeasuring"/>
|
||||
|
||||
<!-- Whether the icon is from a notification for which targetSdk < L -->
|
||||
<item type="id" name="icon_is_pre_L"/>
|
||||
|
||||
|
||||
184
packages/SystemUI/res/xml/media_collapsed.xml
Normal file
184
packages/SystemUI/res/xml/media_collapsed.xml
Normal 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>
|
||||
178
packages/SystemUI/res/xml/media_expanded.xml
Normal file
178
packages/SystemUI/res/xml/media_expanded.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class KeyguardMediaController @Inject constructor(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private var view: MediaHeaderView? = null
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver
|
||||
import com.android.systemui.statusbar.notification.AnimatableProperty
|
||||
import com.android.systemui.statusbar.notification.PropertyAnimator
|
||||
import com.android.systemui.statusbar.notification.stack.AnimationProperties
|
||||
|
||||
/**
|
||||
* A utility class that helps with animations of bound changes designed for motionlayout which
|
||||
* doesn't work together with regular changeBounds.
|
||||
*/
|
||||
class LayoutAnimationHelper {
|
||||
|
||||
private val layout: ViewGroup
|
||||
private var sizeAnimationPending = false
|
||||
private val desiredBounds = mutableMapOf<View, Rect>()
|
||||
private val animationProperties = AnimationProperties()
|
||||
private val layoutListener = object : View.OnLayoutChangeListener {
|
||||
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int,
|
||||
oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||
v?.let {
|
||||
if (v.alpha == 0.0f || v.visibility == View.GONE || oldLeft - oldRight == 0 ||
|
||||
oldTop - oldBottom == 0) {
|
||||
return
|
||||
}
|
||||
if (oldLeft != left || oldTop != top || oldBottom != bottom || oldRight != right) {
|
||||
val rect = desiredBounds.getOrPut(v, { Rect() })
|
||||
rect.set(left, top, right, bottom)
|
||||
onDesiredLocationChanged(v, rect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(layout: ViewGroup) {
|
||||
this.layout = layout
|
||||
val childCount = this.layout.childCount
|
||||
for (i in 0 until childCount) {
|
||||
val child = this.layout.getChildAt(i)
|
||||
child.addOnLayoutChangeListener(layoutListener)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDesiredLocationChanged(v: View, rect: Rect) {
|
||||
if (!sizeAnimationPending) {
|
||||
applyBounds(v, rect, animate = false)
|
||||
}
|
||||
// We need to reapply the current bounds in every frame since the layout may override
|
||||
// the layout bounds making this view jump and not all calls to apply bounds actually
|
||||
// reapply them, for example if there's already an animator to the same target
|
||||
reapplyProperty(v, AnimatableProperty.ABSOLUTE_X);
|
||||
reapplyProperty(v, AnimatableProperty.ABSOLUTE_Y);
|
||||
reapplyProperty(v, AnimatableProperty.WIDTH);
|
||||
reapplyProperty(v, AnimatableProperty.HEIGHT);
|
||||
}
|
||||
|
||||
private fun reapplyProperty(v: View, property: AnimatableProperty) {
|
||||
property.property.set(v, property.property.get(v))
|
||||
}
|
||||
|
||||
private fun applyBounds(v: View, newBounds: Rect, animate: Boolean) {
|
||||
PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_X, newBounds.left.toFloat(),
|
||||
animationProperties, animate)
|
||||
PropertyAnimator.setProperty(v, AnimatableProperty.ABSOLUTE_Y, newBounds.top.toFloat(),
|
||||
animationProperties, animate)
|
||||
PropertyAnimator.setProperty(v, AnimatableProperty.WIDTH, newBounds.width().toFloat(),
|
||||
animationProperties, animate)
|
||||
PropertyAnimator.setProperty(v, AnimatableProperty.HEIGHT, newBounds.height().toFloat(),
|
||||
animationProperties, animate)
|
||||
}
|
||||
|
||||
private fun startBoundAnimation(v: View) {
|
||||
val target = desiredBounds[v] ?: return
|
||||
applyBounds(v, target, animate = true)
|
||||
}
|
||||
|
||||
fun animatePendingSizeChange(duration: Long, delay: Long) {
|
||||
animationProperties.duration = duration
|
||||
animationProperties.delay = delay
|
||||
if (!sizeAnimationPending) {
|
||||
sizeAnimationPending = true
|
||||
layout.viewTreeObserver.addOnPreDrawListener (
|
||||
object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
layout.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
sizeAnimationPending = false
|
||||
val childCount = layout.childCount
|
||||
for (i in 0 until childCount) {
|
||||
val child = layout.getChildAt(i)
|
||||
startBoundAnimation(child)
|
||||
}
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,9 @@ import android.widget.ImageView;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.constraintlayout.motion.widget.Key;
|
||||
import androidx.constraintlayout.motion.widget.KeyAttributes;
|
||||
import androidx.constraintlayout.motion.widget.KeyFrames;
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
|
||||
@@ -59,11 +56,11 @@ import com.android.settingslib.widget.AdaptiveIcon;
|
||||
import com.android.systemui.R;
|
||||
import com.android.systemui.plugins.ActivityStarter;
|
||||
import com.android.systemui.qs.QSMediaBrowser;
|
||||
import com.android.systemui.util.animation.TransitionLayout;
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@@ -87,16 +84,15 @@ public class MediaControlPanel {
|
||||
private final Executor mForegroundExecutor;
|
||||
protected final Executor mBackgroundExecutor;
|
||||
private final ActivityStarter mActivityStarter;
|
||||
private LayoutAnimationHelper mLayoutAnimationHelper;
|
||||
|
||||
private Context mContext;
|
||||
private PlayerViewHolder mViewHolder;
|
||||
private MediaViewController mMediaViewController;
|
||||
private MediaSession.Token mToken;
|
||||
private MediaController mController;
|
||||
private int mBackgroundColor;
|
||||
protected ComponentName mServiceComponent;
|
||||
private boolean mIsRegistered = false;
|
||||
private List<KeyFrames> mKeyFrames;
|
||||
private String mKey;
|
||||
private int mAlbumArtSize;
|
||||
private int mAlbumArtRadius;
|
||||
@@ -133,12 +129,14 @@ public class MediaControlPanel {
|
||||
* @param activityStarter activity starter
|
||||
*/
|
||||
public MediaControlPanel(Context context, Executor foregroundExecutor,
|
||||
DelayableExecutor backgroundExecutor, ActivityStarter activityStarter) {
|
||||
DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
|
||||
MediaHostStatesManager mediaHostStatesManager) {
|
||||
mContext = context;
|
||||
mForegroundExecutor = foregroundExecutor;
|
||||
mBackgroundExecutor = backgroundExecutor;
|
||||
mActivityStarter = activityStarter;
|
||||
mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
|
||||
mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
|
||||
loadDimens();
|
||||
}
|
||||
|
||||
@@ -147,6 +145,7 @@ public class MediaControlPanel {
|
||||
mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
|
||||
}
|
||||
mSeekBarViewModel.onDestroy();
|
||||
mMediaViewController.onDestroy();
|
||||
}
|
||||
|
||||
private void loadDimens() {
|
||||
@@ -164,6 +163,15 @@ public class MediaControlPanel {
|
||||
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.
|
||||
*
|
||||
@@ -187,15 +195,13 @@ public class MediaControlPanel {
|
||||
/** Attaches the player to the view holder. */
|
||||
public void attach(PlayerViewHolder vh) {
|
||||
mViewHolder = vh;
|
||||
MotionLayout motionView = vh.getPlayer();
|
||||
mLayoutAnimationHelper = new LayoutAnimationHelper(motionView);
|
||||
GoneChildrenHideHelper.clipGoneChildrenOnLayout(motionView);
|
||||
mKeyFrames = motionView.getDefinedTransitions().get(0).getKeyFrameList();
|
||||
TransitionLayout player = vh.getPlayer();
|
||||
mSeekBarObserver = new SeekBarObserver(vh);
|
||||
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
|
||||
SeekBar bar = vh.getSeekBar();
|
||||
bar.setOnSeekBarChangeListener(mSeekBarViewModel.getSeekBarListener());
|
||||
bar.setOnTouchListener(mSeekBarViewModel.getSeekBarTouchListener());
|
||||
mMediaViewController.attach(player);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,8 +226,8 @@ public class MediaControlPanel {
|
||||
|
||||
mController = new MediaController(mContext, mToken);
|
||||
|
||||
ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
|
||||
ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
|
||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
||||
|
||||
// Try to find a browser service component for this app
|
||||
// TODO also check for a media button receiver intended for restarting (b/154127084)
|
||||
@@ -247,7 +253,7 @@ public class MediaControlPanel {
|
||||
|
||||
mController.registerCallback(mSessionCallback);
|
||||
|
||||
mViewHolder.getBackground().setBackgroundTintList(
|
||||
mViewHolder.getPlayer().setBackgroundTintList(
|
||||
ColorStateList.valueOf(mBackgroundColor));
|
||||
|
||||
// Click action
|
||||
@@ -356,7 +362,6 @@ public class MediaControlPanel {
|
||||
}
|
||||
});
|
||||
boolean visibleInCompat = actionsWhenCollapsed.contains(i);
|
||||
updateKeyFrameVisibility(actionId, visibleInCompat);
|
||||
setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
|
||||
setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
|
||||
}
|
||||
@@ -374,9 +379,9 @@ public class MediaControlPanel {
|
||||
// Set up long press menu
|
||||
// TODO: b/156036025 bring back media guts
|
||||
|
||||
// Update both constraint sets to regenerate the animation.
|
||||
mViewHolder.getPlayer().updateState(R.id.collapsed, collapsedSet);
|
||||
mViewHolder.getPlayer().updateState(R.id.expanded, expandedSet);
|
||||
// TODO: We don't need to refresh this state constantly, only if the state actually changed
|
||||
// to something which might impact the measurement
|
||||
mMediaViewController.refreshState();
|
||||
}
|
||||
|
||||
@UiThread
|
||||
@@ -411,30 +416,6 @@ public class MediaControlPanel {
|
||||
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
|
||||
@@ -528,8 +509,8 @@ public class MediaControlPanel {
|
||||
}
|
||||
// Hide all the old buttons
|
||||
|
||||
ConstraintSet expandedSet = mViewHolder.getPlayer().getConstraintSet(R.id.expanded);
|
||||
ConstraintSet collapsedSet = mViewHolder.getPlayer().getConstraintSet(R.id.collapsed);
|
||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
||||
for (int i = 1; i < ACTION_IDS.length; i++) {
|
||||
setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
|
||||
setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
|
||||
@@ -571,6 +552,7 @@ public class MediaControlPanel {
|
||||
options.setVisibility(View.VISIBLE);
|
||||
return true; // consumed click
|
||||
});
|
||||
mMediaViewController.refreshState();
|
||||
}
|
||||
|
||||
private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
|
||||
@@ -649,33 +631,4 @@ public class MediaControlPanel {
|
||||
* Called when a player can't be resumed to give it an opportunity to hide or remove itself
|
||||
*/
|
||||
protected void removePlayer() { }
|
||||
|
||||
public void measure(@Nullable MediaMeasurementInput input) {
|
||||
if (mViewHolder == null) {
|
||||
return;
|
||||
}
|
||||
if (input != null) {
|
||||
int width = input.getWidth();
|
||||
setPlayerWidth(width);
|
||||
mViewHolder.getPlayer().measure(input.getWidthMeasureSpec(),
|
||||
input.getHeightMeasureSpec());
|
||||
}
|
||||
}
|
||||
|
||||
public void setPlayerWidth(int width) {
|
||||
if (mViewHolder == null) {
|
||||
return;
|
||||
}
|
||||
MotionLayout view = mViewHolder.getPlayer();
|
||||
ConstraintSet expandedSet = view.getConstraintSet(R.id.expanded);
|
||||
ConstraintSet collapsedSet = view.getConstraintSet(R.id.collapsed);
|
||||
collapsedSet.setGuidelineBegin(R.id.view_width, width);
|
||||
expandedSet.setGuidelineBegin(R.id.view_width, width);
|
||||
view.updateState(R.id.collapsed, collapsedSet);
|
||||
view.updateState(R.id.expanded, expandedSet);
|
||||
}
|
||||
|
||||
public void animatePendingSizeChange(long duration, long startDelay) {
|
||||
mLayoutAnimationHelper.animatePendingSizeChange(duration, startDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,13 @@ import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.IntDef
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.MathUtils
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroupOverlay
|
||||
import com.android.systemui.Interpolators
|
||||
import com.android.systemui.keyguard.WakefulnessLifecycle
|
||||
import com.android.systemui.plugins.statusbar.StatusBarStateController
|
||||
import com.android.systemui.statusbar.NotificationLockscreenUserManager
|
||||
import com.android.systemui.statusbar.StatusBarState
|
||||
@@ -47,8 +50,8 @@ class MediaHierarchyManager @Inject constructor(
|
||||
private val keyguardStateController: KeyguardStateController,
|
||||
private val bypassController: KeyguardBypassController,
|
||||
private val mediaViewManager: MediaViewManager,
|
||||
private val mediaMeasurementProvider: MediaMeasurementManager,
|
||||
private val notifLockscreenUserManager: NotificationLockscreenUserManager
|
||||
private val notifLockscreenUserManager: NotificationLockscreenUserManager,
|
||||
wakefulnessLifecycle: WakefulnessLifecycle
|
||||
) {
|
||||
/**
|
||||
* The root overlay of the hierarchy. This is where the media notification is attached to
|
||||
@@ -56,23 +59,31 @@ class MediaHierarchyManager @Inject constructor(
|
||||
* view is always in its final state when it is attached to a view host.
|
||||
*/
|
||||
private var rootOverlay: ViewGroupOverlay? = null
|
||||
private lateinit var currentState: MediaState
|
||||
private val mediaCarousel
|
||||
|
||||
private var rootView: View? = null
|
||||
private var currentBounds = Rect()
|
||||
private var animationStartBounds: Rect = Rect()
|
||||
private var targetBounds: Rect = Rect()
|
||||
private val mediaFrame
|
||||
get() = mediaViewManager.mediaFrame
|
||||
private var animationStartState: MediaState? = null
|
||||
private var statusbarState: Int = statusBarStateController.state
|
||||
private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
|
||||
interpolator = Interpolators.FAST_OUT_SLOW_IN
|
||||
addUpdateListener {
|
||||
updateTargetState()
|
||||
applyState(animationStartState!!.interpolate(targetState!!, animatedFraction))
|
||||
interpolateBounds(animationStartBounds, targetBounds, animatedFraction,
|
||||
result = currentBounds)
|
||||
applyState(currentBounds)
|
||||
}
|
||||
addListener(object : AnimatorListenerAdapter() {
|
||||
private var cancelled: Boolean = false
|
||||
|
||||
override fun onAnimationCancel(animation: Animator?) {
|
||||
cancelled = true
|
||||
animationPending = false
|
||||
rootView?.removeCallbacks(startAnimation)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
if (!cancelled) {
|
||||
applyTargetStateIfNotAnimating()
|
||||
@@ -81,30 +92,41 @@ class MediaHierarchyManager @Inject constructor(
|
||||
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
cancelled = false
|
||||
animationPending = false
|
||||
}
|
||||
})
|
||||
}
|
||||
private var targetState: MediaState? = null
|
||||
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
|
||||
|
||||
private val mediaHosts = arrayOfNulls<MediaHost>(LOCATION_LOCKSCREEN + 1)
|
||||
/**
|
||||
* The last location where this view was at before going to the desired location. This is
|
||||
* useful for guided transitions.
|
||||
*/
|
||||
@MediaLocation private var previousLocation = -1
|
||||
|
||||
@MediaLocation
|
||||
private var previousLocation = -1
|
||||
/**
|
||||
* The desired location where the view will be at the end of the transition.
|
||||
*/
|
||||
@MediaLocation private var desiredLocation = -1
|
||||
@MediaLocation
|
||||
private var desiredLocation = -1
|
||||
|
||||
/**
|
||||
* The current attachment location where the view is currently attached.
|
||||
* Usually this matches the desired location except for animations whenever a view moves
|
||||
* to the new desired location, during which it is in [IN_OVERLAY].
|
||||
*/
|
||||
@MediaLocation private var currentAttachmentLocation = -1
|
||||
@MediaLocation
|
||||
private var currentAttachmentLocation = -1
|
||||
|
||||
/**
|
||||
* Are we currently waiting on an animation to start?
|
||||
*/
|
||||
private var animationPending: Boolean = false
|
||||
private val startAnimation: Runnable = Runnable { animator.start() }
|
||||
|
||||
/**
|
||||
* The expansion of quick settings
|
||||
*/
|
||||
var qsExpansion: Float = 0.0f
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
@@ -117,6 +139,40 @@ class MediaHierarchyManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are location changes currently blocked?
|
||||
*/
|
||||
private val blockLocationChanges: Boolean
|
||||
get() {
|
||||
return goingToSleep || dozeAnimationRunning
|
||||
}
|
||||
|
||||
/**
|
||||
* Are we currently going to sleep
|
||||
*/
|
||||
private var goingToSleep: Boolean = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
updateDesiredLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the doze animation currently Running
|
||||
*/
|
||||
private var dozeAnimationRunning: Boolean = false
|
||||
private set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (!value) {
|
||||
updateDesiredLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
|
||||
override fun onStatePreChange(oldState: Int, newState: Int) {
|
||||
@@ -129,6 +185,34 @@ class MediaHierarchyManager @Inject constructor(
|
||||
override fun onStateChanged(newState: Int) {
|
||||
updateTargetState()
|
||||
}
|
||||
|
||||
override fun onDozeAmountChanged(linear: Float, eased: Float) {
|
||||
dozeAnimationRunning = linear != 0.0f && linear != 1.0f
|
||||
}
|
||||
|
||||
override fun onDozingChanged(isDozing: Boolean) {
|
||||
if (!isDozing) {
|
||||
dozeAnimationRunning = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
|
||||
override fun onFinishedGoingToSleep() {
|
||||
goingToSleep = false
|
||||
}
|
||||
|
||||
override fun onStartedGoingToSleep() {
|
||||
goingToSleep = true
|
||||
}
|
||||
|
||||
override fun onFinishedWakingUp() {
|
||||
goingToSleep = false
|
||||
}
|
||||
|
||||
override fun onStartedWakingUp() {
|
||||
goingToSleep = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,8 +222,8 @@ class MediaHierarchyManager @Inject constructor(
|
||||
*
|
||||
* @return the hostView associated with this location
|
||||
*/
|
||||
fun register(mediaObject: MediaHost): ViewGroup {
|
||||
val viewHost = createUniqueObjectHost(mediaObject)
|
||||
fun register(mediaObject: MediaHost): UniqueObjectHostView {
|
||||
val viewHost = createUniqueObjectHost()
|
||||
mediaObject.hostView = viewHost
|
||||
mediaHosts[mediaObject.location] = mediaObject
|
||||
if (mediaObject.location == desiredLocation) {
|
||||
@@ -154,22 +238,13 @@ class MediaHierarchyManager @Inject constructor(
|
||||
return viewHost
|
||||
}
|
||||
|
||||
private fun createUniqueObjectHost(host: MediaHost): UniqueObjectHostView {
|
||||
private fun createUniqueObjectHost(): UniqueObjectHostView {
|
||||
val viewHost = UniqueObjectHostView(context)
|
||||
viewHost.measurementCache = mediaMeasurementProvider.obtainCache(host)
|
||||
viewHost.onMeasureListener = { input ->
|
||||
if (host.location == desiredLocation) {
|
||||
// Measurement of the currently active player is happening, Let's make
|
||||
// sure the player width is up to date
|
||||
val measuringInput = host.getMeasuringInput(input)
|
||||
mediaViewManager.setPlayerWidth(measuringInput.width)
|
||||
}
|
||||
}
|
||||
|
||||
viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(p0: View?) {
|
||||
if (rootOverlay == null) {
|
||||
rootOverlay = (viewHost.viewRootImpl.view.overlay as ViewGroupOverlay)
|
||||
rootView = viewHost.viewRootImpl.view
|
||||
rootOverlay = (rootView!!.overlay as ViewGroupOverlay)
|
||||
}
|
||||
viewHost.removeOnAttachStateChangeListener(this)
|
||||
}
|
||||
@@ -195,8 +270,9 @@ class MediaHierarchyManager @Inject constructor(
|
||||
// Let's perform a transition
|
||||
val animate = shouldAnimateTransition(desiredLocation, previousLocation)
|
||||
val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation)
|
||||
mediaViewManager.onDesiredLocationChanged(getHost(desiredLocation)?.currentState,
|
||||
animate, animDuration, delay)
|
||||
val host = getHost(desiredLocation)
|
||||
mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration,
|
||||
delay)
|
||||
performTransitionToNewLocation(isNewView, animate)
|
||||
}
|
||||
}
|
||||
@@ -222,14 +298,18 @@ class MediaHierarchyManager @Inject constructor(
|
||||
// Let's animate to the new position, starting from the current position
|
||||
// We also go in here in case the view was detached, since the bounds wouldn't
|
||||
// be correct anymore
|
||||
animationStartState = currentState.copy()
|
||||
animationStartBounds.set(currentBounds)
|
||||
} else {
|
||||
// otherwise, let's take the freshest state, since the current one could
|
||||
// be outdated
|
||||
animationStartState = previousHost.currentState.copy()
|
||||
animationStartBounds.set(previousHost.currentBounds)
|
||||
}
|
||||
adjustAnimatorForTransition(desiredLocation, previousLocation)
|
||||
animator.start()
|
||||
rootView?.let {
|
||||
// Let's delay the animation start until we finished laying out
|
||||
animationPending = true
|
||||
it.postOnAnimation(startAnimation)
|
||||
}
|
||||
} else {
|
||||
cancelAnimationAndApplyDesiredState()
|
||||
}
|
||||
@@ -239,6 +319,9 @@ class MediaHierarchyManager @Inject constructor(
|
||||
@MediaLocation currentLocation: Int,
|
||||
@MediaLocation previousLocation: Int
|
||||
): Boolean {
|
||||
if (isCurrentlyInGuidedTransformation()) {
|
||||
return false
|
||||
}
|
||||
if (currentLocation == LOCATION_QQS &&
|
||||
previousLocation == LOCATION_LOCKSCREEN &&
|
||||
(statusBarStateController.leaveOpenOnKeyguardHide() ||
|
||||
@@ -247,7 +330,7 @@ class MediaHierarchyManager @Inject constructor(
|
||||
// non-trivial reattaching logic happening that will make the view not-shown earlier
|
||||
return true
|
||||
}
|
||||
return mediaCarousel.isShown || animator.isRunning
|
||||
return mediaFrame.isShown || animator.isRunning || animationPending
|
||||
}
|
||||
|
||||
private fun adjustAnimatorForTransition(desiredLocation: Int, previousLocation: Int) {
|
||||
@@ -279,7 +362,7 @@ class MediaHierarchyManager @Inject constructor(
|
||||
// Let's immediately apply the target state (which is interpolated) if there is
|
||||
// no animation running. Otherwise the animation update will already update
|
||||
// the location
|
||||
applyState(targetState!!)
|
||||
applyState(targetBounds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,14 +374,34 @@ class MediaHierarchyManager @Inject constructor(
|
||||
val progress = getTransformationProgress()
|
||||
val currentHost = getHost(desiredLocation)!!
|
||||
val previousHost = getHost(previousLocation)!!
|
||||
val newState = currentHost.currentState
|
||||
val previousState = previousHost.currentState
|
||||
targetState = previousState.interpolate(newState, progress)
|
||||
val newBounds = currentHost.currentBounds
|
||||
val previousBounds = previousHost.currentBounds
|
||||
targetBounds = interpolateBounds(previousBounds, newBounds, progress)
|
||||
} else {
|
||||
targetState = getHost(desiredLocation)?.currentState
|
||||
val bounds = getHost(desiredLocation)?.currentBounds ?: return
|
||||
targetBounds.set(bounds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun interpolateBounds(
|
||||
startBounds: Rect,
|
||||
endBounds: Rect,
|
||||
progress: Float,
|
||||
result: Rect? = null
|
||||
): Rect {
|
||||
val left = MathUtils.lerp(startBounds.left.toFloat(),
|
||||
endBounds.left.toFloat(), progress).toInt()
|
||||
val top = MathUtils.lerp(startBounds.top.toFloat(),
|
||||
endBounds.top.toFloat(), progress).toInt()
|
||||
val right = MathUtils.lerp(startBounds.right.toFloat(),
|
||||
endBounds.right.toFloat(), progress).toInt()
|
||||
val bottom = MathUtils.lerp(startBounds.bottom.toFloat(),
|
||||
endBounds.bottom.toFloat(), progress).toInt()
|
||||
val resultBounds = result ?: Rect()
|
||||
resultBounds.set(left, top, right, bottom)
|
||||
return resultBounds
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if this transformation is guided by an external progress like a finger
|
||||
*/
|
||||
@@ -339,21 +442,27 @@ class MediaHierarchyManager @Inject constructor(
|
||||
private fun cancelAnimationAndApplyDesiredState() {
|
||||
animator.cancel()
|
||||
getHost(desiredLocation)?.let {
|
||||
applyState(it.currentState)
|
||||
applyState(it.currentBounds, immediately = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyState(state: MediaState) {
|
||||
currentState = state.copy()
|
||||
mediaViewManager.setCurrentState(currentState)
|
||||
/**
|
||||
* Apply the current state to the view, updating it's bounds and desired state
|
||||
*/
|
||||
private fun applyState(bounds: Rect, immediately: Boolean = false) {
|
||||
currentBounds.set(bounds)
|
||||
val currentlyInGuidedTransformation = isCurrentlyInGuidedTransformation()
|
||||
val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1
|
||||
val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f
|
||||
val endLocation = desiredLocation
|
||||
mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately)
|
||||
updateHostAttachment()
|
||||
if (currentAttachmentLocation == IN_OVERLAY) {
|
||||
val boundsOnScreen = state.boundsOnScreen
|
||||
mediaCarousel.setLeftTopRightBottom(
|
||||
boundsOnScreen.left,
|
||||
boundsOnScreen.top,
|
||||
boundsOnScreen.right,
|
||||
boundsOnScreen.bottom)
|
||||
mediaFrame.setLeftTopRightBottom(
|
||||
currentBounds.left,
|
||||
currentBounds.top,
|
||||
currentBounds.right,
|
||||
currentBounds.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,26 +473,29 @@ class MediaHierarchyManager @Inject constructor(
|
||||
currentAttachmentLocation = newLocation
|
||||
|
||||
// Remove the carousel from the old host
|
||||
(mediaCarousel.parent as ViewGroup?)?.removeView(mediaCarousel)
|
||||
(mediaFrame.parent as ViewGroup?)?.removeView(mediaFrame)
|
||||
|
||||
// Add it to the new one
|
||||
val targetHost = getHost(desiredLocation)!!.hostView
|
||||
if (inOverlay) {
|
||||
rootOverlay!!.add(mediaCarousel)
|
||||
rootOverlay!!.add(mediaFrame)
|
||||
} else {
|
||||
targetHost.addView(mediaCarousel)
|
||||
mediaViewManager.onViewReattached()
|
||||
targetHost.addView(mediaFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTransitionRunning(): Boolean {
|
||||
return isCurrentlyInGuidedTransformation() && getTransformationProgress() != 1.0f ||
|
||||
animator.isRunning
|
||||
animator.isRunning || animationPending
|
||||
}
|
||||
|
||||
@MediaLocation
|
||||
private fun calculateLocation(): Int {
|
||||
if (blockLocationChanges) {
|
||||
// Keep the current location until we're allowed to again
|
||||
return desiredLocation
|
||||
}
|
||||
val onLockscreen = (!bypassController.bypassEnabled &&
|
||||
(statusbarState == StatusBarState.KEYGUARD ||
|
||||
statusbarState == StatusBarState.FULLSCREEN_USER_SWITCHER))
|
||||
@@ -396,13 +508,6 @@ class MediaHierarchyManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The expansion of quick settings
|
||||
*/
|
||||
@IntDef(prefix = ["LOCATION_"], value = [LOCATION_QS, LOCATION_QQS, LOCATION_LOCKSCREEN])
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class MediaLocation
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Attached in expanded quick settings
|
||||
@@ -425,3 +530,8 @@ class MediaHierarchyManager @Inject constructor(
|
||||
const val IN_OVERLAY = -1000
|
||||
}
|
||||
}
|
||||
|
||||
@IntDef(prefix = ["LOCATION_"], value = [MediaHierarchyManager.LOCATION_QS,
|
||||
MediaHierarchyManager.LOCATION_QQS, MediaHierarchyManager.LOCATION_LOCKSCREEN])
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class MediaLocation
|
||||
@@ -1,20 +1,22 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.util.MathUtils
|
||||
import android.view.View
|
||||
import android.view.View.OnAttachStateChangeListener
|
||||
import android.view.ViewGroup
|
||||
import com.android.systemui.media.MediaHierarchyManager.MediaLocation
|
||||
import com.android.systemui.util.animation.MeasurementInput
|
||||
import com.android.systemui.util.animation.MeasurementOutput
|
||||
import com.android.systemui.util.animation.UniqueObjectHostView
|
||||
import java.util.Objects
|
||||
import javax.inject.Inject
|
||||
|
||||
class MediaHost @Inject constructor(
|
||||
private val state: MediaHostState,
|
||||
private val state: MediaHostStateHolder,
|
||||
private val mediaHierarchyManager: MediaHierarchyManager,
|
||||
private val mediaDataManager: MediaDataManager
|
||||
) : MediaState by state {
|
||||
lateinit var hostView: ViewGroup
|
||||
private val mediaDataManager: MediaDataManager,
|
||||
private val mediaDataManagerCombineLatest: MediaDataCombineLatest,
|
||||
private val mediaHostStatesManager: MediaHostStatesManager
|
||||
) : MediaHostState by state {
|
||||
lateinit var hostView: UniqueObjectHostView
|
||||
var location: Int = -1
|
||||
private set
|
||||
var visibleChangedListener: ((Boolean) -> Unit)? = null
|
||||
@@ -24,9 +26,9 @@ class MediaHost @Inject constructor(
|
||||
private val tmpLocationOnScreen: IntArray = intArrayOf(0, 0)
|
||||
|
||||
/**
|
||||
* Get the current Media state. This also updates the location on screen
|
||||
* Get the current bounds on the screen. This makes sure the state is fresh and up to date
|
||||
*/
|
||||
val currentState: MediaState
|
||||
val currentBounds: Rect = Rect()
|
||||
get() {
|
||||
hostView.getLocationOnScreen(tmpLocationOnScreen)
|
||||
var left = tmpLocationOnScreen[0] + hostView.paddingLeft
|
||||
@@ -43,8 +45,8 @@ class MediaHost @Inject constructor(
|
||||
bottom = 0
|
||||
top = 0
|
||||
}
|
||||
state.boundsOnScreen.set(left, top, right, bottom)
|
||||
return state
|
||||
field.set(left, top, right, bottom)
|
||||
return field
|
||||
}
|
||||
|
||||
private val listener = object : MediaDataManager.Listener {
|
||||
@@ -59,6 +61,8 @@ class MediaHost @Inject constructor(
|
||||
|
||||
/**
|
||||
* Initialize this MediaObject and create a host view.
|
||||
* All state should already be set on this host before calling this method in order to avoid
|
||||
* unnecessary state changes which lead to remeasurings later on.
|
||||
*
|
||||
* @param location the location this host name has. Used to identify the host during
|
||||
* transitions.
|
||||
@@ -68,14 +72,39 @@ class MediaHost @Inject constructor(
|
||||
hostView = mediaHierarchyManager.register(this)
|
||||
hostView.addOnAttachStateChangeListener(object : OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View?) {
|
||||
mediaDataManager.addListener(listener)
|
||||
// we should listen to the combined state change, since otherwise there might
|
||||
// be a delay until the views and the controllers are initialized, leaving us
|
||||
// with either a blank view or the controllers not yet initialized and the
|
||||
// measuring wrong
|
||||
mediaDataManagerCombineLatest.addListener(listener)
|
||||
updateViewVisibility()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View?) {
|
||||
mediaDataManager.removeListener(listener)
|
||||
mediaDataManagerCombineLatest.removeListener(listener)
|
||||
}
|
||||
})
|
||||
|
||||
// Listen to measurement updates and update our state with it
|
||||
hostView.measurementManager = object : UniqueObjectHostView.MeasurementManager {
|
||||
override fun onMeasure(input: MeasurementInput): MeasurementOutput {
|
||||
// Modify the measurement to exactly match the dimensions
|
||||
if (View.MeasureSpec.getMode(input.widthMeasureSpec) == View.MeasureSpec.AT_MOST) {
|
||||
input.widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(
|
||||
View.MeasureSpec.getSize(input.widthMeasureSpec),
|
||||
View.MeasureSpec.EXACTLY)
|
||||
}
|
||||
// This will trigger a state change that ensures that we now have a state available
|
||||
state.measurementInput = input
|
||||
return mediaHostStatesManager.getPlayerDimensions(state)
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever the state changes, let our state manager know
|
||||
state.changedListener = {
|
||||
mediaHostStatesManager.updateHostState(location, state)
|
||||
}
|
||||
|
||||
updateViewVisibility()
|
||||
}
|
||||
|
||||
@@ -89,71 +118,93 @@ class MediaHost @Inject constructor(
|
||||
visibleChangedListener?.invoke(visible)
|
||||
}
|
||||
|
||||
class MediaHostState @Inject constructor() : MediaState {
|
||||
var measurementInput: MediaMeasurementInput? = null
|
||||
override var expansion: Float = 0.0f
|
||||
override var showsOnlyActiveMedia: Boolean = false
|
||||
override val boundsOnScreen: Rect = Rect()
|
||||
class MediaHostStateHolder @Inject constructor() : MediaHostState {
|
||||
|
||||
override fun copy(): MediaState {
|
||||
val mediaHostState = MediaHostState()
|
||||
override var measurementInput: MeasurementInput? = null
|
||||
set(value) {
|
||||
if (value?.equals(field) != true) {
|
||||
field = value
|
||||
changedListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override var expansion: Float = 0.0f
|
||||
set(value) {
|
||||
if (!value.equals(field)) {
|
||||
field = value
|
||||
changedListener?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
override var showsOnlyActiveMedia: Boolean = false
|
||||
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.showsOnlyActiveMedia = showsOnlyActiveMedia
|
||||
mediaHostState.boundsOnScreen.set(boundsOnScreen)
|
||||
mediaHostState.measurementInput = measurementInput
|
||||
mediaHostState.measurementInput = measurementInput?.copy()
|
||||
return mediaHostState
|
||||
}
|
||||
|
||||
override fun interpolate(other: MediaState, amount: Float): MediaState {
|
||||
val result = MediaHostState()
|
||||
result.expansion = MathUtils.lerp(expansion, other.expansion, amount)
|
||||
val left = MathUtils.lerp(boundsOnScreen.left.toFloat(),
|
||||
other.boundsOnScreen.left.toFloat(), amount).toInt()
|
||||
val top = MathUtils.lerp(boundsOnScreen.top.toFloat(),
|
||||
other.boundsOnScreen.top.toFloat(), amount).toInt()
|
||||
val right = MathUtils.lerp(boundsOnScreen.right.toFloat(),
|
||||
other.boundsOnScreen.right.toFloat(), amount).toInt()
|
||||
val bottom = MathUtils.lerp(boundsOnScreen.bottom.toFloat(),
|
||||
other.boundsOnScreen.bottom.toFloat(), amount).toInt()
|
||||
result.boundsOnScreen.set(left, top, right, bottom)
|
||||
result.showsOnlyActiveMedia = other.showsOnlyActiveMedia || showsOnlyActiveMedia
|
||||
if (amount > 0.0f) {
|
||||
if (other is MediaHostState) {
|
||||
result.measurementInput = other.measurementInput
|
||||
}
|
||||
} else {
|
||||
result.measurementInput
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (!(other is MediaHostState)) {
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 showsOnlyActiveMedia: Boolean
|
||||
val boundsOnScreen: Rect
|
||||
fun copy(): MediaState
|
||||
fun interpolate(other: MediaState, amount: Float): MediaState
|
||||
fun getMeasuringInput(input: MeasurementInput): MediaMeasurementInput
|
||||
}
|
||||
/**
|
||||
* The measurement input for a Media View
|
||||
*/
|
||||
data class MediaMeasurementInput(
|
||||
private val viewInput: MeasurementInput,
|
||||
val expansion: Float
|
||||
) : MeasurementInput by viewInput {
|
||||
|
||||
override fun sameAs(input: MeasurementInput?): Boolean {
|
||||
if (!(input is MediaMeasurementInput)) {
|
||||
return false
|
||||
}
|
||||
return width == input.width && expansion == input.expansion
|
||||
}
|
||||
/**
|
||||
* Is this host only showing active media or is it showing all of them including resumption?
|
||||
*/
|
||||
var showsOnlyActiveMedia: Boolean
|
||||
|
||||
/**
|
||||
* Get a copy of this view state, deepcopying all appropriate members
|
||||
*/
|
||||
fun copy(): MediaHostState
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ import com.android.systemui.dagger.qualifiers.Main
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.qs.PageIndicator
|
||||
import com.android.systemui.statusbar.notification.VisualStabilityManager
|
||||
import com.android.systemui.util.animation.MeasurementOutput
|
||||
import com.android.systemui.util.animation.UniqueObjectHostView
|
||||
import com.android.systemui.util.animation.requiresRemeasuring
|
||||
import com.android.systemui.util.concurrency.DelayableExecutor
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
@@ -36,16 +36,51 @@ class MediaViewManager @Inject constructor(
|
||||
@Background private val backgroundExecutor: DelayableExecutor,
|
||||
private val visualStabilityManager: VisualStabilityManager,
|
||||
private val activityStarter: ActivityStarter,
|
||||
private val mediaHostStatesManager: MediaHostStatesManager,
|
||||
mediaManager: MediaDataCombineLatest
|
||||
) {
|
||||
private var playerWidth: Int = 0
|
||||
|
||||
/**
|
||||
* The desired location where we'll be at the end of the transformation. Usually this matches
|
||||
* the end location, except when we're still waiting on a state update call.
|
||||
*/
|
||||
@MediaLocation
|
||||
private var desiredLocation: Int = -1
|
||||
|
||||
/**
|
||||
* The ending location of the view where it ends when all animations and transitions have
|
||||
* finished
|
||||
*/
|
||||
@MediaLocation
|
||||
private var currentEndLocation: Int = -1
|
||||
|
||||
/**
|
||||
* The ending location of the view where it ends when all animations and transitions have
|
||||
* finished
|
||||
*/
|
||||
@MediaLocation
|
||||
private var currentStartLocation: Int = -1
|
||||
|
||||
/**
|
||||
* The progress of the transition or 1.0 if there is no transition happening
|
||||
*/
|
||||
private var currentTransitionProgress: Float = 1.0f
|
||||
|
||||
/**
|
||||
* The measured width of the carousel
|
||||
*/
|
||||
private var carouselMeasureWidth: Int = 0
|
||||
|
||||
/**
|
||||
* The measured height of the carousel
|
||||
*/
|
||||
private var carouselMeasureHeight: Int = 0
|
||||
private var playerWidthPlusPadding: Int = 0
|
||||
private var desiredState: MediaHost.MediaHostState? = null
|
||||
private var currentState: MediaState? = null
|
||||
private var desiredHostState: MediaHostState? = null
|
||||
private val mediaCarousel: HorizontalScrollView
|
||||
val mediaFrame: ViewGroup
|
||||
val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
|
||||
private val mediaContent: ViewGroup
|
||||
private val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf()
|
||||
private val pageIndicator: PageIndicator
|
||||
private val gestureDetector: GestureDetectorCompat
|
||||
private val visualStabilityCallback: VisualStabilityManager.Callback
|
||||
@@ -115,6 +150,7 @@ class MediaViewManager @Inject constructor(
|
||||
override fun onMediaDataLoaded(key: String, data: MediaData) {
|
||||
updateView(key, data)
|
||||
updatePlayerVisibilities()
|
||||
mediaCarousel.requiresRemeasuring = true
|
||||
}
|
||||
|
||||
override fun onMediaDataRemoved(key: String) {
|
||||
@@ -137,6 +173,13 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
})
|
||||
mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback {
|
||||
override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) {
|
||||
if (location == desiredLocation) {
|
||||
onDesiredLocationChanged(desiredLocation, mediaHostState, animate = false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun inflateMediaCarousel(): ViewGroup {
|
||||
@@ -220,7 +263,7 @@ class MediaViewManager @Inject constructor(
|
||||
var existingPlayer = mediaPlayers[key]
|
||||
if (existingPlayer == null) {
|
||||
existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
|
||||
activityStarter)
|
||||
activityStarter, mediaHostStatesManager)
|
||||
existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
|
||||
mediaContent))
|
||||
mediaPlayers[key] = existingPlayer
|
||||
@@ -228,14 +271,14 @@ class MediaViewManager @Inject constructor(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
existingPlayer.view?.player?.setLayoutParams(lp)
|
||||
existingPlayer.setListening(currentlyExpanded)
|
||||
updatePlayerToState(existingPlayer, noAnimation = true)
|
||||
if (existingPlayer.isPlaying) {
|
||||
mediaContent.addView(existingPlayer.view?.player, 0)
|
||||
} else {
|
||||
mediaContent.addView(existingPlayer.view?.player)
|
||||
}
|
||||
updatePlayerToCurrentState(existingPlayer)
|
||||
} else if (existingPlayer.isPlaying &&
|
||||
mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
|
||||
mediaContent.indexOfChild(existingPlayer.view?.player) != 0) {
|
||||
if (visualStabilityManager.isReorderingAllowed) {
|
||||
mediaContent.removeView(existingPlayer.view?.player)
|
||||
mediaContent.addView(existingPlayer.view?.player, 0)
|
||||
@@ -244,20 +287,10 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
existingPlayer.bind(data)
|
||||
// Resetting the progress to make sure it's taken into account for the latest
|
||||
// motion model
|
||||
existingPlayer.view?.player?.progress = currentState?.expansion ?: 0.0f
|
||||
updateMediaPaddings()
|
||||
updatePageIndicator()
|
||||
}
|
||||
|
||||
private fun updatePlayerToCurrentState(existingPlayer: MediaControlPanel) {
|
||||
if (desiredState != null && desiredState!!.measurementInput != null) {
|
||||
// make sure the player width is set to the current state
|
||||
existingPlayer.setPlayerWidth(playerWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMediaPaddings() {
|
||||
val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding)
|
||||
val childCount = mediaContent.childCount
|
||||
@@ -281,117 +314,101 @@ class MediaViewManager @Inject constructor(
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current state of a view. This is updated often during animations and we shouldn't
|
||||
* do anything expensive.
|
||||
* Set a new interpolated state for all players. This is a state that is usually controlled
|
||||
* by a finger movement where the user drags from one state to the next.
|
||||
*/
|
||||
fun setCurrentState(state: MediaState) {
|
||||
currentState = state
|
||||
currentlyExpanded = state.expansion > 0
|
||||
fun setCurrentState(
|
||||
@MediaLocation startLocation: Int,
|
||||
@MediaLocation endLocation: Int,
|
||||
progress: Float,
|
||||
immediately: Boolean
|
||||
) {
|
||||
// Hack: Since the indicator doesn't move with the player expansion, just make it disappear
|
||||
// and then reappear at the end.
|
||||
pageIndicator.alpha = if (state.expansion == 1f || state.expansion == 0f) 1f else 0f
|
||||
for (mediaPlayer in mediaPlayers.values) {
|
||||
val view = mediaPlayer.view?.player
|
||||
view?.progress = state.expansion
|
||||
pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f
|
||||
if (startLocation != currentStartLocation ||
|
||||
endLocation != currentEndLocation ||
|
||||
progress != currentTransitionProgress ||
|
||||
immediately
|
||||
) {
|
||||
currentStartLocation = startLocation
|
||||
currentEndLocation = endLocation
|
||||
currentTransitionProgress = progress
|
||||
for (mediaPlayer in mediaPlayers.values) {
|
||||
updatePlayerToState(mediaPlayer, immediately)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePlayerToState(mediaPlayer: MediaControlPanel, noAnimation: Boolean) {
|
||||
mediaPlayer.mediaViewController.setCurrentState(
|
||||
startLocation = currentStartLocation,
|
||||
endLocation = currentEndLocation,
|
||||
transitionProgress = currentTransitionProgress,
|
||||
applyImmediately = noAnimation)
|
||||
}
|
||||
|
||||
/**
|
||||
* The desired location of this view has changed. We should remeasure the view to match
|
||||
* the new bounds and kick off bounds animations if necessary.
|
||||
* If an animation is happening, an animation is kicked of externally, which sets a new
|
||||
* current state until we reach the targetState.
|
||||
*
|
||||
* @param desiredState the target state we're transitioning to
|
||||
* @param desiredLocation the location we're going to
|
||||
* @param desiredHostState the target state we're transitioning to
|
||||
* @param animate should this be animated
|
||||
*/
|
||||
fun onDesiredLocationChanged(
|
||||
desiredState: MediaState?,
|
||||
desiredLocation: Int,
|
||||
desiredHostState: MediaHostState?,
|
||||
animate: Boolean,
|
||||
duration: Long,
|
||||
startDelay: Long
|
||||
duration: Long = 200,
|
||||
startDelay: Long = 0
|
||||
) {
|
||||
if (desiredState is MediaHost.MediaHostState) {
|
||||
desiredHostState?.let {
|
||||
// This is a hosting view, let's remeasure our players
|
||||
this.desiredState = desiredState
|
||||
val width = desiredState.boundsOnScreen.width()
|
||||
if (playerWidth != width) {
|
||||
setPlayerWidth(width)
|
||||
for (mediaPlayer in mediaPlayers.values) {
|
||||
if (animate && mediaPlayer.view?.player?.visibility == View.VISIBLE) {
|
||||
mediaPlayer.animatePendingSizeChange(duration, startDelay)
|
||||
}
|
||||
}
|
||||
val widthSpec = desiredState.measurementInput?.widthMeasureSpec ?: 0
|
||||
val heightSpec = desiredState.measurementInput?.heightMeasureSpec ?: 0
|
||||
var left = 0
|
||||
for (i in 0 until mediaContent.childCount) {
|
||||
val view = mediaContent.getChildAt(i)
|
||||
view.measure(widthSpec, heightSpec)
|
||||
view.layout(left, 0, left + width, view.measuredHeight)
|
||||
left = left + playerWidthPlusPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setPlayerWidth(width: Int) {
|
||||
if (width != playerWidth) {
|
||||
playerWidth = width
|
||||
playerWidthPlusPadding = playerWidth + context.resources.getDimensionPixelSize(
|
||||
R.dimen.qs_media_padding)
|
||||
this.desiredLocation = desiredLocation
|
||||
this.desiredHostState = it
|
||||
currentlyExpanded = it.expansion > 0
|
||||
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
|
||||
// it's still at the same place
|
||||
var newScroll = activeMediaIndex * playerWidthPlusPadding
|
||||
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
|
||||
newScroll += playerWidthPlusPadding
|
||||
- (scrollIntoCurrentMedia - playerWidthPlusPadding)
|
||||
} else {
|
||||
newScroll += scrollIntoCurrentMedia
|
||||
}
|
||||
mediaCarousel.scrollX = newScroll
|
||||
updateCarouselSize()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a measurement for the given input state. This measures the first player and returns
|
||||
* its bounds as if it were measured with the given measurement dimensions
|
||||
* Update the size of the carousel, remeasuring it if necessary.
|
||||
*/
|
||||
fun obtainMeasurement(input: MediaMeasurementInput): MeasurementOutput? {
|
||||
val firstPlayer = mediaPlayers.values.firstOrNull() ?: return null
|
||||
var result: MeasurementOutput? = null
|
||||
firstPlayer.view?.player?.let {
|
||||
// Let's measure the size of the first player and return its height
|
||||
val previousProgress = it.progress
|
||||
val previousRight = it.right
|
||||
val previousBottom = it.bottom
|
||||
it.progress = input.expansion
|
||||
firstPlayer.measure(input)
|
||||
// Relayouting is necessary in motionlayout to obtain its size properly ....
|
||||
it.layout(0, 0, it.measuredWidth, it.measuredHeight)
|
||||
result = MeasurementOutput(it.measuredWidth, it.measuredHeight)
|
||||
it.progress = previousProgress
|
||||
if (desiredState != null) {
|
||||
// remeasure it to the old size again!
|
||||
firstPlayer.measure(desiredState!!.measurementInput)
|
||||
it.layout(0, 0, previousRight, previousBottom)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun onViewReattached() {
|
||||
if (desiredState is MediaHost.MediaHostState) {
|
||||
// HACK: MotionLayout doesn't always properly reevalate the state, let's kick of
|
||||
// a measure to force it.
|
||||
val widthSpec = desiredState!!.measurementInput?.widthMeasureSpec ?: 0
|
||||
val heightSpec = desiredState!!.measurementInput?.heightMeasureSpec ?: 0
|
||||
for (mediaPlayer in mediaPlayers.values) {
|
||||
mediaPlayer.view?.player?.measure(widthSpec, heightSpec)
|
||||
private fun updateCarouselSize() {
|
||||
val width = desiredHostState?.measurementInput?.width ?: 0
|
||||
val height = desiredHostState?.measurementInput?.height ?: 0
|
||||
if (width != carouselMeasureWidth && width != 0 ||
|
||||
height != carouselMeasureWidth && height != 0) {
|
||||
carouselMeasureWidth = width
|
||||
carouselMeasureHeight = height
|
||||
playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize(
|
||||
R.dimen.qs_media_padding)
|
||||
// The player width has changed, let's update the scroll position to make sure
|
||||
// it's still at the same place
|
||||
var newScroll = activeMediaIndex * playerWidthPlusPadding
|
||||
if (scrollIntoCurrentMedia > playerWidthPlusPadding) {
|
||||
newScroll += playerWidthPlusPadding -
|
||||
(scrollIntoCurrentMedia - playerWidthPlusPadding)
|
||||
} else {
|
||||
newScroll += scrollIntoCurrentMedia
|
||||
}
|
||||
mediaCarousel.scrollX = newScroll
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,18 +23,15 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.util.animation.TransitionLayout
|
||||
|
||||
/**
|
||||
* ViewHolder for a media player.
|
||||
*/
|
||||
class PlayerViewHolder private constructor(itemView: View) {
|
||||
|
||||
val player = itemView as MotionLayout
|
||||
val background = itemView.requireViewById<View>(R.id.media_background)
|
||||
val player = itemView as TransitionLayout
|
||||
|
||||
// Player information
|
||||
val appIcon = itemView.requireViewById<ImageView>(R.id.icon)
|
||||
@@ -61,7 +58,7 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
|
||||
|
||||
init {
|
||||
(background.background as IlluminationDrawable).let {
|
||||
(player.background as IlluminationDrawable).let {
|
||||
it.setupTouch(seamless, player)
|
||||
it.setupTouch(action0, player)
|
||||
it.setupTouch(action1, player)
|
||||
@@ -95,7 +92,7 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
* @param parent Parent of inflated view.
|
||||
*/
|
||||
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup): PlayerViewHolder {
|
||||
val v = inflater.inflate(R.layout.qs_media_panel, parent, false)
|
||||
val v = inflater.inflate(R.layout.media_view, parent, false)
|
||||
return PlayerViewHolder(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
|
||||
(ViewGroup.MarginLayoutParams) hostView.getLayoutParams();
|
||||
float targetPosition = absoluteBottomPosition - params.bottomMargin
|
||||
- hostView.getHeight();
|
||||
float currentPosition = mediaHost.getCurrentState().getBoundsOnScreen().top
|
||||
float currentPosition = mediaHost.getCurrentBounds().top
|
||||
- hostView.getTranslationY();
|
||||
hostView.setTranslationY(targetPosition - currentPosition);
|
||||
} else {
|
||||
|
||||
@@ -187,9 +187,9 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne
|
||||
}
|
||||
|
||||
protected void addMediaHostView() {
|
||||
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
|
||||
mMediaHost.setExpansion(1.0f);
|
||||
mMediaHost.setShowsOnlyActiveMedia(false);
|
||||
mMediaHost.init(MediaHierarchyManager.LOCATION_QS);
|
||||
ViewGroup hostView = mMediaHost.getHostView();
|
||||
addView(hostView);
|
||||
int sidePaddings = getResources().getDimensionPixelSize(
|
||||
|
||||
@@ -190,9 +190,9 @@ public class QuickQSPanel extends QSPanel {
|
||||
switchTileLayout();
|
||||
return null;
|
||||
});
|
||||
mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
|
||||
mMediaHost.setExpansion(0.0f);
|
||||
mMediaHost.setShowsOnlyActiveMedia(true);
|
||||
mMediaHost.init(MediaHierarchyManager.LOCATION_QQS);
|
||||
reAttachMediaHost();
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ public class CrossFadeHelper {
|
||||
}
|
||||
|
||||
public static void fadeIn(View view, float fadeInAmount) {
|
||||
fadeIn(view, fadeInAmount, true /* remap */);
|
||||
fadeIn(view, fadeInAmount, false /* remap */);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,7 +66,7 @@ public abstract class CustomInterpolatorTransformation
|
||||
return false;
|
||||
}
|
||||
View view = ownState.getTransformedView();
|
||||
CrossFadeHelper.fadeIn(view, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(view, transformationAmount, true /* remap */);
|
||||
ownState.transformViewFullyFrom(otherState, this, transformationAmount);
|
||||
otherState.recycle();
|
||||
return true;
|
||||
|
||||
@@ -95,7 +95,7 @@ public class TransformState {
|
||||
if (sameAs(otherState)) {
|
||||
ensureVisible();
|
||||
} else {
|
||||
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
|
||||
}
|
||||
transformViewFullyFrom(otherState, transformationAmount);
|
||||
}
|
||||
@@ -424,7 +424,7 @@ public class TransformState {
|
||||
if (transformationAmount == 0.0f) {
|
||||
prepareFadeIn();
|
||||
}
|
||||
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(mTransformedView, transformationAmount, true /* remap */);
|
||||
}
|
||||
|
||||
public void disappear(float transformationAmount, TransformableView otherView) {
|
||||
|
||||
@@ -92,7 +92,7 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout
|
||||
// We want to transform from the same y location as the title
|
||||
TransformState otherState = notification.getCurrentState(
|
||||
TRANSFORMING_VIEW_TITLE);
|
||||
CrossFadeHelper.fadeIn(mTextView, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(mTextView, transformationAmount, true /* remap */);
|
||||
if (otherState != null) {
|
||||
ownState.transformViewVerticalFrom(otherState, transformationAmount);
|
||||
otherState.recycle();
|
||||
|
||||
@@ -23,7 +23,6 @@ import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.ArraySet;
|
||||
@@ -107,7 +106,7 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
|
||||
TransformState otherState = notification.getCurrentState(
|
||||
TRANSFORMING_VIEW_TITLE);
|
||||
final View text = ownState.getTransformedView();
|
||||
CrossFadeHelper.fadeIn(text, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(text, transformationAmount, true /* remap */);
|
||||
if (otherState != null) {
|
||||
ownState.transformViewVerticalFrom(otherState, this,
|
||||
transformationAmount);
|
||||
|
||||
@@ -284,7 +284,7 @@ public abstract class NotificationViewWrapper implements TransformableView {
|
||||
|
||||
@Override
|
||||
public void transformFrom(TransformableView notification, float transformationAmount) {
|
||||
CrossFadeHelper.fadeIn(mView, transformationAmount);
|
||||
CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ package com.android.systemui.util.animation
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import com.android.systemui.R
|
||||
|
||||
/**
|
||||
* A special view that is designed to host a single "unique object". The unique object is
|
||||
@@ -34,8 +36,7 @@ import android.widget.FrameLayout
|
||||
class UniqueObjectHostView(
|
||||
context: Context
|
||||
) : FrameLayout(context) {
|
||||
lateinit var measurementCache : GuaranteedMeasurementCache
|
||||
var onMeasureListener: ((MeasurementInput) -> Unit)? = null
|
||||
lateinit var measurementManager: MeasurementManager
|
||||
|
||||
@SuppressLint("DrawAllocation")
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
@@ -45,64 +46,63 @@ class UniqueObjectHostView(
|
||||
val widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.getMode(widthMeasureSpec))
|
||||
val height = MeasureSpec.getSize(heightMeasureSpec) - paddingVertical
|
||||
val heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
|
||||
val measurementInput = MeasurementInputData(widthSpec, heightSpec)
|
||||
onMeasureListener?.apply {
|
||||
invoke(measurementInput)
|
||||
}
|
||||
val measurementInput = MeasurementInput(widthSpec, heightSpec)
|
||||
|
||||
// Let's make sure the measurementManager knows about our size, to ensure that we have
|
||||
// a value available. This might perform a measure internally if we don't have a cached
|
||||
// size.
|
||||
val (cachedWidth, cachedHeight) = measurementManager.onMeasure(measurementInput)
|
||||
|
||||
if (!isCurrentHost()) {
|
||||
// We're not currently the host, let's get the dimension from our cache (this might
|
||||
// perform a measuring if the cache doesn't have it yet)
|
||||
// We're not currently the host, let's use the dimension from our cache
|
||||
// The goal here is that the view will always have a consistent measuring, regardless
|
||||
// if it's attached or not.
|
||||
// The behavior is therefore very similar to the view being persistently attached to
|
||||
// this host, which can prevent flickers. It also makes sure that we always know
|
||||
// the size of the view during transitions even if it has never been attached here
|
||||
// before.
|
||||
val (cachedWidth, cachedHeight) = measurementCache.obtainMeasurement(measurementInput)
|
||||
setMeasuredDimension(cachedWidth + paddingHorizontal, cachedHeight + paddingVertical)
|
||||
} else {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
// Let's update our cache
|
||||
val child = getChildAt(0)!!
|
||||
val output = MeasurementOutput(child.measuredWidth, child.measuredHeight)
|
||||
measurementCache.putMeasurement(measurementInput, output)
|
||||
getChildAt(0)?.requiresRemeasuring = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
||||
if (child?.measuredWidth == 0 || measuredWidth == 0 || child?.requiresRemeasuring == true) {
|
||||
super.addView(child, index, params)
|
||||
return
|
||||
}
|
||||
// Suppress layouts when adding a view. The view should already be laid out with the
|
||||
// right size when being attached to this view
|
||||
invalidate()
|
||||
addViewInLayout(child, index, params, true /* preventRequestLayout */)
|
||||
val left = paddingLeft
|
||||
val top = paddingTop
|
||||
val paddingHorizontal = paddingStart + paddingEnd
|
||||
val paddingVertical = paddingTop + paddingBottom
|
||||
child!!.layout(left,
|
||||
top,
|
||||
left + measuredWidth - paddingHorizontal,
|
||||
top + measuredHeight - paddingVertical)
|
||||
}
|
||||
|
||||
private fun isCurrentHost() = childCount != 0
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic view measurement input
|
||||
*/
|
||||
interface MeasurementInput {
|
||||
fun sameAs(input: MeasurementInput?): Boolean {
|
||||
return equals(input)
|
||||
interface MeasurementManager {
|
||||
fun onMeasure(input: MeasurementInput): MeasurementOutput
|
||||
}
|
||||
val width : Int
|
||||
get() {
|
||||
return View.MeasureSpec.getSize(widthMeasureSpec)
|
||||
}
|
||||
val height : Int
|
||||
get() {
|
||||
return View.MeasureSpec.getSize(heightMeasureSpec)
|
||||
}
|
||||
var widthMeasureSpec: Int
|
||||
var heightMeasureSpec: Int
|
||||
}
|
||||
|
||||
/**
|
||||
* The output of a view measurement
|
||||
* Does this view require remeasuring currently outside of the regular measure flow?
|
||||
*/
|
||||
data class MeasurementOutput(
|
||||
val measuredWidth: Int,
|
||||
val measuredHeight: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* The data object holding a basic view measurement input
|
||||
*/
|
||||
data class MeasurementInputData(
|
||||
override var widthMeasureSpec: Int,
|
||||
override var heightMeasureSpec: Int
|
||||
) : MeasurementInput
|
||||
var View.requiresRemeasuring: Boolean
|
||||
get() {
|
||||
val required = getTag(R.id.requires_remeasuring)
|
||||
return required?.equals(true) ?: false
|
||||
}
|
||||
set(value) {
|
||||
setTag(R.id.requires_remeasuring, value)
|
||||
}
|
||||
|
||||
@@ -31,30 +31,24 @@ import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.constraintlayout.motion.widget.MotionScene
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.test.filters.SmallTest
|
||||
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.util.animation.TransitionLayout
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when` as whenever
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
private const val KEY = "TEST_KEY"
|
||||
private const val APP = "APP"
|
||||
private const val BG_COLOR = Color.RED
|
||||
@@ -78,8 +72,8 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
|
||||
@Mock private lateinit var holder: PlayerViewHolder
|
||||
@Mock private lateinit var motion: MotionLayout
|
||||
private lateinit var background: TextView
|
||||
@Mock private lateinit var view: TransitionLayout
|
||||
@Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
|
||||
private lateinit var appIcon: ImageView
|
||||
private lateinit var appName: TextView
|
||||
private lateinit var albumView: ImageView
|
||||
@@ -107,21 +101,15 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
bgExecutor = FakeExecutor(FakeSystemClock())
|
||||
|
||||
activityStarter = mock(ActivityStarter::class.java)
|
||||
mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
|
||||
|
||||
player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter)
|
||||
player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
|
||||
mediaHostStatesManager)
|
||||
|
||||
// Mock out a view holder for the player to attach to.
|
||||
holder = mock(PlayerViewHolder::class.java)
|
||||
motion = mock(MotionLayout::class.java)
|
||||
val trans: ArrayList<MotionScene.Transition> = ArrayList()
|
||||
trans.add(mock(MotionScene.Transition::class.java))
|
||||
whenever(motion.definedTransitions).thenReturn(trans)
|
||||
val constraintSet = mock(ConstraintSet::class.java)
|
||||
whenever(motion.getConstraintSet(R.id.expanded)).thenReturn(constraintSet)
|
||||
whenever(motion.getConstraintSet(R.id.collapsed)).thenReturn(constraintSet)
|
||||
whenever(holder.player).thenReturn(motion)
|
||||
background = TextView(context)
|
||||
whenever(holder.background).thenReturn(background)
|
||||
view = mock(TransitionLayout::class.java)
|
||||
whenever(holder.player).thenReturn(view)
|
||||
appIcon = ImageView(context)
|
||||
whenever(holder.appIcon).thenReturn(appIcon)
|
||||
appName = TextView(context)
|
||||
@@ -205,7 +193,9 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device)
|
||||
player.bind(state)
|
||||
assertThat(background.getBackgroundTintList()).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
||||
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
||||
verify(view).setBackgroundTintList(list.capture())
|
||||
assertThat(list.value).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,6 @@ class PlayerViewHolderTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun backgroundIsIlluminationDrawable() {
|
||||
val holder = PlayerViewHolder.create(inflater, parent)
|
||||
assertThat(holder.background.background as IlluminationDrawable).isNotNull()
|
||||
assertThat(holder.player.background as IlluminationDrawable).isNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user