resolve merge conflicts of 46378eb881ab96b90a7a2922775ecceb184456c7 to sc-v2-dev
Folder icon polish.
- Increase the overlap between the preview items and the background
so that it appears more intentional (lots of reports of users
thinking that its a bug)
- Fix bug where clip path was still clipping icons when
dragging and swiping to home.
Bug: 194339057
Bug: 193701705
Bug: 191225536
Test: test 2 icon, 3 icon, and 4 icon folders
close folder on 2nd page, ensure clipping
ensure no preview items are clipped in drag view
ensure no preview items are clipped in swipe up to home
test 2x2, 3x3, 4x4, 4x5, and 5x5 grids to ensure no clipping
of folder icons
drag and drop folder in and out of hotseat
create folders (2 icon, 3 icon, 4 icon)
Change-Id: I09ce9ef59e340b1009de3d26eed430a0a8dc4b33
Merged-In: Id49bae33438e10248bef05450d7e2c92c2140748
diff --git a/Android.bp b/Android.bp
index 1b6ffe4..45d022f 100644
--- a/Android.bp
+++ b/Android.bp
@@ -112,6 +112,7 @@
"androidx.preference_preference",
"androidx.slice_slice-view",
"androidx.cardview_cardview",
+ "com.google.android.material_material",
"iconloader_base",
],
manifest: "AndroidManifest-common.xml",
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index d725a16..4eecf29 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -144,7 +144,7 @@
<activity
android:name="com.android.launcher3.settings.SettingsActivity"
android:label="@string/settings_button_text"
- android:theme="@style/HomeSettingsTheme"
+ android:theme="@style/HomeSettings.Theme"
android:exported="true"
android:autoRemoveFromRecents="true">
<intent-filter>
diff --git a/lint-baseline-launcher3.xml b/lint-baseline-launcher3.xml
index 9a68405..94345a6 100644
--- a/lint-baseline-launcher3.xml
+++ b/lint-baseline-launcher3.xml
@@ -576,12 +576,12 @@
<issue
id="NewApi"
message="Call requires API level 31 (current min is 26): `android.appwidget.AppWidgetHostView#setColorResources`"
- errorLine1=" view.setColorResources(mWallpaperColorResources);"
- errorLine2=" ~~~~~~~~~~~~~~~~~">
+ errorLine1=" setColorResources(mWallpaperColorResources);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
- line="381"
- column="18"/>
+ line="528"
+ column="17"/>
</issue>
<issue
@@ -591,7 +591,7 @@
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="packages/apps/Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java"
- line="270"
+ line="288"
column="61"/>
</issue>
diff --git a/quickstep/res/drawable/ic_sysbar_accessibility_button.xml b/quickstep/res/drawable/ic_sysbar_accessibility_button.xml
new file mode 100644
index 0000000..e0d5406
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_accessibility_button.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="21dp"
+ android:height="21dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M20.5,6c-2.61,0.7 -5.67,1 -8.5,1S6.11,6.7 3.5,6L3,8c1.86,0.5 4,0.83 6,1v13h2v-6h2v6h2V9c2,-0.17 4.14,-0.5 6,-1L20.5,6zM12,6c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2s-2,0.9 -2,2S10.9,6 12,6z"
+ android:fillColor="@android:color/white"
+ />
+</vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml
new file mode 100644
index 0000000..ff5cb9e
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_0.xml
@@ -0,0 +1,187 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="-90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml
new file mode 100644
index 0000000..90fedb1
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_ccw_start_90.xml
@@ -0,0 +1,187 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml
new file mode 100644
index 0000000..a89e7a3
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_0.xml
@@ -0,0 +1,187 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="-1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="0"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="0"
+ android:valueTo="90">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml
new file mode 100644
index 0000000..0dc67b0
--- /dev/null
+++ b/quickstep/res/drawable/ic_sysbar_rotate_button_cw_start_90.xml
@@ -0,0 +1,187 @@
+<?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.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector android:name="root"
+ android:width="28dp"
+ android:height="28dp"
+ android:viewportWidth="28.0"
+ android:viewportHeight="28.0">
+ <!-- Use scaleX to flip icon so arrows always point in the direction of motion -->
+ <group android:name="icon" android:pivotX="14" android:pivotY="14"
+ android:scaleX="-1">
+ <!-- Tint color to be set directly -->
+ <path android:fillColor="#FFFFFFFF"
+ android:pathData="M12.02,10.83L9.25,8.06l2.77,-2.77l1.12,1.12l-0.85,0.86h5.16c0.72,0 1.31,0.56 1.31,1.26v9.16l-1.58,-1.58V8.85h-4.89l0.86,0.86L12.02,10.83zM15.98,17.17l-1.12,1.12l0.85,0.86h-4.88v-7.26L9.25,10.3v9.17c0,0.7 0.59,1.26 1.31,1.26h5.16v0.01l-0.85,0.85l1.12,1.12l2.77,-2.77L15.98,17.17z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- Repeat all animations 5 times but don't fade out at the end -->
+ <target android:name="root">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ <!-- Linear fade out -->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="1700"
+ android:valueFrom="1"
+ android:valueTo="0"
+ android:interpolator="@android:anim/linear_interpolator"/>
+ <!-- Linear fade in-->
+ <objectAnimator android:propertyName="alpha"
+ android:duration="100"
+ android:startOffset="100"
+ android:valueFrom="0"
+ android:valueTo="1"
+ android:interpolator="@android:anim/linear_interpolator" />
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="icon">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="100"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+
+ <!-- Reset rotation position for fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:startOffset="1300"
+ android:duration="100"
+ android:valueFrom="90"
+ android:valueTo="90"/>
+
+ <!-- Icon rotation with start timing offset after fade in -->
+ <objectAnimator android:propertyName="rotation"
+ android:duration="600"
+ android:valueFrom="90"
+ android:valueTo="180">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.408,1.181 0.674,1.08 1.0,1.0"/>
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index e680233..c0e0862 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -13,12 +13,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<com.android.launcher3.taskbar.TaskbarDragLayer
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/taskbar_container"
android:layout_width="wrap_content"
- android:layout_height="wrap_content">
+ android:layout_height="wrap_content"
+ android:clipChildren="false">
<com.android.launcher3.taskbar.TaskbarView
android:id="@+id/taskbar_view"
@@ -26,30 +27,52 @@
android:layout_height="wrap_content"
android:gravity="center"
android:forceHasOverlappingRendering="false"
+ android:layout_gravity="bottom"
+ android:clipChildren="false" />
+
+ <FrameLayout
+ android:id="@+id/navbuttons_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
android:layout_gravity="bottom" >
- <LinearLayout
- android:id="@+id/system_button_layout"
+ <FrameLayout
+ android:id="@+id/start_contextual_buttons"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
- android:forceHasOverlappingRendering="false"
- android:gravity="center" />
+ android:gravity="center_vertical"
+ android:layout_gravity="start"/>
<LinearLayout
- android:id="@+id/hotseat_icons_layout"
+ android:id="@+id/end_nav_buttons"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:forceHasOverlappingRendering="false"
- android:gravity="center" />
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
+ android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
+ android:gravity="center_vertical"
+ android:layout_gravity="end"/>
- </com.android.launcher3.taskbar.TaskbarView>
+ <FrameLayout
+ android:id="@+id/end_contextual_buttons"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/taskbar_nav_buttons_spacing"
+ android:paddingRight="@dimen/taskbar_nav_buttons_spacing"
+ android:gravity="center_vertical"
+ android:layout_gravity="end"/>
+ </FrameLayout>
- <com.android.launcher3.taskbar.ImeBarView
- android:id="@+id/ime_bar_view"
- android:layout_width="wrap_content"
+ <View
+ android:id="@+id/stashed_handle"
+ tools:comment1="The actual size and shape will be set as a ViewOutlineProvider at runtime"
+ android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:visibility="gone"/>
+ tools:comment2="TODO: Tint dynamically"
+ android:background="?android:attr/textColorPrimary"
+ android:clipToOutline="true"
+ android:layout_gravity="bottom"/>
</com.android.launcher3.taskbar.TaskbarDragLayer>
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar_nav_button.xml b/quickstep/res/layout/taskbar_nav_button.xml
new file mode 100644
index 0000000..985f928
--- /dev/null
+++ b/quickstep/res/layout/taskbar_nav_button.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/taskbar_nav_buttons_size"
+ android:layout_height="@dimen/taskbar_nav_buttons_size"
+ android:background="@drawable/taskbar_icon_click_feedback_roundrect"
+ android:scaleType="center"/>
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 6cc64e0..f54070e 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -16,7 +16,7 @@
<resources>
<dimen name="task_thumbnail_icon_size">48dp</dimen>
- <dimen name="task_thumbnail_icon_size_grid">32dp</dimen>
+ <dimen name="task_thumbnail_icon_size_grid">40dp</dimen>
<!-- For screens without rounded corners -->
<dimen name="task_corner_radius_small">2dp</dimen>
<!-- For Launchers that want to override the default dialog corner radius -->
@@ -39,10 +39,10 @@
<dimen name="overview_actions_horizontal_margin">16dp</dimen>
<dimen name="overview_grid_top_margin">77dp</dimen>
- <dimen name="overview_grid_bottom_margin">90dp</dimen>
+ <dimen name="overview_grid_bottom_margin">70dp</dimen>
<dimen name="overview_grid_side_margin">54dp</dimen>
<dimen name="overview_grid_row_spacing">42dp</dimen>
- <dimen name="overview_grid_focus_vertical_margin">90dp</dimen>
+ <dimen name="overview_grid_focus_vertical_margin">40dp</dimen>
<dimen name="split_placeholder_size">110dp</dimen>
<!-- These speeds are in dp/s -->
@@ -70,6 +70,8 @@
<item name="content_scale" format="float" type="dimen">0.97</item>
<dimen name="closing_window_trans_y">115dp</dimen>
+ <dimen name="quick_switch_scaling_scroll_threshold">100dp</dimen>
+
<dimen name="recents_empty_message_text_size">16sp</dimen>
<dimen name="recents_empty_message_text_padding">16dp</dimen>
@@ -150,11 +152,12 @@
<!-- Taskbar -->
<dimen name="taskbar_size">60dp</dimen>
- <dimen name="taskbar_icon_size">44dp</dimen>
<dimen name="taskbar_icon_touch_size">48dp</dimen>
<dimen name="taskbar_icon_drag_icon_size">54dp</dimen>
- <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
- <dimen name="taskbar_icon_spacing">8dp</dimen>
<dimen name="taskbar_folder_margin">16dp</dimen>
<dimen name="taskbar_nav_buttons_spacing">16dp</dimen>
+ <dimen name="taskbar_nav_buttons_size">48dp</dimen>
+ <dimen name="taskbar_stashed_size">24dp</dimen>
+ <dimen name="taskbar_stashed_handle_width">220dp</dimen>
+ <dimen name="taskbar_stashed_handle_height">6dp</dimen>
</resources>
diff --git a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
index 7c97b93..1386ac0 100644
--- a/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
+++ b/quickstep/robolectric_tests/src/com/android/launcher3/model/WidgetsPredicationUpdateTaskTest.java
@@ -41,19 +41,11 @@
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.QuickstepModelDelegate.PredictorState;
-import com.android.launcher3.model.data.AppInfo;
-import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.shadows.ShadowDeviceFlag;
-import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.LauncherModelHelper;
-import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import org.junit.Before;
import org.junit.Test;
@@ -67,9 +59,6 @@
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
@@ -239,63 +228,8 @@
}
@Override
- public int getPageToBindSynchronously() {
- return 0;
+ public IntSet getPagesToBindSynchronously() {
+ return IntSet.wrap(0);
}
-
- @Override
- public void clearPendingBinds() { }
-
- @Override
- public void startBinding() { }
-
- @Override
- public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
-
- @Override
- public void bindScreens(IntArray orderedScreenIds) { }
-
- @Override
- public void finishFirstPageBind(ViewOnDrawExecutor executor) { }
-
- @Override
- public void finishBindingItems(int pageBoundFirst) { }
-
- @Override
- public void preAddApps() { }
-
- @Override
- public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
- ArrayList<ItemInfo> addAnimated) { }
-
- @Override
- public void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
-
- @Override
- public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
-
- @Override
- public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
-
- @Override
- public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
-
- @Override
- public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
-
- @Override
- public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
-
- @Override
- public void onPageBoundSynchronously(int page) { }
-
- @Override
- public void executeOnNextDraw(ViewOnDrawExecutor executor) { }
-
- @Override
- public void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
-
- @Override
- public void bindAllApplications(AppInfo[] apps, int flags) { }
}
}
diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
index 32e2f1a..ecd38b4 100644
--- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java
@@ -22,6 +22,7 @@
import static com.android.launcher3.LauncherState.NO_OFFSET;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
+import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_HOME_KEY;
@@ -30,12 +31,15 @@
import android.animation.ValueAnimator;
import android.app.ActivityOptions;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.CancellationSignal;
+import android.os.Handler;
import android.os.IBinder;
+import android.util.Log;
import android.view.View;
import android.window.SplashScreen;
@@ -56,6 +60,8 @@
import com.android.launcher3.taskbar.TaskbarStateHandler;
import com.android.launcher3.uioverrides.RecentsViewStateController;
import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.DisplayController;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ObjectWrapper;
import com.android.launcher3.util.UiThreadHelper;
import com.android.quickstep.RecentsModel;
@@ -85,6 +91,11 @@
public abstract class BaseQuickstepLauncher extends Launcher
implements NavigationModeChangeListener {
+ private static final long BACKOFF_MILLIS = 1000;
+
+ // Max backoff caps at 5 mins
+ private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
+
private DepthController mDepthController = new DepthController(this);
private QuickstepTransitionManager mAppTransitionManager;
@@ -104,12 +115,24 @@
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mTaskbarManager = ((TISBinder) iBinder).getTaskbarManager();
mTaskbarManager.setLauncher(BaseQuickstepLauncher.this);
+ Log.d(TAG, "TIS service connected");
+ resetServiceBindRetryState();
}
@Override
public void onServiceDisconnected(ComponentName componentName) { }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ Log.w(TAG, "TIS binding died");
+ internalBindToTIS();
+ }
};
+
+ private final Runnable mConnectionRunnable = this::internalBindToTIS;
+ private short mConnectionAttempts;
private final TaskbarStateHandler mTaskbarStateHandler = new TaskbarStateHandler(this);
+ private final Handler mHandler = new Handler();
// Will be updated when dragging from taskbar.
private @Nullable DragOptions mNextWorkspaceDragOptions = null;
@@ -128,11 +151,11 @@
SysUINavigationMode.INSTANCE.get(this).removeModeChangeListener(this);
-
unbindService(mTisBinderConnection);
if (mTaskbarManager != null) {
- mTaskbarManager.setLauncher(null);
+ mTaskbarManager.clearLauncher(this);
}
+ resetServiceBindRetryState();
super.onDestroy();
}
@@ -260,8 +283,33 @@
mAppTransitionManager = new QuickstepTransitionManager(this);
mAppTransitionManager.registerRemoteAnimations();
- bindService(new Intent(this, TouchInteractionService.class), mTisBinderConnection, 0);
+ internalBindToTIS();
+ }
+ /**
+ * Binds {@link #mTisBinderConnection} to {@link TouchInteractionService}. If the binding fails,
+ * attempts to retry via {@link #mConnectionRunnable}
+ */
+ private void internalBindToTIS() {
+ boolean bound = bindService(new Intent(this, TouchInteractionService.class),
+ mTisBinderConnection, 0);
+ if (bound) {
+ resetServiceBindRetryState();
+ return;
+ }
+
+ Log.w(TAG, "Retrying TIS Binder connection attempt: " + mConnectionAttempts);
+ final long timeoutMs = (long) Math.min(
+ Math.scalb(BACKOFF_MILLIS, mConnectionAttempts), MAX_BACKOFF_MILLIS);
+ mHandler.postDelayed(mConnectionRunnable, timeoutMs);
+ mConnectionAttempts++;
+ }
+
+ private void resetServiceBindRetryState() {
+ if (mHandler.hasCallbacks(mConnectionRunnable)) {
+ mHandler.removeCallbacks(mConnectionRunnable);
+ }
+ mConnectionAttempts = 0;
}
public void setTaskbarUIController(LauncherTaskbarUIController taskbarUIController) {
@@ -351,14 +399,6 @@
}
@Override
- public float getNormalTaskbarScale() {
- if (mTaskbarUIController != null) {
- return mTaskbarUIController.getTaskbarScaleOnHome();
- }
- return super.getNormalTaskbarScale();
- }
-
- @Override
public void onDragLayerHierarchyChanged() {
onLauncherStateOrFocusChanged();
}
@@ -412,8 +452,8 @@
}
@Override
- public void finishBindingItems(int pageBoundFirst) {
- super.finishBindingItems(pageBoundFirst);
+ public void finishBindingItems(IntSet pagesBoundFirst) {
+ super.finishBindingItems(pagesBoundFirst);
// Instantiate and initialize WellbeingModel now that its loading won't interfere with
// populating workspace.
// TODO: Find a better place for this
@@ -481,4 +521,15 @@
public void setHintUserWillBeActive() {
addActivityFlags(ACTIVITY_STATE_USER_WILL_BE_ACTIVE);
}
+
+ @Override
+ public void onDisplayInfoChanged(Context context, DisplayController.Info info, int flags) {
+ super.onDisplayInfoChanged(context, info, flags);
+ // When changing screens with live tile active, finish the recents animation to close
+ // overview as it should be an interim state
+ if ((flags & CHANGE_ACTIVE_SCREEN) != 0 && ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ RecentsView recentsView = getOverviewPanel();
+ recentsView.finishRecentsAnimation(/* toRecents= */ true, null);
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 5bb76d6..2da8a45 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -40,7 +40,6 @@
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
import static com.android.launcher3.statehandlers.DepthController.DEPTH;
import static com.android.launcher3.util.DisplayController.getSingleFrameMs;
-import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.quickstep.TaskViewUtils.findTaskViewToLaunch;
import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
@@ -1047,7 +1046,7 @@
mLauncherOpenTransition = RemoteAnimationAdapterCompat.buildRemoteTransition(
new LauncherAnimationRunner(mHandler, mWallpaperOpenTransitionRunner,
false /* startAtFrontOfQueue */));
- mLauncherOpenTransition.addHomeOpenCheck();
+ mLauncherOpenTransition.addHomeOpenCheck(mLauncher.getComponentName());
SystemUiProxy.INSTANCE.getNoCreate().registerRemoteTransition(mLauncherOpenTransition);
}
}
@@ -1088,7 +1087,15 @@
}
private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
- return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode);
+ for (RemoteAnimationTargetCompat target : targets) {
+ if (target.mode == mode && target.taskInfo != null
+ // Compare component name instead of task-id because transitions will promote
+ // the target up to the root task while getTaskId returns the leaf.
+ && target.taskInfo.topActivity.equals(mLauncher.getComponentName())) {
+ return true;
+ }
+ }
+ return false;
}
/**
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
index 14b0c5d..c7c2567 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatEduDialog.java
@@ -85,7 +85,7 @@
mSampleHotseat = findViewById(R.id.sample_prediction);
DeviceProfile grid = mActivityContext.getDeviceProfile();
- Rect padding = grid.getHotseatLayoutPadding();
+ Rect padding = grid.getHotseatLayoutPadding(getContext());
mSampleHotseat.getLayoutParams().height = grid.cellHeightPx;
mSampleHotseat.setGridSize(grid.numShownHotseatIcons, 1);
diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
index 85e5ab0..b40a1d5 100644
--- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
+++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java
@@ -262,10 +262,6 @@
} else {
removeOutlineDrawings();
}
-
- if (mLauncher.getTaskbarUIController() != null) {
- mLauncher.getTaskbarUIController().onHotseatUpdated();
- }
}
private void removeOutlineDrawings() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java b/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
deleted file mode 100644
index 540f748..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/ButtonProvider.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2021 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.launcher3.taskbar;
-
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
-import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
-
-import android.annotation.DrawableRes;
-import android.view.View;
-import android.widget.ImageView;
-
-import com.android.launcher3.R;
-import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
-
-/**
- * Creates Buttons for Taskbar for 3 button nav.
- * Can add animations and state management for buttons in this class as things progress.
- */
-public class ButtonProvider {
-
- private final int mMarginLeftRight;
- private final TaskbarActivityContext mContext;
-
- public ButtonProvider(TaskbarActivityContext context) {
- mContext = context;
- mMarginLeftRight = context.getResources()
- .getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
- }
-
- public View getBack() {
- // Back button
- return getButtonForDrawable(R.drawable.ic_sysbar_back, BUTTON_BACK);
- }
-
- public View getDown() {
- // Ime down button
- return getButtonForDrawable(R.drawable.ic_sysbar_back, BUTTON_BACK);
- }
-
- public View getHome() {
- // Home button
- return getButtonForDrawable(R.drawable.ic_sysbar_home, BUTTON_HOME);
- }
-
- public View getRecents() {
- // Recents button
- return getButtonForDrawable(R.drawable.ic_sysbar_recent, BUTTON_RECENTS);
- }
-
- public View getImeSwitcher() {
- // IME Switcher Button
- return getButtonForDrawable(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH);
- }
-
- private View getButtonForDrawable(@DrawableRes int drawableId, @TaskbarButton int buttonType) {
- ImageView buttonView = new ImageView(mContext);
- buttonView.setImageResource(drawableId);
- buttonView.setBackgroundResource(R.drawable.taskbar_icon_click_feedback_roundrect);
- buttonView.setPadding(mMarginLeftRight, 0, mMarginLeftRight, 0);
- buttonView.setOnClickListener(view -> mContext.onNavigationButtonClick(buttonType));
- return buttonView;
- }
-
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java b/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
deleted file mode 100644
index 287caab..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/ImeBarView.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * Copyright 2021 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.launcher3.taskbar;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.RelativeLayout;
-
-import com.android.launcher3.views.ActivityContext;
-
-public class ImeBarView extends RelativeLayout {
-
- private ButtonProvider mButtonProvider;
- private View mImeView;
-
- public ImeBarView(Context context) {
- this(context, null);
- }
-
- public ImeBarView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ImeBarView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- public void init(ButtonProvider buttonProvider) {
- mButtonProvider = buttonProvider;
-
- ActivityContext context = getActivityContext();
- RelativeLayout.LayoutParams imeParams = new RelativeLayout.LayoutParams(
- context.getDeviceProfile().iconSizePx,
- context.getDeviceProfile().iconSizePx
- );
- RelativeLayout.LayoutParams downParams = new RelativeLayout.LayoutParams(imeParams);
-
- imeParams.addRule(ALIGN_PARENT_END);
- imeParams.setMarginEnd(context.getDeviceProfile().iconSizePx);
- downParams.setMarginStart(context.getDeviceProfile().iconSizePx);
- downParams.addRule(ALIGN_PARENT_START);
-
- // Down Arrow
- View downView = mButtonProvider.getDown();
- downView.setLayoutParams(downParams);
- downView.setRotation(-90);
- addView(downView);
-
- // IME switcher button
- mImeView = mButtonProvider.getImeSwitcher();
- mImeView.setLayoutParams(imeParams);
- addView(mImeView);
- }
-
- public void setImeSwitcherVisibility(boolean show) {
- mImeView.setVisibility(show ? VISIBLE : GONE);
- }
-
- private <T extends Context & ActivityContext> T getActivityContext() {
- return ActivityContext.lookupContext(getContext());
- }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index c2d107c..b5c834d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -15,42 +15,58 @@
*/
package com.android.launcher3.taskbar;
+import static com.android.launcher3.LauncherState.HOTSEAT_ICONS;
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_HOME;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.view.MotionEvent;
-import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherState;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.states.StateAnimationConfig;
+import com.android.launcher3.anim.AnimatorListeners;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.RecentsAnimationCallbacks;
+import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
+import com.android.quickstep.RecentsAnimationController;
+import com.android.quickstep.SystemUiProxy;
+import com.android.systemui.shared.recents.model.ThumbnailData;
/**
* A data source which integrates with a Launcher instance
- * TODO: Rename to have Launcher prefix
*/
-
public class LauncherTaskbarUIController extends TaskbarUIController {
private final BaseQuickstepLauncher mLauncher;
private final TaskbarStateHandler mTaskbarStateHandler;
- private final TaskbarAnimationController mTaskbarAnimationController;
- private final TaskbarHotseatController mHotseatController;
private final TaskbarActivityContext mContext;
- final TaskbarDragLayer mTaskbarDragLayer;
- final TaskbarView mTaskbarView;
+ private final TaskbarDragLayer mTaskbarDragLayer;
+ private final TaskbarView mTaskbarView;
- private @Nullable Animator mAnimator;
- private boolean mIsAnimatingToLauncher;
+ private final AnimatedFloat mIconAlignmentForResumedState =
+ new AnimatedFloat(this::onIconAlignmentRatioChanged);
+ private final AnimatedFloat mIconAlignmentForGestureState =
+ new AnimatedFloat(this::onIconAlignmentRatioChanged);
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+ private AnimatedFloat mTaskbarBackgroundAlpha;
+ private AlphaProperty mIconAlphaForHome;
+ private boolean mIsAnimatingToLauncherViaResume;
+ private boolean mIsAnimatingToLauncherViaGesture;
+ private TaskbarKeyguardController mKeyguardController;
+
+ private LauncherState mTargetStateOverride = null;
public LauncherTaskbarUIController(
BaseQuickstepLauncher launcher, TaskbarActivityContext context) {
@@ -60,157 +76,161 @@
mLauncher = launcher;
mTaskbarStateHandler = mLauncher.getTaskbarStateHandler();
- mTaskbarAnimationController = new TaskbarAnimationController(mLauncher,
- createTaskbarAnimationControllerCallbacks());
- mHotseatController = new TaskbarHotseatController(
- mLauncher, mTaskbarView::updateHotseatItems);
}
@Override
- protected void onCreate() {
- mTaskbarStateHandler.setAnimationController(mTaskbarAnimationController);
- mTaskbarAnimationController.init();
- mHotseatController.init();
- setTaskbarViewVisible(!mLauncher.hasBeenResumed());
- alignRealHotseatWithTaskbar();
+ protected void init(TaskbarControllers taskbarControllers) {
+ mControllers = taskbarControllers;
+
+ mTaskbarBackgroundAlpha = mControllers.taskbarDragLayerController
+ .getTaskbarBackgroundAlpha();
+
+ MultiValueAlpha taskbarIconAlpha = mControllers.taskbarViewController.getTaskbarIconAlpha();
+ mIconAlphaForHome = taskbarIconAlpha.getProperty(ALPHA_INDEX_HOME);
+
mLauncher.setTaskbarUIController(this);
+ mKeyguardController = taskbarControllers.taskbarKeyguardController;
+
+ onLauncherResumedOrPaused(mLauncher.hasBeenResumed());
+ mIconAlignmentForResumedState.finishAnimation();
+ onIconAlignmentRatioChanged();
}
@Override
protected void onDestroy() {
- if (mAnimator != null) {
- // End this first, in case it relies on properties that are about to be cleaned up.
- mAnimator.end();
- }
- mTaskbarStateHandler.setAnimationController(null);
- mTaskbarAnimationController.cleanup();
- mHotseatController.cleanup();
- setTaskbarViewVisible(true);
+ onLauncherResumedOrPaused(false);
+ mIconAlignmentForResumedState.finishAnimation();
+ mIconAlignmentForGestureState.finishAnimation();
+
mLauncher.getHotseat().setIconsAlpha(1f);
mLauncher.setTaskbarUIController(null);
}
@Override
protected boolean isTaskbarTouchable() {
- return !mIsAnimatingToLauncher;
+ return !isAnimatingToLauncher() && !mControllers.taskbarStashController.isStashed();
}
- private TaskbarAnimationControllerCallbacks createTaskbarAnimationControllerCallbacks() {
- return new TaskbarAnimationControllerCallbacks() {
- @Override
- public void updateTaskbarBackgroundAlpha(float alpha) {
- mTaskbarDragLayer.setTaskbarBackgroundAlpha(alpha);
- }
+ private boolean isAnimatingToLauncher() {
+ return mIsAnimatingToLauncherViaResume || mIsAnimatingToLauncherViaGesture;
+ }
- @Override
- public void updateTaskbarVisibilityAlpha(float alpha) {
- mTaskbarView.setAlpha(alpha);
- }
-
- @Override
- public void updateImeBarVisibilityAlpha(float alpha) {
- mTaskbarDragLayer.updateImeBarVisibilityAlpha(alpha);
- }
-
- @Override
- public void updateTaskbarScale(float scale) {
- mTaskbarView.setScaleX(scale);
- mTaskbarView.setScaleY(scale);
- }
-
- @Override
- public void updateTaskbarTranslationY(float translationY) {
- if (translationY < 0) {
- // Resize to accommodate the max translation we'll reach.
- mContext.setTaskbarWindowHeight(mContext.getDeviceProfile().taskbarSize
- + mLauncher.getHotseat().getTaskbarOffsetY());
- } else {
- mContext.setTaskbarWindowHeight(mContext.getDeviceProfile().taskbarSize);
- }
- mTaskbarView.setTranslationY(translationY);
- }
- };
+ @Override
+ protected void updateContentInsets(Rect outContentInsets) {
+ int contentHeight = mControllers.taskbarStashController.getContentHeight();
+ outContentInsets.top = mTaskbarDragLayer.getHeight() - contentHeight;
}
/**
* Should be called from onResume() and onPause(), and animates the Taskbar accordingly.
*/
public void onLauncherResumedOrPaused(boolean isResumed) {
+ if (mKeyguardController.isScreenOff()) {
+ if (!isResumed) {
+ return;
+ } else {
+ // Resuming implicitly means device unlocked
+ mKeyguardController.setScreenOn();
+ }
+ }
+
long duration = QuickstepTransitionManager.CONTENT_ALPHA_DURATION;
- if (mAnimator != null) {
- mAnimator.cancel();
+ ObjectAnimator anim = mIconAlignmentForResumedState.animateToValue(
+ getCurrentIconAlignmentRatio(), isResumed ? 1 : 0)
+ .setDuration(duration);
+
+ anim.addListener(AnimatorListeners.forEndCallback(
+ () -> mIsAnimatingToLauncherViaResume = false));
+ anim.start();
+ mIsAnimatingToLauncherViaResume = isResumed;
+
+ if (!isResumed) {
+ TaskbarStashController stashController = mControllers.taskbarStashController;
+ stashController.animateToIsStashed(stashController.isStashedInApp(), duration);
}
- if (isResumed) {
- mAnimator = createAnimToLauncher(null, duration);
- } else {
- mAnimator = createAnimToApp(duration);
- }
- mAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimator = null;
- }
- });
- mAnimator.start();
+ SystemUiProxy.INSTANCE.get(mContext).notifyTaskbarStatus(!isResumed,
+ mControllers.taskbarStashController.isStashedInApp());
}
/**
- * Create Taskbar animation when going from an app to Launcher.
+ * Create Taskbar animation when going from an app to Launcher as part of recents transition.
* @param toState If known, the state we will end up in when reaching Launcher.
+ * @param callbacks callbacks to track the recents animation lifecycle. The state change is
+ * automatically reset once the recents animation finishes
*/
- public Animator createAnimToLauncher(@Nullable LauncherState toState, long duration) {
- PendingAnimation anim = new PendingAnimation(duration);
- anim.add(mTaskbarAnimationController.createAnimToBackgroundAlpha(0, duration));
- if (toState != null) {
- mTaskbarStateHandler.setStateWithAnimation(toState, new StateAnimationConfig(), anim);
+ public Animator createAnimToLauncher(@NonNull LauncherState toState,
+ @NonNull RecentsAnimationCallbacks callbacks,
+ long duration) {
+ TaskbarStashController stashController = mControllers.taskbarStashController;
+ ObjectAnimator animator = mIconAlignmentForGestureState
+ .animateToValue(1)
+ .setDuration(duration);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mTargetStateOverride = null;
+ animator.removeListener(this);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mTargetStateOverride = toState;
+ mIsAnimatingToLauncherViaGesture = true;
+ // TODO: base this on launcher state
+ stashController.animateToIsStashed(false, duration);
+ }
+ });
+ callbacks.addListener(new RecentsAnimationListener() {
+ @Override
+ public void onRecentsAnimationCanceled(ThumbnailData thumbnailData) {
+ endGestureStateOverride(true);
+ }
+
+ @Override
+ public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+ endGestureStateOverride(!controller.getFinishTargetIsLauncher());
+ }
+
+ private void endGestureStateOverride(boolean finishedToApp) {
+ callbacks.removeListener(this);
+ mIsAnimatingToLauncherViaGesture = false;
+
+ mIconAlignmentForGestureState
+ .animateToValue(0)
+ .start();
+
+ if (finishedToApp) {
+ // We only need this for the exiting live tile case.
+ stashController.animateToIsStashed(stashController.isStashedInApp());
+ }
+ }
+ });
+ return animator;
+ }
+
+ private float getCurrentIconAlignmentRatio() {
+ return Math.max(mIconAlignmentForResumedState.value, mIconAlignmentForGestureState.value);
+ }
+
+ private void onIconAlignmentRatioChanged() {
+ if (mControllers == null) {
+ return;
}
+ float alignment = getCurrentIconAlignmentRatio();
+ mControllers.taskbarViewController.setLauncherIconAlignment(
+ alignment, mLauncher.getDeviceProfile());
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mIsAnimatingToLauncher = true;
- mTaskbarView.setHolesAllowedInLayout(true);
- mTaskbarView.updateHotseatItemsVisibility();
- }
+ mTaskbarBackgroundAlpha.updateValue(1 - alignment);
- @Override
- public void onAnimationEnd(Animator animation) {
- mIsAnimatingToLauncher = false;
- setTaskbarViewVisible(false);
- }
- });
-
- return anim.buildAnim();
- }
-
- private Animator createAnimToApp(long duration) {
- PendingAnimation anim = new PendingAnimation(duration);
- anim.add(mTaskbarAnimationController.createAnimToBackgroundAlpha(1, duration));
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mTaskbarView.updateHotseatItemsVisibility();
- setTaskbarViewVisible(true);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mTaskbarView.setHolesAllowedInLayout(false);
- }
- });
- return anim.buildAnim();
- }
-
- @Override
- protected void onImeVisible(TaskbarDragLayer containerView, boolean isVisible) {
- mTaskbarAnimationController.animateToVisibilityForIme(isVisible ? 0 : 1);
- }
-
- /**
- * Should be called when one or more items in the Hotseat have changed.
- */
- public void onHotseatUpdated() {
- mHotseatController.onHotseatUpdated();
+ LauncherState state = mTargetStateOverride != null ? mTargetStateOverride
+ : mLauncher.getStateManager().getState();
+ if ((state.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) {
+ // If the hotseat icons are visible, then switch taskbar in last frame
+ setTaskbarViewVisible(alignment < 1);
+ } else {
+ mLauncher.getHotseat().setIconsAlpha(1);
+ mIconAlphaForHome.setValue(1 - alignment);
+ }
}
/**
@@ -222,54 +242,11 @@
}
public boolean isDraggingItem() {
- return mTaskbarView.isDraggingItem();
- }
-
- /**
- * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
- */
- @Override
- public void alignRealHotseatWithTaskbar() {
- Rect hotseatBounds = new Rect();
- DeviceProfile grid = mLauncher.getDeviceProfile();
- int hotseatHeight = grid.workspacePadding.bottom + grid.taskbarSize;
- int taskbarOffset = mLauncher.getHotseat().getTaskbarOffsetY();
- int hotseatTopDiff = hotseatHeight - grid.taskbarSize - taskbarOffset;
- int hotseatBottomDiff = taskbarOffset;
-
- RectF hotseatBoundsF = mTaskbarView.getHotseatBounds();
- Utilities.scaleRectFAboutPivot(hotseatBoundsF, getTaskbarScaleOnHome(),
- mTaskbarView.getPivotX(), mTaskbarView.getPivotY());
- hotseatBoundsF.round(hotseatBounds);
- mLauncher.getHotseat().setPadding(hotseatBounds.left,
- hotseatBounds.top + hotseatTopDiff,
- mTaskbarView.getWidth() - hotseatBounds.right,
- mTaskbarView.getHeight() - hotseatBounds.bottom + hotseatBottomDiff);
- }
-
- /**
- * Returns the ratio of the taskbar icon size on home vs in an app.
- */
- public float getTaskbarScaleOnHome() {
- DeviceProfile inAppDp = mContext.getDeviceProfile();
- DeviceProfile onHomeDp = mLauncher.getDeviceProfile();
- return (float) onHomeDp.cellWidthPx / inAppDp.cellWidthPx;
+ return mContext.getDragController().isDragging();
}
void setTaskbarViewVisible(boolean isVisible) {
- mTaskbarView.setIconsVisibility(isVisible);
+ mIconAlphaForHome.setValue(isVisible ? 1 : 0);
mLauncher.getHotseat().setIconsAlpha(isVisible ? 0f : 1f);
}
-
- /**
- * Contains methods that TaskbarAnimationController can call to interface with
- * TaskbarController.
- */
- protected interface TaskbarAnimationControllerCallbacks {
- void updateTaskbarBackgroundAlpha(float alpha);
- void updateTaskbarVisibilityAlpha(float alpha);
- void updateImeBarVisibilityAlpha(float alpha);
- void updateTaskbarScale(float scale);
- void updateTaskbarTranslationY(float translationY);
- }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
new file mode 100644
index 0000000..357dc7b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_A11Y_LONG_CLICK;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_BACK;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_HOME;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_IME_SWITCH;
+import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_RECENTS;
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_IME;
+import static com.android.launcher3.taskbar.TaskbarViewController.ALPHA_INDEX_KEYGUARD;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SWITCHER_SHOWING;
+
+import android.animation.ObjectAnimator;
+import android.annotation.DrawableRes;
+import android.annotation.IdRes;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Region.Op;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.util.Property;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnHoverListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.taskbar.contextual.RotationButton;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.AnimatedFloat;
+
+import java.util.ArrayList;
+import java.util.function.IntPredicate;
+
+/**
+ * Controller for managing nav bar buttons in taskbar
+ */
+public class NavbarButtonsViewController {
+
+ private final Rect mTempRect = new Rect();
+
+ private static final int FLAG_SWITCHER_SUPPORTED = 1 << 0;
+ private static final int FLAG_IME_VISIBLE = 1 << 1;
+ private static final int FLAG_ROTATION_BUTTON_VISIBLE = 1 << 2;
+ private static final int FLAG_A11Y_VISIBLE = 1 << 3;
+ private static final int FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE = 1 << 4;
+ private static final int FLAG_KEYGUARD_VISIBLE = 1 << 5;
+
+ private static final int MASK_IME_SWITCHER_VISIBLE = FLAG_SWITCHER_SUPPORTED | FLAG_IME_VISIBLE;
+
+ private View.OnLongClickListener mA11yLongClickListener;
+ private final ArrayList<StatePropertyHolder> mPropertyHolders = new ArrayList<>();
+ private final ArrayList<View> mAllButtons = new ArrayList<>();
+ private int mState;
+
+ private final TaskbarActivityContext mContext;
+ private final FrameLayout mNavButtonsView;
+ private final ViewGroup mNavButtonContainer;
+ // Used for IME+A11Y buttons
+ private final ViewGroup mEndContextualContainer;
+ private final ViewGroup mStartContextualContainer;
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+ private View mA11yButton;
+ private int mSysuiStateFlags;
+ private View mBackButton;
+
+ public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
+ mContext = context;
+ mNavButtonsView = navButtonsView;
+ mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
+ mEndContextualContainer = mNavButtonsView.findViewById(R.id.end_contextual_buttons);
+ mStartContextualContainer = mNavButtonsView.findViewById(R.id.start_contextual_buttons);
+ }
+
+ /**
+ * Initializes the controller
+ */
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+ mNavButtonsView.getLayoutParams().height = mContext.getDeviceProfile().taskbarSize;
+
+ mA11yLongClickListener = view -> {
+ mControllers.navButtonController.onButtonClick(BUTTON_A11Y_LONG_CLICK);
+ return true;
+ };
+
+ mPropertyHolders.add(new StatePropertyHolder(
+ mControllers.taskbarViewController.getTaskbarIconAlpha()
+ .getProperty(ALPHA_INDEX_IME),
+ flags -> (flags & FLAG_IME_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
+
+ // IME switcher
+ View imeSwitcherButton = addButton(R.drawable.ic_ime_switcher, BUTTON_IME_SWITCH,
+ mEndContextualContainer, mControllers.navButtonController, R.id.ime_switcher);
+ mPropertyHolders.add(new StatePropertyHolder(imeSwitcherButton,
+ flags -> ((flags & MASK_IME_SWITCHER_VISIBLE) == MASK_IME_SWITCHER_VISIBLE)
+ && ((flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0)
+ && ((flags & FLAG_A11Y_VISIBLE) == 0)));
+
+ View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
+ mStartContextualContainer, mControllers.navButtonController, R.id.back);
+ imeDownButton.setRotation(Utilities.isRtl(mContext.getResources()) ? 90 : -90);
+ // Rotate when Ime visible
+ mPropertyHolders.add(new StatePropertyHolder(imeDownButton,
+ flags -> (flags & FLAG_IME_VISIBLE) != 0));
+
+ if (mContext.isThreeButtonNav()) {
+ initButtons(mNavButtonContainer, mEndContextualContainer,
+ mControllers.navButtonController);
+
+ // Animate taskbar background when IME shows
+ mPropertyHolders.add(new StatePropertyHolder(
+ mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
+ flags -> (flags & FLAG_IME_VISIBLE) == 0,
+ AnimatedFloat.VALUE, 0, 1));
+ mPropertyHolders.add(new StatePropertyHolder(
+ mControllers.taskbarViewController.getTaskbarIconAlpha()
+ .getProperty(ALPHA_INDEX_KEYGUARD),
+ flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0, MultiValueAlpha.VALUE, 1, 0));
+
+ // Rotation button
+ RotationButton rotationButton = new RotationButtonImpl(
+ addButton(mEndContextualContainer, R.id.rotate_suggestion));
+ rotationButton.hide();
+ mControllers.rotationButtonController.setRotationButton(rotationButton);
+ } else {
+ mControllers.rotationButtonController.setRotationButton(new RotationButton() {});
+ }
+
+ applyState();
+ mPropertyHolders.forEach(StatePropertyHolder::endAnimation);
+ }
+
+ private void initButtons(ViewGroup navContainer, ViewGroup endContainer,
+ TaskbarNavButtonController navButtonController) {
+
+ // Hide when keyguard is showing, show when bouncer is showing
+ mPropertyHolders.add(new StatePropertyHolder(mBackButton,
+ flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0 ||
+ (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+
+ mBackButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
+ mNavButtonContainer, mControllers.navButtonController, R.id.back);
+ mPropertyHolders.add(new StatePropertyHolder(mBackButton,
+ flags -> (flags & FLAG_IME_VISIBLE) == 0));
+
+ // home and recents buttons
+ View homeButton = addButton(R.drawable.ic_sysbar_home, BUTTON_HOME, navContainer,
+ navButtonController, R.id.home);
+ mPropertyHolders.add(new StatePropertyHolder(homeButton,
+ flags -> (flags & FLAG_IME_VISIBLE) == 0 &&
+ (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+ View recentsButton = addButton(R.drawable.ic_sysbar_recent, BUTTON_RECENTS,
+ navContainer, navButtonController, R.id.recent_apps);
+ mPropertyHolders.add(new StatePropertyHolder(recentsButton,
+ flags -> (flags & FLAG_IME_VISIBLE) == 0 &&
+ (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+
+ // A11y button
+ mA11yButton = addButton(R.drawable.ic_sysbar_accessibility_button, BUTTON_A11Y,
+ endContainer, navButtonController, R.id.accessibility_button);
+ mPropertyHolders.add(new StatePropertyHolder(mA11yButton,
+ flags -> (flags & FLAG_A11Y_VISIBLE) != 0
+ && (flags & FLAG_ROTATION_BUTTON_VISIBLE) == 0));
+ mA11yButton.setOnLongClickListener(mA11yLongClickListener);
+ }
+
+ public void updateStateForSysuiFlags(int systemUiStateFlags, boolean forceUpdate) {
+ boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
+ boolean isImeSwitcherShowing = (systemUiStateFlags & SYSUI_STATE_IME_SWITCHER_SHOWING) != 0;
+ boolean a11yVisible = (systemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_CLICKABLE) != 0;
+ boolean a11yLongClickable =
+ (systemUiStateFlags & SYSUI_STATE_A11Y_BUTTON_LONG_CLICKABLE) != 0;
+
+ if (!forceUpdate && systemUiStateFlags == mSysuiStateFlags) {
+ return;
+ }
+ mSysuiStateFlags = systemUiStateFlags;
+
+ updateStateForFlag(FLAG_IME_VISIBLE, isImeVisible);
+ updateStateForFlag(FLAG_SWITCHER_SUPPORTED, isImeSwitcherShowing);
+ updateStateForFlag(FLAG_A11Y_VISIBLE, a11yVisible);
+ if (mA11yButton != null) {
+ // Only used in 3 button
+ mA11yButton.setLongClickable(a11yLongClickable);
+ }
+ applyState();
+ }
+
+ /**
+ * Should be called when we need to show back button for bouncer
+ */
+ public void setBackForBouncer(boolean isBouncerVisible) {
+ updateStateForFlag(FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE, isBouncerVisible);
+ applyState();
+ }
+
+ /**
+ * Slightly misnamed, but should be called when only keyguard OR AOD is showing
+ */
+ public void setKeyguardVisible(boolean isKeyguardVisible) {
+ updateStateForFlag(FLAG_KEYGUARD_VISIBLE, isKeyguardVisible);
+ applyState();
+ }
+
+ /**
+ * Returns true if IME bar is visible
+ */
+ public boolean isImeVisible() {
+ return (mState & FLAG_IME_VISIBLE) != 0;
+ }
+
+ /**
+ * Adds the bounds corresponding to all visible buttons to provided region
+ */
+ public void addVisibleButtonsRegion(TaskbarDragLayer parent, Region outRegion) {
+ int count = mAllButtons.size();
+ for (int i = 0; i < count; i++) {
+ View button = mAllButtons.get(i);
+ if (button.getVisibility() == View.VISIBLE) {
+ parent.getDescendantRectRelativeToSelf(button, mTempRect);
+ outRegion.op(mTempRect, Op.UNION);
+ }
+ }
+ }
+
+ /**
+ * Does not call {@link #applyState()}. Don't forget to!
+ */
+ private void updateStateForFlag(int flag, boolean enabled) {
+ if (enabled) {
+ mState |= flag;
+ } else {
+ mState &= ~flag;
+ }
+ }
+
+ private void applyState() {
+ int count = mPropertyHolders.size();
+ for (int i = 0; i < count; i++) {
+ mPropertyHolders.get(i).setState(mState);
+ }
+ }
+
+ private ImageView addButton(@DrawableRes int drawableId, @TaskbarButton int buttonType,
+ ViewGroup parent, TaskbarNavButtonController navButtonController, @IdRes int id) {
+ ImageView buttonView = addButton(parent, id);
+ buttonView.setImageResource(drawableId);
+ buttonView.setOnClickListener(view -> navButtonController.onButtonClick(buttonType));
+ return buttonView;
+ }
+
+ private ImageView addButton(ViewGroup parent, int id) {
+ ImageView buttonView = (ImageView) mContext.getLayoutInflater()
+ .inflate(R.layout.taskbar_nav_button, parent, false);
+ buttonView.setId(id);
+ parent.addView(buttonView);
+ mAllButtons.add(buttonView);
+ return buttonView;
+ }
+
+ private class RotationButtonImpl implements RotationButton {
+
+ private final ImageView mButton;
+ private AnimatedVectorDrawable mImageDrawable;
+
+ RotationButtonImpl(ImageView button) {
+ mButton = button;
+ }
+
+ @Override
+ public void setRotationButtonController(RotationButtonController rotationButtonController) {
+ // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
+ mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
+ .getDrawable(rotationButtonController.getIconResId());
+ mButton.setImageDrawable(mImageDrawable);
+ mImageDrawable.setCallback(mButton);
+ }
+
+ @Override
+ public View getCurrentView() {
+ return mButton;
+ }
+
+ @Override
+ public void show() {
+ mButton.setVisibility(View.VISIBLE);
+ mState |= FLAG_ROTATION_BUTTON_VISIBLE;
+ applyState();
+ }
+
+ @Override
+ public void hide() {
+ mButton.setVisibility(View.GONE);
+ mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
+ applyState();
+ }
+
+ @Override
+ public boolean isVisible() {
+ return mButton.getVisibility() == View.VISIBLE;
+ }
+
+ @Override
+ public void updateIcon(int lightIconColor, int darkIconColor) {
+ // TODO(b/187754252): UI Polish
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener onClickListener) {
+ mButton.setOnClickListener(onClickListener);
+ }
+
+ @Override
+ public void setOnHoverListener(OnHoverListener onHoverListener) {
+ mButton.setOnHoverListener(onHoverListener);
+ }
+
+ @Override
+ public AnimatedVectorDrawable getImageDrawable() {
+ return mImageDrawable;
+ }
+
+ @Override
+ public void setDarkIntensity(float darkIntensity) {
+ // TODO(b/187754252) UI polish
+ }
+
+ @Override
+ public boolean acceptRotationProposal() {
+ return mButton.isAttachedToWindow();
+ }
+ }
+
+ private static class StatePropertyHolder {
+
+ private final float mEnabledValue, mDisabledValue;
+ private final ObjectAnimator mAnimator;
+ private final IntPredicate mEnableCondition;
+
+ private boolean mIsEnabled = true;
+
+ StatePropertyHolder(View view, IntPredicate enableCondition) {
+ this(view, enableCondition, LauncherAnimUtils.VIEW_ALPHA, 1, 0);
+ mAnimator.addListener(new AlphaUpdateListener(view));
+ }
+
+ <T> StatePropertyHolder(T target, IntPredicate enabledCondition,
+ Property<T, Float> property, float enabledValue, float disabledValue) {
+ mEnableCondition = enabledCondition;
+ mEnabledValue = enabledValue;
+ mDisabledValue = disabledValue;
+ mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue);
+ }
+
+ public void setState(int flags) {
+ boolean isEnabled = mEnableCondition.test(flags);
+ if (mIsEnabled != isEnabled) {
+ mIsEnabled = isEnabled;
+ mAnimator.cancel();
+ mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue);
+ mAnimator.start();
+ }
+ }
+
+ public void endAnimation() {
+ if (mAnimator.isRunning()) {
+ mAnimator.end();
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
new file mode 100644
index 0000000..df37261
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import android.animation.Animator;
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.anim.RevealOutlineAnimation;
+import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Handles properties/data collection, then passes the results to our stashed handle View to render.
+ */
+public class StashedHandleViewController {
+
+ private final TaskbarActivityContext mActivity;
+ private final View mStashedHandleView;
+ private final int mStashedHandleWidth;
+ private final int mStashedHandleHeight;
+ private final AnimatedFloat mTaskbarStashedHandleAlpha = new AnimatedFloat(
+ this::updateStashedHandleAlpha);
+ private final AnimatedFloat mTaskbarStashedHandleHintScale = new AnimatedFloat(
+ this::updateStashedHandleHintScale);
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+
+ // The bounds we want to clip to in the settled state when showing the stashed handle.
+ private final Rect mStashedHandleBounds = new Rect();
+ private float mStashedHandleRadius;
+
+ private boolean mIsAtStashedRevealBounds = true;
+
+ public StashedHandleViewController(TaskbarActivityContext activity, View stashedHandleView) {
+ mActivity = activity;
+ mStashedHandleView = stashedHandleView;
+ final Resources resources = mActivity.getResources();
+ mStashedHandleWidth = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width);
+ mStashedHandleHeight = resources.getDimensionPixelSize(
+ R.dimen.taskbar_stashed_handle_height);
+ }
+
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+ mStashedHandleView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+ updateStashedHandleAlpha();
+ mTaskbarStashedHandleHintScale.updateValue(1f);
+
+ final int stashedTaskbarHeight = mControllers.taskbarStashController.getStashedHeight();
+ mStashedHandleView.setOutlineProvider(new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ final int stashedCenterX = view.getWidth() / 2;
+ final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+ mStashedHandleBounds.set(
+ stashedCenterX - mStashedHandleWidth / 2,
+ stashedCenterY - mStashedHandleHeight / 2,
+ stashedCenterX + mStashedHandleWidth / 2,
+ stashedCenterY + mStashedHandleHeight / 2);
+ mStashedHandleRadius = view.getHeight() / 2f;
+ outline.setRoundRect(mStashedHandleBounds, mStashedHandleRadius);
+ }
+ });
+
+ mStashedHandleView.addOnLayoutChangeListener((view, i, i1, i2, i3, i4, i5, i6, i7) -> {
+ final int stashedCenterX = view.getWidth() / 2;
+ final int stashedCenterY = view.getHeight() - stashedTaskbarHeight / 2;
+
+ view.setPivotX(stashedCenterX);
+ view.setPivotY(stashedCenterY);
+ });
+ }
+
+ public AnimatedFloat getStashedHandleAlpha() {
+ return mTaskbarStashedHandleAlpha;
+ }
+
+ public AnimatedFloat getStashedHandleHintScale() {
+ return mTaskbarStashedHandleHintScale;
+ }
+
+ /**
+ * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the stashed handle
+ * shape and size. When stashed, the shape is a thin rounded pill. When unstashed, the shape
+ * morphs into the size of where the taskbar icons will be.
+ */
+ public @Nullable Animator createRevealAnimToIsStashed(boolean isStashed) {
+ if (mIsAtStashedRevealBounds == isStashed) {
+ return null;
+ }
+ mIsAtStashedRevealBounds = isStashed;
+ final RevealOutlineAnimation handleRevealProvider = new RoundedRectRevealOutlineProvider(
+ mStashedHandleRadius, mStashedHandleRadius,
+ mControllers.taskbarViewController.getIconLayoutBounds(), mStashedHandleBounds);
+ return handleRevealProvider.createRevealAnimator(mStashedHandleView, !isStashed);
+ }
+
+ protected void updateStashedHandleAlpha() {
+ mStashedHandleView.setAlpha(mTaskbarStashedHandleAlpha.value);
+ }
+
+ protected void updateStashedHandleHintScale() {
+ mStashedHandleView.setScaleX(mTaskbarStashedHandleHintScale.value);
+ mStashedHandleView.setScaleY(mTaskbarStashedHandleHintScale.value);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 4ba0ee0..dbe528f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -17,7 +17,7 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
+import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_BOTTOM_TAPPABLE_ELEMENT;
import static com.android.systemui.shared.system.WindowManagerWrapper.ITYPE_EXTRA_NAVIGATION_BAR;
@@ -28,9 +28,7 @@
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.graphics.PixelFormat;
-import android.graphics.Point;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.SystemProperties;
import android.util.Log;
@@ -40,31 +38,25 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
+import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.DragSource;
-import com.android.launcher3.DropTarget;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
-import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarButton;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.TraceHelper;
+import com.android.launcher3.util.ViewCache;
import com.android.launcher3.views.ActivityContext;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.SysUINavigationMode.Mode;
@@ -88,45 +80,55 @@
private final DeviceProfile mDeviceProfile;
private final LayoutInflater mLayoutInflater;
private final TaskbarDragLayer mDragLayer;
- private final TaskbarIconController mIconController;
- private final MyDragController mDragController;
+ private final TaskbarControllers mControllers;
private final WindowManager mWindowManager;
private WindowManager.LayoutParams mWindowLayoutParams;
+ private boolean mIsFullscreen;
+ // The size we should return to when we call setTaskbarWindowFullscreen(false)
+ private int mLastRequestedNonFullscreenHeight;
private final SysUINavigationMode.Mode mNavMode;
- private final TaskbarNavButtonController mNavButtonController;
+ private final ViewCache mViewCache = new ViewCache();
private final boolean mIsSafeModeEnabled;
-
- @NonNull
- private TaskbarUIController mUIController = TaskbarUIController.DEFAULT;
-
- private final View.OnClickListener mOnTaskbarIconClickListener;
- private final View.OnLongClickListener mOnTaskbarIconLongClickListener;
+ private boolean mIsDestroyed = false;
public TaskbarActivityContext(Context windowContext, DeviceProfile dp,
TaskbarNavButtonController buttonController) {
super(windowContext, Themes.getActivityThemeRes(windowContext));
mDeviceProfile = dp;
- mNavButtonController = buttonController;
+
mNavMode = SysUINavigationMode.getMode(windowContext);
mIsSafeModeEnabled = TraceHelper.allowIpcs("isSafeMode",
() -> getPackageManager().isSafeMode());
- mOnTaskbarIconLongClickListener =
- new TaskbarDragController(this)::startSystemDragOnLongClick;
- mOnTaskbarIconClickListener = this::onTaskbarIconClicked;
-
float taskbarIconSize = getResources().getDimension(R.dimen.taskbar_icon_size);
+ mDeviceProfile.updateIconSize(1, getResources());
float iconScale = taskbarIconSize / mDeviceProfile.iconSizePx;
mDeviceProfile.updateIconSize(iconScale, getResources());
mLayoutInflater = LayoutInflater.from(this).cloneInContext(this);
- mDragLayer = (TaskbarDragLayer) mLayoutInflater
- .inflate(R.layout.taskbar, null, false);
- mIconController = new TaskbarIconController(this, mDragLayer);
- mDragController = new MyDragController(this);
+
+ // Inflate views.
+ mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(
+ R.layout.taskbar, null, false);
+ TaskbarView taskbarView = mDragLayer.findViewById(R.id.taskbar_view);
+ FrameLayout navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
+ View stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
+
+ // Construct controllers.
+ mControllers = new TaskbarControllers(this,
+ new TaskbarDragController(this),
+ buttonController,
+ new NavbarButtonsViewController(this, navButtonsView),
+ new RotationButtonController(this, R.color.popup_color_primary_light,
+ R.color.popup_color_primary_light),
+ new TaskbarDragLayerController(this, mDragLayer),
+ new TaskbarViewController(this, taskbarView),
+ new TaskbarKeyguardController(this),
+ new StashedHandleViewController(this, stashedHandleView),
+ new TaskbarStashController(this));
Display display = windowContext.getDisplay();
Context c = display.getDisplayId() == Display.DEFAULT_DISPLAY
@@ -136,10 +138,11 @@
}
public void init() {
+ mLastRequestedNonFullscreenHeight = mDeviceProfile.taskbarSize;
mWindowLayoutParams = new WindowManager.LayoutParams(
MATCH_PARENT,
- mDeviceProfile.taskbarSize,
- TYPE_APPLICATION_OVERLAY,
+ mLastRequestedNonFullscreenHeight,
+ TYPE_NAVIGATION_BAR_PANEL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
mWindowLayoutParams.setTitle(WINDOW_TITLE);
@@ -148,7 +151,6 @@
mWindowLayoutParams.setFitInsetsTypes(0);
mWindowLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING;
mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
- mWindowLayoutParams.setSystemApplicationOverlay(true);
WindowManagerWrapper wmWrapper = WindowManagerWrapper.getInstance();
wmWrapper.setProvidesInsetsTypes(
@@ -156,23 +158,14 @@
new int[] { ITYPE_EXTRA_NAVIGATION_BAR, ITYPE_BOTTOM_TAPPABLE_ELEMENT }
);
- mIconController.init(mOnTaskbarIconClickListener, mOnTaskbarIconLongClickListener);
+ // Initialize controllers after all are constructed.
+ mControllers.init();
+
mWindowManager.addView(mDragLayer, mWindowLayoutParams);
}
- /**
- * Updates the TaskbarContainer height (pass deviceProfile.taskbarSize to reset).
- */
- public void setTaskbarWindowHeight(int height) {
- if (mWindowLayoutParams.height == height) {
- return;
- }
- mWindowLayoutParams.height = height;
- mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
- }
-
- public boolean canShowNavButtons() {
- return ENABLE_THREE_BUTTON_TASKBAR && mNavMode == Mode.THREE_BUTTONS;
+ public boolean isThreeButtonNav() {
+ return mNavMode == Mode.THREE_BUTTONS;
}
@Override
@@ -192,59 +185,89 @@
@Override
public Rect getFolderBoundingBox() {
- return mDragLayer.getFolderBoundingBox();
+ return mControllers.taskbarDragLayerController.getFolderBoundingBox();
}
@Override
- public DragController getDragController() {
- return mDragController;
+ public TaskbarDragController getDragController() {
+ return mControllers.taskbarDragController;
+ }
+
+ @Override
+ public ViewCache getViewCache() {
+ return mViewCache;
}
/**
* Sets a new data-source for this taskbar instance
*/
public void setUIController(@NonNull TaskbarUIController uiController) {
- mUIController.onDestroy();
- mUIController = uiController;
- mIconController.setUIController(mUIController);
- mUIController.onCreate();
+ mControllers.uiController.onDestroy();
+ mControllers.uiController = uiController;
+ mControllers.uiController.init(mControllers);
}
/**
* Called when this instance of taskbar is no longer needed
*/
public void onDestroy() {
+ mIsDestroyed = true;
setUIController(TaskbarUIController.DEFAULT);
- mIconController.onDestroy();
+ mControllers.onDestroy();
mWindowManager.removeViewImmediate(mDragLayer);
}
- void onNavigationButtonClick(@TaskbarButton int buttonType) {
- mNavButtonController.onButtonClick(buttonType);
+ public void updateSysuiStateFlags(int systemUiStateFlags, boolean forceUpdate) {
+ mControllers.navbarButtonsViewController.updateStateForSysuiFlags(
+ systemUiStateFlags, forceUpdate);
+ mControllers.taskbarViewController.setImeIsVisible(
+ mControllers.navbarButtonsViewController.isImeVisible());
+ mControllers.taskbarKeyguardController.updateStateForSysuiFlags(systemUiStateFlags);
}
- /**
- * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
- */
- public void setImeIsVisible(boolean isImeVisible) {
- mIconController.setImeIsVisible(isImeVisible);
+ public void onRotationProposal(int rotation, boolean isValid) {
+ mControllers.rotationButtonController.onRotationProposal(rotation, isValid);
}
- /**
- * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
- * instantiating at all, which is what's responsible for sending sysui state flags over.
- *
- * @param vis IME visibility flag
- */
- public void updateImeStatus(int displayId, int vis, boolean showImeSwitcher) {
- mIconController.updateImeStatus(displayId, vis, showImeSwitcher);
+ public void disableNavBarElements(int displayId, int state1, int state2, boolean animate) {
+ if (displayId != getDisplayId()) {
+ return;
+ }
+ mControllers.rotationButtonController.onDisable2FlagChanged(state2);
+ mControllers.taskbarKeyguardController.disableNavbarElements(state1, state2);
+ }
+
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ mControllers.rotationButtonController.onBehaviorChanged(displayId, behavior);
}
/**
* Updates the TaskbarContainer to MATCH_PARENT vs original Taskbar size.
*/
- protected void setTaskbarWindowFullscreen(boolean fullscreen) {
- setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : getDeviceProfile().taskbarSize);
+ public void setTaskbarWindowFullscreen(boolean fullscreen) {
+ mIsFullscreen = fullscreen;
+ setTaskbarWindowHeight(fullscreen ? MATCH_PARENT : mLastRequestedNonFullscreenHeight);
+ }
+
+ /**
+ * Updates the TaskbarContainer height (pass deviceProfile.taskbarSize to reset).
+ */
+ public void setTaskbarWindowHeight(int height) {
+ if (mWindowLayoutParams.height == height || mIsDestroyed) {
+ return;
+ }
+ if (height != MATCH_PARENT) {
+ mLastRequestedNonFullscreenHeight = height;
+ if (mIsFullscreen) {
+ // We still need to be fullscreen, so defer any change to our height until we call
+ // setTaskbarWindowFullscreen(false). For example, this could happen when dragging
+ // from the gesture region, as the drag will cancel the gesture and reset launcher's
+ // state, which in turn normally would reset the taskbar window height as well.
+ return;
+ }
+ }
+ mWindowLayoutParams.height = height;
+ mWindowManager.updateViewLayout(mDragLayer, mWindowLayoutParams);
}
protected void onTaskbarIconClicked(View view) {
@@ -262,8 +285,8 @@
folder.animateOpen();
folder.iterateOverItems((itemInfo, itemView) -> {
- itemView.setOnClickListener(mOnTaskbarIconClickListener);
- itemView.setOnLongClickListener(mOnTaskbarIconLongClickListener);
+ mControllers.taskbarViewController
+ .setClickAndLongClickListenersForIcon(itemView);
// To play haptic when dragging, like other Taskbar items do.
itemView.setHapticFeedbackEnabled(true);
return false;
@@ -271,7 +294,9 @@
});
} else if (tag instanceof WorkspaceItemInfo) {
WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
- if (!(info.isDisabled() && ItemClickHandler.handleDisabledItemClicked(info, this))) {
+ if (info.isDisabled()) {
+ ItemClickHandler.handleDisabledItemClicked(info, this);
+ } else {
Intent intent = new Intent(info.getIntent())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
@@ -308,26 +333,19 @@
AbstractFloatingView.closeAllOpenViews(this);
}
- private static class MyDragController extends DragController<TaskbarActivityContext> {
- MyDragController(TaskbarActivityContext activity) {
- super(activity);
- }
+ /**
+ * Called when we detect a long press in the nav region before passing the gesture slop.
+ * @return Whether taskbar handled the long press, and thus should cancel the gesture.
+ */
+ public boolean onLongPressToUnstashTaskbar() {
+ return mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
+ }
- @Override
- protected DragView startDrag(@Nullable Drawable drawable, @Nullable View view,
- DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source,
- ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale,
- float dragViewScaleOnDrop, DragOptions options) {
- return null;
- }
-
- @Override
- protected void exitDrag() {
- }
-
- @Override
- protected DropTarget getDefaultDropTarget(int[] dropCoordinates) {
- return null;
- }
+ /**
+ * Called when we detect a motion down or up/cancel in the nav region while stashed.
+ * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
+ */
+ public void startTaskbarUnstashHint(boolean animateForward) {
+ mControllers.taskbarStashController.startUnstashHint(animateForward);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java
deleted file mode 100644
index e20ddf8..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAnimationController.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright (C) 2021 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.launcher3.taskbar;
-
-import static com.android.launcher3.LauncherState.TASKBAR;
-
-import android.animation.Animator;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.taskbar.LauncherTaskbarUIController.TaskbarAnimationControllerCallbacks;
-import com.android.quickstep.AnimatedFloat;
-import com.android.quickstep.SystemUiProxy;
-import com.android.systemui.shared.system.QuickStepContract;
-
-/**
- * Works with TaskbarController to update the TaskbarView's visual properties based on factors such
- * as LauncherState, whether Launcher is in the foreground, etc.
- */
-public class TaskbarAnimationController {
-
- private static final long IME_VISIBILITY_ALPHA_DURATION = 120;
-
- private final BaseQuickstepLauncher mLauncher;
- private final TaskbarAnimationControllerCallbacks mTaskbarCallbacks;
-
- // Background alpha.
- private final AnimatedFloat mTaskbarBackgroundAlpha = new AnimatedFloat(
- this::onTaskbarBackgroundAlphaChanged);
-
- // Overall visibility.
- private final AnimatedFloat mTaskbarVisibilityAlphaForLauncherState = new AnimatedFloat(
- this::updateVisibilityAlpha);
- private final AnimatedFloat mTaskbarVisibilityAlphaForIme = new AnimatedFloat(
- this::updateVisibilityAlphaForIme);
-
- // Scale.
- private final AnimatedFloat mTaskbarScaleForLauncherState = new AnimatedFloat(
- this::updateScale);
-
- // TranslationY.
- private final AnimatedFloat mTaskbarTranslationYForLauncherState = new AnimatedFloat(
- this::updateTranslationY);
-
- public TaskbarAnimationController(BaseQuickstepLauncher launcher,
- TaskbarAnimationControllerCallbacks taskbarCallbacks) {
- mLauncher = launcher;
- mTaskbarCallbacks = taskbarCallbacks;
- }
-
- protected void init() {
- mTaskbarBackgroundAlpha.updateValue(mLauncher.hasBeenResumed() ? 0f : 1f);
- boolean isVisibleForLauncherState = (mLauncher.getStateManager().getState()
- .getVisibleElements(mLauncher) & TASKBAR) != 0;
- mTaskbarVisibilityAlphaForLauncherState.updateValue(isVisibleForLauncherState ? 1f : 0f);
- boolean isImeVisible = (SystemUiProxy.INSTANCE.get(mLauncher).getLastSystemUiStateFlags()
- & QuickStepContract.SYSUI_STATE_IME_SHOWING) != 0;
- mTaskbarVisibilityAlphaForIme.updateValue(isImeVisible ? 0f : 1f);
-
- onTaskbarBackgroundAlphaChanged();
- updateVisibilityAlpha();
- }
-
- protected void cleanup() {
- setNavBarButtonAlpha(1f);
- }
-
- protected AnimatedFloat getTaskbarVisibilityForLauncherState() {
- return mTaskbarVisibilityAlphaForLauncherState;
- }
-
- protected AnimatedFloat getTaskbarScaleForLauncherState() {
- return mTaskbarScaleForLauncherState;
- }
-
- protected AnimatedFloat getTaskbarTranslationYForLauncherState() {
- return mTaskbarTranslationYForLauncherState;
- }
-
- protected Animator createAnimToBackgroundAlpha(float toAlpha, long duration) {
- return mTaskbarBackgroundAlpha.animateToValue(mTaskbarBackgroundAlpha.value, toAlpha)
- .setDuration(duration);
- }
-
- protected void animateToVisibilityForIme(float toAlpha) {
- mTaskbarVisibilityAlphaForIme.animateToValue(mTaskbarVisibilityAlphaForIme.value, toAlpha)
- .setDuration(IME_VISIBILITY_ALPHA_DURATION).start();
- }
-
- private void onTaskbarBackgroundAlphaChanged() {
- mTaskbarCallbacks.updateTaskbarBackgroundAlpha(mTaskbarBackgroundAlpha.value);
- updateVisibilityAlpha();
- updateScale();
- updateTranslationY();
- }
-
- private void updateVisibilityAlpha() {
- // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
- // assumption being that Taskbar should always be visible regardless of the current
- // LauncherState if Launcher is paused.
- float alphaDueToIme = mTaskbarVisibilityAlphaForIme.value;
- float alphaDueToLauncher = Math.max(mTaskbarBackgroundAlpha.value,
- mTaskbarVisibilityAlphaForLauncherState.value);
- float taskbarAlpha = alphaDueToLauncher * alphaDueToIme;
- mTaskbarCallbacks.updateTaskbarVisibilityAlpha(taskbarAlpha);
-
- // Make the nav bar invisible if taskbar is visible.
- setNavBarButtonAlpha(1f - taskbarAlpha);
- }
-
- private void updateVisibilityAlphaForIme() {
- updateVisibilityAlpha();
- float taskbarAlphaDueToIme = mTaskbarVisibilityAlphaForIme.value;
- mTaskbarCallbacks.updateImeBarVisibilityAlpha(1f - taskbarAlphaDueToIme);
- }
-
- private void updateScale() {
- // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
- // assumption being that Taskbar should always be at scale 1f regardless of the current
- // LauncherState if Launcher is paused.
- float scale = mTaskbarScaleForLauncherState.value;
- scale = Utilities.mapRange(mTaskbarBackgroundAlpha.value, scale, 1f);
- mTaskbarCallbacks.updateTaskbarScale(scale);
- }
-
- private void updateTranslationY() {
- // We use mTaskbarBackgroundAlpha as a proxy for whether Launcher is resumed/paused, the
- // assumption being that Taskbar should always be at translationY 0f regardless of the
- // current LauncherState if Launcher is paused.
- float translationY = mTaskbarTranslationYForLauncherState.value;
- translationY = Utilities.mapRange(mTaskbarBackgroundAlpha.value, translationY, 0f);
- mTaskbarCallbacks.updateTaskbarTranslationY(translationY);
- }
-
- private void setNavBarButtonAlpha(float navBarAlpha) {
- SystemUiProxy.INSTANCE.get(mLauncher).setNavBarButtonAlpha(navBarAlpha, false);
- }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
new file mode 100644
index 0000000..be26913
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
+
+/**
+ * Hosts various taskbar controllers to facilitate passing between one another.
+ */
+public class TaskbarControllers {
+
+ public final TaskbarActivityContext taskbarActivityContext;
+ public final TaskbarDragController taskbarDragController;
+ public final TaskbarNavButtonController navButtonController;
+ public final NavbarButtonsViewController navbarButtonsViewController;
+ public final RotationButtonController rotationButtonController;
+ public final TaskbarDragLayerController taskbarDragLayerController;
+ public final TaskbarViewController taskbarViewController;
+ public final TaskbarKeyguardController taskbarKeyguardController;
+ public final StashedHandleViewController stashedHandleViewController;
+ public final TaskbarStashController taskbarStashController;
+
+ /** Do not store this controller, as it may change at runtime. */
+ @NonNull public TaskbarUIController uiController = TaskbarUIController.DEFAULT;
+
+ public TaskbarControllers(TaskbarActivityContext taskbarActivityContext,
+ TaskbarDragController taskbarDragController,
+ TaskbarNavButtonController navButtonController,
+ NavbarButtonsViewController navbarButtonsViewController,
+ RotationButtonController rotationButtonController,
+ TaskbarDragLayerController taskbarDragLayerController,
+ TaskbarViewController taskbarViewController,
+ TaskbarKeyguardController taskbarKeyguardController,
+ StashedHandleViewController stashedHandleViewController,
+ TaskbarStashController taskbarStashController) {
+ this.taskbarActivityContext = taskbarActivityContext;
+ this.taskbarDragController = taskbarDragController;
+ this.navButtonController = navButtonController;
+ this.navbarButtonsViewController = navbarButtonsViewController;
+ this.rotationButtonController = rotationButtonController;
+ this.taskbarDragLayerController = taskbarDragLayerController;
+ this.taskbarViewController = taskbarViewController;
+ this.taskbarKeyguardController = taskbarKeyguardController;
+ this.stashedHandleViewController = stashedHandleViewController;
+ this.taskbarStashController = taskbarStashController;
+ }
+
+ /**
+ * Initializes all controllers. Note that controllers can now reference each other through this
+ * TaskbarControllers instance, but should be careful to only access things that were created
+ * in constructors for now, as some controllers may still be waiting for init().
+ */
+ public void init() {
+ navbarButtonsViewController.init(this);
+ if (taskbarActivityContext.isThreeButtonNav()) {
+ rotationButtonController.init();
+ }
+ taskbarDragLayerController.init(this);
+ taskbarViewController.init(this);
+ taskbarKeyguardController.init(navbarButtonsViewController);
+ stashedHandleViewController.init(this);
+ taskbarStashController.init(this);
+ }
+
+ /**
+ * Cleans up all controllers.
+ */
+ public void onDestroy() {
+ uiController.onDestroy();
+ rotationButtonController.onDestroy();
+ taskbarDragLayerController.onDestroy();
+ taskbarKeyguardController.onDestroy();
+ taskbarViewController.onDestroy();
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
index ee44927..67ebc02 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragController.java
@@ -20,19 +20,35 @@
import android.content.ClipData;
import android.content.ClipDescription;
-import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherApps;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.view.DragEvent;
+import android.view.MotionEvent;
import android.view.View;
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.DragSource;
+import com.android.launcher3.DropTarget;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
+import com.android.launcher3.accessibility.DragViewStateAnnouncer;
+import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.dragndrop.DragDriver;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.dragndrop.DraggableView;
+import com.android.launcher3.graphics.DragPreviewProvider;
+import com.android.launcher3.icons.FastBitmapDrawable;
+import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ClipDescriptionCompat;
@@ -41,14 +57,20 @@
/**
* Handles long click on Taskbar items to start a system drag and drop operation.
*/
-public class TaskbarDragController {
+public class TaskbarDragController extends DragController<TaskbarActivityContext> {
- private final Context mContext;
private final int mDragIconSize;
+ private final int[] mTempXY = new int[2];
- public TaskbarDragController(Context context) {
- mContext = context;
- Resources resources = mContext.getResources();
+ // Where the initial touch was relative to the dragged icon.
+ private int mRegistrationX;
+ private int mRegistrationY;
+
+ private boolean mIsSystemDragInProgress;
+
+ public TaskbarDragController(TaskbarActivityContext activity) {
+ super(activity);
+ Resources resources = mActivity.getResources();
mDragIconSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_drag_icon_size);
}
@@ -57,18 +79,146 @@
* generate the ClipDescription and Intent.
* @return Whether {@link View#startDragAndDrop} started successfully.
*/
- protected boolean startSystemDragOnLongClick(View view) {
+ protected boolean startDragOnLongClick(View view) {
if (!(view instanceof BubbleTextView)) {
return false;
}
BubbleTextView btv = (BubbleTextView) view;
- View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(view) {
+
+ mActivity.setTaskbarWindowFullscreen(true);
+ view.post(() -> {
+ startInternalDrag(btv);
+ btv.setVisibility(INVISIBLE);
+ });
+ return true;
+ }
+
+ private void startInternalDrag(BubbleTextView btv) {
+ float iconScale = 1f;
+ Drawable icon = btv.getIcon();
+ if (icon instanceof FastBitmapDrawable) {
+ iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
+ }
+
+ // Clear the pressed state if necessary
+ btv.clearFocus();
+ btv.setPressed(false);
+ btv.clearPressedBackground();
+
+ final DragPreviewProvider previewProvider = new DragPreviewProvider(btv);
+ final Drawable drawable = previewProvider.createDrawable();
+ final float scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
+ int dragLayerX = mTempXY[0];
+ int dragLayerY = mTempXY[1];
+
+ Rect dragRect = new Rect();
+ btv.getSourceVisualDragBounds(dragRect);
+ dragLayerY += dragRect.top;
+
+ DragOptions dragOptions = new DragOptions();
+ // TODO: open popup/pre-drag
+ // PopupContainerWithArrow popupContainer = PopupContainerWithArrow.showForIcon(view);
+ // if (popupContainer != null) {
+ // dragOptions.preDragCondition = popupContainer.createPreDragCondition();
+ // }
+
+ startDrag(
+ drawable,
+ /* view = */ null,
+ /* originalView = */ btv,
+ dragLayerX,
+ dragLayerY,
+ (View target, DropTarget.DragObject d, boolean success) -> {} /* DragSource */,
+ (WorkspaceItemInfo) btv.getTag(),
+ /* dragVisualizeOffset = */ null,
+ dragRect,
+ scale * iconScale,
+ scale,
+ dragOptions);
+ }
+
+ @Override
+ protected DragView startDrag(@Nullable Drawable drawable, @Nullable View view,
+ DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source,
+ ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale,
+ float dragViewScaleOnDrop, DragOptions options) {
+ mOptions = options;
+
+ mRegistrationX = mMotionDown.x - dragLayerX;
+ mRegistrationY = mMotionDown.y - dragLayerY;
+
+ final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
+ final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
+
+ mLastDropTarget = null;
+
+ mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext());
+ mDragObject.originalView = originalView;
+
+ mIsInPreDrag = mOptions.preDragCondition != null
+ && !mOptions.preDragCondition.shouldStartDrag(0);
+
+ float scalePx = mDragIconSize - dragRegion.width();
+ final DragView dragView = mDragObject.dragView = new TaskbarDragView(
+ mActivity,
+ drawable,
+ mRegistrationX,
+ mRegistrationY,
+ initialDragViewScale,
+ dragViewScaleOnDrop,
+ scalePx);
+ dragView.setItemInfo(dragInfo);
+ mDragObject.dragComplete = false;
+
+ mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
+ mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
+
+ mDragDriver = DragDriver.create(this, mOptions, /* secondaryEventConsumer = */ ev -> {});
+ if (!mOptions.isAccessibleDrag) {
+ mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
+ }
+
+ mDragObject.dragSource = source;
+ mDragObject.dragInfo = dragInfo;
+ mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
+
+ if (dragRegion != null) {
+ dragView.setDragRegion(new Rect(dragRegion));
+ }
+
+ dragView.show(mLastTouch.x, mLastTouch.y);
+ mDistanceSinceScroll = 0;
+
+ if (!mIsInPreDrag) {
+ callOnDragStart();
+ } else if (mOptions.preDragCondition != null) {
+ mOptions.preDragCondition.onPreDragStart(mDragObject);
+ }
+
+ handleMoveEvent(mLastTouch.x, mLastTouch.y);
+
+ return dragView;
+ }
+
+ @Override
+ protected void callOnDragStart() {
+ super.callOnDragStart();
+ // Pre-drag has ended, start the global system drag.
+ AbstractFloatingView.closeAllOpenViews(mActivity);
+ startSystemDrag((BubbleTextView) mDragObject.originalView);
+ }
+
+ private void startSystemDrag(BubbleTextView btv) {
+ View.DragShadowBuilder shadowBuilder = new View.DragShadowBuilder(btv) {
+
@Override
public void onProvideShadowMetrics(Point shadowSize, Point shadowTouchPoint) {
shadowSize.set(mDragIconSize, mDragIconSize);
- // TODO: should be based on last touch point on the icon.
- shadowTouchPoint.set(shadowSize.x / 2, shadowSize.y / 2);
+ // The registration point was taken before the icon scaled to mDragIconSize, so
+ // offset the registration to where the touch is on the new size.
+ int offset = (mDragIconSize - btv.getIconSize()) / 2;
+ shadowTouchPoint.set(mRegistrationX + offset, mRegistrationY + offset);
}
@Override
@@ -81,12 +231,12 @@
}
};
- Object tag = view.getTag();
+ Object tag = btv.getTag();
ClipDescription clipDescription = null;
Intent intent = null;
if (tag instanceof WorkspaceItemInfo) {
WorkspaceItemInfo item = (WorkspaceItemInfo) tag;
- LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
+ LauncherApps launcherApps = mActivity.getSystemService(LauncherApps.class);
clipDescription = new ClipDescription(item.title,
new String[] {
item.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
@@ -116,28 +266,82 @@
if (clipDescription != null && intent != null) {
ClipData clipData = new ClipData(clipDescription, new ClipData.Item(intent));
- view.setOnDragListener(getDraggedViewDragListener());
- return view.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
- View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE);
+ if (btv.startDragAndDrop(clipData, shadowBuilder, null /* localState */,
+ View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_OPAQUE)) {
+ onSystemDragStarted();
+ }
}
- return false;
}
- /**
- * Hide the original Taskbar item while it is being dragged.
- */
- private View.OnDragListener getDraggedViewDragListener() {
- return (view, dragEvent) -> {
+ private void onSystemDragStarted() {
+ mIsSystemDragInProgress = true;
+ mActivity.getDragLayer().setOnDragListener((view, dragEvent) -> {
switch (dragEvent.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
- view.setVisibility(INVISIBLE);
+ // Return true to tell system we are interested in events, so we get DRAG_ENDED.
return true;
case DragEvent.ACTION_DRAG_ENDED:
- view.setVisibility(VISIBLE);
- view.setOnDragListener(null);
+ mIsSystemDragInProgress = false;
+ maybeOnDragEnd();
return true;
}
return false;
- };
+ });
+ }
+
+ @Override
+ public boolean isDragging() {
+ return super.isDragging() || mIsSystemDragInProgress;
+ }
+
+ private void maybeOnDragEnd() {
+ if (!isDragging()) {
+ ((View) mDragObject.originalView).setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ protected void callOnDragEnd() {
+ super.callOnDragEnd();
+ maybeOnDragEnd();
+ }
+
+ @Override
+ protected float getX(MotionEvent ev) {
+ // We will resize to fill the screen while dragging, so use screen coordinates. This ensures
+ // we start at the correct position even though touch down is on the smaller DragLayer size.
+ return ev.getRawX();
+ }
+
+ @Override
+ protected float getY(MotionEvent ev) {
+ // We will resize to fill the screen while dragging, so use screen coordinates. This ensures
+ // we start at the correct position even though touch down is on the smaller DragLayer size.
+ return ev.getRawY();
+ }
+
+ @Override
+ protected Point getClampedDragLayerPos(float x, float y) {
+ // No need to clamp, as we will take up the entire screen.
+ mTmpPoint.set(Math.round(x), Math.round(y));
+ return mTmpPoint;
+ }
+
+ @Override
+ protected void exitDrag() {
+ if (mDragObject != null) {
+ mActivity.getDragLayer().removeView(mDragObject.dragView);
+ }
+ }
+
+ @Override
+ public void addDropTarget(DropTarget target) {
+ // No-op as Taskbar currently doesn't support any drop targets internally.
+ // Note: if we do add internal DropTargets, we'll still need to ignore Folder.
+ }
+
+ @Override
+ protected DropTarget getDefaultDropTarget(int[] dropCoordinates) {
+ return null;
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
index 45ec911..cd1baf7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java
@@ -18,14 +18,16 @@
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
-import android.graphics.Rect;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
import com.android.systemui.shared.system.ViewTreeObserverWrapper;
@@ -37,14 +39,13 @@
*/
public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> {
- private final int mFolderMargin;
private final Paint mTaskbarBackgroundPaint;
-
- private TaskbarIconController.Callbacks mControllerCallbacks;
- private TaskbarView mTaskbarView;
-
private final OnComputeInsetsListener mTaskbarInsetsComputer = this::onComputeTaskbarInsets;
+ private TaskbarDragLayerController.TaskbarDragLayerCallbacks mControllerCallbacks;
+
+ private float mTaskbarBackgroundOffset;
+
public TaskbarDragLayer(@NonNull Context context) {
this(context, null);
}
@@ -61,25 +62,25 @@
public TaskbarDragLayer(@NonNull Context context, @Nullable AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, 1 /* alphaChannelCount */);
- mFolderMargin = getResources().getDimensionPixelSize(R.dimen.taskbar_folder_margin);
mTaskbarBackgroundPaint = new Paint();
mTaskbarBackgroundPaint.setColor(getResources().getColor(R.color.taskbar_background));
+ mTaskbarBackgroundPaint.setAlpha(0);
+ }
+
+ public void init(TaskbarDragLayerController.TaskbarDragLayerCallbacks callbacks) {
+ mControllerCallbacks = callbacks;
recreateControllers();
}
@Override
public void recreateControllers() {
- mControllers = new TouchController[0];
- }
-
- public void init(TaskbarIconController.Callbacks callbacks, TaskbarView taskbarView) {
- mControllerCallbacks = callbacks;
- mTaskbarView = taskbarView;
+ mControllers = new TouchController[] {mActivity.getDragController()};
}
private void onComputeTaskbarInsets(InsetsInfo insetsInfo) {
if (mControllerCallbacks != null) {
mControllerCallbacks.updateInsetsTouchability(insetsInfo);
+ mControllerCallbacks.updateContentInsets(insetsInfo.contentInsets);
}
}
@@ -108,12 +109,6 @@
return true;
}
- public void updateImeBarVisibilityAlpha(float alpha) {
- if (mControllerCallbacks != null) {
- mControllerCallbacks.updateImeBarVisibilityAlpha(alpha);
- }
- }
-
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
@@ -124,22 +119,14 @@
@Override
protected void dispatchDraw(Canvas canvas) {
- canvas.drawRect(0, canvas.getHeight() - mTaskbarView.getHeight(), canvas.getWidth(),
+ float backgroundHeight = mControllerCallbacks.getTaskbarBackgroundHeight()
+ * (1f - mTaskbarBackgroundOffset);
+ canvas.drawRect(0, canvas.getHeight() - backgroundHeight, canvas.getWidth(),
canvas.getHeight(), mTaskbarBackgroundPaint);
super.dispatchDraw(canvas);
}
/**
- * @return Bounds (in our coordinates) where an opened Folder can display.
- */
- protected Rect getFolderBoundingBox() {
- Rect boundingBox = new Rect(0, 0, getWidth(), getHeight() - mTaskbarView.getHeight());
- boundingBox.inset(mFolderMargin, mFolderMargin);
- return boundingBox;
- }
-
-
- /**
* Sets the alpha of the background color behind all the Taskbar contents.
* @param alpha 0 is fully transparent, 1 is fully opaque.
*/
@@ -147,4 +134,19 @@
mTaskbarBackgroundPaint.setAlpha((int) (alpha * 255));
invalidate();
}
+
+ /**
+ * Sets the translation of the background color behind all the Taskbar contents.
+ * @param offset 0 is fully onscreen, 1 is fully offscreen.
+ */
+ protected void setTaskbarBackgroundOffset(float offset) {
+ mTaskbarBackgroundOffset = offset;
+ invalidate();
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ TestLogging.recordMotionEvent(TestProtocol.SEQUENCE_MAIN, "Touch event", ev);
+ return super.dispatchTouchEvent(ev);
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
new file mode 100644
index 0000000..e15e9ff
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayerController.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME;
+import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
+
+import android.content.res.Resources;
+import android.graphics.Rect;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AlphaUpdateListener;
+import com.android.quickstep.AnimatedFloat;
+import com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo;
+
+/**
+ * Handles properties/data collection, then passes the results to TaskbarDragLayer to render.
+ */
+public class TaskbarDragLayerController {
+
+ private final TaskbarActivityContext mActivity;
+ private final TaskbarDragLayer mTaskbarDragLayer;
+ private final int mFolderMargin;
+ // Alpha properties for taskbar background.
+ private final AnimatedFloat mBgTaskbar = new AnimatedFloat(this::updateBackgroundAlpha);
+ private final AnimatedFloat mBgNavbar = new AnimatedFloat(this::updateBackgroundAlpha);
+ // Translation property for taskbar background.
+ private final AnimatedFloat mBgOffset = new AnimatedFloat(this::updateBackgroundOffset);
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+
+ public TaskbarDragLayerController(TaskbarActivityContext activity,
+ TaskbarDragLayer taskbarDragLayer) {
+ mActivity = activity;
+ mTaskbarDragLayer = taskbarDragLayer;
+ final Resources resources = mTaskbarDragLayer.getResources();
+ mFolderMargin = resources.getDimensionPixelSize(R.dimen.taskbar_folder_margin);
+ }
+
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+ mTaskbarDragLayer.init(new TaskbarDragLayerCallbacks());
+ }
+
+ public void onDestroy() {
+ mTaskbarDragLayer.onDestroy();
+ }
+
+ /**
+ * @return Bounds (in TaskbarDragLayer coordinates) where an opened Folder can display.
+ */
+ public Rect getFolderBoundingBox() {
+ Rect boundingBox = new Rect(0, 0, mTaskbarDragLayer.getWidth(),
+ mTaskbarDragLayer.getHeight() - mActivity.getDeviceProfile().taskbarSize);
+ boundingBox.inset(mFolderMargin, mFolderMargin);
+ return boundingBox;
+ }
+
+ public AnimatedFloat getTaskbarBackgroundAlpha() {
+ return mBgTaskbar;
+ }
+
+ public AnimatedFloat getNavbarBackgroundAlpha() {
+ return mBgNavbar;
+ }
+
+ public AnimatedFloat getTaskbarBackgroundOffset() {
+ return mBgOffset;
+ }
+
+ private void updateBackgroundAlpha() {
+ mTaskbarDragLayer.setTaskbarBackgroundAlpha(Math.max(mBgNavbar.value, mBgTaskbar.value));
+ }
+
+ private void updateBackgroundOffset() {
+ mTaskbarDragLayer.setTaskbarBackgroundOffset(mBgOffset.value);
+ }
+
+ /**
+ * Callbacks for {@link TaskbarDragLayer} to interact with its controller.
+ */
+ public class TaskbarDragLayerCallbacks {
+
+ /**
+ * Called to update the touchable insets.
+ * @see InsetsInfo#setTouchableInsets(int)
+ */
+ public void updateInsetsTouchability(InsetsInfo insetsInfo) {
+ insetsInfo.touchableRegion.setEmpty();
+ if (mActivity.isThreeButtonNav()) {
+ // Always have nav buttons be touchable
+ mControllers.navbarButtonsViewController.addVisibleButtonsRegion(
+ mTaskbarDragLayer, insetsInfo.touchableRegion);
+ }
+
+ if (mTaskbarDragLayer.getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
+ // Let touches pass through us.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ } else if (mControllers.navbarButtonsViewController.isImeVisible()) {
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+ } else if (!mControllers.uiController.isTaskbarTouchable()) {
+ // Let touches pass through us.
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ } else if (mControllers.taskbarViewController.areIconsVisible()) {
+ // Buttons are visible, take over the full taskbar area
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
+ } else {
+ insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
+ }
+ }
+
+ /**
+ * Called to update the {@link InsetsInfo#contentInsets}.
+ */
+ public void updateContentInsets(Rect outContentInsets) {
+ mControllers.uiController.updateContentInsets(outContentInsets);
+ }
+
+ /**
+ * Called when a child is removed from TaskbarDragLayer.
+ */
+ public void onDragLayerViewRemoved() {
+ if (AbstractFloatingView.getOpenView(mActivity, TYPE_ALL) == null) {
+ mActivity.setTaskbarWindowFullscreen(false);
+ }
+ }
+
+ /**
+ * Returns how tall the background should be drawn at the bottom of the screen.
+ */
+ public int getTaskbarBackgroundHeight() {
+ return mActivity.getDeviceProfile().taskbarSize;
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragView.java
new file mode 100644
index 0000000..cf28eff
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragView.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import android.graphics.drawable.Drawable;
+
+import com.android.launcher3.R;
+import com.android.launcher3.dragndrop.DragView;
+
+/**
+ * A DragView drawn/used by the Taskbar. Note that this is only for the internal drag-and-drop,
+ * while the pre-drag is still in progress (i.e. when the long press popup is still open). After
+ * that ends, we switch to a system drag and drop view instead.
+ */
+public class TaskbarDragView extends DragView<TaskbarActivityContext> {
+ public TaskbarDragView(TaskbarActivityContext launcher, Drawable drawable, int registrationX,
+ int registrationY, float initialScale, float scaleOnDrop, float finalScaleDps) {
+ super(launcher, drawable, registrationX, registrationY, initialScale, scaleOnDrop,
+ finalScaleDps);
+ }
+
+ @Override
+ public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) {
+ Runnable onAnimationEnd = () -> {
+ if (onCompleteRunnable != null) {
+ onCompleteRunnable.run();
+ }
+ mActivity.getDragLayer().removeView(this);
+ };
+
+ duration = Math.max(duration,
+ getResources().getInteger(R.integer.config_dropAnimMinDuration));
+
+ animate()
+ .translationX(toTouchX - mRegistrationX)
+ .translationY(toTouchY - mRegistrationY)
+ .scaleX(mScaleOnDrop)
+ .scaleY(mScaleOnDrop)
+ .withEndAction(onAnimationEnd)
+ .setDuration(duration)
+ .start();
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
deleted file mode 100644
index 91cf7ef..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarHotseatController.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (C) 2021 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.launcher3.taskbar;
-
-import android.view.View;
-
-import com.android.launcher3.BaseQuickstepLauncher;
-import com.android.launcher3.CellLayout;
-import com.android.launcher3.DropTarget;
-import com.android.launcher3.Hotseat;
-import com.android.launcher3.ShortcutAndWidgetContainer;
-import com.android.launcher3.dragndrop.DragController;
-import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.model.data.ItemInfo;
-
-import java.util.function.Consumer;
-
-/**
- * Works with TaskbarController to update the TaskbarView's Hotseat items.
- */
-public class TaskbarHotseatController {
-
- private final BaseQuickstepLauncher mLauncher;
- private final Hotseat mHotseat;
- private final Consumer<ItemInfo[]> mTaskbarCallbacks;
- private final int mNumHotseatIcons;
-
- private final DragController.DragListener mDragListener = new DragController.DragListener() {
- @Override
- public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { }
-
- @Override
- public void onDragEnd() {
- onHotseatUpdated();
- }
- };
-
- public TaskbarHotseatController(
- BaseQuickstepLauncher launcher, Consumer<ItemInfo[]> taskbarCallbacks) {
- mLauncher = launcher;
- mHotseat = mLauncher.getHotseat();
- mTaskbarCallbacks = taskbarCallbacks;
- mNumHotseatIcons = mLauncher.getDeviceProfile().numShownHotseatIcons;
- }
-
- protected void init() {
- mLauncher.getDragController().addDragListener(mDragListener);
- onHotseatUpdated();
- }
-
- protected void cleanup() {
- mLauncher.getDragController().removeDragListener(mDragListener);
- }
-
- /**
- * Called when any Hotseat item changes, and reports the new list of items to TaskbarController.
- */
- protected void onHotseatUpdated() {
- ShortcutAndWidgetContainer shortcutsAndWidgets = mHotseat.getShortcutsAndWidgets();
- ItemInfo[] hotseatItemInfos = new ItemInfo[mNumHotseatIcons];
- for (int i = 0; i < shortcutsAndWidgets.getChildCount(); i++) {
- View child = shortcutsAndWidgets.getChildAt(i);
- Object tag = shortcutsAndWidgets.getChildAt(i).getTag();
- if (tag instanceof ItemInfo) {
- ItemInfo itemInfo = (ItemInfo) tag;
- CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- // Since the hotseat might be laid out vertically or horizontally, use whichever
- // index is higher.
- int index = Math.max(lp.cellX, lp.cellY);
- if (0 <= index && index < hotseatItemInfos.length) {
- hotseatItemInfos[index] = itemInfo;
- }
- }
- }
-
- mTaskbarCallbacks.accept(hotseatItemInfos);
- }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
deleted file mode 100644
index 683a5b9..0000000
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarIconController.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright (C) 2021 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.launcher3.taskbar;
-
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
-
-import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_FRAME;
-import static com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo.TOUCHABLE_INSETS_REGION;
-
-import android.graphics.Rect;
-import android.inputmethodservice.InputMethodService;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.view.View.OnLongClickListener;
-
-import androidx.annotation.NonNull;
-
-import com.android.launcher3.R;
-import com.android.launcher3.anim.AlphaUpdateListener;
-import com.android.systemui.shared.system.ViewTreeObserverWrapper.InsetsInfo;
-
-/**
- * Controller for taskbar icon UI
- */
-public class TaskbarIconController {
-
- private final Rect mTempRect = new Rect();
-
- private final TaskbarActivityContext mActivity;
- private final TaskbarDragLayer mDragLayer;
-
- private final TaskbarView mTaskbarView;
- private final ImeBarView mImeBarView;
-
- @NonNull
- private TaskbarUIController mUIController = TaskbarUIController.DEFAULT;
-
- TaskbarIconController(TaskbarActivityContext activity, TaskbarDragLayer dragLayer) {
- mActivity = activity;
- mDragLayer = dragLayer;
- mTaskbarView = mDragLayer.findViewById(R.id.taskbar_view);
- mImeBarView = mDragLayer.findViewById(R.id.ime_bar_view);
- }
-
- public void init(OnClickListener clickListener, OnLongClickListener longClickListener) {
- mDragLayer.addOnLayoutChangeListener((v, a, b, c, d, e, f, g, h) ->
- mUIController.alignRealHotseatWithTaskbar());
-
- ButtonProvider buttonProvider = new ButtonProvider(mActivity);
- mImeBarView.init(buttonProvider);
- mTaskbarView.construct(clickListener, longClickListener, buttonProvider);
- mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
-
- mDragLayer.init(new Callbacks(), mTaskbarView);
- }
-
- public void onDestroy() {
- mDragLayer.onDestroy();
- }
-
- public void setUIController(@NonNull TaskbarUIController uiController) {
- mUIController = uiController;
- }
-
- /**
- * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
- * instantiating at all, which is what's responsible for sending sysui state flags over.
- *
- * @param vis IME visibility flag
- */
- public void updateImeStatus(int displayId, int vis, boolean showImeSwitcher) {
- if (displayId != mActivity.getDisplayId() || !mActivity.canShowNavButtons()) {
- return;
- }
-
- mImeBarView.setImeSwitcherVisibility(showImeSwitcher);
- setImeIsVisible((vis & InputMethodService.IME_VISIBLE) != 0);
- }
-
- /**
- * Should be called when the IME visibility changes, so we can hide/show Taskbar accordingly.
- */
- public void setImeIsVisible(boolean isImeVisible) {
- mTaskbarView.setTouchesEnabled(!isImeVisible);
- mUIController.onImeVisible(mDragLayer, isImeVisible);
- }
-
- /**
- * Callbacks for {@link TaskbarDragLayer} to interact with the icon controller
- */
- public class Callbacks {
-
- /**
- * Called to update the touchable insets
- */
- public void updateInsetsTouchability(InsetsInfo insetsInfo) {
- insetsInfo.touchableRegion.setEmpty();
- if (mDragLayer.getAlpha() < AlphaUpdateListener.ALPHA_CUTOFF_THRESHOLD) {
- // Let touches pass through us.
- insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
- } else if (mImeBarView.getVisibility() == VISIBLE) {
- insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
- } else if (!mUIController.isTaskbarTouchable()) {
- // Let touches pass through us.
- insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
- } else if (mTaskbarView.areIconsVisible()) {
- // Buttons are visible, take over the full taskbar area
- insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_FRAME);
- } else {
- if (mTaskbarView.mSystemButtonContainer.getVisibility() == VISIBLE) {
- mDragLayer.getDescendantRectRelativeToSelf(
- mTaskbarView.mSystemButtonContainer, mTempRect);
- insetsInfo.touchableRegion.set(mTempRect);
- }
- insetsInfo.setTouchableInsets(TOUCHABLE_INSETS_REGION);
- }
-
- // TaskbarContainerView provides insets to other apps based on contentInsets. These
- // insets should stay consistent even if we expand TaskbarContainerView's bounds, e.g.
- // to show a floating view like Folder. Thus, we set the contentInsets to be where
- // mTaskbarView is, since its position never changes and insets rather than overlays.
- insetsInfo.contentInsets.left = mTaskbarView.getLeft();
- insetsInfo.contentInsets.top = mTaskbarView.getTop();
- insetsInfo.contentInsets.right = mDragLayer.getWidth() - mTaskbarView.getRight();
- insetsInfo.contentInsets.bottom = mDragLayer.getHeight() - mTaskbarView.getBottom();
- }
-
- public void onDragLayerViewRemoved() {
- int count = mDragLayer.getChildCount();
- // Ensure no other children present (like Folders, etc)
- for (int i = 0; i < count; i++) {
- View v = mDragLayer.getChildAt(i);
- if (!((v instanceof TaskbarView) || (v instanceof ImeBarView))) {
- return;
- }
- }
- mActivity.setTaskbarWindowFullscreen(false);
- }
-
- public void updateImeBarVisibilityAlpha(float alpha) {
- if (!mActivity.canShowNavButtons()) {
- // TODO Remove sysui IME bar for gesture nav as well
- return;
- }
- mImeBarView.setAlpha(alpha);
- mImeBarView.setVisibility(alpha == 0 ? GONE : VISIBLE);
- }
- }
-}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java
new file mode 100644
index 0000000..2936bd2
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarKeyguardController.java
@@ -0,0 +1,104 @@
+package com.android.launcher3.taskbar;
+
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
+
+import android.app.KeyguardManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.view.View;
+
+/**
+ * Controller for managing keyguard state for taskbar
+ */
+public class TaskbarKeyguardController {
+
+ private static final int KEYGUARD_SYSUI_FLAGS = SYSUI_STATE_BOUNCER_SHOWING |
+ SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING | SYSUI_STATE_DEVICE_DOZING;
+
+ private final TaskbarActivityContext mContext;
+ private int mDisabledNavIcons;
+ private int mKeyguardSysuiFlags;
+ private boolean mBouncerShowing;
+ private NavbarButtonsViewController mNavbarButtonsViewController;
+ private final KeyguardManager mKeyguardManager;
+ private boolean mIsScreenOff;
+
+ private final BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIsScreenOff = true;
+ }
+ };
+
+ public TaskbarKeyguardController(TaskbarActivityContext context) {
+ mContext = context;
+ mKeyguardManager = mContext.getSystemService(KeyguardManager.class);
+ }
+
+ public void init(NavbarButtonsViewController navbarButtonUIController) {
+ mNavbarButtonsViewController = navbarButtonUIController;
+ mContext.registerReceiver(mScreenOffReceiver, new IntentFilter(Intent.ACTION_SCREEN_OFF));
+ }
+
+ public void updateStateForSysuiFlags(int systemUiStateFlags) {
+ boolean bouncerShowing = (systemUiStateFlags & SYSUI_STATE_BOUNCER_SHOWING) != 0;
+ boolean keyguardShowing = (systemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING)
+ != 0;
+ boolean dozing = (systemUiStateFlags & SYSUI_STATE_DEVICE_DOZING) != 0;
+
+ int interestingKeyguardFlags = systemUiStateFlags & KEYGUARD_SYSUI_FLAGS;
+ if (interestingKeyguardFlags == mKeyguardSysuiFlags) {
+ return;
+ }
+ mKeyguardSysuiFlags = interestingKeyguardFlags;
+
+ mBouncerShowing = bouncerShowing;
+ if (!mContext.isThreeButtonNav()) {
+ // For gesture nav we don't need to deal with bouncer or showing taskbar when locked
+ return;
+ }
+
+ mNavbarButtonsViewController.setKeyguardVisible(keyguardShowing || dozing);
+ updateIconsForBouncer();
+ }
+
+ public boolean isScreenOff() {
+ return mIsScreenOff;
+ }
+
+ public void setScreenOn() {
+ mIsScreenOff = false;
+ }
+
+ public void disableNavbarElements(int state1, int state2) {
+ if (mDisabledNavIcons == state1) {
+ // no change
+ return;
+ }
+ mDisabledNavIcons = state1;
+ updateIconsForBouncer();
+ }
+
+ /**
+ * Hides/shows taskbar when keyguard is up
+ */
+ private void updateIconsForBouncer() {
+ boolean disableBack = (mDisabledNavIcons & View.STATUS_BAR_DISABLE_BACK) != 0;
+ boolean disableRecent = (mDisabledNavIcons & View.STATUS_BAR_DISABLE_RECENT) != 0;
+ boolean disableHome = (mDisabledNavIcons & View.STATUS_BAR_DISABLE_HOME) != 0;
+ boolean onlyBackEnabled = !disableBack && disableRecent && disableHome;
+
+ boolean showBackForBouncer = onlyBackEnabled &&
+ mKeyguardManager.isDeviceSecure() &&
+ mBouncerShowing;
+ mNavbarButtonsViewController.setBackForBouncer(showBackForBouncer);
+ }
+
+ public void onDestroy() {
+ mContext.unregisterReceiver(mScreenOffReceiver);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index d026bfb..45eabed 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -21,13 +21,12 @@
import static com.android.launcher3.util.DisplayController.CHANGE_ACTIVE_SCREEN;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import static com.android.launcher3.util.DisplayController.CHANGE_SUPPORTED_BOUNDS;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING;
import android.content.Context;
import android.hardware.display.DisplayManager;
-import android.inputmethodservice.InputMethodService;
import android.view.Display;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.BaseQuickstepLauncher;
@@ -38,10 +37,11 @@
import com.android.launcher3.util.DisplayController.Info;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.SysUINavigationMode.Mode;
+import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TouchInteractionService;
/**
- * Class to manager taskbar lifecycle
+ * Class to manage taskbar lifecycle
*/
public class TaskbarManager implements DisplayController.DisplayInfoChangeListener,
SysUINavigationMode.NavigationModeChangeListener {
@@ -53,6 +53,11 @@
private TaskbarActivityContext mTaskbarActivityContext;
private BaseQuickstepLauncher mLauncher;
+ /**
+ * Cache a copy here so we can initialize state whenever taskbar is recreated, since
+ * this class does not get re-initialized w/ new taskbars.
+ */
+ private int mSysuiStateFlags;
private static final int CHANGE_FLAGS =
CHANGE_ACTIVE_SCREEN | CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS;
@@ -100,29 +105,43 @@
}
/**
- * Sets or clears a launcher to act as taskbar callback
+ * Sets a launcher to act as taskbar callback
*/
- public void setLauncher(@Nullable BaseQuickstepLauncher launcher) {
+ public void setLauncher(@NonNull BaseQuickstepLauncher launcher) {
mLauncher = launcher;
if (mTaskbarActivityContext != null) {
- mTaskbarActivityContext.setUIController(mLauncher == null
- ? TaskbarUIController.DEFAULT
- : new LauncherTaskbarUIController(launcher, mTaskbarActivityContext));
+ mTaskbarActivityContext.setUIController(
+ new LauncherTaskbarUIController(launcher, mTaskbarActivityContext));
+ }
+ }
+
+ /**
+ * Clears a previously set Launcher
+ */
+ public void clearLauncher(@NonNull BaseQuickstepLauncher launcher) {
+ if (mLauncher == launcher) {
+ mLauncher = null;
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.setUIController(TaskbarUIController.DEFAULT);
+ }
}
}
private void recreateTaskbar() {
destroyExistingTaskbar();
- if (!FeatureFlags.ENABLE_TASKBAR.get()) {
+
+ DeviceProfile dp =
+ mUserUnlocked ? LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
+
+ boolean isTaskBarEnabled =
+ FeatureFlags.ENABLE_TASKBAR.get() && dp != null && dp.isTaskbarPresent;
+
+ if (!isTaskBarEnabled) {
+ SystemUiProxy.INSTANCE.get(mContext)
+ .notifyTaskbarStatus(/* visible */ false, /* stashed */ false);
return;
}
- if (!mUserUnlocked) {
- return;
- }
- DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext);
- if (!dp.isTaskbarPresent) {
- return;
- }
+
mTaskbarActivityContext = new TaskbarActivityContext(
mContext, dp.copy(mContext), mNavButtonController);
mTaskbarActivityContext.init();
@@ -130,31 +149,35 @@
mTaskbarActivityContext.setUIController(
new LauncherTaskbarUIController(mLauncher, mTaskbarActivityContext));
}
+ onSysuiFlagsChangedInternal(mSysuiStateFlags, true /* forceUpdate */);
}
- /**
- * See {@link com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags}
- * @param systemUiStateFlags The latest SystemUiStateFlags
- */
public void onSystemUiFlagsChanged(int systemUiStateFlags) {
- boolean isImeVisible = (systemUiStateFlags & SYSUI_STATE_IME_SHOWING) != 0;
+ onSysuiFlagsChangedInternal(systemUiStateFlags, false /* forceUpdate */);
+ }
+
+ private void onSysuiFlagsChangedInternal(int systemUiStateFlags, boolean forceUpdate) {
+ mSysuiStateFlags = systemUiStateFlags;
if (mTaskbarActivityContext != null) {
- mTaskbarActivityContext.setImeIsVisible(isImeVisible);
+ mTaskbarActivityContext.updateSysuiStateFlags(systemUiStateFlags, forceUpdate);
}
}
- /**
- * When in 3 button nav, the above doesn't get called since we prevent sysui nav bar from
- * instantiating at all, which is what's responsible for sending sysui state flags over.
- *
- * @param vis IME visibility flag
- * @param backDisposition Used to determine back button behavior for software keyboard
- * See BACK_DISPOSITION_* constants in {@link InputMethodService}
- */
- public void updateImeStatus(int displayId, int vis, int backDisposition,
- boolean showImeSwitcher) {
+ public void onRotationProposal(int rotation, boolean isValid) {
if (mTaskbarActivityContext != null) {
- mTaskbarActivityContext.updateImeStatus(displayId, vis, showImeSwitcher);
+ mTaskbarActivityContext.onRotationProposal(rotation, isValid);
+ }
+ }
+
+ public void disableNavBarElements(int displayId, int state1, int state2, boolean animate) {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.disableNavBarElements(displayId, state1, state2, animate);
+ }
+ }
+
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ if (mTaskbarActivityContext != null) {
+ mTaskbarActivityContext.onSystemBarAttributesChanged(displayId, behavior);
}
}
@@ -166,4 +189,8 @@
mDisplayController.removeChangeListener(this);
mSysUINavigationMode.removeModeChangeListener(this);
}
+
+ public @Nullable TaskbarActivityContext getCurrentActivityContext() {
+ return mTaskbarActivityContext;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
new file mode 100644
index 0000000..fc5abd0
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarModelCallbacks.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import android.util.SparseArray;
+import android.view.View;
+
+import com.android.launcher3.LauncherSettings.Favorites;
+import com.android.launcher3.model.BgDataModel;
+import com.android.launcher3.model.BgDataModel.FixedContainerItems;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Launcher model Callbacks for rendering taskbar.
+ */
+public class TaskbarModelCallbacks implements
+ BgDataModel.Callbacks, LauncherBindableItemsContainer {
+
+ private final SparseArray<ItemInfo> mHotseatItems = new SparseArray<>();
+ private List<ItemInfo> mPredictedItems = Collections.emptyList();
+
+ private final TaskbarActivityContext mContext;
+ private final TaskbarView mContainer;
+
+ private boolean mBindInProgress = false;
+
+ public TaskbarModelCallbacks(
+ TaskbarActivityContext context, TaskbarView container) {
+ mContext = context;
+ mContainer = container;
+ }
+
+ @Override
+ public void startBinding() {
+ mBindInProgress = true;
+ mHotseatItems.clear();
+ mPredictedItems = Collections.emptyList();
+ }
+
+ @Override
+ public void finishBindingItems(IntSet pagesBoundFirst) {
+ mBindInProgress = false;
+ commitItemsToUI();
+ }
+
+ @Override
+ public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
+ ArrayList<ItemInfo> addAnimated) {
+ boolean add1 = handleItemsAdded(addNotAnimated);
+ boolean add2 = handleItemsAdded(addAnimated);
+ if (add1 || add2) {
+ commitItemsToUI();
+ }
+ }
+
+ @Override
+ public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) {
+ if (handleItemsAdded(shortcuts)) {
+ commitItemsToUI();
+ }
+ }
+
+ private boolean handleItemsAdded(List<ItemInfo> items) {
+ boolean modified = false;
+ for (ItemInfo item : items) {
+ if (item.container == Favorites.CONTAINER_HOTSEAT) {
+ mHotseatItems.put(item.screenId, item);
+ modified = true;
+ }
+ }
+ return modified;
+ }
+
+
+ @Override
+ public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
+ updateWorkspaceItems(updated, mContext);
+ }
+
+ @Override
+ public void bindRestoreItemsChange(HashSet<ItemInfo> updates) {
+ updateRestoreItems(updates, mContext);
+ }
+
+ @Override
+ public void mapOverItems(ItemOperator op) {
+ final int itemCount = mContainer.getChildCount();
+ for (int itemIdx = 0; itemIdx < itemCount; itemIdx++) {
+ View item = mContainer.getChildAt(itemIdx);
+ if (op.evaluate((ItemInfo) item.getTag(), item)) {
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) {
+ if (handleItemsRemoved(matcher)) {
+ commitItemsToUI();
+ }
+ }
+
+ private boolean handleItemsRemoved(ItemInfoMatcher matcher) {
+ boolean modified = false;
+ for (int i = mHotseatItems.size() - 1; i >= 0; i--) {
+ if (matcher.matchesInfo(mHotseatItems.valueAt(i))) {
+ modified = true;
+ mHotseatItems.removeAt(i);
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public void bindItemsModified(List<ItemInfo> items) {
+ boolean removed = handleItemsRemoved(ItemInfoMatcher.ofItems(items));
+ boolean added = handleItemsAdded(items);
+ if (removed || added) {
+ commitItemsToUI();
+ }
+ }
+
+ @Override
+ public void bindExtraContainerItems(FixedContainerItems item) {
+ if (item.containerId == Favorites.CONTAINER_HOTSEAT_PREDICTION) {
+ mPredictedItems = item.items;
+ commitItemsToUI();
+ }
+ }
+
+ private void commitItemsToUI() {
+ if (mBindInProgress) {
+ return;
+ }
+
+ ItemInfo[] hotseatItemInfos =
+ new ItemInfo[mContext.getDeviceProfile().numShownHotseatIcons];
+ int predictionSize = mPredictedItems.size();
+ int predictionNextIndex = 0;
+
+ for (int i = 0; i < hotseatItemInfos.length; i++) {
+ hotseatItemInfos[i] = mHotseatItems.get(i);
+ if (hotseatItemInfos[i] == null && predictionNextIndex < predictionSize) {
+ hotseatItemInfos[i] = mPredictedItems.get(predictionNextIndex);
+ hotseatItemInfos[i].screenId = i;
+ predictionNextIndex++;
+ }
+ }
+ mContainer.updateHotseatItems(hotseatItemInfos);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
index 3b5afad..dd7c403 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java
@@ -23,6 +23,8 @@
import androidx.annotation.IntDef;
+import com.android.launcher3.testing.TestLogging;
+import com.android.launcher3.testing.TestProtocol;
import com.android.quickstep.OverviewCommandHelper;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TouchInteractionService;
@@ -44,7 +46,9 @@
BUTTON_BACK,
BUTTON_HOME,
BUTTON_RECENTS,
- BUTTON_IME_SWITCH
+ BUTTON_IME_SWITCH,
+ BUTTON_A11Y,
+ BUTTON_A11Y_LONG_CLICK
})
public @interface TaskbarButton {}
@@ -53,6 +57,8 @@
static final int BUTTON_HOME = BUTTON_BACK << 1;
static final int BUTTON_RECENTS = BUTTON_HOME << 1;
static final int BUTTON_IME_SWITCH = BUTTON_RECENTS << 1;
+ static final int BUTTON_A11Y = BUTTON_IME_SWITCH << 1;
+ static final int BUTTON_A11Y_LONG_CLICK = BUTTON_A11Y << 1;
private final TouchInteractionService mService;
@@ -74,6 +80,12 @@
case BUTTON_IME_SWITCH:
showIMESwitcher();
break;
+ case BUTTON_A11Y:
+ notifyImeClick(false /* longClick */);
+ break;
+ case BUTTON_A11Y_LONG_CLICK:
+ notifyImeClick(true /* longClick */);
+ break;
}
}
@@ -84,8 +96,8 @@
}
private void navigateToOverview() {
- mService.getOverviewCommandHelper()
- .addCommand(OverviewCommandHelper.TYPE_SHOW);
+ TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onOverviewToggle");
+ mService.getOverviewCommandHelper().addCommand(OverviewCommandHelper.TYPE_TOGGLE);
}
private void executeBack() {
@@ -97,4 +109,13 @@
.showInputMethodPickerFromSystem(true /* showAuxiliarySubtypes */,
DEFAULT_DISPLAY);
}
+
+ private void notifyImeClick(boolean longClick) {
+ SystemUiProxy systemUiProxy = SystemUiProxy.INSTANCE.getNoCreate();
+ if (longClick) {
+ systemUiProxy.notifyAccessibilityButtonLongClicked();
+ } else {
+ systemUiProxy.notifyAccessibilityButtonClicked(mService.getDisplayId());
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
new file mode 100644
index 0000000..0efec53
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import static android.view.HapticFeedbackConstants.LONG_PRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.annotation.Nullable;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.view.ViewConfiguration;
+
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
+import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SystemUiProxy;
+
+/**
+ * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to
+ * create a cohesive animation between stashed/unstashed states.
+ */
+public class TaskbarStashController {
+
+ /**
+ * How long to stash/unstash when manually invoked via long press.
+ */
+ private static final long TASKBAR_STASH_DURATION = 300;
+
+ /**
+ * The scale TaskbarView animates to when being stashed.
+ */
+ private static final float STASHED_TASKBAR_SCALE = 0.5f;
+
+ /**
+ * How long the hint animation plays, starting on motion down.
+ */
+ private static final long TASKBAR_HINT_STASH_DURATION =
+ ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT;
+
+ /**
+ * The scale that TaskbarView animates to when hinting towards the stashed state.
+ */
+ private static final float STASHED_TASKBAR_HINT_SCALE = 0.9f;
+
+ /**
+ * The scale that the stashed handle animates to when hinting towards the unstashed state.
+ */
+ private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
+
+ /**
+ * The SharedPreferences key for whether user has manually stashed the taskbar.
+ */
+ private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed";
+
+ /**
+ * Whether taskbar should be stashed out of the box.
+ */
+ private static final boolean DEFAULT_STASHED_PREF = false;
+
+ private final TaskbarActivityContext mActivity;
+ private final SharedPreferences mPrefs;
+ private final int mStashedHeight;
+ private final int mUnstashedHeight;
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+ // Taskbar background properties.
+ private AnimatedFloat mTaskbarBackgroundOffset;
+ // TaskbarView icon properties.
+ private AlphaProperty mIconAlphaForStash;
+ private AnimatedFloat mIconScaleForStash;
+ private AnimatedFloat mIconTranslationYForStash;
+ // Stashed handle properties.
+ private AnimatedFloat mTaskbarStashedHandleAlpha;
+ private AnimatedFloat mTaskbarStashedHandleHintScale;
+
+ /** Whether the user has manually invoked taskbar stashing, which we persist. */
+ private boolean mIsStashedInApp;
+ /** Whether we are currently visually stashed (might change based on launcher state). */
+ private boolean mIsStashed = false;
+
+ private @Nullable AnimatorSet mAnimator;
+
+ public TaskbarStashController(TaskbarActivityContext activity) {
+ mActivity = activity;
+ mPrefs = Utilities.getPrefs(mActivity);
+ final Resources resources = mActivity.getResources();
+ mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size);
+ mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize;
+ }
+
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+
+ TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController;
+ mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset();
+
+ TaskbarViewController taskbarViewController = controllers.taskbarViewController;
+ mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty(
+ TaskbarViewController.ALPHA_INDEX_STASH);
+ mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash();
+ mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash();
+
+ StashedHandleViewController stashedHandleController =
+ controllers.stashedHandleViewController;
+ mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha();
+ mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
+
+ mIsStashedInApp = supportsStashing()
+ && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
+
+ SystemUiProxy.INSTANCE.get(mActivity)
+ .notifyTaskbarStatus(/* visible */ true, /* stashed */ mIsStashedInApp);
+ }
+
+ /**
+ * Returns whether the user can manually stash the taskbar based on the current device state.
+ */
+ private boolean supportsStashing() {
+ return !mActivity.isThreeButtonNav()
+ && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || supportsStashingForTests());
+ }
+
+ private boolean supportsStashingForTests() {
+ // TODO: enable this for tests that specifically check stash/unstash behavior.
+ return false;
+ }
+
+ /**
+ * Returns whether the taskbar is currently visually stashed.
+ */
+ public boolean isStashed() {
+ return mIsStashed;
+ }
+
+ /**
+ * Returns whether the user has manually stashed the taskbar in apps.
+ */
+ public boolean isStashedInApp() {
+ return mIsStashedInApp;
+ }
+
+ public int getContentHeight() {
+ return isStashed() ? mStashedHeight : mUnstashedHeight;
+ }
+
+ public int getStashedHeight() {
+ return mStashedHeight;
+ }
+
+ /**
+ * Should be called when long pressing the nav region when taskbar is present.
+ * @return Whether taskbar was stashed and now is unstashed.
+ */
+ public boolean onLongPressToUnstashTaskbar() {
+ if (!isStashed()) {
+ // We only listen for long press on the nav region to unstash the taskbar. To stash the
+ // taskbar, we use an OnLongClickListener on TaskbarView instead.
+ return false;
+ }
+ if (updateAndAnimateIsStashedInApp(false)) {
+ mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Updates whether we should stash the taskbar when in apps, and animates to the changed state.
+ * @return Whether we started an animation to either be newly stashed or unstashed.
+ */
+ public boolean updateAndAnimateIsStashedInApp(boolean isStashedInApp) {
+ if (!supportsStashing()) {
+ return false;
+ }
+ if (mIsStashedInApp != isStashedInApp) {
+ boolean wasStashed = mIsStashedInApp;
+ mIsStashedInApp = isStashedInApp;
+ mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, mIsStashedInApp).apply();
+ boolean isStashed = mIsStashedInApp;
+ if (wasStashed != isStashed) {
+ SystemUiProxy.INSTANCE.get(mActivity)
+ .notifyTaskbarStatus(/* visible */ true, /* stashed */ isStashed);
+ createAnimToIsStashed(isStashed, TASKBAR_STASH_DURATION).start();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Starts an animation to the new stashed state with a default duration.
+ */
+ public void animateToIsStashed(boolean isStashed) {
+ animateToIsStashed(isStashed, TASKBAR_STASH_DURATION);
+ }
+
+ /**
+ * Starts an animation to the new stashed state with the specified duration.
+ */
+ public void animateToIsStashed(boolean isStashed, long duration) {
+ createAnimToIsStashed(isStashed, duration).start();
+ }
+
+ private Animator createAnimToIsStashed(boolean isStashed, long duration) {
+ AnimatorSet fullLengthAnimatorSet = new AnimatorSet();
+ // Not exactly half and may overlap. See [first|second]HalfDurationScale below.
+ AnimatorSet firstHalfAnimatorSet = new AnimatorSet();
+ AnimatorSet secondHalfAnimatorSet = new AnimatorSet();
+
+ final float firstHalfDurationScale;
+ final float secondHalfDurationScale;
+
+ if (isStashed) {
+ firstHalfDurationScale = 0.75f;
+ secondHalfDurationScale = 0.5f;
+ final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f;
+
+ fullLengthAnimatorSet.playTogether(
+ mTaskbarBackgroundOffset.animateToValue(1),
+ mIconTranslationYForStash.animateToValue(stashTranslation)
+ );
+ firstHalfAnimatorSet.playTogether(
+ mIconAlphaForStash.animateToValue(0),
+ mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE)
+ );
+ secondHalfAnimatorSet.playTogether(
+ mTaskbarStashedHandleAlpha.animateToValue(1)
+ );
+ } else {
+ firstHalfDurationScale = 0.5f;
+ secondHalfDurationScale = 0.75f;
+
+ fullLengthAnimatorSet.playTogether(
+ mTaskbarBackgroundOffset.animateToValue(0),
+ mIconScaleForStash.animateToValue(1),
+ mIconTranslationYForStash.animateToValue(0)
+ );
+ firstHalfAnimatorSet.playTogether(
+ mTaskbarStashedHandleAlpha.animateToValue(0)
+ );
+ secondHalfAnimatorSet.playTogether(
+ mIconAlphaForStash.animateToValue(1)
+ );
+ }
+
+ Animator stashedHandleRevealAnim = mControllers.stashedHandleViewController
+ .createRevealAnimToIsStashed(isStashed);
+ if (stashedHandleRevealAnim != null) {
+ fullLengthAnimatorSet.play(stashedHandleRevealAnim);
+ }
+ // Return the stashed handle to its default scale in case it was changed as part of the
+ // feedforward hint. Note that the reveal animation above also visually scales it.
+ fullLengthAnimatorSet.play(mTaskbarStashedHandleHintScale.animateToValue(1f));
+
+ fullLengthAnimatorSet.setDuration(duration);
+ firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale));
+ secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale));
+ secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale)));
+
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ mAnimator = new AnimatorSet();
+ mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet,
+ secondHalfAnimatorSet);
+ mAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mIsStashed = isStashed;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimator = null;
+ }
+ });
+ return mAnimator;
+ }
+
+ /**
+ * Creates and starts a partial stash animation, hinting at the new state that will trigger when
+ * long press is detected.
+ * @param animateForward Whether we are going towards the new stashed state or returning to the
+ * unstashed state.
+ */
+ public void startStashHint(boolean animateForward) {
+ if (isStashed() || !supportsStashing()) {
+ // Already stashed, no need to hint in that direction.
+ return;
+ }
+ mIconScaleForStash.animateToValue(
+ animateForward ? STASHED_TASKBAR_HINT_SCALE : 1)
+ .setDuration(TASKBAR_HINT_STASH_DURATION).start();
+ }
+
+ /**
+ * Creates and starts a partial unstash animation, hinting at the new state that will trigger
+ * when long press is detected.
+ * @param animateForward Whether we are going towards the new unstashed state or returning to
+ * the stashed state.
+ */
+ public void startUnstashHint(boolean animateForward) {
+ if (!isStashed()) {
+ // Already unstashed, no need to hint in that direction.
+ return;
+ }
+ mTaskbarStashedHandleHintScale.animateToValue(
+ animateForward ? UNSTASHED_TASKBAR_HANDLE_HINT_SCALE : 1)
+ .setDuration(TASKBAR_HINT_STASH_DURATION).start();
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
index a701aae..edd2a22 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStateHandler.java
@@ -16,11 +16,8 @@
package com.android.launcher3.taskbar;
import static com.android.launcher3.LauncherState.TASKBAR;
-import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
import static com.android.launcher3.anim.Interpolators.LINEAR;
-import androidx.annotation.Nullable;
-
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.anim.PendingAnimation;
@@ -28,29 +25,26 @@
import com.android.launcher3.statemanager.StateManager;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.quickstep.AnimatedFloat;
+import com.android.quickstep.SystemUiProxy;
/**
- * StateHandler to animate Taskbar according to Launcher's state machine. Does nothing if Taskbar
- * isn't present (i.e. {@link #setAnimationController} is never called).
+ * StateHandler to animate Taskbar according to Launcher's state machine.
*/
public class TaskbarStateHandler implements StateManager.StateHandler<LauncherState> {
private final BaseQuickstepLauncher mLauncher;
- // Contains Taskbar-related methods and fields we should aniamte. If null, don't do anything.
- private @Nullable TaskbarAnimationController mAnimationController = null;
+ private AnimatedFloat mNavbarButtonAlpha = new AnimatedFloat(this::updateNavbarButtonAlpha);
public TaskbarStateHandler(BaseQuickstepLauncher launcher) {
mLauncher = launcher;
}
- public void setAnimationController(TaskbarAnimationController callbacks) {
- mAnimationController = callbacks;
- }
-
@Override
public void setState(LauncherState state) {
setState(state, PropertySetter.NO_ANIM_PROPERTY_SETTER);
+ // Force update the alpha in case it was not initialized properly
+ updateNavbarButtonAlpha();
}
@Override
@@ -59,17 +53,19 @@
setState(toState, animation);
}
- private void setState(LauncherState toState, PropertySetter setter) {
- if (mAnimationController == null) {
- return;
- }
-
+ /**
+ * Sets the provided state
+ */
+ public void setState(LauncherState toState, PropertySetter setter) {
boolean isTaskbarVisible = (toState.getVisibleElements(mLauncher) & TASKBAR) != 0;
- setter.setFloat(mAnimationController.getTaskbarVisibilityForLauncherState(),
- AnimatedFloat.VALUE, isTaskbarVisible ? 1f : 0f, LINEAR);
- setter.setFloat(mAnimationController.getTaskbarScaleForLauncherState(),
- AnimatedFloat.VALUE, toState.getTaskbarScale(mLauncher), LINEAR);
- setter.setFloat(mAnimationController.getTaskbarTranslationYForLauncherState(),
- AnimatedFloat.VALUE, toState.getTaskbarTranslationY(mLauncher), ACCEL_DEACCEL);
+ // Make the nav bar visible in states that taskbar isn't visible.
+ // TODO: We should draw our own handle instead of showing the nav bar.
+ float navbarButtonAlpha = isTaskbarVisible ? 0f : 1f;
+ setter.setFloat(mNavbarButtonAlpha, AnimatedFloat.VALUE, navbarButtonAlpha, LINEAR);
+ }
+
+
+ private void updateNavbarButtonAlpha() {
+ SystemUiProxy.INSTANCE.get(mLauncher).setNavBarButtonAlpha(mNavbarButtonAlpha.value, false);
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index 50adead..260cedc 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -15,6 +15,8 @@
*/
package com.android.launcher3.taskbar;
+import android.graphics.Rect;
+
/**
* Base class for providing different taskbar UI
*/
@@ -22,12 +24,7 @@
public static final TaskbarUIController DEFAULT = new TaskbarUIController();
- /**
- * Pads the Hotseat to line up exactly with Taskbar's copy of the Hotseat.
- */
- public void alignRealHotseatWithTaskbar() { }
-
- protected void onCreate() { }
+ protected void init(TaskbarControllers taskbarControllers) { }
protected void onDestroy() { }
@@ -35,7 +32,5 @@
return true;
}
- protected void onImeVisible(TaskbarDragLayer container, boolean isVisible) {
- container.updateImeBarVisibilityAlpha(isVisible ? 1 : 0);
- }
+ protected void updateContentInsets(Rect outContentInsets) { }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index c6573a6..a4a92f7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -15,76 +15,54 @@
*/
package com.android.launcher3.taskbar;
-import static android.view.View.MeasureSpec.EXACTLY;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Rect;
-import android.graphics.RectF;
import android.util.AttributeSet;
-import android.view.DragEvent;
-import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewConfiguration;
-import android.widget.LinearLayout;
+import android.widget.FrameLayout;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.views.ActivityContext;
/**
* Hosts the Taskbar content such as Hotseat and Recent Apps. Drawn on top of other apps.
*/
-public class TaskbarView extends LinearLayout implements FolderIcon.FolderIconParent, Insettable {
+public class TaskbarView extends FrameLayout implements FolderIcon.FolderIconParent, Insettable {
- private final int mIconTouchSize;
- private final boolean mIsRtl;
- private final int mTouchSlop;
- private final RectF mTempDelegateBounds = new RectF();
- private final RectF mDelegateSlopBounds = new RectF();
private final int[] mTempOutLocation = new int[2];
+ private final Rect mIconLayoutBounds = new Rect();
+ private final int mIconTouchSize;
private final int mItemMarginLeftRight;
+ private final int mItemPadding;
private final TaskbarActivityContext mActivityContext;
- // Initialized in TaskbarController constructor.
+ // Initialized in init.
+ private TaskbarViewController.TaskbarViewCallbacks mControllerCallbacks;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
- LinearLayout mSystemButtonContainer;
- LinearLayout mHotseatIconsContainer;
-
- // Delegate touches to the closest view if within mIconTouchSize.
- private boolean mDelegateTargeted;
- private View mDelegateView;
// Prevents dispatching touches to children if true
private boolean mTouchEnabled = true;
- private boolean mIsDraggingItem;
// Only non-null when the corresponding Folder is open.
private @Nullable FolderIcon mLeaveBehindFolderIcon;
- /** Provider of buttons added to taskbar in 3 button nav */
- private ButtonProvider mButtonProvider;
-
- private boolean mDisableRelayout;
- private boolean mAreHolesAllowed;
-
public TaskbarView(@NonNull Context context) {
this(context, null);
}
@@ -105,85 +83,76 @@
Resources resources = getResources();
mIconTouchSize = resources.getDimensionPixelSize(R.dimen.taskbar_icon_touch_size);
- mItemMarginLeftRight = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
- mIsRtl = Utilities.isRtl(resources);
- mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ int actualMargin = resources.getDimensionPixelSize(R.dimen.taskbar_icon_spacing);
+ int actualIconSize = mActivityContext.getDeviceProfile().iconSizePx;
+
+ // We layout the icons to be of mIconTouchSize in width and height
+ mItemMarginLeftRight = actualMargin - (mIconTouchSize - actualIconSize) / 2;
+ mItemPadding = (mIconTouchSize - actualIconSize) / 2;
}
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mSystemButtonContainer = findViewById(R.id.system_button_layout);
- mHotseatIconsContainer = findViewById(R.id.hotseat_icons_layout);
+ protected void init(TaskbarViewController.TaskbarViewCallbacks callbacks) {
+ mControllerCallbacks = callbacks;
+ mIconClickListener = mControllerCallbacks.getIconOnClickListener();
+ mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
+
+ setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
}
- protected void construct(OnClickListener clickListener, OnLongClickListener longClickListener,
- ButtonProvider buttonProvider) {
- mIconClickListener = clickListener;
- mIconLongClickListener = longClickListener;
- mButtonProvider = buttonProvider;
-
- if (mActivityContext.canShowNavButtons()) {
- createNavButtons();
- } else {
- mSystemButtonContainer.setVisibility(GONE);
+ private void removeAndRecycle(View view) {
+ removeView(view);
+ view.setOnClickListener(null);
+ view.setOnLongClickListener(null);
+ if (!(view.getTag() instanceof FolderInfo)) {
+ mActivityContext.getViewCache().recycleView(view.getSourceLayoutResId(), view);
}
-
- int numHotseatIcons = mActivityContext.getDeviceProfile().numShownHotseatIcons;
- updateHotseatItems(new ItemInfo[numHotseatIcons]);
- }
-
- /**
- * Enables/disables empty icons in taskbar so that the layout matches with Launcher
- */
- public void setHolesAllowedInLayout(boolean areHolesAllowed) {
- if (mAreHolesAllowed != areHolesAllowed) {
- mAreHolesAllowed = areHolesAllowed;
- updateHotseatItemsVisibility();
- // TODO: Add animation
- }
- }
-
- private void setHolesAllowedInLayoutNoAnimation(boolean areHolesAllowed) {
- if (mAreHolesAllowed != areHolesAllowed) {
- mAreHolesAllowed = areHolesAllowed;
- updateHotseatItemsVisibility();
- onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
- makeMeasureSpec(getMeasuredHeight(), EXACTLY));
- onLayout(false, getLeft(), getTop(), getRight(), getBottom());
- }
+ view.setTag(null);
}
/**
* Inflates/binds the Hotseat views to show in the Taskbar given their ItemInfos.
*/
protected void updateHotseatItems(ItemInfo[] hotseatItemInfos) {
+ int nextViewIndex = 0;
+
for (int i = 0; i < hotseatItemInfos.length; i++) {
- ItemInfo hotseatItemInfo = hotseatItemInfos[
- !mIsRtl ? i : hotseatItemInfos.length - i - 1];
- View hotseatView = mHotseatIconsContainer.getChildAt(i);
+ ItemInfo hotseatItemInfo = hotseatItemInfos[i];
+ if (hotseatItemInfo == null) {
+ continue;
+ }
// Replace any Hotseat views with the appropriate type if it's not already that type.
final int expectedLayoutResId;
boolean isFolder = false;
- boolean needsReinflate = false;
- if (hotseatItemInfo != null && hotseatItemInfo.isPredictedItem()) {
+ if (hotseatItemInfo.isPredictedItem()) {
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
} else if (hotseatItemInfo instanceof FolderInfo) {
expectedLayoutResId = R.layout.folder_icon;
isFolder = true;
- // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation, so
- // if the info changes we need to reinflate. This should only happen if a new folder
- // is dragged to the position that another folder previously existed.
- needsReinflate = hotseatView != null && hotseatView.getTag() != hotseatItemInfo;
} else {
expectedLayoutResId = R.layout.taskbar_app_icon;
}
- if (hotseatView == null
- || hotseatView.getSourceLayoutResId() != expectedLayoutResId
- || needsReinflate) {
- mHotseatIconsContainer.removeView(hotseatView);
+
+ View hotseatView = null;
+ while (nextViewIndex < getChildCount()) {
+ hotseatView = getChildAt(nextViewIndex);
+
+ // see if the view can be reused
+ if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
+ || (isFolder && (hotseatView.getTag() != hotseatItemInfo))) {
+ // Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation,
+ // so if the info changes we need to reinflate. This should only happen if a new
+ // folder is dragged to the position that another folder previously existed.
+ removeAndRecycle(hotseatView);
+ hotseatView = null;
+ } else {
+ // View found
+ break;
+ }
+ }
+
+ if (hotseatView == null) {
if (isFolder) {
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
@@ -193,10 +162,9 @@
} else {
hotseatView = inflate(expectedLayoutResId);
}
- int iconSize = mActivityContext.getDeviceProfile().iconSizePx;
- LayoutParams lp = new LayoutParams(iconSize, iconSize);
- lp.setMargins(mItemMarginLeftRight, 0, mItemMarginLeftRight, 0);
- mHotseatIconsContainer.addView(hotseatView, i, lp);
+ LayoutParams lp = new LayoutParams(mIconTouchSize, mIconTouchSize);
+ hotseatView.setPadding(mItemPadding, mItemPadding, mItemPadding, mItemPadding);
+ addView(hotseatView, nextViewIndex, lp);
}
// Apply the Hotseat ItemInfos, or hide the view if there is none for a given index.
@@ -204,29 +172,52 @@
&& hotseatItemInfo instanceof WorkspaceItemInfo) {
((BubbleTextView) hotseatView).applyFromWorkspaceItem(
(WorkspaceItemInfo) hotseatItemInfo);
- hotseatView.setOnClickListener(mIconClickListener);
- hotseatView.setOnLongClickListener(mIconLongClickListener);
- } else if (isFolder) {
- hotseatView.setOnClickListener(mIconClickListener);
- hotseatView.setOnLongClickListener(mIconLongClickListener);
- } else {
- hotseatView.setOnClickListener(null);
- hotseatView.setOnLongClickListener(null);
- hotseatView.setTag(null);
}
- updateHotseatItemVisibility(hotseatView);
+ setClickAndLongClickListenersForIcon(hotseatView);
+ nextViewIndex++;
+ }
+ // Remove remaining views
+ while (nextViewIndex < getChildCount()) {
+ removeAndRecycle(getChildAt(nextViewIndex));
}
}
- protected void updateHotseatItemsVisibility() {
- for (int i = mHotseatIconsContainer.getChildCount() - 1; i >= 0; i--) {
- updateHotseatItemVisibility(mHotseatIconsContainer.getChildAt(i));
- }
+ /**
+ * Sets OnClickListener and OnLongClickListener for the given view.
+ */
+ public void setClickAndLongClickListenersForIcon(View icon) {
+ icon.setOnClickListener(mIconClickListener);
+ icon.setOnLongClickListener(mIconLongClickListener);
}
- private void updateHotseatItemVisibility(View hotseatView) {
- hotseatView.setVisibility(
- hotseatView.getTag() != null ? VISIBLE : (mAreHolesAllowed ? INVISIBLE : GONE));
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int count = getChildCount();
+ int spaceNeeded = count * (mItemMarginLeftRight * 2 + mIconTouchSize);
+ int navSpaceNeeded = ApiWrapper.getHotseatEndOffset(getContext());
+ boolean layoutRtl = isLayoutRtl();
+ int iconEnd = right - (right - left - spaceNeeded) / 2;
+ boolean needMoreSpaceForNav = layoutRtl ?
+ navSpaceNeeded > (iconEnd - spaceNeeded) :
+ iconEnd > (right - navSpaceNeeded);
+ if (needMoreSpaceForNav) {
+ int offset = layoutRtl ?
+ navSpaceNeeded - (iconEnd - spaceNeeded) :
+ (right - navSpaceNeeded) - iconEnd;
+ iconEnd += offset;
+ }
+ // Layout the children
+ mIconLayoutBounds.right = iconEnd;
+ mIconLayoutBounds.top = (bottom - top - mIconTouchSize) / 2;
+ mIconLayoutBounds.bottom = mIconLayoutBounds.top + mIconTouchSize;
+ for (int i = count; i > 0; i--) {
+ View child = getChildAt(i - 1);
+ iconEnd -= mItemMarginLeftRight;
+ int iconStart = iconEnd - mIconTouchSize;
+ child.layout(iconStart, mIconLayoutBounds.top, iconEnd, mIconLayoutBounds.bottom);
+ iconEnd = iconStart - mItemMarginLeftRight;
+ }
+ mIconLayoutBounds.left = iconEnd;
}
@Override
@@ -239,8 +230,11 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
- boolean handled = delegateTouchIfNecessary(event);
- return super.onTouchEvent(event) || handled;
+ if (!mTouchEnabled) {
+ return true;
+ }
+ mControllerCallbacks.onTouchEvent(event);
+ return super.onTouchEvent(event);
}
public void setTouchesEnabled(boolean touchEnabled) {
@@ -248,149 +242,18 @@
}
/**
- * User touched the Taskbar background. Determine whether the touch is close enough to a view
- * that we should forward the touches to it.
- * @return Whether a delegate view was chosen and it handled the touch event.
- */
- private boolean delegateTouchIfNecessary(MotionEvent event) {
- final float x = event.getX();
- final float y = event.getY();
- if (mDelegateView == null && event.getAction() == MotionEvent.ACTION_DOWN) {
- View delegateView = findDelegateView(x, y);
- if (delegateView != null) {
- mDelegateTargeted = true;
- mDelegateView = delegateView;
- mDelegateSlopBounds.set(mTempDelegateBounds);
- mDelegateSlopBounds.inset(-mTouchSlop, -mTouchSlop);
- }
- }
-
- boolean sendToDelegate = mDelegateTargeted;
- boolean inBounds = true;
- switch (event.getAction()) {
- case MotionEvent.ACTION_MOVE:
- inBounds = mDelegateSlopBounds.contains(x, y);
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- mDelegateTargeted = false;
- break;
- }
-
- boolean handled = false;
- if (sendToDelegate) {
- if (inBounds) {
- // Offset event coordinates to be inside the target view
- event.setLocation(mDelegateView.getWidth() / 2f, mDelegateView.getHeight() / 2f);
- } else {
- // Offset event coordinates to be outside the target view (in case it does
- // something like tracking pressed state)
- event.setLocation(-mTouchSlop * 2, -mTouchSlop * 2);
- }
- handled = mDelegateView.dispatchTouchEvent(event);
- // Cleanup if this was the last event to send to the delegate.
- if (!mDelegateTargeted) {
- mDelegateView = null;
- }
- }
- return handled;
- }
-
- /**
- * Return an item whose touch bounds contain the given coordinates,
- * or null if no such item exists.
- *
- * Also sets {@link #mTempDelegateBounds} to be the touch bounds of the chosen delegate view.
- */
- private @Nullable View findDelegateView(float x, float y) {
- for (int i = 0; i < getChildCount(); i++) {
- View child = getChildAt(i);
- if (!child.isShown() || !child.isClickable()) {
- continue;
- }
- int childCenterX = child.getLeft() + child.getWidth() / 2;
- int childCenterY = child.getTop() + child.getHeight() / 2;
- mTempDelegateBounds.set(
- childCenterX - mIconTouchSize / 2f,
- childCenterY - mIconTouchSize / 2f,
- childCenterX + mIconTouchSize / 2f,
- childCenterY + mIconTouchSize / 2f);
- if (mTempDelegateBounds.contains(x, y)) {
- return child;
- }
- }
- return null;
- }
-
- /**
* Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
* touch bounds.
*/
public boolean isEventOverAnyItem(MotionEvent ev) {
getLocationOnScreen(mTempOutLocation);
- float xInOurCoordinates = ev.getX() - mTempOutLocation[0];
- float yInOurCoorindates = ev.getY() - mTempOutLocation[1];
- return findDelegateView(xInOurCoordinates, yInOurCoorindates) != null;
+ int xInOurCoordinates = (int) ev.getX() - mTempOutLocation[0];
+ int yInOurCoorindates = (int) ev.getY() - mTempOutLocation[1];
+ return isShown() && mIconLayoutBounds.contains(xInOurCoordinates, yInOurCoorindates);
}
- /**
- * Add back/home/recents buttons into a single ViewGroup that will be inserted at
- * {@param navButtonStartIndex}
- */
- private void createNavButtons() {
- LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
- mActivityContext.getDeviceProfile().iconSizePx,
- mActivityContext.getDeviceProfile().iconSizePx
- );
- buttonParams.gravity = Gravity.CENTER;
-
- mSystemButtonContainer.addView(mButtonProvider.getBack(), buttonParams);
- mSystemButtonContainer.addView(mButtonProvider.getHome(), buttonParams);
- mSystemButtonContainer.addView(mButtonProvider.getRecents(), buttonParams);
- }
-
- @Override
- public boolean onDragEvent(DragEvent event) {
- switch (event.getAction()) {
- case DragEvent.ACTION_DRAG_STARTED:
- mIsDraggingItem = true;
- AbstractFloatingView.closeAllOpenViews(mActivityContext);
- return true;
- case DragEvent.ACTION_DRAG_ENDED:
- mIsDraggingItem = false;
- break;
- }
- return super.onDragEvent(event);
- }
-
- public boolean isDraggingItem() {
- return mIsDraggingItem;
- }
-
- /**
- * @return The bounding box of where the hotseat elements are relative to this TaskbarView.
- */
- protected RectF getHotseatBounds() {
- RectF result;
- mDisableRelayout = true;
- boolean wereHolesAllowed = mAreHolesAllowed;
- setHolesAllowedInLayoutNoAnimation(true);
- result = new RectF(
- mHotseatIconsContainer.getLeft(),
- mHotseatIconsContainer.getTop(),
- mHotseatIconsContainer.getRight(),
- mHotseatIconsContainer.getBottom());
- setHolesAllowedInLayoutNoAnimation(wereHolesAllowed);
- mDisableRelayout = false;
-
- return result;
- }
-
- @Override
- public void requestLayout() {
- if (!mDisableRelayout) {
- super.requestLayout();
- }
+ public Rect getIconLayoutBounds() {
+ return mIconLayoutBounds;
}
// FolderIconParent implemented methods.
@@ -421,7 +284,7 @@
}
private View inflate(@LayoutRes int layoutResId) {
- return mActivityContext.getLayoutInflater().inflate(layoutResId, this, false);
+ return mActivityContext.getViewCache().getView(layoutResId, mActivityContext, this);
}
@Override
@@ -429,11 +292,8 @@
// Ignore, we just implement Insettable to draw behind system insets.
}
- public void setIconsVisibility(boolean isVisible) {
- mHotseatIconsContainer.setVisibility(isVisible ? VISIBLE : INVISIBLE);
- }
-
public boolean areIconsVisible() {
- return mHotseatIconsContainer.getVisibility() == VISIBLE;
+ // Consider the overall visibility
+ return getVisibility() == VISIBLE;
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
new file mode 100644
index 0000000..6b95f08
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (C) 2021 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.launcher3.taskbar;
+
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.Utilities.squaredHypot;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.quickstep.AnimatedFloat.VALUE;
+
+import android.graphics.Rect;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherAppState;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.util.MultiValueAlpha;
+import com.android.quickstep.AnimatedFloat;
+
+/**
+ * Handles properties/data collection, then passes the results to TaskbarView to render.
+ */
+public class TaskbarViewController {
+ private static final Runnable NO_OP = () -> { };
+
+ public static final int ALPHA_INDEX_HOME = 0;
+ public static final int ALPHA_INDEX_IME = 1;
+ public static final int ALPHA_INDEX_KEYGUARD = 2;
+ public static final int ALPHA_INDEX_STASH = 3;
+
+ private final TaskbarActivityContext mActivity;
+ private final TaskbarView mTaskbarView;
+ private final MultiValueAlpha mTaskbarIconAlpha;
+ private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
+ private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
+ this::updateTranslationY);
+ private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
+ this::updateTranslationY);
+
+ private final TaskbarModelCallbacks mModelCallbacks;
+
+ // Initialized in init.
+ private TaskbarControllers mControllers;
+
+ // Animation to align icons with Launcher, created lazily. This allows the controller to be
+ // active only during the animation and does not need to worry about layout changes.
+ private AnimatorPlaybackController mIconAlignControllerLazy = null;
+ private Runnable mOnControllerPreCreateCallback = NO_OP;
+
+ public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
+ mActivity = activity;
+ mTaskbarView = taskbarView;
+ mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, 4);
+ mTaskbarIconAlpha.setUpdateVisibility(true);
+ mModelCallbacks = new TaskbarModelCallbacks(activity, mTaskbarView);
+ }
+
+ public void init(TaskbarControllers controllers) {
+ mControllers = controllers;
+ mTaskbarView.init(new TaskbarViewCallbacks());
+ mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
+
+ mTaskbarIconScaleForStash.updateValue(1f);
+ LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks);
+ }
+
+ public void onDestroy() {
+ LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
+ }
+
+ public boolean areIconsVisible() {
+ return mTaskbarView.areIconsVisible();
+ }
+
+ public MultiValueAlpha getTaskbarIconAlpha() {
+ return mTaskbarIconAlpha;
+ }
+
+ /**
+ * Should be called when the IME visibility changes, so we can make Taskbar not steal touches.
+ */
+ public void setImeIsVisible(boolean isImeVisible) {
+ mTaskbarView.setTouchesEnabled(!isImeVisible);
+ }
+
+ /**
+ * Sets OnClickListener and OnLongClickListener for the given view.
+ */
+ public void setClickAndLongClickListenersForIcon(View icon) {
+ mTaskbarView.setClickAndLongClickListenersForIcon(icon);
+ }
+
+ public Rect getIconLayoutBounds() {
+ return mTaskbarView.getIconLayoutBounds();
+ }
+
+ public AnimatedFloat getTaskbarIconScaleForStash() {
+ return mTaskbarIconScaleForStash;
+ }
+
+ public AnimatedFloat getTaskbarIconTranslationYForStash() {
+ return mTaskbarIconTranslationYForStash;
+ }
+
+ /**
+ * Applies scale properties for the entire TaskbarView (rather than individual icons).
+ */
+ private void updateScale() {
+ float scale = mTaskbarIconScaleForStash.value;
+ mTaskbarView.setScaleX(scale);
+ mTaskbarView.setScaleY(scale);
+ }
+
+ private void updateTranslationY() {
+ mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
+ + mTaskbarIconTranslationYForStash.value);
+ }
+
+ /**
+ * Sets the taskbar icon alignment relative to Launcher hotseat icons
+ * @param alignmentRatio [0, 1]
+ * 0 => not aligned
+ * 1 => fully aligned
+ */
+ public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
+ if (mIconAlignControllerLazy == null) {
+ mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
+ }
+ mIconAlignControllerLazy.setPlayFraction(alignmentRatio);
+ if (alignmentRatio <= 0 || alignmentRatio >= 1) {
+ // Cleanup lazy controller so that it is created again in next animation
+ mIconAlignControllerLazy = null;
+ }
+ }
+
+ /**
+ * Creates an animation for aligning the taskbar icons with the provided Launcher device profile
+ */
+ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
+ mOnControllerPreCreateCallback.run();
+ PendingAnimation setter = new PendingAnimation(100);
+ Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
+ float scaleUp = ((float) launcherDp.iconSizePx) / mActivity.getDeviceProfile().iconSizePx;
+ int hotseatCellSize =
+ (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right)
+ / launcherDp.numShownHotseatIcons;
+
+ int offsetY = launcherDp.getTaskbarOffsetY();
+ setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR);
+
+ int collapsedHeight = mActivity.getDeviceProfile().taskbarSize;
+ int expandedHeight = collapsedHeight + offsetY;
+ setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight(
+ anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight));
+
+ int count = mTaskbarView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = mTaskbarView.getChildAt(i);
+ ItemInfo info = (ItemInfo) child.getTag();
+ setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR);
+
+ float childCenter = (child.getLeft() + child.getRight()) / 2;
+ float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId
+ + hotseatCellSize / 2;
+ setter.setFloat(child, VIEW_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR);
+ }
+
+ AnimatorPlaybackController controller = setter.createPlaybackController();
+ mOnControllerPreCreateCallback = () -> controller.setPlayFraction(0);
+ return controller;
+ }
+
+ /**
+ * Callbacks for {@link TaskbarView} to interact with its controller.
+ */
+ public class TaskbarViewCallbacks {
+ private final float mSquaredTouchSlop = Utilities.squaredTouchSlop(mActivity);
+
+ private float mDownX, mDownY;
+ private boolean mCanceledStashHint;
+
+ public View.OnClickListener getIconOnClickListener() {
+ return mActivity::onTaskbarIconClicked;
+ }
+
+ public View.OnLongClickListener getIconOnLongClickListener() {
+ return mControllers.taskbarDragController::startDragOnLongClick;
+ }
+
+ public View.OnLongClickListener getBackgroundOnLongClickListener() {
+ return view -> mControllers.taskbarStashController.updateAndAnimateIsStashedInApp(true);
+ }
+
+ public void onTouchEvent(MotionEvent motionEvent) {
+ final float x = motionEvent.getRawX();
+ final float y = motionEvent.getRawY();
+ switch (motionEvent.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = x;
+ mDownY = y;
+ mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
+ mCanceledStashHint = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!mCanceledStashHint
+ && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
+ mControllers.taskbarStashController.startStashHint(
+ /* animateForward= */ false);
+ mCanceledStashHint = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (!mCanceledStashHint) {
+ mControllers.taskbarStashController.startStashHint(
+ /* animateForward= */ false);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java
new file mode 100644
index 0000000..4093097
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButton.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021 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.launcher3.taskbar.contextual;
+
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.view.View;
+
+/**
+ * Interface of a rotation button that interacts {@link RotationButtonController}.
+ * This interface exists because of the two different styles of rotation button in Sysui,
+ * one in contextual for 3 button nav and a floating rotation button for gestural.
+ * Keeping the interface for eventual migration of floating button, so some methods are
+ * pass through to "super" while others are trivially implemented.
+ *
+ * Changes:
+ * * Directly use AnimatedVectorDrawable instead of KeyButtonDrawable
+ */
+public interface RotationButton {
+ default void setRotationButtonController(RotationButtonController rotationButtonController) { }
+
+ default View getCurrentView() {
+ return null;
+ }
+ default void show() { }
+ default void hide() { }
+ default boolean isVisible() {
+ return false;
+ }
+
+ default void updateIcon(int lightIconColor, int darkIconColor) { }
+ default void setOnClickListener(View.OnClickListener onClickListener) { }
+ default void setOnHoverListener(View.OnHoverListener onHoverListener) { }
+ default AnimatedVectorDrawable getImageDrawable() {
+ return null;
+ }
+ default void setDarkIntensity(float darkIntensity) { }
+ default boolean acceptRotationProposal() {
+ return getCurrentView() != null;
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java
new file mode 100644
index 0000000..99dc282
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/contextual/RotationButtonController.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright 2021 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.launcher3.taskbar.contextual;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.internal.view.RotationPolicy.NATURAL_ROTATION;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.SuppressLint;
+import android.app.StatusBarManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.IRotationWatcher;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowInsetsController;
+import android.view.WindowManagerGlobal;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.internal.view.RotationPolicy;
+import com.android.launcher3.R;
+import com.android.launcher3.util.DisplayController;
+import com.android.systemui.shared.recents.utilities.Utilities;
+import com.android.systemui.shared.recents.utilities.ViewRippler;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+import java.util.Optional;
+
+/**
+ * Copied over from the SysUI equivalent class. Known issues/things not ported over
+ * * When rotation button visible and in auto-hide mode, we ask auto-hide controller to
+ * keep the navbar around longer. Will need to implement if we use auto-hide on taskbar
+ *
+ * Contains logic that deals with showing a rotate suggestion button with animation.
+ */
+public class RotationButtonController {
+
+ private static final String TAG = "StatusBar/RotationButtonController";
+ private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
+ private static final int NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS = 20000;
+
+ private static final int NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION = 3;
+
+ private final Context mContext;
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private final UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
+ private final ViewRippler mViewRippler = new ViewRippler();
+ private final DisplayController mDisplayController;
+ private RotationButton mRotationButton;
+
+ private int mLastRotationSuggestion;
+ private boolean mPendingRotationSuggestion;
+ private boolean mHoveringRotationSuggestion;
+ private final AccessibilityManager mAccessibilityManager;
+ private final TaskStackListenerImpl mTaskStackListener;
+ private boolean mListenersRegistered = false;
+ private boolean mIsTaskbarShowing;
+ @SuppressLint("InlinedApi")
+ private @WindowInsetsController.Behavior
+ int mBehavior = WindowInsetsController.BEHAVIOR_DEFAULT;
+ private boolean mSkipOverrideUserLockPrefsOnce;
+ private final int mLightIconColor;
+ private final int mDarkIconColor;
+ private int mIconResId = R.drawable.ic_sysbar_rotate_button_ccw_start_90;
+
+ private final Runnable mRemoveRotationProposal =
+ () -> setRotateSuggestionButtonState(false /* visible */);
+ private final Runnable mCancelPendingRotationProposal =
+ () -> mPendingRotationSuggestion = false;
+ private Animator mRotateHideAnimator;
+
+
+ private final IRotationWatcher.Stub mRotationWatcher = new IRotationWatcher.Stub() {
+ @Override
+ public void onRotationChanged(final int rotation) {
+ // We need this to be scheduled as early as possible to beat the redrawing of
+ // window in response to the orientation change.
+ mMainThreadHandler.postAtFrontOfQueue(() -> {
+ // If the screen rotation changes while locked, potentially update lock to flow with
+ // new screen rotation and hide any showing suggestions.
+ if (isRotationLocked()) {
+ if (shouldOverrideUserLockPrefs(rotation)) {
+ setRotationLockedAtAngle(rotation);
+ }
+ setRotateSuggestionButtonState(false /* visible */, true /* forced */);
+ }
+ });
+ }
+ };
+
+ /**
+ * Determines if rotation suggestions disabled2 flag exists in flag
+ * @param disable2Flags see if rotation suggestion flag exists in this flag
+ * @return whether flag exists
+ */
+ static boolean hasDisable2RotateSuggestionFlag(int disable2Flags) {
+ return (disable2Flags & StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS) != 0;
+ }
+
+ public RotationButtonController(Context context, @ColorInt int lightIconColor,
+ @ColorInt int darkIconColor) {
+ mContext = context;
+ mLightIconColor = lightIconColor;
+ mDarkIconColor = darkIconColor;
+
+ mAccessibilityManager = AccessibilityManager.getInstance(context);
+ mTaskStackListener = new TaskStackListenerImpl();
+ mDisplayController = DisplayController.INSTANCE.get(context);
+ }
+
+ public void setRotationButton(RotationButton rotationButton) {
+ mRotationButton = rotationButton;
+ mRotationButton.setRotationButtonController(this);
+ mRotationButton.setOnClickListener(this::onRotateSuggestionClick);
+ mRotationButton.setOnHoverListener(this::onRotateSuggestionHover);
+ }
+
+ public void init() {
+ registerListeners();
+ if (mDisplayController.getInfo().id != DEFAULT_DISPLAY) {
+ // Currently there is no accelerometer sensor on non-default display, disable fixed
+ // rotation for non-default display
+ onDisable2FlagChanged(StatusBarManager.DISABLE2_ROTATE_SUGGESTIONS);
+ }
+ }
+
+ public void onDestroy() {
+ unregisterListeners();
+ }
+
+ private void registerListeners() {
+ if (mListenersRegistered) {
+ return;
+ }
+
+ mListenersRegistered = true;
+ try {
+ WindowManagerGlobal.getWindowManagerService()
+ .watchRotation(mRotationWatcher, mDisplayController.getInfo().id);
+ } catch (IllegalArgumentException e) {
+ mListenersRegistered = false;
+ Log.w(TAG, "RegisterListeners for the display failed");
+ } catch (RemoteException e) {
+ Log.e(TAG, "RegisterListeners caught a RemoteException", e);
+ return;
+ }
+
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener);
+ }
+
+ void unregisterListeners() {
+ if (!mListenersRegistered) {
+ return;
+ }
+
+ mListenersRegistered = false;
+ try {
+ WindowManagerGlobal.getWindowManagerService().removeRotationWatcher(mRotationWatcher);
+ } catch (RemoteException e) {
+ Log.e(TAG, "UnregisterListeners caught a RemoteException", e);
+ return;
+ }
+
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
+ }
+
+ void setRotationLockedAtAngle(int rotationSuggestion) {
+ RotationPolicy.setRotationLockAtAngle(mContext, true, rotationSuggestion);
+ }
+
+ public boolean isRotationLocked() {
+ return RotationPolicy.isRotationLocked(mContext);
+ }
+
+ public void setRotateSuggestionButtonState(boolean visible) {
+ setRotateSuggestionButtonState(visible, false /* force */);
+ }
+
+ void setRotateSuggestionButtonState(final boolean visible, final boolean force) {
+ // At any point the button can become invisible because an a11y service became active.
+ // Similarly, a call to make the button visible may be rejected because an a11y service is
+ // active. Must account for this.
+ // Rerun a show animation to indicate change but don't rerun a hide animation
+ if (!visible && !mRotationButton.isVisible()) return;
+
+ final View view = mRotationButton.getCurrentView();
+ if (view == null) return;
+
+ final AnimatedVectorDrawable currentDrawable = mRotationButton.getImageDrawable();
+ if (currentDrawable == null) return;
+
+ // Clear any pending suggestion flag as it has either been nullified or is being shown
+ mPendingRotationSuggestion = false;
+ mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
+
+ // Handle the visibility change and animation
+ if (visible) { // Appear and change (cannot force)
+ // Stop and clear any currently running hide animations
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+ mRotateHideAnimator.cancel();
+ }
+ mRotateHideAnimator = null;
+
+ // Reset the alpha if any has changed due to hide animation
+ view.setAlpha(1f);
+
+ // Run the rotate icon's animation if it has one
+ currentDrawable.reset();
+ currentDrawable.start();
+
+ // TODO(b/187754252): No idea why this doesn't work. If we remove the "false"
+ // we see the animation show the pressed state... but it only shows the first time.
+ if (!isRotateSuggestionIntroduced()) mViewRippler.start(view);
+
+ // Set visibility unless a11y service is active.
+ mRotationButton.show();
+ } else { // Hide
+ mViewRippler.stop(); // Prevent any pending ripples, force hide or not
+
+ if (force) {
+ // If a hide animator is running stop it and make invisible
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+ mRotateHideAnimator.pause();
+ }
+ mRotationButton.hide();
+ return;
+ }
+
+ // Don't start any new hide animations if one is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+
+ ObjectAnimator fadeOut = ObjectAnimator.ofFloat(view, "alpha", 0f);
+ fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
+ fadeOut.setInterpolator(LINEAR);
+ fadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRotationButton.hide();
+ }
+ });
+
+ mRotateHideAnimator = fadeOut;
+ fadeOut.start();
+ }
+ }
+
+ void setDarkIntensity(float darkIntensity) {
+ mRotationButton.setDarkIntensity(darkIntensity);
+ }
+
+ public void onRotationProposal(int rotation, boolean isValid) {
+ int windowRotation = mDisplayController.getInfo().rotation;
+
+ if (!mRotationButton.acceptRotationProposal()) {
+ return;
+ }
+
+ // This method will be called on rotation suggestion changes even if the proposed rotation
+ // is not valid for the top app. Use invalid rotation choices as a signal to remove the
+ // rotate button if shown.
+ if (!isValid) {
+ setRotateSuggestionButtonState(false /* visible */);
+ return;
+ }
+
+ // If window rotation matches suggested rotation, remove any current suggestions
+ if (rotation == windowRotation) {
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ setRotateSuggestionButtonState(false /* visible */);
+ return;
+ }
+
+ // Prepare to show the navbar icon by updating the icon style to change anim params
+ mLastRotationSuggestion = rotation; // Remember rotation for click
+ final boolean rotationCCW = Utilities.isRotationAnimationCCW(windowRotation, rotation);
+ if (windowRotation == Surface.ROTATION_0 || windowRotation == Surface.ROTATION_180) {
+ mIconResId = rotationCCW
+ ? R.drawable.ic_sysbar_rotate_button_ccw_start_90
+ : R.drawable.ic_sysbar_rotate_button_cw_start_90;
+ } else { // 90 or 270
+ mIconResId = rotationCCW
+ ? R.drawable.ic_sysbar_rotate_button_ccw_start_0
+ : R.drawable.ic_sysbar_rotate_button_ccw_start_0;
+ }
+ mRotationButton.updateIcon(mLightIconColor, mDarkIconColor);
+
+ if (canShowRotationButton()) {
+ // The navbar is visible / it's in visual immersive mode, so show the icon right away
+ showAndLogRotationSuggestion();
+ } else {
+ // If the navbar isn't shown, flag the rotate icon to be shown should the navbar become
+ // visible given some time limit.
+ mPendingRotationSuggestion = true;
+ mMainThreadHandler.removeCallbacks(mCancelPendingRotationProposal);
+ mMainThreadHandler.postDelayed(mCancelPendingRotationProposal,
+ NAVBAR_HIDDEN_PENDING_ICON_TIMEOUT_MS);
+ }
+ }
+
+ public void onDisable2FlagChanged(int state2) {
+ final boolean rotateSuggestionsDisabled = hasDisable2RotateSuggestionFlag(state2);
+ if (rotateSuggestionsDisabled) onRotationSuggestionsDisabled();
+ }
+
+ public void onBehaviorChanged(int displayId, @WindowInsetsController.Behavior int behavior) {
+ if (mDisplayController.getInfo().id != displayId) {
+ return;
+ }
+
+ if (mBehavior != behavior) {
+ mBehavior = behavior;
+ showPendingRotationButtonIfNeeded();
+ }
+ }
+
+ public void onTaskBarVisibilityChange(boolean showing) {
+ if (mIsTaskbarShowing != showing) {
+ mIsTaskbarShowing = showing;
+ showPendingRotationButtonIfNeeded();
+ }
+ }
+
+ private void showPendingRotationButtonIfNeeded() {
+ if (canShowRotationButton() && mPendingRotationSuggestion) {
+ showAndLogRotationSuggestion();
+ }
+ }
+
+ /** Return true when either the task bar is visible or it's in visual immersive mode. */
+ @SuppressLint("InlinedApi")
+ private boolean canShowRotationButton() {
+ return mIsTaskbarShowing || mBehavior == WindowInsetsController.BEHAVIOR_DEFAULT;
+ }
+
+ public @DrawableRes
+ int getIconResId() {
+ return mIconResId;
+ }
+
+ public @ColorInt int getLightIconColor() {
+ return mLightIconColor;
+ }
+
+ public @ColorInt int getDarkIconColor() {
+ return mDarkIconColor;
+ }
+
+ private void onRotateSuggestionClick(View v) {
+ mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_ACCEPTED);
+ incrementNumAcceptedRotationSuggestionsIfNeeded();
+ setRotationLockedAtAngle(mLastRotationSuggestion);
+ }
+
+ private boolean onRotateSuggestionHover(View v, MotionEvent event) {
+ final int action = event.getActionMasked();
+ mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
+ || (action == MotionEvent.ACTION_HOVER_MOVE);
+ rescheduleRotationTimeout(true /* reasonHover */);
+ return false; // Must return false so a11y hover events are dispatched correctly.
+ }
+
+ private void onRotationSuggestionsDisabled() {
+ // Immediately hide the rotate button and clear any planned removal
+ setRotateSuggestionButtonState(false /* visible */, true /* force */);
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ }
+
+ private void showAndLogRotationSuggestion() {
+ setRotateSuggestionButtonState(true /* visible */);
+ rescheduleRotationTimeout(false /* reasonHover */);
+ mUiEventLogger.log(RotationButtonEvent.ROTATION_SUGGESTION_SHOWN);
+ }
+
+ /**
+ * Makes {@link #shouldOverrideUserLockPrefs} always return {@code false} once. It is used to
+ * avoid losing original user rotation when display rotation is changed by entering the fixed
+ * orientation overview.
+ */
+ void setSkipOverrideUserLockPrefsOnce() {
+ mSkipOverrideUserLockPrefsOnce = true;
+ }
+
+ private boolean shouldOverrideUserLockPrefs(final int rotation) {
+ if (mSkipOverrideUserLockPrefsOnce) {
+ mSkipOverrideUserLockPrefsOnce = false;
+ return false;
+ }
+ // Only override user prefs when returning to the natural rotation (normally portrait).
+ // Don't let apps that force landscape or 180 alter user lock.
+ return rotation == NATURAL_ROTATION;
+ }
+
+ private void rescheduleRotationTimeout(final boolean reasonHover) {
+ // May be called due to a new rotation proposal or a change in hover state
+ if (reasonHover) {
+ // Don't reschedule if a hide animator is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+ // Don't reschedule if not visible
+ if (!mRotationButton.isVisible()) return;
+ }
+
+ // Stop any pending removal
+ mMainThreadHandler.removeCallbacks(mRemoveRotationProposal);
+ // Schedule timeout
+ mMainThreadHandler.postDelayed(mRemoveRotationProposal,
+ computeRotationProposalTimeout());
+ }
+
+ private int computeRotationProposalTimeout() {
+ return mAccessibilityManager.getRecommendedTimeoutMillis(
+ mHoveringRotationSuggestion ? 16000 : 5000,
+ AccessibilityManager.FLAG_CONTENT_CONTROLS);
+ }
+
+ private boolean isRotateSuggestionIntroduced() {
+ ContentResolver cr = mContext.getContentResolver();
+ return Settings.Secure.getInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0)
+ >= NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION;
+ }
+
+ private void incrementNumAcceptedRotationSuggestionsIfNeeded() {
+ // Get the number of accepted suggestions
+ ContentResolver cr = mContext.getContentResolver();
+ final int numSuggestions = Settings.Secure.getInt(cr,
+ Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED, 0);
+
+ // Increment the number of accepted suggestions only if it would change intro mode
+ if (numSuggestions < NUM_ACCEPTED_ROTATION_SUGGESTIONS_FOR_INTRODUCTION) {
+ Settings.Secure.putInt(cr, Settings.Secure.NUM_ROTATION_SUGGESTIONS_ACCEPTED,
+ numSuggestions + 1);
+ }
+ }
+
+ private class TaskStackListenerImpl extends TaskStackChangeListener {
+ // Invalidate any rotation suggestion on task change or activity orientation change
+ // Note: all callbacks happen on main thread
+
+ @Override
+ public void onTaskStackChanged() {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
+ // Only hide the icon if the top task changes its requestedOrientation
+ // Launcher can alter its requestedOrientation while it's not on top, don't hide on this
+ Optional.ofNullable(ActivityManagerWrapper.getInstance())
+ .map(ActivityManagerWrapper::getRunningTask)
+ .ifPresent(a -> {
+ if (a.id == taskId) setRotateSuggestionButtonState(false /* visible */);
+ });
+ }
+ }
+
+ enum RotationButtonEvent implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "The rotation button was shown")
+ ROTATION_SUGGESTION_SHOWN(206),
+ @UiEvent(doc = "The rotation button was clicked")
+ ROTATION_SUGGESTION_ACCEPTED(207);
+
+ private final int mId;
+ RotationButtonEvent(int id) {
+ mId = id;
+ }
+ @Override public int getId() {
+ return mId;
+ }
+ }
+}
+
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 76a5782..85943b6 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -17,10 +17,15 @@
package com.android.launcher3.uioverrides;
import android.app.Person;
+import android.content.Context;
import android.content.pm.ShortcutInfo;
+import android.content.res.Resources;
import android.view.Display;
+import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.quickstep.SysUINavigationMode;
+import com.android.quickstep.SysUINavigationMode.Mode;
public class ApiWrapper {
@@ -37,4 +42,18 @@
public static boolean isInternalDisplay(Display display) {
return display.getType() == Display.TYPE_INTERNAL;
}
+
+ /**
+ * Returns the minimum space that should be left empty at the end of hotseat
+ */
+ public static int getHotseatEndOffset(Context context) {
+ if (SysUINavigationMode.INSTANCE.get(context).getMode() == Mode.THREE_BUTTONS) {
+ Resources res = context.getResources();
+ return 2 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_spacing)
+ + 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size);
+ } else {
+ return 0;
+ }
+
+ }
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
index 1d52315..d74b6c5 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/BaseRecentsViewStateController.java
@@ -17,6 +17,8 @@
package com.android.launcher3.uioverrides;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
@@ -112,8 +114,9 @@
mRecentsView, getTaskModalnessProperty(),
toState.getOverviewModalness(),
config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
- setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
- toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile()) ? 1f : 0f, LINEAR);
+ boolean showAsGrid = toState.displayOverviewTasksAsGrid(mLauncher.getDeviceProfile());
+ setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, showAsGrid ? 1f : 0f,
+ showAsGrid ? INSTANT : FINAL_FRAME);
}
abstract FloatProperty getTaskModalnessProperty();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
index c115bbb..c46809a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/DeviceFlag.java
@@ -17,21 +17,17 @@
package com.android.launcher3.uioverrides;
import android.annotation.TargetApi;
-import android.content.Context;
import android.os.Build;
import android.provider.DeviceConfig;
import com.android.launcher3.config.FeatureFlags.DebugFlag;
-import java.util.ArrayList;
-
@TargetApi(Build.VERSION_CODES.P)
public class DeviceFlag extends DebugFlag {
public static final String NAMESPACE_LAUNCHER = "launcher";
private final boolean mDefaultValueInCode;
- ArrayList<Runnable> mListeners;
public DeviceFlag(String key, boolean defaultValue, String description) {
super(key, getDeviceValue(key, defaultValue), description);
@@ -44,53 +40,11 @@
}
@Override
- public void initialize(Context context) {
- super.initialize(context);
- if (mListeners == null) {
- mListeners = new ArrayList<>();
- registerDeviceConfigChangedListener(context);
- }
- }
-
- @Override
- public void addChangeListener(Context context, Runnable r) {
- if (mListeners == null) {
- initialize(context);
- }
- mListeners.add(r);
- }
-
- @Override
- public void removeChangeListener(Runnable r) {
- if (mListeners == null) {
- return;
- }
- mListeners.remove(r);
- }
-
- @Override
public boolean get() {
// Override this method in order to let Robolectric ShadowDeviceFlag to stub it.
return super.get();
}
- private void registerDeviceConfigChangedListener(Context context) {
- DeviceConfig.addOnPropertiesChangedListener(
- NAMESPACE_LAUNCHER,
- context.getMainExecutor(),
- properties -> {
- if (!NAMESPACE_LAUNCHER.equals(properties.getNamespace())
- || !properties.getKeyset().contains(key)) {
- return;
- }
- defaultValue = getDeviceValue(key, mDefaultValueInCode);
- initialize(context);
- for (Runnable r: mListeners) {
- r.run();
- }
- });
- }
-
protected static boolean getDeviceValue(String key, boolean defaultValue) {
return DeviceConfig.getBoolean(NAMESPACE_LAUNCHER, key, defaultValue);
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index ec9893c..2009cd7 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -17,7 +17,6 @@
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
-import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
@@ -55,7 +54,6 @@
import com.android.launcher3.logging.StatsLogManager.StatsLogger;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
import com.android.launcher3.uioverrides.states.QuickstepAtomicAnimationFactory;
@@ -84,7 +82,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
@@ -231,15 +228,6 @@
}
@Override
- public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
- super.bindWorkspaceItemsChanged(updated);
- if (getTaskbarUIController() != null && updated.stream()
- .filter(w -> w.container == CONTAINER_HOTSEAT).findFirst().isPresent()) {
- getTaskbarUIController().onHotseatUpdated();
- }
- }
-
- @Override
public void onDestroy() {
super.onDestroy();
mHotseatPredictionController.destroy();
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 996d36a..6cad3dd 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -106,8 +106,7 @@
float clearAllButtonAlpha = state.areElementsVisible(mLauncher, CLEAR_ALL_BUTTON) ? 1 : 0;
propertySetter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
clearAllButtonAlpha, LINEAR);
- float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS)
- && mRecentsView.shouldShowOverviewActionsForState(state) ? 1 : 0;
+ float overviewButtonAlpha = state.areElementsVisible(mLauncher, OVERVIEW_ACTIONS) ? 1 : 0;
propertySetter.setFloat(mLauncher.getActionsView().getVisibilityAlpha(),
MultiValueAlpha.VALUE, overviewButtonAlpha, config.getInterpolator(
ANIM_OVERVIEW_ACTIONS_FADE, LINEAR));
diff --git a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 8c128c8..90e17c0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -83,16 +83,6 @@
}
@Override
- public float getTaskbarScale(Launcher launcher) {
- return 1f;
- }
-
- @Override
- public float getTaskbarTranslationY(Launcher launcher) {
- return 0f;
- }
-
- @Override
public PageAlphaProvider getWorkspacePageAlphaProvider(Launcher launcher) {
return new PageAlphaProvider(DEACCEL_2) {
@Override
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 180af0b..051485a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -34,7 +34,6 @@
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PendingAnimation;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.touch.BaseSwipeDetector;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -158,26 +157,21 @@
mTaskBeingDragged = view;
int upDirection = mRecentsView.getPagedOrientationHandler()
.getUpDirection(mIsRtl);
- if (!SysUINavigationMode.getMode(mActivity).hasGestures || (
- mActivity.getDeviceProfile().isTablet
- && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
- // Don't allow swipe down to open if we don't support swipe up
- // to enter overview, or when grid layout is enabled.
- directionsToDetectScroll = upDirection;
- mAllowGoingUp = true;
- mAllowGoingDown = false;
- } else {
- // The task can be dragged up to dismiss it,
- // and down to open if it's the current page.
- mAllowGoingUp = true;
- if (i == mRecentsView.getCurrentPage()) {
- mAllowGoingDown = true;
- directionsToDetectScroll = DIRECTION_BOTH;
- } else {
- mAllowGoingDown = false;
- directionsToDetectScroll = upDirection;
- }
- }
+
+ // The task can be dragged up to dismiss it
+ mAllowGoingUp = true;
+
+ // The task can be dragged down to open it if:
+ // - It's the current page
+ // - We support gestures to enter overview
+ // - It's the focused task if in grid view
+ // - The task is snapped
+ mAllowGoingDown = i == mRecentsView.getCurrentPage()
+ && SysUINavigationMode.getMode(mActivity).hasGestures
+ && (!mRecentsView.showAsGrid() || mTaskBeingDragged.isFocusedTask())
+ && mRecentsView.isTaskSnapped(i);
+
+ directionsToDetectScroll = mAllowGoingDown ? DIRECTION_BOTH : upDirection;
break;
}
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index ac1772c..357d407 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -212,6 +212,8 @@
public static final long RECENTS_ATTACH_DURATION = 300;
+ private static final float MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS = 0.07f;
+
/**
* Used as the page index for logging when we return to the last task at the end of the gesture.
*/
@@ -252,6 +254,9 @@
private SwipePipToHomeAnimator mSwipePipToHomeAnimator;
protected boolean mIsSwipingPipToHome;
+ // Interpolate RecentsView scale from start of quick switch scroll until this scroll threshold
+ private final float mQuickSwitchScaleScrollThreshold;
+
public AbsSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
TaskAnimationManager taskAnimationManager, GestureState gestureState,
long touchTimeMs, boolean continuingLastGesture,
@@ -267,6 +272,8 @@
mTaskAnimationManager = taskAnimationManager;
mTouchTimeMs = touchTimeMs;
mContinuingLastGesture = continuingLastGesture;
+ mQuickSwitchScaleScrollThreshold = context.getResources().getDimension(
+ R.dimen.quick_switch_scaling_scroll_threshold);
initAfterSubclassConstructor();
initStateCallbacks();
@@ -539,7 +546,7 @@
@Override
public void onMotionPauseDetected() {
mHasMotionEverBeenPaused = true;
- maybeUpdateRecentsAttachedState();
+ maybeUpdateRecentsAttachedState(true/* animate */, true/* moveFocusedTask */);
performHapticFeedback();
}
@@ -550,18 +557,24 @@
};
}
- public void maybeUpdateRecentsAttachedState() {
+ private void maybeUpdateRecentsAttachedState() {
maybeUpdateRecentsAttachedState(true /* animate */);
}
+ private void maybeUpdateRecentsAttachedState(boolean animate) {
+ maybeUpdateRecentsAttachedState(animate, false /* moveFocusedTask */);
+ }
+
/**
* Determines whether to show or hide RecentsView. The window is always
* synchronized with its corresponding TaskView in RecentsView, so if
* RecentsView is shown, it will appear to be attached to the window.
*
* Note this method has no effect unless the navigation mode is NO_BUTTON.
+ * @param animate whether to animate when attaching RecentsView
+ * @param moveFocusedTask whether to move focused task to front when attaching
*/
- private void maybeUpdateRecentsAttachedState(boolean animate) {
+ private void maybeUpdateRecentsAttachedState(boolean animate, boolean moveFocusedTask) {
if (!mDeviceState.isFullyGesturalNavMode() || mRecentsView == null) {
return;
}
@@ -580,6 +593,12 @@
} else {
recentsAttachedToAppWindow = mHasMotionEverBeenPaused || mIsLikelyToStartNewTask;
}
+ if (moveFocusedTask && !mAnimationFactory.hasRecentsEverAttachedToAppWindow()
+ && recentsAttachedToAppWindow) {
+ // Only move focused task if RecentsView has never been attached before, to avoid
+ // TaskView jumping to new position as we move the tasks.
+ mRecentsView.moveFocusedTaskToFront();
+ }
mAnimationFactory.setRecentsAttachedToAppWindow(recentsAttachedToAppWindow, animate);
// Reapply window transform throughout the attach animation, as the animation affects how
@@ -669,7 +688,8 @@
|| !canCreateNewOrUpdateExistingLauncherTransitionController()) {
return;
}
- mLauncherTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
+ mLauncherTransitionController.setProgress(
+ Math.max(mCurrentShift.value, getScaleProgressDueToScroll()), mDragLengthFactor);
}
/**
@@ -742,6 +762,8 @@
mRecentsAnimationStartCallbacks.clear();
}
+ TaskViewUtils.setDividerBarShown(mRecentsAnimationTargets.nonApps, false);
+
// Only add the callback to enable the input consumer after we actually have the controller
mStateCallback.runOnceAtState(STATE_APP_CONTROLLER_RECEIVED | STATE_GESTURE_STARTED,
mRecentsAnimationController::enableInputConsumer);
@@ -756,6 +778,8 @@
mActivityInitListener.unregister();
mStateCallback.setStateOnUiThread(STATE_GESTURE_CANCELLED | STATE_HANDLER_INVALIDATED);
+ TaskViewUtils.setDividerBarShown(mRecentsAnimationTargets.nonApps, true);
+
// Defer clearing the controller and the targets until after we've updated the state
mRecentsAnimationController = null;
mRecentsAnimationTargets = null;
@@ -884,6 +908,7 @@
break;
case LAST_TASK:
mStateCallback.setState(STATE_RESUME_LAST_TASK);
+ TaskViewUtils.setDividerBarShown(mRecentsAnimationTargets.nonApps, true);
break;
}
ActiveGestureLog.INSTANCE.addLog("onSettledOnEndTarget " + endTarget);
@@ -1020,9 +1045,6 @@
if (mRecentsView != null) {
int nearestPage = mRecentsView.getDestinationPage();
boolean isScrolling = false;
- // Update page scroll before snapping to page to make sure we snapped to the
- // position calculated with target gesture in mind.
- mRecentsView.updateScrollSynchronously();
if (mRecentsView.getNextPage() != nearestPage) {
// We shouldn't really scroll to the next page when swiping up to recents.
// Only allow settling on the next page if it's nearest to the center.
@@ -1128,7 +1150,8 @@
mActivityRestartListener);
mParallelRunningAnim = mActivityInterface.getParallelAnimationToLauncher(
- mGestureState.getEndTarget(), duration);
+ mGestureState.getEndTarget(), duration,
+ mTaskAnimationManager.getCurrentCallbacks());
if (mParallelRunningAnim != null) {
mParallelRunningAnim.start();
}
@@ -1180,7 +1203,8 @@
mLauncherTransitionController = null;
if (mRecentsView != null) {
- mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget());
+ mRecentsView.onPrepareGestureEndAnimation(null, mGestureState.getEndTarget(),
+ mTaskViewSimulator);
}
} else {
AnimatorSet animatorSet = new AnimatorSet();
@@ -1222,7 +1246,7 @@
animatorSet.play(windowAnim);
if (mRecentsView != null) {
mRecentsView.onPrepareGestureEndAnimation(
- animatorSet, mGestureState.getEndTarget());
+ animatorSet, mGestureState.getEndTarget(), mTaskViewSimulator);
}
animatorSet.setDuration(duration).setInterpolator(interpolator);
animatorSet.start();
@@ -1725,6 +1749,9 @@
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
+ if (!controller.getFinishTargetIsLauncher()) {
+ TaskViewUtils.setDividerBarShown(mRecentsAnimationTargets.nonApps, true);
+ }
mRecentsAnimationController = null;
mRecentsAnimationTargets = null;
if (mRecentsView != null) {
@@ -1777,7 +1804,9 @@
*/
protected void applyWindowTransform() {
if (mWindowTransitionController != null) {
- mWindowTransitionController.setProgress(mCurrentShift.value, mDragLengthFactor);
+ mWindowTransitionController.setProgress(
+ Math.max(mCurrentShift.value, getScaleProgressDueToScroll()),
+ mDragLengthFactor);
}
// No need to apply any transform if there is ongoing swipe-pip-to-home animator since
// that animator handles the leash solely.
@@ -1790,6 +1819,35 @@
ProtoTracer.INSTANCE.get(mContext).scheduleFrameUpdate();
}
+ // Scaling of RecentsView during quick switch based on amount of recents scroll
+ private float getScaleProgressDueToScroll() {
+ if (!mActivity.getDeviceProfile().isTablet || mRecentsView == null
+ || !mRecentsViewScrollLinked) {
+ return 0;
+ }
+
+ float scrollOffset = Math.abs(mRecentsView.getScrollOffset(mRecentsView.getCurrentPage()));
+ int maxScrollOffset = mRecentsView.getPagedOrientationHandler().getPrimaryValue(
+ mRecentsView.getLastComputedTaskSize().width(),
+ mRecentsView.getLastComputedTaskSize().height());
+ maxScrollOffset += mRecentsView.getPageSpacing();
+
+ float maxScaleProgress =
+ MAX_QUICK_SWITCH_RECENTS_SCALE_PROGRESS * mRecentsView.getMaxScaleForFullScreen();
+ float scaleProgress = maxScaleProgress;
+
+ if (scrollOffset < mQuickSwitchScaleScrollThreshold) {
+ scaleProgress = Utilities.mapToRange(scrollOffset, 0, mQuickSwitchScaleScrollThreshold,
+ 0, maxScaleProgress, ACCEL_DEACCEL);
+ } else if (scrollOffset > (maxScrollOffset - mQuickSwitchScaleScrollThreshold)) {
+ scaleProgress = Utilities.mapToRange(scrollOffset,
+ (maxScrollOffset - mQuickSwitchScaleScrollThreshold), maxScrollOffset,
+ maxScaleProgress, 0, ACCEL_DEACCEL);
+ }
+
+ return scaleProgress;
+ }
+
/**
* Used for winscope tracing, see launcher_trace.proto
* @see com.android.systemui.shared.tracing.ProtoTraceable#writeToProto
diff --git a/quickstep/src/com/android/quickstep/AnimatedFloat.java b/quickstep/src/com/android/quickstep/AnimatedFloat.java
index f7e8781..95c8710 100644
--- a/quickstep/src/com/android/quickstep/AnimatedFloat.java
+++ b/quickstep/src/com/android/quickstep/AnimatedFloat.java
@@ -53,6 +53,16 @@
mUpdateCallback = updateCallback;
}
+ /**
+ * Returns an animation from the current value to the given value.
+ */
+ public ObjectAnimator animateToValue(float end) {
+ return animateToValue(value, end);
+ }
+
+ /**
+ * Returns an animation from the given start value to the given end value.
+ */
public ObjectAnimator animateToValue(float start, float end) {
cancelAnimation();
mValueAnimator = ObjectAnimator.ofFloat(this, VALUE, start, end);
diff --git a/quickstep/src/com/android/quickstep/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index fac4d52..11fb1d0 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -211,17 +211,18 @@
Rect gridRect = new Rect();
calculateGridSize(context, dp, gridRect);
- int verticalMargin = res.getDimensionPixelSize(
- R.dimen.overview_grid_focus_vertical_margin);
- float taskHeight = gridRect.height() - verticalMargin * 2;
+ int verticalMargin = Math.max(
+ res.getDimensionPixelSize(R.dimen.overview_grid_focus_vertical_margin),
+ res.getDimensionPixelSize(R.dimen.overview_actions_height));
+ float taskHeight =
+ gridRect.height() - verticalMargin * 2 - dp.overviewTaskThumbnailTopMarginPx;
PointF taskDimension = getTaskDimension(context, dp);
- float scale = taskHeight / Math.max(taskDimension.x, taskDimension.y);
+ float scale = taskHeight / taskDimension.y;
int outWidth = Math.round(scale * taskDimension.x);
int outHeight = Math.round(scale * taskDimension.y);
- int gravity = Gravity.CENTER_VERTICAL;
- gravity |= orientedState.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT;
+ int gravity = Gravity.CENTER;
Gravity.apply(gravity, outWidth, outHeight, gridRect, outRect);
} else {
int taskMargin = dp.overviewTaskMarginPx;
@@ -322,8 +323,7 @@
float rowHeight = (gridRect.height() - rowSpacing) / 2f;
PointF taskDimension = getTaskDimension(context, dp);
- float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / Math.max(
- taskDimension.x, taskDimension.y);
+ float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / taskDimension.y;
int outWidth = Math.round(scale * taskDimension.x);
int outHeight = Math.round(scale * taskDimension.y);
@@ -358,7 +358,8 @@
* an optional additional animation with the same duration.
*/
public @Nullable Animator getParallelAnimationToLauncher(
- GestureState.GestureEndTarget endTarget, long duration) {
+ GestureState.GestureEndTarget endTarget, long duration,
+ RecentsAnimationCallbacks callbacks) {
if (endTarget == RECENTS) {
ACTIVITY_TYPE activity = getCreatedActivity();
if (activity == null) {
@@ -399,6 +400,10 @@
default boolean isRecentsAttachedToAppWindow() {
return false;
}
+
+ default boolean hasRecentsEverAttachedToAppWindow() {
+ return false;
+ }
}
class DefaultAnimationFactory implements AnimationFactory {
@@ -408,6 +413,7 @@
private final Consumer<AnimatorControllerWithResistance> mCallback;
private boolean mIsAttachedToWindow;
+ private boolean mHasEverAttachedToWindow;
DefaultAnimationFactory(Consumer<AnimatorControllerWithResistance> callback) {
mCallback = callback;
@@ -461,6 +467,9 @@
}
mIsAttachedToWindow = attached;
RecentsView recentsView = mActivity.getOverviewPanel();
+ if (attached) {
+ mHasEverAttachedToWindow = true;
+ }
Animator fadeAnim = mActivity.getStateManager()
.createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
@@ -490,6 +499,11 @@
return mIsAttachedToWindow;
}
+ @Override
+ public boolean hasRecentsEverAttachedToAppWindow() {
+ return mHasEverAttachedToWindow;
+ }
+
protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
// Scale down recents from being full screen to being in overview.
RecentsView recentsView = activity.getOverviewPanel();
diff --git a/quickstep/src/com/android/quickstep/InputConsumer.java b/quickstep/src/com/android/quickstep/InputConsumer.java
index 0b2a057..3580ee5 100644
--- a/quickstep/src/com/android/quickstep/InputConsumer.java
+++ b/quickstep/src/com/android/quickstep/InputConsumer.java
@@ -39,6 +39,7 @@
int TYPE_OVERSCROLL = 1 << 9;
int TYPE_SYSUI_OVERLAY = 1 << 10;
int TYPE_ONE_HANDED = 1 << 11;
+ int TYPE_TASKBAR_STASH = 1 << 12;
String[] NAMES = new String[] {
"TYPE_NO_OP", // 0
@@ -53,6 +54,7 @@
"TYPE_OVERSCROLL", // 9
"TYPE_SYSUI_OVERLAY", // 10
"TYPE_ONE_HANDED", // 11
+ "TYPE_TASKBAR_STASH", // 12
};
InputConsumer NO_OP = () -> TYPE_NO_OP;
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index fb1391a..5deb75e 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -299,14 +299,15 @@
@Override
public @Nullable Animator getParallelAnimationToLauncher(GestureEndTarget endTarget,
- long duration) {
+ long duration, RecentsAnimationCallbacks callbacks) {
LauncherTaskbarUIController uiController = getTaskbarController();
- Animator superAnimator = super.getParallelAnimationToLauncher(endTarget, duration);
- if (uiController == null) {
+ Animator superAnimator = super.getParallelAnimationToLauncher(
+ endTarget, duration, callbacks);
+ if (uiController == null || callbacks == null) {
return superAnimator;
}
LauncherState toState = stateFromGestureEndTarget(endTarget);
- Animator taskbarAnimator = uiController.createAnimToLauncher(toState, duration);
+ Animator taskbarAnimator = uiController.createAnimToLauncher(toState, callbacks, duration);
if (superAnimator == null) {
return taskbarAnimator;
} else {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
index a21c714..239233b 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationCallbacks.java
@@ -19,6 +19,7 @@
import android.graphics.Rect;
import android.util.ArraySet;
+import android.view.RemoteAnimationTarget;
import androidx.annotation.BinderThread;
import androidx.annotation.UiThread;
@@ -39,6 +40,7 @@
com.android.systemui.shared.system.RecentsAnimationListener {
private final Set<RecentsAnimationListener> mListeners = new ArraySet<>();
+ private final SystemUiProxy mSystemUiProxy;
private final boolean mAllowMinimizeSplitScreen;
// TODO(141886704): Remove these references when they are no longer needed
@@ -46,7 +48,9 @@
private boolean mCancelled;
- public RecentsAnimationCallbacks(boolean allowMinimizeSplitScreen) {
+ public RecentsAnimationCallbacks(SystemUiProxy systemUiProxy,
+ boolean allowMinimizeSplitScreen) {
+ mSystemUiProxy = systemUiProxy;
mAllowMinimizeSplitScreen = allowMinimizeSplitScreen;
}
@@ -89,8 +93,11 @@
RemoteAnimationTargetCompat[] appTargets,
RemoteAnimationTargetCompat[] wallpaperTargets,
Rect homeContentInsets, Rect minimizedHomeBounds) {
+ RemoteAnimationTarget[] nonAppTargets =
+ mSystemUiProxy.onGoingToRecentsLegacy(mCancelled);
RecentsAnimationTargets targets = new RecentsAnimationTargets(appTargets,
- wallpaperTargets, homeContentInsets, minimizedHomeBounds);
+ wallpaperTargets, RemoteAnimationTargetCompat.wrap(nonAppTargets),
+ homeContentInsets, minimizedHomeBounds);
mController = new RecentsAnimationController(animationController,
mAllowMinimizeSplitScreen, this::onAnimationFinished);
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationController.java b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
index 53b6675..f343485 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java
@@ -46,6 +46,8 @@
private boolean mUseLauncherSysBarFlags = false;
private boolean mSplitScreenMinimized = false;
private boolean mFinishRequested = false;
+ // Only valid when mFinishRequested == true.
+ private boolean mFinishTargetIsLauncher;
private RunnableList mPendingFinishCallbacks = new RunnableList();
public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -145,6 +147,7 @@
// Finish not yet requested
mFinishRequested = true;
+ mFinishTargetIsLauncher = toRecents;
mOnFinishedListener.accept(this);
mPendingFinishCallbacks.add(callback);
UI_HELPER_EXECUTOR.execute(() -> {
@@ -217,4 +220,12 @@
public RecentsAnimationControllerCompat getController() {
return mController;
}
+
+ /**
+ * RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
+ * the animation was finished to launcher vs an app.
+ */
+ public boolean getFinishTargetIsLauncher() {
+ return mFinishTargetIsLauncher;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
index 3861bab..b6d9016 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java
@@ -31,9 +31,9 @@
public final Rect minimizedHomeBounds;
public RecentsAnimationTargets(RemoteAnimationTargetCompat[] apps,
- RemoteAnimationTargetCompat[] wallpapers, Rect homeContentInsets,
- Rect minimizedHomeBounds) {
- super(apps, wallpapers, new RemoteAnimationTargetCompat[0], MODE_CLOSING);
+ RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps,
+ Rect homeContentInsets, Rect minimizedHomeBounds) {
+ super(apps, wallpapers, nonApps, MODE_CLOSING);
this.homeContentInsets = homeContentInsets;
this.minimizedHomeBounds = minimizedHomeBounds;
}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index d040904..11ca4b1 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -33,6 +33,8 @@
import android.os.UserHandle;
import android.util.Log;
import android.view.MotionEvent;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import com.android.launcher3.util.MainThreadInitializedObject;
@@ -85,6 +87,7 @@
private float mLastNavButtonAlpha;
private boolean mLastNavButtonAnimate;
private boolean mHasNavButtonAlphaBeenSet = false;
+ private Runnable mPendingSetNavButtonAlpha = null;
// TODO(141886704): Find a way to remove this
private int mLastSystemUiStateFlags;
@@ -157,6 +160,11 @@
setSmartspaceCallback(mPendingSmartspaceCallback);
mPendingSmartspaceCallback = null;
}
+
+ if (mPendingSetNavButtonAlpha != null) {
+ mPendingSetNavButtonAlpha.run();
+ mPendingSetNavButtonAlpha = null;
+ }
}
public void clearProxy() {
@@ -240,14 +248,18 @@
boolean changed = Float.compare(alpha, mLastNavButtonAlpha) != 0
|| animate != mLastNavButtonAnimate
|| !mHasNavButtonAlphaBeenSet;
- if (mSystemUiProxy != null && changed) {
- mLastNavButtonAlpha = alpha;
- mLastNavButtonAnimate = animate;
- mHasNavButtonAlphaBeenSet = true;
- try {
- mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
- } catch (RemoteException e) {
- Log.w(TAG, "Failed call setNavBarButtonAlpha", e);
+ if (changed) {
+ if (mSystemUiProxy == null) {
+ mPendingSetNavButtonAlpha = () -> setNavBarButtonAlpha(alpha, animate);
+ } else {
+ mLastNavButtonAlpha = alpha;
+ mLastNavButtonAnimate = animate;
+ mHasNavButtonAlphaBeenSet = true;
+ try {
+ mSystemUiProxy.setNavBarButtonAlpha(alpha, animate);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call setNavBarButtonAlpha", e);
+ }
}
}
}
@@ -346,7 +358,7 @@
try {
mSystemUiProxy.setSplitScreenMinimized(minimized);
} catch (RemoteException e) {
- Log.w(TAG, "Failed call stopScreenPinning", e);
+ Log.w(TAG, "Failed call setSplitScreenMinimized", e);
}
}
}
@@ -388,6 +400,18 @@
}
@Override
+ public void notifyTaskbarStatus(boolean visible, boolean stashed) {
+ if (mSystemUiProxy != null) {
+ try {
+ mSystemUiProxy.notifyTaskbarStatus(visible, stashed);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call notifyTaskbarStatus with arg: " +
+ visible + ", " + stashed, e);
+ }
+ }
+ }
+
+ @Override
public void handleImageBundleAsScreenshot(Bundle screenImageBundle, Rect locationInScreen,
Insets visibleInsets, Task.TaskKey task) {
if (mSystemUiProxy != null) {
@@ -552,6 +576,22 @@
}
}
+ /**
+ * Start multiple tasks in split-screen simultaneously.
+ */
+ public void startTasksWithLegacyTransition(int mainTaskId, Bundle mainOptions, int sideTaskId,
+ Bundle sideOptions, @SplitConfigurationOptions.StagePosition int sidePosition,
+ RemoteAnimationAdapter adapter) {
+ if (mSystemUiProxy != null) {
+ try {
+ mSplitScreen.startTasksWithLegacyTransition(mainTaskId, mainOptions, sideTaskId,
+ sideOptions, sidePosition, adapter);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call startTasksWithLegacyTransition");
+ }
+ }
+ }
+
public void startShortcut(String packageName, String shortcutId, int stage, int position,
Bundle options, UserHandle user) {
if (mSplitScreen != null) {
@@ -585,6 +625,24 @@
}
}
+ /**
+ * Call this when going to recents so that shell can set-up and provide appropriate leashes
+ * for animation (eg. DividerBar).
+ *
+ * @param cancel true if recents starting is being cancelled.
+ * @return RemoteAnimationTargets of windows that need to animate but only exist in shell.
+ */
+ public RemoteAnimationTarget[] onGoingToRecentsLegacy(boolean cancel) {
+ if (mSplitScreen != null) {
+ try {
+ return mSplitScreen.onGoingToRecentsLegacy(cancel);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call onGoingToRecentsLegacy");
+ }
+ }
+ return null;
+ }
+
//
// One handed
//
diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
index 67bd85f..fe07cbd 100644
--- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java
+++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java
@@ -28,6 +28,7 @@
import android.os.SystemProperties;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.Utilities;
@@ -108,7 +109,8 @@
final BaseActivityInterface activityInterface = gestureState.getActivityInterface();
mLastGestureState = gestureState;
- mCallbacks = new RecentsAnimationCallbacks(activityInterface.allowMinimizeSplitScreen());
+ mCallbacks = new RecentsAnimationCallbacks(SystemUiProxy.INSTANCE.get(mCtx),
+ activityInterface.allowMinimizeSplitScreen());
mCallbacks.addListener(new RecentsAnimationCallbacks.RecentsAnimationListener() {
@Override
public void onRecentsAnimationStart(RecentsAnimationController controller,
@@ -262,6 +264,11 @@
mLastAppearedTaskTarget = null;
}
+ @Nullable
+ public RecentsAnimationCallbacks getCurrentCallbacks() {
+ return mCallbacks;
+ }
+
public void dump() {
// TODO
}
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index e75d751..080533b 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -68,13 +68,14 @@
final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
SystemShortcut shortcut = menuOption.getShortcut(activity, taskView);
- if (menuOption == TaskShortcutFactory.SPLIT_SCREEN &&
- FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
- addSplitOptions(shortcuts, activity, taskView, deviceProfile);
+ if (shortcut == null) {
continue;
}
- if (shortcut != null) {
+ if (menuOption == TaskShortcutFactory.SPLIT_SCREEN &&
+ FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
+ addSplitOptions(shortcuts, activity, taskView, deviceProfile);
+ } else {
shortcuts.add(shortcut);
}
}
@@ -88,7 +89,6 @@
SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT
.getShortcut(activity, taskView);
if (screenshotShortcut != null) {
- screenshotShortcut.setHasFinishRecentsInAction(true);
shortcuts.add(screenshotShortcut);
}
@@ -97,7 +97,6 @@
SystemShortcut modalShortcut = TaskShortcutFactory.MODAL
.getShortcut(activity, taskView);
if (modalShortcut != null) {
- modalShortcut.setHasFinishRecentsInAction(true);
shortcuts.add(modalShortcut);
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 37fda73..f292f1a 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -15,6 +15,7 @@
*/
package com.android.quickstep;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -405,77 +406,39 @@
}
/** Legacy version (until shell transitions are enabled) */
- public static void composeRecentsSplitLaunchAnimatorLegacy(@NonNull AnimatorSet anim,
+ public static void composeRecentsSplitLaunchAnimatorLegacy(@NonNull TaskView initialView,
@NonNull TaskView v, @NonNull RemoteAnimationTargetCompat[] appTargets,
@NonNull RemoteAnimationTargetCompat[] wallpaperTargets,
- @NonNull RemoteAnimationTargetCompat[] nonAppTargets, boolean launcherClosing,
- @NonNull StateManager stateManager, @NonNull DepthController depthController,
- int targetStage) {
- PendingAnimation out = new PendingAnimation(RECENTS_LAUNCH_DURATION);
- boolean isRunningTask = v.isRunningTask();
- TransformParams params = null;
- TaskViewSimulator tvs = null;
- RecentsView recentsView = v.getRecentsView();
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask) {
- params = recentsView.getLiveTileParams();
- tvs = recentsView.getLiveTileTaskViewSimulator();
+ @NonNull RemoteAnimationTargetCompat[] nonAppTargets,
+ @NonNull Runnable finishCallback) {
+
+ final int[] splitRoots = new int[2];
+ for (int i = 0; i < appTargets.length; ++i) {
+ final int taskId = appTargets[i].taskInfo != null ? appTargets[i].taskInfo.taskId : -1;
+ final int mode = appTargets[i].mode;
+ if (taskId == initialView.getTask().key.id || taskId == v.getTask().key.id) {
+ if (mode != MODE_OPENING) {
+ throw new IllegalStateException(
+ "Expected task to be opening, but it is " + mode);
+ }
+ splitRoots[taskId == initialView.getTask().key.id ? 0 : 1] = i;
+ }
}
- boolean inLiveTileMode =
- ENABLE_QUICKSTEP_LIVE_TILE.get() && recentsView.getRunningTaskIndex() != -1;
- final RemoteAnimationTargets targets =
- new RemoteAnimationTargets(appTargets, wallpaperTargets, nonAppTargets,
- inLiveTileMode ? MODE_CLOSING : MODE_OPENING);
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
- if (params == null) {
- SurfaceTransactionApplier applier = new SurfaceTransactionApplier(v);
- targets.addReleaseCheck(applier);
-
- params = new TransformParams()
- .setSyncTransactionApplier(applier)
- .setTargetSet(targets);
+ // This is where we should animate the split roots. For now, though, just make them visible.
+ for (int i = 0; i < 2; ++i) {
+ t.show(appTargets[splitRoots[i]].leash.getSurfaceControl());
+ t.setAlpha(appTargets[splitRoots[i]].leash.getSurfaceControl(), 1.f);
}
- Rect crop = new Rect();
- Context context = v.getContext();
- DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
- if (tvs == null && targets.apps.length > 0) {
- tvs = new TaskViewSimulator(recentsView.getContext(), recentsView.getSizeStrategy());
- tvs.setDp(dp);
+ // This contains the initial state (before animation), so apply this at the beginning of
+ // the animation.
+ t.apply();
- // RecentsView never updates the display rotation until swipe-up so the value may
- // be stale. Use the display value instead.
- int displayRotation = DisplayController.INSTANCE.get(recentsView.getContext())
- .getInfo().rotation;
- tvs.getOrientationState().update(displayRotation, displayRotation);
-
- tvs.setPreview(targets.apps[targets.apps.length - 1]);
- tvs.fullScreenProgress.value = 0;
- tvs.recentsViewScale.value = 1;
-// tvs.setScroll(startScroll);
-
- // Fade in the task during the initial 20% of the animation
- out.addFloat(params, TransformParams.TARGET_ALPHA, 0, 1,
- clampToProgress(LINEAR, 0, 0.2f));
- }
-
- TaskViewSimulator topMostSimulator = null;
-
- if (tvs != null) {
- out.setFloat(tvs.fullScreenProgress,
- AnimatedFloat.VALUE, 1, TOUCH_RESPONSE_INTERPOLATOR);
- out.setFloat(tvs.recentsViewScale,
- AnimatedFloat.VALUE, tvs.getFullScreenScale(), TOUCH_RESPONSE_INTERPOLATOR);
- out.setFloat(tvs.recentsViewScroll,
- AnimatedFloat.VALUE, 0, TOUCH_RESPONSE_INTERPOLATOR);
-
- TaskViewSimulator finalTsv = tvs;
- TransformParams finalParams = params;
- out.addOnFrameCallback(() -> finalTsv.apply(finalParams));
- topMostSimulator = tvs;
- }
-
- anim.play(out.buildAnim());
+ // Once there is an animation, this should be called AFTER the animation completes.
+ finishCallback.run();
}
public static void composeRecentsLaunchAnimator(@NonNull AnimatorSet anim, @NonNull View v,
@@ -490,6 +453,10 @@
PendingAnimation pa = new PendingAnimation(RECENTS_LAUNCH_DURATION);
createRecentsWindowAnimator(taskView, skipLauncherChanges, appTargets, wallpaperTargets,
nonAppTargets, depthController, pa);
+ if (launcherClosing) {
+ // TODO(b/182592057): differentiate between "restore split" vs "launch fullscreen app"
+ TaskViewUtils.setDividerBarShown(nonAppTargets, true);
+ }
Animator childStateAnimation = null;
// Found a visible recents task that matches the opening app, lets launch the app from there
@@ -542,4 +509,19 @@
stateManager.setCurrentAnimation(anim, childStateAnimation);
anim.addListener(windowAnimEndListener);
}
+
+ static void setDividerBarShown(RemoteAnimationTargetCompat[] nonApps, boolean shown) {
+ // TODO(b/182592057): make this part of the animations instead.
+ if (nonApps != null && nonApps.length > 0) {
+ for (int i = 0; i < nonApps.length; ++i) {
+ final RemoteAnimationTargetCompat targ = nonApps[i];
+ if (targ.windowType == TYPE_DOCK_DIVIDER) {
+ SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ t.setVisibility(targ.leash.getSurfaceControl(), shown);
+ t.apply();
+ t.close();
+ }
+ }
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 069ed11..b038c7b 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -96,6 +96,7 @@
import com.android.quickstep.inputconsumers.ResetGestureInputConsumer;
import com.android.quickstep.inputconsumers.ScreenPinnedInputConsumer;
import com.android.quickstep.inputconsumers.SysUiOverlayInputConsumer;
+import com.android.quickstep.inputconsumers.TaskbarStashInputConsumer;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AssistantUtilities;
import com.android.quickstep.util.ProtoTracer;
@@ -262,16 +263,35 @@
}
@Override
- public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
+ public void onSplitScreenSecondaryBoundsChanged(Rect bounds, Rect insets) {
WindowBounds wb = new WindowBounds(bounds, insets);
MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
}
@Override
- public void onImeWindowStatusChanged(int displayId, IBinder token, int vis,
- int backDisposition, boolean showImeSwitcher) {
- MAIN_EXECUTOR.execute(() -> mTaskbarManager.updateImeStatus(
- displayId, vis, backDisposition, showImeSwitcher));
+ public void onRotationProposal(int rotation, boolean isValid) {
+ executeForTaskbarManager(() -> mTaskbarManager.onRotationProposal(rotation, isValid));
+ }
+
+ @Override
+ public void disable(int displayId, int state1, int state2, boolean animate) {
+ executeForTaskbarManager(() -> mTaskbarManager
+ .disableNavBarElements(displayId, state1, state2, animate));
+ }
+
+ @Override
+ public void onSystemBarAttributesChanged(int displayId, int behavior) {
+ executeForTaskbarManager(() -> mTaskbarManager
+ .onSystemBarAttributesChanged(displayId, behavior));
+ }
+
+ private void executeForTaskbarManager(final Runnable r) {
+ MAIN_EXECUTOR.execute(() -> {
+ if (mTaskbarManager == null) {
+ return;
+ }
+ r.run();
+ });
}
public TaskbarManager getTaskbarManager() {
@@ -287,7 +307,6 @@
return sConnected;
}
-
public static boolean isInitialized() {
return sIsInitialized;
}
@@ -655,6 +674,14 @@
mDeviceState, event);
}
+ // If Taskbar is present, we listen for long press to unstash it.
+ BaseActivityInterface activityInterface = newGestureState.getActivityInterface();
+ StatefulActivity activity = activityInterface.getCreatedActivity();
+ if (activity != null && activity.getDeviceProfile().isTaskbarPresent) {
+ base = new TaskbarStashInputConsumer(this, base, mInputMonitorCompat,
+ mTaskbarManager.getCurrentActivityContext());
+ }
+
if (FeatureFlags.ENABLE_QUICK_CAPTURE_GESTURE.get()) {
OverscrollPlugin plugin = null;
if (FeatureFlags.FORCE_LOCAL_OVERSCROLL_PLUGIN.get()) {
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
index f0364eb..50b69dc 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsStateController.java
@@ -15,6 +15,8 @@
*/
package com.android.quickstep.fallback;
+import static com.android.launcher3.anim.Interpolators.FINAL_FRAME;
+import static com.android.launcher3.anim.Interpolators.INSTANT;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_MODAL;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_OVERVIEW_SCALE;
@@ -77,9 +79,7 @@
float clearAllButtonAlpha = state.hasClearAllButton() ? 1 : 0;
setter.setFloat(mRecentsView.getClearAllButton(), ClearAllButton.VISIBILITY_ALPHA,
clearAllButtonAlpha, LINEAR);
- float overviewButtonAlpha =
- state.hasOverviewActions() && mRecentsView.shouldShowOverviewActionsForState(state)
- ? 1 : 0;
+ float overviewButtonAlpha = state.hasOverviewActions() ? 1 : 0;
setter.setFloat(mActivity.getActionsView().getVisibilityAlpha(),
MultiValueAlpha.VALUE, overviewButtonAlpha, LINEAR);
@@ -94,8 +94,9 @@
setter.setFloat(mRecentsView, TASK_MODALNESS, state.getOverviewModalness(),
config.getInterpolator(ANIM_OVERVIEW_MODAL, LINEAR));
setter.setFloat(mRecentsView, FULLSCREEN_PROGRESS, state.isFullScreen() ? 1 : 0, LINEAR);
- setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS,
- state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile()) ? 1f : 0f, LINEAR);
+ boolean showAsGrid = state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile());
+ setter.setFloat(mRecentsView, RECENTS_GRID_PROGRESS, showAsGrid ? 1f : 0f,
+ showAsGrid ? INSTANT : FINAL_FRAME);
setter.setViewBackgroundColor(mActivity.getScrimView(), state.getScrimColor(mActivity),
config.getInterpolator(ANIM_SCRIM_FADE, LINEAR));
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index ac3fb27..3bf79f1 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -26,17 +26,17 @@
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
-import android.util.Log;
+import android.view.MotionEvent;
import androidx.annotation.Nullable;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.statemanager.StateManager.StateListener;
-import com.android.launcher3.testing.TestProtocol;
import com.android.quickstep.FallbackActivityInterface;
import com.android.quickstep.GestureState;
import com.android.quickstep.RecentsActivity;
+import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.SplitPlaceholderView;
@@ -90,8 +90,9 @@
*/
@Override
public void onPrepareGestureEndAnimation(
- @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) {
- super.onPrepareGestureEndAnimation(animatorSet, endTarget);
+ @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
+ TaskViewSimulator taskViewSimulator) {
+ super.onPrepareGestureEndAnimation(animatorSet, endTarget, taskViewSimulator);
if (mHomeTaskInfo != null && endTarget == RECENTS && animatorSet != null) {
TaskView tv = getTaskView(mHomeTaskInfo.taskId);
if (tv != null) {
@@ -122,6 +123,12 @@
}
}
+ @Nullable
+ @Override
+ protected TaskView getHomeTaskView() {
+ return mHomeTaskInfo != null ? getTaskView(mHomeTaskInfo.taskId) : null;
+ }
+
@Override
protected boolean shouldAddStubTaskView(RunningTaskInfo runningTaskInfo) {
if (mHomeTaskInfo != null && runningTaskInfo != null &&
@@ -205,4 +212,11 @@
setDisallowScrollToClearAll(!state.hasClearAllButton());
}
}
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ boolean result = super.onTouchEvent(ev);
+ // Do not let touch escape to siblings below this view.
+ return result || mActivity.getStateManager().getState().overviewUi();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsState.java b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
index b6cfdce..111a940 100644
--- a/quickstep/src/com/android/quickstep/fallback/RecentsState.java
+++ b/quickstep/src/com/android/quickstep/fallback/RecentsState.java
@@ -40,15 +40,16 @@
private static final int FLAG_SHOW_AS_GRID = BaseState.getFlag(4);
private static final int FLAG_SCRIM = BaseState.getFlag(5);
private static final int FLAG_LIVE_TILE = BaseState.getFlag(6);
+ private static final int FLAG_OVERVIEW_UI = BaseState.getFlag(7);
public static final RecentsState DEFAULT = new RecentsState(0,
FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_SHOW_AS_GRID | FLAG_SCRIM
- | FLAG_LIVE_TILE);
+ | FLAG_LIVE_TILE | FLAG_OVERVIEW_UI);
public static final RecentsState MODAL_TASK = new ModalState(1,
FLAG_DISABLE_RESTORE | FLAG_CLEAR_ALL_BUTTON | FLAG_OVERVIEW_ACTIONS | FLAG_MODAL
- | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE);
+ | FLAG_SHOW_AS_GRID | FLAG_SCRIM | FLAG_LIVE_TILE | FLAG_OVERVIEW_UI);
public static final RecentsState BACKGROUND_APP = new BackgroundAppState(2,
- FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN);
+ FLAG_DISABLE_RESTORE | FLAG_NON_INTERACTIVE | FLAG_FULL_SCREEN | FLAG_OVERVIEW_UI);
public static final RecentsState HOME = new RecentsState(3, 0);
public static final RecentsState BG_LAUNCHER = new LauncherState(4, 0);
@@ -140,6 +141,13 @@
return deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get();
}
+ /**
+ * True if the state has overview panel visible.
+ */
+ public boolean overviewUi() {
+ return hasFlag(FLAG_OVERVIEW_UI);
+ }
+
private static class ModalState extends RecentsState {
public ModalState(int id, int flags) {
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
new file mode 100644
index 0000000..dbe260a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarStashInputConsumer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2021 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.quickstep.inputconsumers;
+
+import static com.android.launcher3.Utilities.squaredHypot;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.MotionEvent;
+
+import com.android.launcher3.Utilities;
+import com.android.launcher3.taskbar.TaskbarActivityContext;
+import com.android.quickstep.InputConsumer;
+import com.android.systemui.shared.system.InputMonitorCompat;
+
+/**
+ * Listens for a long press, and cancels the current gesture if that causes Taskbar to be unstashed.
+ */
+public class TaskbarStashInputConsumer extends DelegateInputConsumer {
+
+ private final TaskbarActivityContext mTaskbarActivityContext;
+ private final GestureDetector mLongPressDetector;
+ private final float mSquaredTouchSlop;
+
+ private float mDownX, mDownY;
+ private boolean mCanceledUnstashHint;
+
+ public TaskbarStashInputConsumer(Context context, InputConsumer delegate,
+ InputMonitorCompat inputMonitor, TaskbarActivityContext taskbarActivityContext) {
+ super(delegate, inputMonitor);
+ mTaskbarActivityContext = taskbarActivityContext;
+ mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
+
+ mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
+ @Override
+ public void onLongPress(MotionEvent motionEvent) {
+ onLongPressDetected(motionEvent);
+ }
+ });
+ }
+
+ @Override
+ public int getType() {
+ return TYPE_TASKBAR_STASH | mDelegate.getType();
+ }
+
+ @Override
+ public void onMotionEvent(MotionEvent ev) {
+ mLongPressDetector.onTouchEvent(ev);
+ if (mState != STATE_ACTIVE) {
+ mDelegate.onMotionEvent(ev);
+
+ if (mTaskbarActivityContext != null) {
+ final float x = ev.getRawX();
+ final float y = ev.getRawY();
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDownX = x;
+ mDownY = y;
+ mTaskbarActivityContext.startTaskbarUnstashHint(
+ /* animateForward = */ true);
+ mCanceledUnstashHint = false;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (!mCanceledUnstashHint
+ && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
+ mTaskbarActivityContext.startTaskbarUnstashHint(
+ /* animateForward = */ false);
+ mCanceledUnstashHint = true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (!mCanceledUnstashHint) {
+ mTaskbarActivityContext.startTaskbarUnstashHint(
+ /* animateForward = */ false);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ private void onLongPressDetected(MotionEvent motionEvent) {
+ if (mTaskbarActivityContext != null
+ && mTaskbarActivityContext.onLongPressToUnstashTaskbar()) {
+ setActive(motionEvent);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
index 7eca360..6bdc284 100644
--- a/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
+++ b/quickstep/src/com/android/quickstep/logging/SettingsChangeLogger.java
@@ -16,7 +16,7 @@
package com.android.quickstep.logging;
-import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_HOTSEAT_COUNT;
+import static com.android.launcher3.InvariantDeviceProfile.KEY_MIGRATION_SRC_WORKSPACE_SIZE;
import static com.android.launcher3.Utilities.getDevicePrefs;
import static com.android.launcher3.Utilities.getPrefs;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_GRID_SIZE_2;
@@ -43,6 +43,7 @@
import com.android.launcher3.AutoInstallsLayout;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
@@ -133,7 +134,8 @@
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (LAST_PREDICTION_ENABLED_STATE.equals(key) || KEY_MIGRATION_SRC_HOTSEAT_COUNT.equals(key)
+ if (LAST_PREDICTION_ENABLED_STATE.equals(key)
+ || KEY_MIGRATION_SRC_WORKSPACE_SIZE.equals(key)
|| mLoggablePrefs.containsKey(key)) {
dispatchUserEvent();
}
@@ -153,23 +155,25 @@
SharedPreferences prefs = getPrefs(mContext);
StatsLogManager.LauncherEvent gridSizeChangedEvent = null;
- // TODO(b/184981523): This doesn't work for 2-panel grid, which has 6 hotseat icons
- switch (prefs.getInt(KEY_MIGRATION_SRC_HOTSEAT_COUNT, -1)) {
- case 5:
- gridSizeChangedEvent = LAUNCHER_GRID_SIZE_5;
- break;
- case 4:
- gridSizeChangedEvent = LAUNCHER_GRID_SIZE_4;
- break;
- case 3:
- gridSizeChangedEvent = LAUNCHER_GRID_SIZE_3;
- break;
- case 2:
- gridSizeChangedEvent = LAUNCHER_GRID_SIZE_2;
- break;
- default:
- // Ignore illegal input.
- break;
+ String workspaceSize = prefs.getString(KEY_MIGRATION_SRC_WORKSPACE_SIZE, null);
+ if (workspaceSize != null) {
+ switch (Utilities.parsePoint(workspaceSize).x) {
+ case 5:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_5;
+ break;
+ case 4:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_4;
+ break;
+ case 3:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_3;
+ break;
+ case 2:
+ gridSizeChangedEvent = LAUNCHER_GRID_SIZE_2;
+ break;
+ default:
+ // Ignore illegal input.
+ break;
+ }
}
if (gridSizeChangedEvent != null) {
logger.log(gridSizeChangedEvent);
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index 7f94839..f1b4e3d 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -49,6 +49,7 @@
private enum RecentsResistanceParams {
FROM_APP(0.75f, 0.5f, 1f),
+ FROM_APP_TABLET(0.9f, 0.75f, 1f),
FROM_OVERVIEW(1f, 0.75f, 0.5f);
RecentsResistanceParams(float scaleStartResist, float scaleMaxResist,
@@ -228,7 +229,7 @@
// These are not required, or can have a default value that is generally correct.
@Nullable public PendingAnimation resistAnim = null;
- public RecentsResistanceParams resistanceParams = RecentsResistanceParams.FROM_APP;
+ public RecentsResistanceParams resistanceParams;
public float startScale = 1f;
public float startTranslation = 0f;
@@ -242,6 +243,11 @@
this.scaleProperty = scaleProperty;
this.translationTarget = translationTarget;
this.translationProperty = translationProperty;
+ if (dp.isTablet) {
+ resistanceParams = RecentsResistanceParams.FROM_APP_TABLET;
+ } else {
+ resistanceParams = RecentsResistanceParams.FROM_APP;
+ }
}
private RecentsParams setResistAnim(PendingAnimation resistAnim) {
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index a147b68..2351a4e 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -22,33 +22,26 @@
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
-import android.animation.AnimatorSet;
-import android.app.ActivityOptions;
+import android.app.ActivityThread;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
-import android.util.Pair;
import android.view.Gravity;
+import android.view.RemoteAnimationAdapter;
import android.view.SurfaceControl;
import android.window.TransitionInfo;
import androidx.annotation.Nullable;
-import com.android.launcher3.BaseActivity;
-import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InsettableFrameLayout;
-import com.android.launcher3.LauncherAnimationRunner;
-import com.android.launcher3.LauncherAnimationRunner.RemoteAnimationFactory;
import com.android.launcher3.R;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskAnimationManager;
import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.views.TaskView;
-import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
@@ -92,37 +85,27 @@
? new int[]{mInitialTaskView.getTask().key.id, taskView.getTask().key.id}
: new int[]{taskView.getTask().key.id, mInitialTaskView.getTask().key.id};
- RemoteSplitLaunchAnimationRunner animationRunner =
- new RemoteSplitLaunchAnimationRunner(mInitialTaskView, taskView);
+ RemoteSplitLaunchTransitionRunner animationRunner =
+ new RemoteSplitLaunchTransitionRunner(mInitialTaskView, taskView);
mSystemUiProxy.startTasks(taskIds[0], null /* mainOptions */, taskIds[1],
null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT,
new RemoteTransitionCompat(animationRunner, MAIN_EXECUTOR));
- return;
+ } else {
+ // Assume initial task is for top/left part of screen
+ final int[] taskIds = mInitialPosition.mStagePosition == STAGE_POSITION_TOP_OR_LEFT
+ ? new int[]{mInitialTaskView.getTask().key.id, taskView.getTask().key.id}
+ : new int[]{taskView.getTask().key.id, mInitialTaskView.getTask().key.id};
+
+ RemoteSplitLaunchAnimationRunner animationRunner =
+ new RemoteSplitLaunchAnimationRunner(mInitialTaskView, taskView);
+ final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter(
+ RemoteAnimationAdapterCompat.wrapRemoteAnimationRunner(animationRunner),
+ 300, 150,
+ ActivityThread.currentActivityThread().getApplicationThread());
+
+ mSystemUiProxy.startTasksWithLegacyTransition(taskIds[0], null /* mainOptions */,
+ taskIds[1], null /* sideOptions */, STAGE_POSITION_BOTTOM_OR_RIGHT, adapter);
}
- // Assume initial mInitialTaskId is for top/left part of screen
- RemoteAnimationFactory initialSplitRunnerWrapped = new SplitLaunchAnimationRunner(
- mInitialTaskView, 0);
- RemoteAnimationFactory secondarySplitRunnerWrapped = new SplitLaunchAnimationRunner(
- taskView, 1);
- RemoteAnimationRunnerCompat initialSplitRunner = new LauncherAnimationRunner(
- new Handler(Looper.getMainLooper()), initialSplitRunnerWrapped,
- true /* startAtFrontOfQueue */);
- RemoteAnimationRunnerCompat secondarySplitRunner = new LauncherAnimationRunner(
- new Handler(Looper.getMainLooper()), secondarySplitRunnerWrapped,
- true /* startAtFrontOfQueue */);
- ActivityOptions initialOptions = ActivityOptionsCompat.makeRemoteAnimation(
- new RemoteAnimationAdapterCompat(initialSplitRunner, 300, 150));
- ActivityOptions secondaryOptions = ActivityOptionsCompat.makeRemoteAnimation(
- new RemoteAnimationAdapterCompat(secondarySplitRunner, 300, 150));
- mSystemUiProxy.startTask(mInitialTaskView.getTask().key.id, mInitialPosition.mStageType,
- mInitialPosition.mStagePosition,
- /*null*/ initialOptions.toBundle());
- Pair<Integer, Integer> compliment = getComplimentaryStageAndPosition(mInitialPosition);
- mSystemUiProxy.startTask(taskView.getTask().key.id, compliment.first,
- compliment.second,
- /*null*/ secondaryOptions.toBundle());
- // After successful launch, call resetState
- resetState();
}
/**
@@ -153,12 +136,12 @@
/**
* Requires Shell Transitions
*/
- private class RemoteSplitLaunchAnimationRunner implements RemoteTransitionRunner {
+ private class RemoteSplitLaunchTransitionRunner implements RemoteTransitionRunner {
private final TaskView mInitialTaskView;
private final TaskView mTaskView;
- RemoteSplitLaunchAnimationRunner(TaskView initialTaskView, TaskView taskView) {
+ RemoteSplitLaunchTransitionRunner(TaskView initialTaskView, TaskView taskView) {
mInitialTaskView = initialTaskView;
mTaskView = taskView;
}
@@ -175,48 +158,34 @@
/**
* LEGACY
- * @return the opposite stage and position from the {@param position} provided as first and
- * second object, respectively
- * Ex. If position is has stage = Main and position = Top/Left, this will return
- * Pair(stage=Side, position=Bottom/Left)
- */
- private Pair<Integer, Integer> getComplimentaryStageAndPosition(SplitPositionOption position) {
- // Right now this is as simple as flipping between 0 and 1
- int complimentStageType = position.mStageType ^ 1;
- int complimentStagePosition = position.mStagePosition ^ 1;
- return new Pair<>(complimentStageType, complimentStagePosition);
- }
-
- /**
- * LEGACY
* Remote animation runner for animation to launch an app.
*/
- private class SplitLaunchAnimationRunner implements RemoteAnimationFactory {
+ private class RemoteSplitLaunchAnimationRunner implements RemoteAnimationRunnerCompat {
- private final TaskView mV;
- private final int mTargetState;
+ private final TaskView mInitialTaskView;
+ private final TaskView mTaskView;
- SplitLaunchAnimationRunner(TaskView v, int targetState) {
- mV = v;
- mTargetState = targetState;
+ RemoteSplitLaunchAnimationRunner(TaskView initialTaskView, TaskView taskView) {
+ mInitialTaskView = initialTaskView;
+ mTaskView = taskView;
}
@Override
- public void onCreateAnimation(int transit,
- RemoteAnimationTargetCompat[] appTargets,
- RemoteAnimationTargetCompat[] wallpaperTargets,
- RemoteAnimationTargetCompat[] nonAppTargets,
- LauncherAnimationRunner.AnimationResult result) {
- AnimatorSet anim = new AnimatorSet();
- BaseQuickstepLauncher activity = BaseActivity.fromContext(mV.getContext());
- TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(anim, mV,
- appTargets, wallpaperTargets, nonAppTargets, true, activity.getStateManager(),
- activity.getDepthController(), mTargetState);
- result.setAnimation(anim, activity);
+ public void onAnimationStart(int transit, RemoteAnimationTargetCompat[] apps,
+ RemoteAnimationTargetCompat[] wallpapers, RemoteAnimationTargetCompat[] nonApps,
+ Runnable finishedCallback) {
+ TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(mInitialTaskView, mTaskView, apps,
+ wallpapers, nonApps, finishedCallback);
+ // After successful launch, call resetState
+ resetState();
+ }
+
+ @Override
+ public void onAnimationCancelled() {
+ resetState();
}
}
-
/**
* To be called if split select was cancelled
*/
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index c97ca32..7eee415 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -15,7 +15,6 @@
*/
package com.android.quickstep.util;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.states.RotationHelper.deltaRotation;
import static com.android.launcher3.touch.PagedOrientationHandler.MATRIX_POST_TRANSLATE;
import static com.android.quickstep.util.RecentsOrientedState.postDisplayRotation;
@@ -62,7 +61,6 @@
private final boolean mIsRecentsRtl;
private final Rect mTaskRect = new Rect();
- private boolean mDrawsBelowRecents;
private final PointF mPivot = new PointF();
private DeviceProfile mDp;
@@ -163,10 +161,6 @@
recentsViewScroll.value = scroll;
}
- public void setDrawsBelowRecents(boolean drawsBelowRecents) {
- mDrawsBelowRecents = drawsBelowRecents;
- }
-
/**
* Adds animation for all the components corresponding to transition from an app to overview.
*/
@@ -256,8 +250,8 @@
float fullScreenProgress = Utilities.boundToRange(this.fullScreenProgress.value, 0, 1);
mCurrentFullscreenParams.setProgress(
- fullScreenProgress, recentsViewScale.value, mTaskRect.width(), mDp,
- mPositionHelper);
+ fullScreenProgress, recentsViewScale.value, /*taskViewScale=*/1f, mTaskRect.width(),
+ mDp, mPositionHelper);
// Apply thumbnail matrix
RectF insets = mCurrentFullscreenParams.mCurrentDrawnInsets;
@@ -301,12 +295,6 @@
builder.withMatrix(mMatrix)
.withWindowCrop(mTmpCropRect)
.withCornerRadius(getCurrentCornerRadius());
-
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && params.getRecentsSurface() != null) {
- // When relativeLayer = 0, it reverts the surfaces back to the original order.
- builder.withRelativeLayerTo(params.getRecentsSurface(),
- mDrawsBelowRecents ? Integer.MIN_VALUE : 0);
- }
}
/**
diff --git a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
index 8c115e5..563bb53 100644
--- a/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
+++ b/quickstep/src/com/android/quickstep/views/OverviewActionsView.java
@@ -180,7 +180,6 @@
} else {
mDisabledFlags &= ~disabledFlags;
}
- //
boolean isEnabled = (mDisabledFlags & ~DISABLED_ROTATED) == 0;
LayoutUtils.setViewEnabled(this, isEnabled);
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4e1bcc9..edda439 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -32,7 +32,6 @@
import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.Utilities.squaredTouchSlop;
import static com.android.launcher3.anim.Interpolators.ACCEL;
-import static com.android.launcher3.anim.Interpolators.ACCEL_0_5;
import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
@@ -129,8 +128,8 @@
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.ResourceBasedOverride.Overrides;
-import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.RunnableList;
+import com.android.launcher3.util.SplitConfigurationOptions;
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
import com.android.launcher3.util.Themes;
import com.android.launcher3.util.TranslateEdgeEffect;
@@ -348,6 +347,9 @@
private static final int ADDITION_TASK_DURATION = 200;
private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f;
private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f;
+ private static final float ANIMATION_DISMISS_PROGRESS_MIDPOINT = 0.5f;
+
+ private static final float SIGNIFICANT_MOVE_THRESHOLD_TABLET = 0.15f;
protected final RecentsOrientedState mOrientationState;
protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
@@ -355,6 +357,13 @@
protected SurfaceTransactionApplier mSyncTransactionApplier;
protected int mTaskWidth;
protected int mTaskHeight;
+ // Used to position the top of a task in the top row of the grid
+ private float mTaskGridVerticalDiff;
+ // The vertical space one grid task takes + space between top and bottom row.
+ private float mTopBottomRowHeightDiff;
+ // mTaskGridVerticalDiff and mTopBottomRowHeightDiff summed together provides the top
+ // position for bottom row of grid tasks.
+
protected final TransformParams mLiveTileParams = new TransformParams();
protected final TaskViewSimulator mLiveTileTaskViewSimulator;
protected final Rect mLastComputedTaskSize = new Rect();
@@ -367,6 +376,7 @@
protected final Rect mTempRect = new Rect();
protected final RectF mTempRectF = new RectF();
private final PointF mTempPointF = new PointF();
+ private final Matrix mTempMatrix = new Matrix();
private final float[] mTempFloat = new float[1];
private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>();
@@ -493,9 +503,8 @@
protected boolean mRunningTaskTileHidden;
private Task mTmpRunningTask;
protected int mFocusedTaskId = -1;
- private float mFocusedTaskRatio;
- private boolean mRunningTaskIconScaledDown = false;
+ private boolean mTaskIconScaledDown = false;
private boolean mRunningTaskShowScreenshot = false;
private boolean mOverviewStateEnabled;
@@ -554,6 +563,10 @@
* removed from recentsView
*/
private int mSplitHiddenTaskViewIndex;
+ /**
+ * The task to be removed and immediately re-added. Should not be added to task pool.
+ */
+ private TaskView mMovingTaskView;
// Keeps track of the index where the first TaskView should be
private int mTaskViewStartIndex = 0;
@@ -632,7 +645,6 @@
mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy());
mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
mLiveTileTaskViewSimulator.setOrientationState(mOrientationState);
- mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
mTintingColor = getForegroundScrimDimColor(context);
}
@@ -818,11 +830,14 @@
public void onViewRemoved(View child) {
super.onViewRemoved(child);
- // Clear the task data for the removed child if it was visible unless it's the initial
- // taskview for entering split screen, we only pretend to dismiss the task
- if (child instanceof TaskView && child != mSplitHiddenTaskView) {
+ // Clear the task data for the removed child if it was visible unless:
+ // - It's the initial taskview for entering split screen, we only pretend to dismiss the
+ // task
+ // - It's the focused task to be moved to the front, we immediately re-add the task
+ if (child instanceof TaskView && child != mSplitHiddenTaskView
+ && child != mMovingTaskView) {
TaskView taskView = (TaskView) child;
- mHasVisibleTaskData.delete(taskView.getTask().key.id);
+ mHasVisibleTaskData.delete(taskView.getTaskId());
mTaskViewPool.recycle(taskView);
mActionsView.updateHiddenFlags(HIDDEN_NO_TASKS, getTaskViewCount() == 0);
}
@@ -941,10 +956,20 @@
&& taskEnd <= end);
}
+ /**
+ * Returns true if the task is snapped.
+ *
+ * @param taskIndex the index of the task
+ */
+ public boolean isTaskSnapped(int taskIndex) {
+ return getScrollForPage(taskIndex + mTaskViewStartIndex)
+ == getPagedOrientationHandler().getPrimaryScroll(this);
+ }
+
public TaskView getTaskView(int taskId) {
for (int i = 0; i < getTaskViewCount(); i++) {
TaskView taskView = getTaskViewAt(i);
- if (taskView.hasTaskId(taskId)) {
+ if (taskView.getTaskId() == taskId) {
return taskView;
}
}
@@ -988,6 +1013,7 @@
super.onPageEndTransition();
if (isClearAllHidden()) {
mActionsView.updateDisabledFlags(OverviewActionsView.DISABLED_SCROLLING, false);
+ } else {
}
if (getNextPage() > 0) {
setSwipeDownShouldLaunchApp(true);
@@ -995,6 +1021,12 @@
}
@Override
+ protected float getSignificantMoveThreshold() {
+ return mActivity.getDeviceProfile().isTablet ? SIGNIFICANT_MOVE_THRESHOLD_TABLET
+ : super.getSignificantMoveThreshold();
+ }
+
+ @Override
public boolean onTouchEvent(MotionEvent ev) {
super.onTouchEvent(ev);
@@ -1096,6 +1128,42 @@
}
}
+ /**
+ * Moves the focused task to the front of the carousel in tablets, to minimize animation
+ * required to focus the task in grid.
+ */
+ public void moveFocusedTaskToFront() {
+ if (!(mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
+ return;
+ }
+
+ TaskView focusedTaskView = getFocusedTaskView();
+ if (focusedTaskView == null) {
+ return;
+ }
+
+ if (indexOfChild(focusedTaskView) != mCurrentPage) {
+ return;
+ }
+
+ if (mCurrentPage == mTaskViewStartIndex) {
+ return;
+ }
+
+ int primaryScroll = mOrientationHandler.getPrimaryScroll(this);
+ int currentPageScroll = getScrollForPage(mCurrentPage);
+ mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
+
+ mMovingTaskView = focusedTaskView;
+ removeView(focusedTaskView);
+ mMovingTaskView = null;
+ focusedTaskView.onRecycle();
+ addView(focusedTaskView, mTaskViewStartIndex);
+ setCurrentPage(mTaskViewStartIndex);
+
+ updateGridProperties();
+ }
+
protected void applyLoadPlan(ArrayList<Task> tasks) {
if (mPendingAnimation != null) {
mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks));
@@ -1143,6 +1211,11 @@
final TaskView taskView = (TaskView) getChildAt(pageIndex);
taskView.bind(task, mOrientationState);
}
+
+ // If the list changed, maybe the focused task doesn't exist anymore
+ if (getFocusedTaskView() == null && getTaskViewCount() > 0) {
+ mFocusedTaskId = getTaskViewAt(0).getTaskId();
+ }
updateTaskSize();
if (mNextPage == INVALID_PAGE) {
@@ -1204,8 +1277,9 @@
public void resetTaskVisuals() {
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
TaskView taskView = getTaskViewAt(i);
- if (mIgnoreResetTaskId != taskView.getTask().key.id) {
+ if (mIgnoreResetTaskId != taskView.getTaskId()) {
taskView.resetViewTransforms();
+ taskView.setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
taskView.setStableAlpha(mContentAlpha);
taskView.setFullscreenProgress(mFullscreenProgress);
taskView.setModalness(mTaskModalness);
@@ -1220,6 +1294,8 @@
mLiveTileTaskViewSimulator.fullScreenProgress.value = 0;
mLiveTileTaskViewSimulator.recentsViewScale.value = 1;
+ mLiveTileParams.setTargetAlpha(1);
+
// Similar to setRunningTaskHidden below, reapply the state before runningTaskView is
// null.
if (!mRunningTaskShowScreenshot) {
@@ -1230,11 +1306,6 @@
setRunningTaskHidden(mRunningTaskTileHidden);
}
- // Force apply the scale.
- if (mIgnoreResetTaskId != mRunningTaskId) {
- applyRunningTaskIconScale();
- }
-
updateCurveProperties();
// Update the set of visible task's data
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
@@ -1341,6 +1412,11 @@
mSizeStrategy.calculateGridTaskSize(mActivity, mActivity.getDeviceProfile(),
mLastComputedGridTaskSize, mOrientationHandler);
+ mTaskGridVerticalDiff = mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
+ mTopBottomRowHeightDiff =
+ mLastComputedGridTaskSize.height() + dp.overviewTaskThumbnailTopMarginPx
+ + mRowSpacing;
+
// Force TaskView to update size from thumbnail
updateTaskSize();
@@ -1371,6 +1447,15 @@
* Updates TaskView scaling and translation required to support variable width.
*/
private void updateTaskSize() {
+ updateTaskSize(false);
+ }
+
+ /**
+ * Updates TaskView scaling and translation required to support variable width.
+ *
+ * @param isTaskDismissal indicates if update was called due to task dismissal
+ */
+ private void updateTaskSize(boolean isTaskDismissal) {
final int taskCount = getTaskViewCount();
if (taskCount == 0) {
return;
@@ -1380,18 +1465,17 @@
for (int i = 0; i < taskCount; i++) {
TaskView taskView = getTaskViewAt(i);
taskView.updateTaskSize();
- taskView.getPrimaryFullscreenTranslationProperty().set(taskView,
- accumulatedTranslationX);
- taskView.getSecondaryFullscreenTranslationProperty().set(taskView, 0f);
+ taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
+ taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
// Compensate space caused by TaskView scaling.
float widthDiff =
- taskView.getLayoutParams().width * (1 - taskView.getFullscreenScale());
+ taskView.getLayoutParams().width * (1 - taskView.getNonGridScale());
accumulatedTranslationX += mIsRtl ? widthDiff : -widthDiff;
}
mClearAllButton.setFullscreenTranslationPrimary(accumulatedTranslationX);
- updateGridProperties();
+ updateGridProperties(isTaskDismissal);
}
public void getTaskSize(Rect outRect) {
@@ -1406,19 +1490,7 @@
public Point getSelectedTaskSize() {
mSizeStrategy.calculateTaskSize(mActivity, mActivity.getDeviceProfile(), mTempRect,
mOrientationHandler);
- int taskWidth = mTempRect.width();
- int taskHeight = mTempRect.height();
- if (mFocusedTaskId != -1) {
- int boxLength = Math.max(taskWidth, taskHeight);
- if (mFocusedTaskRatio > 1) {
- taskWidth = boxLength;
- taskHeight = (int) (boxLength / mFocusedTaskRatio);
- } else {
- taskWidth = (int) (boxLength * mFocusedTaskRatio);
- taskHeight = boxLength;
- }
- }
- return new Point(taskWidth, taskHeight);
+ return new Point(mTempRect.width(), mTempRect.height());
}
/** Gets the last computed task size */
@@ -1450,13 +1522,7 @@
loadVisibleTaskData(TaskView.FLAG_UPDATE_ALL);
// After scrolling, update ActionsView's visibility.
- TaskView focusedTaskView = getFocusedTaskView();
- if (focusedTaskView != null) {
- float scrollDiff = Math.abs(getScrollForPage(indexOfChild(focusedTaskView))
- - mOrientationHandler.getPrimaryScroll(this));
- float delta = (mGridSideMargin - scrollDiff) / (float) mGridSideMargin;
- mActionsView.getScrollAlpha().setValue(Utilities.boundToRange(delta, 0, 1));
- }
+ updateActionsViewScrollAlpha();
}
// Update the high res thumbnail loader state
@@ -1464,6 +1530,20 @@
return scrolling;
}
+ private void updateActionsViewScrollAlpha() {
+ float scrollAlpha = 1f;
+ if (showAsGrid()) {
+ TaskView focusedTaskView = getFocusedTaskView();
+ if (focusedTaskView != null) {
+ float scrollDiff = Math.abs(getScrollForPage(indexOfChild(focusedTaskView))
+ - mOrientationHandler.getPrimaryScroll(this));
+ float delta = (mGridSideMargin - scrollDiff) / (float) mGridSideMargin;
+ scrollAlpha = Utilities.boundToRange(delta, 0, 1);
+ }
+ }
+ mActionsView.getScrollAlpha().setValue(scrollAlpha);
+ }
+
/**
* Scales and adjusts translation of adjacent pages as if on a curved carousel.
*/
@@ -1505,7 +1585,8 @@
* and unloads the associated task data for tasks that are no longer visible.
*/
public void loadVisibleTaskData(@TaskView.TaskDataChanges int dataChanges) {
- if (!mOverviewStateEnabled || mTaskListChangeId == -1) {
+ boolean hasLeftOverview = !mOverviewStateEnabled && mScroller.isFinished();
+ if (hasLeftOverview || mTaskListChangeId == -1) {
// Skip loading visible task data if we've already left the overview state, or if the
// task list hasn't been loaded yet (the task views will not reflect the task list)
return;
@@ -1606,7 +1687,7 @@
setCurrentTask(-1);
mIgnoreResetTaskId = -1;
mTaskListChangeId = -1;
- mFocusedTaskId = -1;
+ mFocusedTaskId = getTaskViewCount() > 0 ? getTaskViewAt(0).getTaskId() : -1;
if (mRecentsAnimationController != null) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile) {
@@ -1618,7 +1699,6 @@
}
setEnableDrawingLiveTile(false);
mLiveTileParams.setTargetSet(null);
- mLiveTileTaskViewSimulator.setDrawsBelowRecents(true);
// These are relatively expensive and don't need to be done this frame (RecentsView isn't
// visible anyway), so defer by a frame to get off the critical path, e.g. app to home.
@@ -1648,11 +1728,8 @@
return getTaskView(mFocusedTaskId);
}
- /**
- * Returns the width to height ratio of the focused {@link TaskView}.
- */
- public float getFocusedTaskRatio() {
- return mFocusedTaskRatio;
+ protected @Nullable TaskView getHomeTaskView() {
+ return null;
}
/**
@@ -1691,7 +1768,7 @@
setEnableFreeScroll(false);
setEnableDrawingLiveTile(false);
setRunningTaskHidden(true);
- setRunningTaskIconScaledDown(true);
+ setTaskIconScaledDown(true);
}
/**
@@ -1699,9 +1776,7 @@
* {@link #onGestureAnimationStart} and {@link #onGestureAnimationEnd()}.
*/
public void onSwipeUpAnimationSuccess() {
- if (getRunningTaskView() != null) {
- animateUpRunningTaskIconScale();
- }
+ animateUpTaskIconScale();
setSwipeDownShouldLaunchApp(true);
}
@@ -1748,24 +1823,33 @@
* Called when a gesture from an app has finished, and an end target has been determined.
*/
public void onPrepareGestureEndAnimation(
- @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget) {
+ @Nullable AnimatorSet animatorSet, GestureState.GestureEndTarget endTarget,
+ TaskViewSimulator taskViewSimulator) {
+ mCurrentGestureEndTarget = endTarget;
+ if (endTarget == GestureState.GestureEndTarget.RECENTS) {
+ setEnableFreeScroll(true);
+ updateGridProperties();
+ }
+
if (mSizeStrategy.stateFromGestureEndTarget(endTarget)
.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
+ TaskView runningTaskView = getRunningTaskView();
+ float runningTaskPrimaryGridTranslation = 0;
+ if (runningTaskView != null && indexOfChild(runningTaskView) != getNextPage()) {
+ // Apply the gird translation to running task unless it's being snapped to.
+ runningTaskPrimaryGridTranslation = mOrientationHandler.getPrimaryValue(
+ runningTaskView.getGridTranslationX(),
+ runningTaskView.getGridTranslationY());
+ }
if (animatorSet == null) {
setGridProgress(1);
+ taskViewSimulator.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
+ animatorSet.play(taskViewSimulator.taskPrimaryTranslation.animateToValue(
+ runningTaskPrimaryGridTranslation));
}
}
- mCurrentGestureEndTarget = endTarget;
- if (endTarget == GestureState.GestureEndTarget.NEW_TASK
- || endTarget == GestureState.GestureEndTarget.LAST_TASK) {
- // When switching to tasks in quick switch, ensures the snapped page's scroll maintain
- // invariant between quick switch and overview, to ensure a smooth animation transition.
- updateGridProperties();
- } else if (endTarget == GestureState.GestureEndTarget.RECENTS) {
- setEnableFreeScroll(true);
- }
}
/**
@@ -1783,12 +1867,8 @@
setRunningTaskViewShowScreenshot(true);
}
setRunningTaskHidden(false);
- animateUpRunningTaskIconScale();
-
- if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.RECENTS
- && (!showAsGrid() || getFocusedTaskView() != null)) {
- animateActionsViewIn();
- }
+ animateUpTaskIconScale();
+ animateActionsViewIn();
mCurrentGestureEndTarget = null;
}
@@ -1830,9 +1910,7 @@
boolean runningTaskTileHidden = mRunningTaskTileHidden;
int runningTaskId = runningTaskInfo == null ? -1 : runningTaskInfo.taskId;
setCurrentTask(runningTaskId);
- if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
- setFocusedTask(runningTaskId);
- }
+ mFocusedTaskId = runningTaskId;
setCurrentPage(getRunningTaskIndex());
setRunningTaskViewShowScreenshot(false);
setRunningTaskHidden(runningTaskTileHidden);
@@ -1853,7 +1931,7 @@
if (mRunningTaskId != -1) {
// Reset the state on the old running task view
- setRunningTaskIconScaledDown(false);
+ setTaskIconScaledDown(false);
setRunningTaskViewShowScreenshot(true);
setRunningTaskHidden(false);
}
@@ -1861,15 +1939,6 @@
}
/**
- * Sets the focused task id and store the width to height ratio of the focused task.
- */
- protected void setFocusedTask(int focusedTaskId) {
- mFocusedTaskId = focusedTaskId;
- mFocusedTaskRatio =
- mLastComputedTaskSize.width() / (float) mLastComputedTaskSize.height();
- }
-
- /**
* Hides the tile associated with {@link #mRunningTaskId}
*/
public void setRunningTaskHidden(boolean isHidden) {
@@ -1894,21 +1963,13 @@
}
}
- public void setRunningTaskIconScaledDown(boolean isScaledDown) {
- if (mRunningTaskIconScaledDown != isScaledDown) {
- mRunningTaskIconScaledDown = isScaledDown;
- applyRunningTaskIconScale();
- }
- }
-
- public boolean isTaskIconScaledDown(TaskView taskView) {
- return mRunningTaskIconScaledDown && getRunningTaskView() == taskView;
- }
-
- private void applyRunningTaskIconScale() {
- TaskView firstTask = getRunningTaskView();
- if (firstTask != null) {
- firstTask.setIconScaleAndDim(mRunningTaskIconScaledDown ? 0 : 1);
+ public void setTaskIconScaledDown(boolean isScaledDown) {
+ if (mTaskIconScaledDown != isScaledDown) {
+ mTaskIconScaledDown = isScaledDown;
+ int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ getTaskViewAt(i).setIconScaleAndDim(mTaskIconScaledDown ? 0 : 1);
+ }
}
}
@@ -1919,19 +1980,13 @@
anim.start();
}
- private void animateActionsViewOut() {
- ObjectAnimator anim = ObjectAnimator.ofFloat(
- mActionsView.getVisibilityAlpha(), MultiValueAlpha.VALUE, 1, 0);
- anim.setDuration(TaskView.SCALE_ICON_DURATION);
- anim.start();
- }
-
- public void animateUpRunningTaskIconScale() {
- mRunningTaskIconScaledDown = false;
- TaskView firstTask = getRunningTaskView();
- if (firstTask != null) {
- firstTask.setIconScaleAnimStartProgress(0f);
- firstTask.animateIconScaleAndDimIntoView();
+ public void animateUpTaskIconScale() {
+ mTaskIconScaledDown = false;
+ int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ taskView.setIconScaleAnimStartProgress(0f);
+ taskView.animateIconScaleAndDimIntoView();
}
}
@@ -1957,20 +2012,8 @@
return;
}
- final int boxLength = Math.max(mLastComputedGridTaskSize.width(),
- mLastComputedGridTaskSize.height());
int taskTopMargin = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
- /*
- * taskGridVerticalDiff is used to position the top of a task in the top row of the grid
- * heightOffset is the vertical space one grid task takes + space between top and
- * bottom row
- * Summed together they provide the top position for bottom row of grid tasks
- */
- final float taskGridVerticalDiff =
- mLastComputedGridTaskSize.top - mLastComputedTaskSize.top;
- final float heightOffset = (boxLength + taskTopMargin) + mRowSpacing;
-
int topRowWidth = 0;
int bottomRowWidth = 0;
float topAccumulatedTranslationX = 0;
@@ -1990,6 +2033,8 @@
int snappedTaskRowWidth = 0;
int snappedPage = getNextPage();
TaskView snappedTaskView = getTaskViewAtByAbsoluteIndex(snappedPage);
+ TaskView homeTaskView = getHomeTaskView();
+ TaskView nextFocusedTaskView = null;
if (!isTaskDismissal) {
mTopRowIdSet.clear();
@@ -2027,15 +2072,20 @@
// calculate the distance focused task need to shift.
focusedTaskShift += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
}
- int taskId = taskView.getTask().key.id;
+ int taskId = taskView.getTaskId();
boolean isTopRow = isTaskDismissal ? mTopRowIdSet.contains(taskId)
: topRowWidth <= bottomRowWidth;
if (isTopRow) {
- topRowWidth += taskWidthAndSpacing;
+ if (homeTaskView != null && nextFocusedTaskView == null) {
+ // TaskView will be focused when swipe up, don't count towards row width.
+ nextFocusedTaskView = taskView;
+ } else {
+ topRowWidth += taskWidthAndSpacing;
+ }
topSet.add(i);
mTopRowIdSet.add(taskId);
- taskView.setGridTranslationY(taskGridVerticalDiff);
+ taskView.setGridTranslationY(mTaskGridVerticalDiff);
// Move horizontally into empty space.
float widthOffset = 0;
@@ -2054,7 +2104,7 @@
bottomSet.add(i);
// Move into bottom row.
- taskView.setGridTranslationY(heightOffset + taskGridVerticalDiff);
+ taskView.setGridTranslationY(mTopBottomRowHeightDiff + mTaskGridVerticalDiff);
// Move horizontally into empty space.
float widthOffset = 0;
@@ -2078,22 +2128,14 @@
// We need to maintain snapped task's page scroll invariant between quick switch and
// overview, so we sure snapped task's grid translation is 0, and add a non-fullscreen
// translationX that is the same as snapped task's full scroll adjustment.
- float snappedTaskFullscreenScrollAdjustment = 0;
+ float snappedTaskNonGridScrollAdjustment = 0;
float snappedTaskGridTranslationX = 0;
if (snappedTaskView != null) {
- snappedTaskFullscreenScrollAdjustment = snappedTaskView.getScrollAdjustment(
+ snappedTaskNonGridScrollAdjustment = snappedTaskView.getScrollAdjustment(
/*fullscreenEnabled=*/true, /*gridEnabled=*/false);
snappedTaskGridTranslationX = gridTranslations[snappedPage - mTaskViewStartIndex];
}
- for (int i = 0; i < taskCount; i++) {
- TaskView taskView = getTaskViewAt(i);
- taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX);
- taskView.getPrimaryNonFullscreenTranslationProperty().set(taskView,
- snappedTaskFullscreenScrollAdjustment);
- taskView.getSecondaryNonFullscreenTranslationProperty().set(taskView, 0f);
- }
-
// Use the accumulated translation of the row containing the last task.
float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
? topAccumulatedTranslationX : bottomAccumulatedTranslationX;
@@ -2126,7 +2168,7 @@
float clearAllTotalTranslationX =
clearAllAccumulatedTranslation + clearAllShorterRowCompensation
- + clearAllShortTotalCompensation + snappedTaskFullscreenScrollAdjustment;
+ + clearAllShortTotalCompensation + snappedTaskNonGridScrollAdjustment;
if (focusedTaskIndex < taskCount) {
// Shift by focused task's width and spacing if a task is focused.
clearAllTotalTranslationX +=
@@ -2136,15 +2178,23 @@
// Make sure there are enough space between snapped page and ClearAllButton, for the case
// of swiping up after quick switch.
if (snappedTaskView != null) {
- int distanceFromClearAll = longRowWidth - snappedTaskRowWidth;
+ int distanceFromClearAll = longRowWidth - snappedTaskRowWidth + mPageSpacing;
+ // ClearAllButton should be off screen when snapped task is in its snapped position.
int minimumDistance =
- mLastComputedGridSize.width() - snappedTaskView.getLayoutParams().width;
+ mTaskWidth - snappedTaskView.getLayoutParams().width
+ + (mLastComputedGridSize.width() - mTaskWidth) / 2;
if (distanceFromClearAll < minimumDistance) {
int distanceDifference = minimumDistance - distanceFromClearAll;
- clearAllTotalTranslationX += mIsRtl ? -distanceDifference : distanceDifference;
+ snappedTaskGridTranslationX += mIsRtl ? distanceDifference : -distanceDifference;
}
}
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX
+ + snappedTaskNonGridScrollAdjustment);
+ }
+
mClearAllButton.setGridTranslationPrimary(
clearAllTotalTranslationX - snappedTaskGridTranslationX);
mClearAllButton.setGridScrollOffset(
@@ -2158,8 +2208,8 @@
if (taskView1 == null || taskView2 == null) {
return false;
}
- int taskId1 = taskView1.getTask().key.id;
- int taskId2 = taskView2.getTask().key.id;
+ int taskId1 = taskView1.getTaskId();
+ int taskId2 = taskView2.getTaskId();
if (taskId1 == mFocusedTaskId || taskId2 == mFocusedTaskId) {
return false;
}
@@ -2237,6 +2287,10 @@
// Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
// alpha is set to 0 so that it can be recycled in the view pool properly
anim.setFloat(taskView, VIEW_ALPHA, 0, clampToProgress(ACCEL, 0, 0.5f));
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
+ anim.setFloat(mLiveTileParams, TransformParams.TARGET_ALPHA, 0,
+ clampToProgress(ACCEL, 0, 0.5f));
+ }
SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
ResourceProvider rp = DynamicResource.provider(mActivity);
@@ -2289,8 +2343,16 @@
}
}
- public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView,
- boolean shouldRemoveTask, long duration) {
+ /**
+ * Creates a {@link PendingAnimation} for dismissing the specified {@link TaskView}.
+ * @param dismissedTaskView the {@link TaskView} to be dismissed
+ * @param animateTaskView whether the {@link TaskView} to be dismissed should be animated
+ * @param shouldRemoveTask whether the associated {@link Task} should be removed from
+ * ActivityManager after dismissal
+ * @param duration duration of the animation
+ */
+ public PendingAnimation createTaskDismissAnimation(TaskView dismissedTaskView,
+ boolean animateTaskView, boolean shouldRemoveTask, long duration) {
if (mPendingAnimation != null) {
mPendingAnimation.createPlaybackController().dispatchOnCancel().dispatchOnEnd();
}
@@ -2301,30 +2363,65 @@
return anim;
}
+ boolean showAsGrid = showAsGrid();
+ int taskCount = getTaskViewCount();
+ int dismissedIndex = indexOfChild(dismissedTaskView);
+ int dismissedTaskId = dismissedTaskView.getTaskId();
+
+ // Grid specific properties.
+ boolean isFocusedTaskDismissed = false;
+ TaskView nextFocusedTaskView = null;
+ boolean nextFocusedTaskFromTop = false;
+ float dismissedTaskWidth = 0;
+ float nextFocusedTaskWidth = 0;
+
+ // Non-grid specific properties.
int[] oldScroll = new int[count];
int[] newScroll = new int[count];
- getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
- getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
- int taskCount = getTaskViewCount();
int scrollDiffPerPage = 0;
- if (count > 1) {
- scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
- }
- int draggedIndex = indexOfChild(taskView);
-
- boolean isFocusedTaskDismissed = taskView.getTask().key.id == mFocusedTaskId;
- if (isFocusedTaskDismissed && showAsGrid()) {
- anim.setFloat(mActionsView, VIEW_ALPHA, 0, clampToProgress(ACCEL_0_5, 0, 0.5f));
- }
- float dismissedTaskWidth = taskView.getLayoutParams().width + mPageSpacing;
boolean needsCurveUpdates = false;
+
+ if (showAsGrid) {
+ dismissedTaskWidth = dismissedTaskView.getLayoutParams().width + mPageSpacing;
+ isFocusedTaskDismissed = dismissedTaskId == mFocusedTaskId;
+ if (isFocusedTaskDismissed) {
+ nextFocusedTaskFromTop =
+ mTopRowIdSet.size() > 0 && mTopRowIdSet.size() >= (taskCount - 1) / 2f;
+ // Pick the next focused task from the preferred row.
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ if (taskView == dismissedTaskView) {
+ continue;
+ }
+ boolean isTopRow = mTopRowIdSet.contains(taskView.getTaskId());
+ if ((nextFocusedTaskFromTop && isTopRow
+ || (!nextFocusedTaskFromTop && !isTopRow))) {
+ nextFocusedTaskView = taskView;
+ break;
+ }
+ }
+ if (nextFocusedTaskView != null) {
+ nextFocusedTaskWidth =
+ nextFocusedTaskView.getLayoutParams().width + mPageSpacing;
+ }
+ }
+ } else {
+ getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+ getPageScrolls(newScroll, false,
+ v -> v.getVisibility() != GONE && v != dismissedTaskView);
+ if (count > 1) {
+ scrollDiffPerPage = Math.abs(oldScroll[1] - oldScroll[0]);
+ }
+ }
+
+ int distanceFromDismissedTask = 0;
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
- if (child == taskView) {
+ if (child == dismissedTaskView) {
if (animateTaskView) {
- addDismissedTaskAnimations(taskView, duration, anim);
+ addDismissedTaskAnimations(dismissedTaskView, duration, anim);
}
- } else if (!showAsGrid()) {
+ } else if (!showAsGrid) {
// Compute scroll offsets from task dismissal for animation.
// If we just take newScroll - oldScroll, everything to the right of dragged task
// translates to the left. We need to offset this in some cases:
@@ -2333,15 +2430,15 @@
// - Current page is rightmost page (leftmost for RTL)
// - Dragging an adjacent page on the left side (right side for RTL)
int offset = mIsRtl ? scrollDiffPerPage : 0;
- if (mCurrentPage == draggedIndex) {
+ if (mCurrentPage == dismissedIndex) {
int lastPage = taskCount - 1;
if (mCurrentPage == lastPage) {
offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
}
} else {
- // Dragging an adjacent page.
+ // Dismissing an adjacent page.
int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR)
- if (draggedIndex == negativeAdjacent) {
+ if (dismissedIndex == negativeAdjacent) {
offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage;
}
}
@@ -2354,7 +2451,7 @@
float additionalDismissDuration =
ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
- i - draggedIndex);
+ i - dismissedIndex);
anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR,
Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ additionalDismissDuration, 0f, 1f), 1));
@@ -2371,21 +2468,46 @@
needsCurveUpdates = true;
}
} else if (child instanceof TaskView) {
+ TaskView taskView = (TaskView) child;
+ if (isFocusedTaskDismissed) {
+ if (!isSameGridRow(taskView, nextFocusedTaskView)) {
+ continue;
+ }
+ } else {
+ if (i < dismissedIndex || !isSameGridRow(taskView, dismissedTaskView)) {
+ continue;
+ }
+ }
// Animate task with index >= dismissed index and in the same row as the
- // dismissed index, or if the dismissed task was the focused task. Offset
- // successive task dismissal durations for a staggered effect.
- if (isFocusedTaskDismissed || (i >= draggedIndex && isSameGridRow((TaskView) child,
- taskView))) {
- FloatProperty translationProperty =
- ((TaskView) child).getPrimaryDismissTranslationProperty();
- float additionalDismissDuration =
- ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
- i - draggedIndex);
- anim.setFloat(child, translationProperty,
- !mIsRtl ? -dismissedTaskWidth : dismissedTaskWidth,
- clampToProgress(LINEAR, Utilities.boundToRange(
- INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
- + additionalDismissDuration, 0f, 1f), 1));
+ // dismissed index or next focused index. Offset successive task dismissal
+ // durations for a staggered effect.
+ float animationStartProgress = Utilities.boundToRange(
+ INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ + ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+ * ++distanceFromDismissedTask, 0f, 1f);
+ if (taskView == nextFocusedTaskView) {
+ // Enlarge the task to be focused next, and translate into focus position.
+ float scale = mTaskWidth / (float) mLastComputedGridTaskSize.width();
+ anim.setFloat(taskView, TaskView.SNAPSHOT_SCALE, scale,
+ clampToProgress(LINEAR, animationStartProgress, 1f));
+ anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
+ mIsRtl ? dismissedTaskWidth : -dismissedTaskWidth,
+ clampToProgress(LINEAR, animationStartProgress, 1f));
+ float secondaryTranslation = -mTaskGridVerticalDiff;
+ if (!nextFocusedTaskFromTop) {
+ secondaryTranslation -= mTopBottomRowHeightDiff;
+ }
+ anim.setFloat(taskView, taskView.getSecondaryDissmissTranslationProperty(),
+ secondaryTranslation,
+ clampToProgress(LINEAR, animationStartProgress, 1f));
+ anim.setFloat(taskView, TaskView.FOCUS_TRANSITION, 0f,
+ clampToProgress(LINEAR, 0f, ANIMATION_DISMISS_PROGRESS_MIDPOINT));
+ } else {
+ float primaryTranslation =
+ isFocusedTaskDismissed ? nextFocusedTaskWidth : dismissedTaskWidth;
+ anim.setFloat(taskView, taskView.getPrimaryDismissTranslationProperty(),
+ mIsRtl ? primaryTranslation : -primaryTranslation,
+ clampToProgress(LINEAR, animationStartProgress, 1f));
}
}
}
@@ -2396,15 +2518,16 @@
// Add a tiny bit of translation Z, so that it draws on top of other views
if (animateTaskView) {
- taskView.setTranslationZ(0.1f);
+ dismissedTaskView.setTranslationZ(0.1f);
}
mPendingAnimation = anim;
+ final TaskView finalNextFocusedTaskView = nextFocusedTaskView;
mPendingAnimation.addEndListener(new Consumer<Boolean>() {
@Override
public void accept(Boolean success) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && mEnableDrawingLiveTile
- && taskView.isRunningTask() && success) {
+ && dismissedTaskView.isRunningTask() && success) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
() -> onEnd(success));
} else {
@@ -2416,15 +2539,16 @@
private void onEnd(boolean success) {
if (success) {
if (shouldRemoveTask) {
- if (taskView.getTask() != null) {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask()) {
+ if (dismissedTaskView.getTask() != null) {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()
+ && dismissedTaskView.isRunningTask()) {
finishRecentsAnimation(true /* toRecents */, false /* shouldPip */,
- () -> removeTaskInternal(taskView));
+ () -> removeTaskInternal(dismissedTaskId));
} else {
- removeTaskInternal(taskView);
+ removeTaskInternal(dismissedTaskId);
}
mActivity.getStatsLogManager().logger()
- .withItemInfo(taskView.getItemInfo())
+ .withItemInfo(dismissedTaskView.getItemInfo())
.log(LAUNCHER_TASK_DISMISS_SWIPE_UP);
}
}
@@ -2434,32 +2558,34 @@
resetTaskVisuals();
int pageToSnapTo = mCurrentPage;
- // Snap to start if focused task was dismissed, as after quick switch it could
- // be at any page but the focused task always displays at the start.
- if (taskView.getTask().key.id == mFocusedTaskId) {
- pageToSnapTo = mTaskViewStartIndex;
- } else if (draggedIndex < pageToSnapTo || pageToSnapTo == (getTaskViewCount()
- - 1)) {
+ if ((dismissedIndex < pageToSnapTo && !showAsGrid)
+ || pageToSnapTo == taskCount - 1) {
pageToSnapTo -= 1;
}
- removeViewInLayout(taskView);
+ if (showAsGrid) {
+ int primaryScroll = mOrientationHandler.getPrimaryScroll(RecentsView.this);
+ int currentPageScroll = getScrollForPage(pageToSnapTo);
+ mCurrentPageScrollDiff = primaryScroll - currentPageScroll;
+ }
+ removeViewInLayout(dismissedTaskView);
+ mTopRowIdSet.remove(dismissedTaskId);
- if (getTaskViewCount() == 0) {
+ if (taskCount == 1) {
removeViewInLayout(mClearAllButton);
startHome();
} else {
- snapToPageImmediately(pageToSnapTo);
- dispatchScrollChanged();
- // Grid got messed up, reapply.
- updateGridProperties(true);
- if (showAsGrid() && getFocusedTaskView() == null
- && mActionsView.getVisibilityAlpha().getValue() == 1) {
- animateActionsViewOut();
+ // Update focus task and its size.
+ if (finalNextFocusedTaskView != null) {
+ mFocusedTaskId = finalNextFocusedTaskView.getTaskId();
+ mTopRowIdSet.remove(mFocusedTaskId);
+ finalNextFocusedTaskView.animateIconScaleAndDimIntoView();
}
+ updateTaskSize(true);
+ // Update scroll and snap to page.
+ updateScrollSynchronously();
+ setCurrentPage(pageToSnapTo);
+ dispatchScrollChanged();
}
- // Update the layout synchronously so that the position of next view is
- // immediately available.
- onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
}
onDismissAnimationEnds();
mPendingAnimation = null;
@@ -2468,10 +2594,9 @@
return anim;
}
- private void removeTaskInternal(TaskView taskView) {
- UI_HELPER_EXECUTOR.getHandler().postDelayed(() ->
- ActivityManagerWrapper.getInstance().removeTask(
- taskView.getTask().key.id),
+ private void removeTaskInternal(int dismissedTaskId) {
+ UI_HELPER_EXECUTOR.getHandler().postDelayed(
+ () -> ActivityManagerWrapper.getInstance().removeTask(dismissedTaskId),
REMOVE_TASK_WAIT_FOR_APP_STOP_MS);
}
@@ -2502,7 +2627,8 @@
int taskCount = getTaskViewCount();
for (int i = 0; i < taskCount; i++) {
TaskView taskView = getTaskViewAt(i);
- if (taskView == mSplitHiddenTaskView && taskView != getFocusedTaskView()) {
+ if (taskView == mSplitHiddenTaskView
+ && !(showAsGrid() && taskView == getFocusedTaskView())) {
// Case where the hidden task view would have overlapped w/ placeholder,
// but because it's going to hide we don't care
// TODO (b/187312247) edge case for thumbnails that are off screen but scroll on
@@ -2517,6 +2643,7 @@
}
protected void onDismissAnimationEnds() {
+ AccessibilityManagerCompat.sendDismissAnimationEndsEventToTest(getContext());
}
public PendingAnimation createAllTasksDismissAnimation(long duration) {
@@ -2650,7 +2777,7 @@
mContentAlpha = alpha;
for (int i = getTaskViewCount() - 1; i >= 0; i--) {
TaskView child = getTaskViewAt(i);
- if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) {
+ if (!mRunningTaskTileHidden || child.getTaskId() != mRunningTaskId) {
child.setStableAlpha(alpha);
}
}
@@ -2779,6 +2906,12 @@
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // If we're going to HOME, avoid unnecessary onLayout that cause TaskViews to re-arrange
+ // during animation to HOME.
+ if (mCurrentGestureEndTarget == GestureState.GestureEndTarget.HOME) {
+ return;
+ }
+
super.onLayout(changed, left, top, right, bottom);
updateEmptyStateUi(changed);
@@ -2793,6 +2926,7 @@
mLastComputedTaskStartPushOutDistance = null;
mLastComputedTaskEndPushOutDistance = null;
updatePageOffsets();
+ mLiveTileTaskViewSimulator.setScroll(getScrollOffset());
setImportantForAccessibility(isModal() ? IMPORTANT_FOR_ACCESSIBILITY_NO
: IMPORTANT_FOR_ACCESSIBILITY_AUTO);
}
@@ -2875,6 +3009,12 @@
outRect.offset(taskView.getPersistentTranslationX(),
taskView.getPersistentTranslationY());
outRect.top += mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
+
+ mTempMatrix.reset();
+ float persistentScale = taskView.getPersistentScale();
+ mTempMatrix.postScale(persistentScale, persistentScale,
+ mIsRtl ? outRect.right : outRect.left, outRect.top);
+ mTempMatrix.mapRect(outRect);
}
outRect.offset(mOrientationHandler.getPrimaryValue(-midPointScroll, 0),
mOrientationHandler.getSecondaryValue(-midPointScroll, 0));
@@ -2993,6 +3133,9 @@
splitController.getLayoutParamsForActivePosition(getResources(),
mActivity.getDeviceProfile()));
mSplitPlaceholderView.setIcon(taskView.getIconView());
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
+ finishRecentsAnimation(true, null);
+ }
}
public PendingAnimation createSplitSelectInitAnimation() {
@@ -3480,9 +3623,13 @@
}
/**
- * Updates page scroll synchronously and layout child views.
+ * Updates page scroll synchronously after measure and layout child views.
*/
public void updateScrollSynchronously() {
+ // onMeasure is needed to update child's measured width which is used in scroll calculation,
+ // in case TaskView sizes has changed when being focused/unfocused.
+ onMeasure(makeMeasureSpec(getMeasuredWidth(), EXACTLY),
+ makeMeasureSpec(getMeasuredHeight(), EXACTLY));
onLayout(false /* changed */, getLeft(), getTop(), getRight(), getBottom());
updateMinAndMaxScrollX();
}
@@ -3545,18 +3692,28 @@
}
boolean pageScrollChanged = false;
- final int childCount = getChildCount();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- float scrollDiff = 0;
- if (child instanceof TaskView) {
- scrollDiff = ((TaskView) child).getScrollAdjustment(showAsFullscreen, showAsGrid);
- } else if (child instanceof ClearAllButton) {
- scrollDiff = ((ClearAllButton) child).getScrollAdjustment(showAsFullscreen,
- showAsGrid);
- }
- final int pageScroll = newPageScrolls[i] + (int) scrollDiff;
+ int clearAllIndex = indexOfChild(mClearAllButton);
+ int clearAllScroll = 0;
+ int clearAllWidth = mOrientationHandler.getPrimarySize(mClearAllButton);
+ if (clearAllIndex != -1 && clearAllIndex < outPageScrolls.length) {
+ float scrollDiff = mClearAllButton.getScrollAdjustment(showAsFullscreen, showAsGrid);
+ clearAllScroll = newPageScrolls[clearAllIndex] + (int) scrollDiff;
+ if (outPageScrolls[clearAllIndex] != clearAllScroll) {
+ pageScrollChanged = true;
+ outPageScrolls[clearAllIndex] = clearAllScroll;
+ }
+ }
+
+ final int taskCount = getTaskViewCount();
+ for (int i = 0; i < taskCount; i++) {
+ TaskView taskView = getTaskViewAt(i);
+ float scrollDiff = taskView.getScrollAdjustment(showAsFullscreen, showAsGrid);
+ int pageScroll = newPageScrolls[i + mTaskViewStartIndex] + (int) scrollDiff;
+ if ((mIsRtl && pageScroll < clearAllScroll)
+ || (!mIsRtl && pageScroll > clearAllScroll)) {
+ pageScroll = clearAllScroll + (mIsRtl ? clearAllWidth : -clearAllWidth);
+ }
if (outPageScrolls[i] != pageScroll) {
pageScrollChanged = true;
outPageScrolls[i] = pageScroll;
@@ -3680,6 +3837,7 @@
public void setOverviewGridEnabled(boolean overviewGridEnabled) {
if (mOverviewGridEnabled != overviewGridEnabled) {
mOverviewGridEnabled = overviewGridEnabled;
+ updateActionsViewScrollAlpha();
// Request layout to ensure scroll position is recalculated with updated mGridProgress.
requestLayout();
}
@@ -3825,11 +3983,6 @@
&& mCurrentGestureEndTarget != GestureState.GestureEndTarget.RECENTS;
}
- public boolean shouldShowOverviewActionsForState(STATE_TYPE state) {
- return !state.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())
- || getFocusedTaskView() != null;
- }
-
/**
* Used to register callbacks for when our empty message state changes.
*
diff --git a/quickstep/src/com/android/quickstep/views/TaskMenuView.java b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
index 906e854..1345a94 100644
--- a/quickstep/src/com/android/quickstep/views/TaskMenuView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskMenuView.java
@@ -16,7 +16,6 @@
package com.android.quickstep.views;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA;
import android.animation.Animator;
@@ -221,17 +220,7 @@
menuOptionView, mActivity.getDeviceProfile());
menuOptionView.setEnabled(menuOption.isEnabled());
menuOptionView.setAlpha(menuOption.isEnabled() ? 1 : 0.5f);
- menuOptionView.setOnClickListener(view -> {
- if (ENABLE_QUICKSTEP_LIVE_TILE.get() && !menuOption.hasFinishRecentsInAction()) {
- RecentsView recentsView = mTaskView.getRecentsView();
- recentsView.switchToScreenshot(null,
- () -> recentsView.finishRecentsAnimation(true /* toRecents */,
- false /* shouldPip */,
- () -> menuOption.onClick(view)));
- } else {
- menuOption.onClick(view);
- }
- });
+ menuOptionView.setOnClickListener(menuOption::onClick);
mOptionLayout.addView(menuOptionView);
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index c70596d..1ced86b 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -19,7 +19,6 @@
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
-import static com.android.launcher3.Utilities.comp;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN;
@@ -32,8 +31,6 @@
import android.graphics.Insets;
import android.graphics.Matrix;
import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
@@ -85,7 +82,6 @@
private TaskOverlay mOverlay;
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- private final Paint mClearPaint = new Paint();
private final Paint mDimmingPaintAfterClearing = new Paint();
private final int mDimColor;
@@ -116,7 +112,6 @@
super(context, attrs, defStyleAttr);
mPaint.setFilterBitmap(true);
mBackgroundPaint.setColor(Color.WHITE);
- mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mActivity = BaseActivity.fromContext(context);
// Initialize with placeholder value. It is overridden later by TaskView
mFullscreenParams = TEMP_PARAMS.get(context);
@@ -168,6 +163,7 @@
mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mPaint.setShader(mBitmapShader);
updateThumbnailMatrix();
+ refreshOverlay();
} else {
mBitmapShader = null;
mThumbnailData = null;
@@ -309,17 +305,7 @@
float cornerRadius) {
if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) {
- // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly.
- // Round up only when the live tile task is displayed in Overview.
- float rounding = comp(mFullscreenParams.mFullscreenProgress);
- float left = x + rounding / 2;
- float top = y + rounding / 2;
- float right = width - rounding;
- float bottom = height - rounding;
-
- canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
- mClearPaint);
- canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius,
+ canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius,
mDimmingPaintAfterClearing);
return;
}
@@ -431,7 +417,9 @@
*/
public static class PreviewPositionHelper {
- // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0.
+ private static final RectF EMPTY_RECT_F = new RectF();
+
+ // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1.
private final RectF mClippedInsets = new RectF();
private final Matrix mMatrix = new Matrix();
private boolean mIsOrientationChanged;
@@ -636,15 +624,17 @@
break;
}
mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale);
- mMatrix.postTranslate(translateX - mClippedInsets.left,
- translateY - mClippedInsets.top);
+ mMatrix.postTranslate(translateX, translateY);
+ if (TaskView.FULL_THUMBNAIL) {
+ mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top);
+ }
}
/**
* Insets to used for clipping the thumbnail (in case it is drawing outside its own space)
*/
public RectF getInsetsToDrawInFullscreen() {
- return mClippedInsets;
+ return TaskView.FULL_THUMBNAIL ? mClippedInsets : EMPTY_RECT_F;
}
}
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 2d322e9..159052a 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -147,13 +147,18 @@
*/
public static final boolean CLIP_STATUS_AND_NAV_BARS = false;
+ /**
+ * Should the TaskView scale down to fit whole thumbnail in fullscreen.
+ */
+ public static final boolean FULL_THUMBNAIL = false;
+
private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f;
private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f;
public static final long SCALE_ICON_DURATION = 120;
private static final long DIM_ANIM_DURATION = 700;
- private static final Interpolator FULLSCREEN_INTERPOLATOR = ACCEL_DEACCEL;
+ private static final Interpolator GRID_INTERPOLATOR = ACCEL_DEACCEL;
/**
* This technically can be a vanilla {@link TouchDelegate} class, however that class requires
@@ -167,7 +172,7 @@
private static final List<Rect> SYSTEM_GESTURE_EXCLUSION_RECT =
Collections.singletonList(new Rect());
- private static final FloatProperty<TaskView> FOCUS_TRANSITION =
+ public static final FloatProperty<TaskView> FOCUS_TRANSITION =
new FloatProperty<TaskView>("focusTransition") {
@Override
public void setValue(TaskView taskView, float v) {
@@ -284,55 +289,42 @@
}
};
- private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_X =
- new FloatProperty<TaskView>("fullscreenTranslationX") {
+ private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_X =
+ new FloatProperty<TaskView>("nonGridTranslationX") {
@Override
public void setValue(TaskView taskView, float v) {
- taskView.setFullscreenTranslationX(v);
+ taskView.setNonGridTranslationX(v);
}
@Override
public Float get(TaskView taskView) {
- return taskView.mFullscreenTranslationX;
+ return taskView.mNonGridTranslationX;
}
};
- private static final FloatProperty<TaskView> FULLSCREEN_TRANSLATION_Y =
- new FloatProperty<TaskView>("fullscreenTranslationY") {
+ private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_Y =
+ new FloatProperty<TaskView>("nonGridTranslationY") {
@Override
public void setValue(TaskView taskView, float v) {
- taskView.setFullscreenTranslationY(v);
+ taskView.setNonGridTranslationY(v);
}
@Override
public Float get(TaskView taskView) {
- return taskView.mFullscreenTranslationY;
+ return taskView.mNonGridTranslationY;
}
};
- private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_X =
- new FloatProperty<TaskView>("nonFullscreenTranslationX") {
+ public static final FloatProperty<TaskView> SNAPSHOT_SCALE =
+ new FloatProperty<TaskView>("snapshotScale") {
@Override
public void setValue(TaskView taskView, float v) {
- taskView.setNonFullscreenTranslationX(v);
+ taskView.setSnapshotScale(v);
}
@Override
public Float get(TaskView taskView) {
- return taskView.mNonFullscreenTranslationX;
- }
- };
-
- private static final FloatProperty<TaskView> NON_FULLSCREEN_TRANSLATION_Y =
- new FloatProperty<TaskView>("nonFullscreenTranslationY") {
- @Override
- public void setValue(TaskView taskView, float v) {
- taskView.setNonFullscreenTranslationY(v);
- }
-
- @Override
- public Float get(TaskView taskView) {
- return taskView.mNonFullscreenTranslationY;
+ return taskView.mSnapshotView.getScaleX();
}
};
@@ -344,7 +336,8 @@
private final DigitalWellBeingToast mDigitalWellBeingToast;
private float mFullscreenProgress;
private float mGridProgress;
- private float mFullscreenScale = 1;
+ private float mNonGridScale = 1;
+ private float mDismissScale = 1;
private final FullscreenDrawParams mCurrentFullscreenParams;
private final StatefulActivity mActivity;
@@ -356,16 +349,14 @@
private float mTaskResistanceTranslationX;
private float mTaskResistanceTranslationY;
// The following translation variables should only be used in the same orientation as Launcher.
- private float mFullscreenTranslationX;
- private float mFullscreenTranslationY;
- // Applied as a complement to fullscreenTranslation, for adjusting the carousel overview, or the
- // in transition carousel before forming the grid on tablets.
- private float mNonFullscreenTranslationX;
- private float mNonFullscreenTranslationY;
private float mBoxTranslationY;
// The following grid translations scales with mGridProgress.
private float mGridTranslationX;
private float mGridTranslationY;
+ // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick
+ // switch.
+ private float mNonGridTranslationX;
+ private float mNonGridTranslationY;
// Used when in SplitScreenSelectState
private float mSplitSelectTranslationY;
private float mSplitSelectTranslationX;
@@ -524,8 +515,8 @@
return mTask;
}
- public boolean hasTaskId(int taskId) {
- return mTask != null && mTask.key != null && mTask.key.id == taskId;
+ public int getTaskId() {
+ return mTask != null && mTask.key != null ? mTask.key.id : -1;
}
public TaskThumbnailView getThumbnail() {
@@ -540,6 +531,9 @@
if (getTask() == null) {
return;
}
+ if (confirmSecondSplitSelectApp()) {
+ return;
+ }
if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
if (!mIsClickableAsLiveTile) {
return;
@@ -558,7 +552,7 @@
if (targets == null) {
// If the recents animation is cancelled somehow between the parent if block and
// here, try to launch the task as a non live tile task.
- launcherNonLiveTileTask();
+ launchTaskAnimated();
return;
}
@@ -570,31 +564,28 @@
recentsView.getDepthController());
anim.addListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationStart(Animator animator) {
- recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(false);
- }
-
- @Override
public void onAnimationEnd(Animator animator) {
- recentsView.getLiveTileTaskViewSimulator().setDrawsBelowRecents(true);
mIsClickableAsLiveTile = true;
}
});
anim.start();
} else {
- launcherNonLiveTileTask();
+ launchTaskAnimated();
}
mActivity.getStatsLogManager().logger().withItemInfo(getItemInfo())
.log(LAUNCHER_TASK_LAUNCH_TAP);
}
- private void launcherNonLiveTileTask() {
- if (mActivity.isInState(OVERVIEW_SPLIT_SELECT)) {
- // User tapped to select second split screen app
+ /**
+ * @return {@code true} if user is already in split select mode and this tap was to choose the
+ * second app. {@code false} otherwise
+ */
+ private boolean confirmSecondSplitSelectApp() {
+ boolean isSelectingSecondSplitApp = mActivity.isInState(OVERVIEW_SPLIT_SELECT);
+ if (isSelectingSecondSplitApp) {
getRecentsView().confirmSplitSelect(this);
- } else {
- launchTaskAnimated();
}
+ return isSelectingSecondSplitApp;
}
/**
@@ -747,7 +738,16 @@
private void setIcon(Drawable icon) {
if (icon != null) {
mIconView.setDrawable(icon);
- mIconView.setOnClickListener(v -> showTaskMenu());
+ mIconView.setOnClickListener(v -> {
+ if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
+ RecentsView recentsView = getRecentsView();
+ recentsView.switchToScreenshot(
+ () -> recentsView.finishRecentsAnimation(true /* toRecents */,
+ this::showTaskMenu));
+ } else {
+ showTaskMenu();
+ }
+ });
mIconView.setOnLongClickListener(v -> {
requestDisallowInterceptTouchEvent(true);
return showTaskMenu();
@@ -800,7 +800,7 @@
mIconView.setRotation(orientationHandler.getDegreesRotated());
snapshotParams.topMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
mSnapshotView.setLayoutParams(snapshotParams);
- getThumbnail().getTaskOverlay().updateOrientationState(orientationState);
+ mSnapshotView.getTaskOverlay().updateOrientationState(orientationState);
}
private void setIconAndDimTransitionProgress(float progress, boolean invert) {
@@ -862,6 +862,7 @@
mSplitSelectTranslationX = 0f;
mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY =
mSplitSelectTranslationY = 0f;
+ setSnapshotScale(1f);
applyTranslationX();
applyTranslationY();
setTranslationZ(0);
@@ -877,9 +878,8 @@
@Override
public void onRecycle() {
- mFullscreenTranslationX = mFullscreenTranslationY = mNonFullscreenTranslationX =
- mNonFullscreenTranslationY = mGridTranslationX = mGridTranslationY =
- mBoxTranslationY = 0f;
+ mNonGridTranslationX = mNonGridTranslationY =
+ mGridTranslationX = mGridTranslationY = mBoxTranslationY = 0f;
resetViewTransforms();
// Clear any references to the thumbnail (it will be re-read either from the cache or the
// system on next bind)
@@ -962,13 +962,19 @@
}
}
- private void setFullscreenScale(float fullscreenScale) {
- mFullscreenScale = fullscreenScale;
+ private void setNonGridScale(float nonGridScale) {
+ mNonGridScale = nonGridScale;
+ updateCornerRadius();
applyScale();
}
- public float getFullscreenScale() {
- return mFullscreenScale;
+ public float getNonGridScale() {
+ return mNonGridScale;
+ }
+
+ private void setSnapshotScale(float dismissScale) {
+ mDismissScale = dismissScale;
+ applyScale();
}
/**
@@ -985,12 +991,23 @@
private void applyScale() {
float scale = 1;
- float fullScreenProgress = FULLSCREEN_INTERPOLATOR.getInterpolation(mFullscreenProgress);
- scale *= Utilities.mapRange(fullScreenProgress, 1f, mFullscreenScale);
+ scale *= getPersistentScale();
+ scale *= mDismissScale;
setScaleX(scale);
setScaleY(scale);
}
+ /**
+ * Returns multiplication of scale that is persistent (e.g. fullscreen and grid), and does not
+ * change according to a temporary state.
+ */
+ public float getPersistentScale() {
+ float scale = 1;
+ float gridProgress = GRID_INTERPOLATOR.getInterpolation(mGridProgress);
+ scale *= Utilities.mapRange(gridProgress, mNonGridScale, 1f);
+ return scale;
+ }
+
private void setSplitSelectTranslationX(float x) {
mSplitSelectTranslationX = x;
applyTranslationX();
@@ -1030,23 +1047,13 @@
applyTranslationY();
}
- private void setFullscreenTranslationX(float fullscreenTranslationX) {
- mFullscreenTranslationX = fullscreenTranslationX;
+ private void setNonGridTranslationX(float nonGridTranslationX) {
+ mNonGridTranslationX = nonGridTranslationX;
applyTranslationX();
}
- private void setFullscreenTranslationY(float fullscreenTranslationY) {
- mFullscreenTranslationY = fullscreenTranslationY;
- applyTranslationY();
- }
-
- private void setNonFullscreenTranslationX(float nonFullscreenTranslationX) {
- mNonFullscreenTranslationX = nonFullscreenTranslationX;
- applyTranslationX();
- }
-
- private void setNonFullscreenTranslationY(float nonFullscreenTranslationY) {
- mNonFullscreenTranslationY = nonFullscreenTranslationY;
+ private void setNonGridTranslationY(float nonGridTranslationY) {
+ mNonGridTranslationY = nonGridTranslationY;
applyTranslationY();
}
@@ -1070,13 +1077,10 @@
public float getScrollAdjustment(boolean fullscreenEnabled, boolean gridEnabled) {
float scrollAdjustment = 0;
- if (fullscreenEnabled) {
- scrollAdjustment += getPrimaryFullscreenTranslationProperty().get(this);
- } else {
- scrollAdjustment += getPrimaryNonFullscreenTranslationProperty().get(this);
- }
if (gridEnabled) {
scrollAdjustment += mGridTranslationX;
+ } else {
+ scrollAdjustment += getPrimaryNonGridTranslationProperty().get(this);
}
return scrollAdjustment;
}
@@ -1088,7 +1092,7 @@
public float getSizeAdjustment(boolean fullscreenEnabled) {
float sizeAdjustment = 1;
if (fullscreenEnabled) {
- sizeAdjustment *= mFullscreenScale;
+ sizeAdjustment *= mNonGridScale;
}
return sizeAdjustment;
}
@@ -1113,9 +1117,7 @@
* change according to a temporary state (e.g. task offset).
*/
public float getPersistentTranslationX() {
- return getFullscreenTrans(mFullscreenTranslationX)
- + getNonFullscreenTrans(mNonFullscreenTranslationX)
- + getGridTrans(mGridTranslationX);
+ return getNonGridTrans(mNonGridTranslationX) + getGridTrans(mGridTranslationX);
}
/**
@@ -1124,8 +1126,7 @@
*/
public float getPersistentTranslationY() {
return mBoxTranslationY
- + getFullscreenTrans(mFullscreenTranslationY)
- + getNonFullscreenTrans(mNonFullscreenTranslationY)
+ + getNonGridTrans(mNonGridTranslationY)
+ getGridTrans(mGridTranslationY);
}
@@ -1159,24 +1160,14 @@
TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
}
- public FloatProperty<TaskView> getPrimaryFullscreenTranslationProperty() {
+ public FloatProperty<TaskView> getPrimaryNonGridTranslationProperty() {
return getPagedOrientationHandler().getPrimaryValue(
- FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
+ NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
}
- public FloatProperty<TaskView> getSecondaryFullscreenTranslationProperty() {
+ public FloatProperty<TaskView> getSecondaryNonGridTranslationProperty() {
return getPagedOrientationHandler().getSecondaryValue(
- FULLSCREEN_TRANSLATION_X, FULLSCREEN_TRANSLATION_Y);
- }
-
- public FloatProperty<TaskView> getPrimaryNonFullscreenTranslationProperty() {
- return getPagedOrientationHandler().getPrimaryValue(
- NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
- }
-
- public FloatProperty<TaskView> getSecondaryNonFullscreenTranslationProperty() {
- return getPagedOrientationHandler().getSecondaryValue(
- NON_FULLSCREEN_TRANSLATION_X, NON_FULLSCREEN_TRANSLATION_Y);
+ NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
}
@Override
@@ -1312,28 +1303,21 @@
progress = Utilities.boundToRange(progress, 0, 1);
mFullscreenProgress = progress;
mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
- getThumbnail().getTaskOverlay().setFullscreenProgress(progress);
+ mSnapshotView.getTaskOverlay().setFullscreenProgress(progress);
- applyTranslationX();
- applyTranslationY();
- applyScale();
+ updateCornerRadius();
- TaskThumbnailView thumbnail = getThumbnail();
- updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
-
- if (!getRecentsView().isTaskIconScaledDown(this)) {
- // Some of the items in here are dependent on the current fullscreen params, but don't
- // update them if the icon is supposed to be scaled down.
- setIconScaleAndDim(progress, true /* invert */);
- }
-
- thumbnail.setFullscreenParams(mCurrentFullscreenParams);
+ mSnapshotView.setFullscreenParams(mCurrentFullscreenParams);
mOutlineProvider.updateParams(
mCurrentFullscreenParams,
mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx);
invalidateOutline();
}
+ private void updateCornerRadius() {
+ updateCurrentFullscreenParams(mSnapshotView.getPreviewPositionHelper());
+ }
+
void updateCurrentFullscreenParams(PreviewPositionHelper previewPositionHelper) {
if (getRecentsView() == null) {
return;
@@ -1341,6 +1325,7 @@
mCurrentFullscreenParams.setProgress(
mFullscreenProgress,
getRecentsView().getScaleX(),
+ mNonGridScale,
getWidth(), mActivity.getDeviceProfile(),
previewPositionHelper);
}
@@ -1351,7 +1336,7 @@
*/
void updateTaskSize() {
ViewGroup.LayoutParams params = getLayoutParams();
- float fullscreenScale;
+ float nonGridScale;
float boxTranslationY;
int expectedWidth;
int expectedHeight;
@@ -1364,56 +1349,36 @@
int boxWidth;
int boxHeight;
- float thumbnailRatio;
boolean isFocusedTask = isFocusedTask();
if (isFocusedTask) {
// Task will be focused and should use focused task size. Use focusTaskRatio
// that is associated with the original orientation of the focused task.
boxWidth = taskWidth;
boxHeight = taskHeight;
- thumbnailRatio = getRecentsView().getFocusedTaskRatio();
} else {
// Otherwise task is in grid, and should use lastComputedGridTaskSize.
Rect lastComputedGridTaskSize = getRecentsView().getLastComputedGridTaskSize();
boxWidth = lastComputedGridTaskSize.width();
boxHeight = lastComputedGridTaskSize.height();
- thumbnailRatio = mTask != null ? mTask.getVisibleThumbnailRatio(
- TaskView.CLIP_STATUS_AND_NAV_BARS) : 0f;
}
- int boxLength = Math.max(boxWidth, boxHeight);
// Bound width/height to the box size.
- if (thumbnailRatio == 0f) {
- expectedWidth = boxWidth;
- expectedHeight = boxHeight + thumbnailPadding;
- } else if (thumbnailRatio > 1) {
- expectedWidth = boxLength;
- expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
- } else {
- expectedWidth = (int) (boxLength * thumbnailRatio);
- expectedHeight = boxLength + thumbnailPadding;
- }
+ expectedWidth = boxWidth;
+ expectedHeight = boxHeight + thumbnailPadding;
// Scale to to fit task Rect.
- fullscreenScale = taskWidth / (float) boxWidth;
-
- // In full screen, scale back TaskView to original size.
- if (expectedWidth > boxWidth) {
- fullscreenScale *= boxWidth / (float) expectedWidth;
- } else if (expectedHeight - thumbnailPadding > boxHeight) {
- fullscreenScale *= boxHeight / (float) (expectedHeight - thumbnailPadding);
- }
+ nonGridScale = taskWidth / (float) boxWidth;
// Align to top of task Rect.
boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
} else {
- fullscreenScale = 1f;
+ nonGridScale = 1f;
boxTranslationY = 0f;
expectedWidth = ViewGroup.LayoutParams.MATCH_PARENT;
expectedHeight = ViewGroup.LayoutParams.MATCH_PARENT;
}
- setFullscreenScale(fullscreenScale);
+ setNonGridScale(nonGridScale);
setBoxTranslationY(boxTranslationY);
if (params.width != expectedWidth || params.height != expectedHeight) {
params.width = expectedWidth;
@@ -1422,20 +1387,15 @@
}
}
- private float getFullscreenTrans(float endTranslation) {
- float progress = FULLSCREEN_INTERPOLATOR.getInterpolation(mFullscreenProgress);
- return Utilities.mapRange(progress, 0, endTranslation);
- }
-
- private float getNonFullscreenTrans(float endTranslation) {
- return endTranslation - getFullscreenTrans(endTranslation);
- }
-
private float getGridTrans(float endTranslation) {
- float progress = ACCEL_DEACCEL.getInterpolation(mGridProgress);
+ float progress = GRID_INTERPOLATOR.getInterpolation(mGridProgress);
return Utilities.mapRange(progress, 0, endTranslation);
}
+ private float getNonGridTrans(float endTranslation) {
+ return endTranslation - getGridTrans(endTranslation);
+ }
+
public boolean isRunningTask() {
if (getRecentsView() == null) {
return false;
@@ -1487,7 +1447,6 @@
private final float mCornerRadius;
private final float mWindowCornerRadius;
- public float mFullscreenProgress;
public RectF mCurrentDrawnInsets = new RectF();
public float mCurrentDrawnCornerRadius;
/** The current scale we apply to the thumbnail to adjust for new left/right insets. */
@@ -1503,9 +1462,8 @@
/**
* Sets the progress in range [0, 1]
*/
- public void setProgress(float fullscreenProgress, float parentScale, int previewWidth,
- DeviceProfile dp, PreviewPositionHelper pph) {
- mFullscreenProgress = fullscreenProgress;
+ public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale,
+ int previewWidth, DeviceProfile dp, PreviewPositionHelper pph) {
RectF insets = pph.getInsetsToDrawInFullscreen();
float currentInsetsLeft = insets.left * fullscreenProgress;
@@ -1516,7 +1474,7 @@
mCurrentDrawnCornerRadius =
Utilities.mapRange(fullscreenProgress, mCornerRadius, fullscreenCornerRadius)
- / parentScale;
+ / parentScale / taskViewScale;
// We scaled the thumbnail to fit the content (excluding insets) within task view width.
// Now that we are drawing left/right insets again, we need to scale down to fit them.
diff --git a/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java b/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java
new file mode 100644
index 0000000..af5819a
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/NavigationBarRotationContextTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2021 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.quickstep;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.view.View;
+import android.view.WindowInsetsController;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.launcher3.taskbar.contextual.RotationButton;
+import com.android.launcher3.taskbar.contextual.RotationButtonController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+
+/** SysUI equivalent */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NavigationBarRotationContextTest {
+ private static final int DEFAULT_ROTATE = 0;
+ private static final int DEFAULT_DISPLAY = 0;
+
+
+ private RotationButtonController mRotationButtonController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ Context mTargetContext = InstrumentationRegistry.getTargetContext();
+ final View view = new View(mTargetContext);
+ RotationButton rotationButton = mock(RotationButton.class);
+ mRotationButtonController = new RotationButtonController(mTargetContext, 0, 0);
+ mRotationButtonController.setRotationButton(rotationButton);
+ // Due to a mockito issue, only spy the object after setting the initial state
+ mRotationButtonController = spy(mRotationButtonController);
+ final AnimatedVectorDrawable kbd = mock(AnimatedVectorDrawable.class);
+ doReturn(view).when(rotationButton).getCurrentView();
+ doReturn(true).when(rotationButton).acceptRotationProposal();
+ }
+
+ @Test
+ public void testOnInvalidRotationProposal() {
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ false /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Test
+ public void testOnSameRotationProposal() {
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE,
+ true /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(false /* visible */);
+ }
+
+ @Test
+ public void testOnRotationProposalShowButtonShowNav() {
+ // No navigation bar should not call to set visibility state
+ mRotationButtonController.onBehaviorChanged(DEFAULT_DISPLAY,
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ mRotationButtonController.onTaskBarVisibilityChange(false /* showing */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ false /* visible */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ true /* visible */);
+
+ // No navigation bar with rotation change should not call to set visibility state
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ true /* isValid */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ false /* visible */);
+ verify(mRotationButtonController, times(0)).setRotateSuggestionButtonState(
+ true /* visible */);
+
+ // Since rotation has changed rotation should be pending, show mButton when showing nav bar
+ mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
+ verify(mRotationButtonController, times(1)).setRotateSuggestionButtonState(
+ true /* visible */);
+ }
+
+ @Test
+ public void testOnRotationProposalShowButton() {
+ // Navigation bar being visible should not call to set visibility state
+ mRotationButtonController.onTaskBarVisibilityChange(true /* showing */);
+ verify(mRotationButtonController, times(0))
+ .setRotateSuggestionButtonState(false /* visible */);
+ verify(mRotationButtonController, times(0))
+ .setRotateSuggestionButtonState(true /* visible */);
+
+ // Navigation bar is visible and rotation requested
+ mRotationButtonController.onRotationProposal(DEFAULT_ROTATE + 1,
+ true /* isValid */);
+ verify(mRotationButtonController, times(1))
+ .setRotateSuggestionButtonState(true /* visible */);
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
index 6e19436..f44a812 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
@@ -25,6 +25,7 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.Launcher;
+import com.android.launcher3.ui.TaplTestsLauncher3;
import com.android.launcher3.util.RaceConditionReproducer;
import com.android.quickstep.NavigationModeSwitchRule.Mode;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
@@ -45,6 +46,7 @@
@Before
public void setUp() throws Exception {
super.setUp();
+ TaplTestsLauncher3.initialize(this);
// b/143488140
mLauncher.pressHome();
// Start an activity where the gestures start.
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index a5038a1..c14a590 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -23,7 +23,6 @@
import static org.junit.Assert.assertTrue;
import android.content.Intent;
-import android.util.Log;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
@@ -37,8 +36,8 @@
import com.android.launcher3.tapl.Overview;
import com.android.launcher3.tapl.OverviewActions;
import com.android.launcher3.tapl.OverviewTask;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.ui.TaplTestsLauncher3;
+import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.quickstep.NavigationModeSwitchRule.NavigationModeSwitch;
import com.android.quickstep.views.RecentsView;
@@ -94,6 +93,7 @@
@Test
@PortraitLandscape
+ @ScreenRecord //b/191344757
public void testOverview() throws Exception {
startTestAppsWithCheck();
// mLauncher.pressHome() also tests an important case of pressing home while in background.
@@ -145,12 +145,8 @@
launcher -> assertEquals("Dismissing a task didn't remove 1 task from Overview",
numTasks - 1, getTaskCount(launcher)));
- // Test UIDevice.pressHome, once we are in AllApps.
- mDevice.pressHome();
- waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
-
// Test dismissing all tasks.
- mLauncher.getWorkspace().switchToOverview().dismissAllTasks();
+ mLauncher.pressHome().switchToOverview().dismissAllTasks();
waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
executeOnLauncher(
launcher -> assertEquals("Still have tasks after dismissing all",
@@ -163,6 +159,7 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
+ @ScreenRecord //b/191344757
public void testOverviewActions() throws Exception {
// Experimenting for b/165029151:
final Overview overview = mLauncher.pressHome().switchToOverview();
@@ -188,6 +185,7 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
+ @ScreenRecord //b/191344757
public void testSwitchToOverview() throws Exception {
assertNotNull("Workspace.switchToOverview() returned null",
mLauncher.pressHome().switchToOverview());
@@ -198,6 +196,7 @@
@Test
@NavigationModeSwitch
@PortraitLandscape
+ @ScreenRecord //b/191344757
public void testBackground() throws Exception {
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
final Background background = getAndAssertBackground();
@@ -219,6 +218,7 @@
@Test
@PortraitLandscape
+ @ScreenRecord //b/191344757
public void testAllAppsFromHome() throws Exception {
// Test opening all apps
assertNotNull("switchToAllApps() returned null",
diff --git a/res/color-v31/home_settings_switch_thumb_color.xml b/res/color-v31/home_settings_switch_thumb_color.xml
new file mode 100644
index 0000000..91d3d9b
--- /dev/null
+++ b/res/color-v31/home_settings_switch_thumb_color.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Disabled status of thumb -->
+ <item android:state_enabled="false"
+ android:color="@color/home_settings_thumb_off_color" />
+ <!-- Toggle off status of thumb -->
+ <item android:state_checked="false"
+ android:color="@color/home_settings_thumb_off_color" />
+ <!-- Enabled or toggle on status of thumb -->
+ <item android:color="@color/home_settings_state_on_color" />
+</selector>
diff --git a/res/color-v31/home_settings_switch_track_color.xml b/res/color-v31/home_settings_switch_track_color.xml
new file mode 100644
index 0000000..50784f5
--- /dev/null
+++ b/res/color-v31/home_settings_switch_track_color.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- Disabled status of thumb -->
+ <item android:state_enabled="false"
+ android:color="@color/home_settings_track_off_color"
+ android:alpha="?android:attr/disabledAlpha" />
+ <!-- Toggle off status of thumb -->
+ <item android:state_checked="false"
+ android:color="@color/home_settings_track_off_color" />
+ <!-- Enabled or toggle on status of thumb -->
+ <item android:color="@color/home_settings_track_on_color" />
+</selector>
diff --git a/res/drawable-v31/home_settings_switch_thumb.xml b/res/drawable-v31/home_settings_switch_thumb.xml
new file mode 100644
index 0000000..260d5ea
--- /dev/null
+++ b/res/drawable-v31/home_settings_switch_thumb.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:top="4dp"
+ android:left="4dp"
+ android:right="4dp"
+ android:bottom="4dp">
+ <shape android:shape="oval" >
+ <size android:height="20dp" android:width="20dp" />
+ <solid android:color="@color/home_settings_switch_thumb_color" />
+ </shape>
+ </item>
+</layer-list>
\ No newline at end of file
diff --git a/res/drawable-v31/home_settings_switch_track.xml b/res/drawable-v31/home_settings_switch_track.xml
new file mode 100644
index 0000000..502a300
--- /dev/null
+++ b/res/drawable-v31/home_settings_switch_track.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle"
+ android:width="52dp"
+ android:height="28dp">
+
+ <solid android:color="@color/home_settings_switch_track_color" />
+ <corners android:radius="35dp" />
+</shape>
\ No newline at end of file
diff --git a/res/layout-v31/settings_activity.xml b/res/layout-v31/settings_activity.xml
new file mode 100644
index 0000000..59e14f2
--- /dev/null
+++ b/res/layout-v31/settings_activity.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+-->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/content_parent"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/app_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/colorPrimary"
+ android:fitsSystemWindows="true"
+ android:outlineAmbientShadowColor="@android:color/transparent"
+ android:outlineSpotShadowColor="@android:color/transparent"
+ android:theme="@style/HomeSettings.CollapsingToolbar">
+
+ <com.google.android.material.appbar.CollapsingToolbarLayout
+ android:id="@+id/collapsing_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="226dp"
+ android:clipToPadding="false"
+ app:collapsedTitleTextAppearance="@style/HomeSettings.CollapsedToolbarTitle"
+ app:contentScrim="@color/home_settings_header_collapsed"
+ app:expandedTitleMarginEnd="24dp"
+ app:expandedTitleMarginStart="24dp"
+ app:expandedTitleTextAppearance="@style/HomeSettings.ExpandedToolbarTitle"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
+ app:maxLines="3"
+ app:scrimAnimationDuration="50"
+ app:scrimVisibleHeightTrigger="174dp"
+ app:statusBarScrim="@null"
+ app:titleCollapseMode="fade"
+ app:toolbarId="@id/action_bar">
+
+ <Toolbar
+ android:id="@+id/action_bar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ android:theme="?android:attr/actionBarTheme"
+ android:transitionName="shared_element_view"
+ app:layout_collapseMode="pin" />
+
+ </com.google.android.material.appbar.CollapsingToolbarLayout>
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <FrameLayout
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index 0e06690..c57b75a 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -64,12 +64,19 @@
android:textColor="?android:attr/textColorSecondary"
android:alpha="0.7"/>
- <include layout="@layout/widget_cell"
- android:id="@+id/widget_cell"
+ <ScrollView
+ android:id="@+id/widget_preview_scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_weight="1"
- android:layout_marginVertical="16dp" />
+ android:layout_marginVertical="16dp"
+ android:layout_weight="1">
+
+ <include
+ android:id="@+id/widget_cell"
+ layout="@layout/widget_cell"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </ScrollView>
<LinearLayout
android:layout_width="match_parent"
@@ -80,9 +87,11 @@
<Button
style="@style/Button.FullRounded.Colored"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:textSize="14sp"
+ android:maxLines="2"
+ android:ellipsize="end"
android:textColor="@color/button_text"
android:text="@android:string/cancel"
android:onClick="onCancelClick"/>
@@ -94,9 +103,11 @@
<Button
style="@style/Button.FullRounded.Colored"
android:layout_width="wrap_content"
- android:layout_height="36dp"
+ android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:textSize="14sp"
+ android:maxLines="2"
+ android:ellipsize="end"
android:textColor="@color/button_text"
android:text="@string/add_to_home_screen"
android:onClick="onPlaceAutomaticallyClick"/>
diff --git a/res/layout/launcher_preview_two_panel_layout.xml b/res/layout/launcher_preview_two_panel_layout.xml
new file mode 100644
index 0000000..7b227e0
--- /dev/null
+++ b/res/layout/launcher_preview_two_panel_layout.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 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.
+-->
+<view class="com.android.launcher3.graphics.LauncherPreviewRenderer$LauncherPreviewLayout"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:launcher="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="false">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.launcher3.CellLayout
+ android:id="@+id/workspace_left"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:theme="@style/HomeScreenElementTheme"
+ launcher:containerType="workspace"
+ launcher:layout_constraintStart_toStartOf="parent"
+ launcher:layout_constraintTop_toTopOf="parent"
+ launcher:layout_constraintEnd_toStartOf="@id/workspace"
+ launcher:layout_constraintBottom_toBottomOf="parent"
+ launcher:pageIndicator="@+id/page_indicator" />
+
+ <com.android.launcher3.CellLayout
+ android:id="@+id/workspace"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:theme="@style/HomeScreenElementTheme"
+ launcher:containerType="workspace"
+ launcher:layout_constraintStart_toEndOf="@id/workspace_left"
+ launcher:layout_constraintTop_toTopOf="parent"
+ launcher:layout_constraintEnd_toEndOf="parent"
+ launcher:layout_constraintBottom_toBottomOf="parent"
+ launcher:pageIndicator="@+id/page_indicator" />
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+ <include
+ android:id="@+id/hotseat"
+ layout="@layout/hotseat" />
+
+</view>
\ No newline at end of file
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
new file mode 100644
index 0000000..2c1bc90
--- /dev/null
+++ b/res/values-night-v31/colors.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+/*
+* Copyright (C) 2021 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.
+*/
+-->
+
+<resources>
+ <color name="home_settings_header_accent">@android:color/system_accent1_100</color>
+ <color name="home_settings_header_collapsed">@android:color/system_neutral1_700</color>
+ <color name="home_settings_header_expanded">@android:color/system_neutral1_900</color>
+
+ <color name="home_settings_thumb_off_color">@android:color/system_neutral2_300</color>
+ <color name="home_settings_track_on_color">@android:color/system_accent2_700</color>
+ <color name="home_settings_track_off_color">@android:color/system_neutral1_700</color>
+</resources>
\ No newline at end of file
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index d73ee57..c2ebeff 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -42,6 +42,16 @@
<color name="folder_dot_color">@android:color/system_accent2_50</color>
+ <color name="home_settings_header_accent">@android:color/system_accent1_600</color>
+ <color name="home_settings_header_collapsed">@android:color/system_neutral1_100</color>
+ <color name="home_settings_header_expanded">@android:color/system_neutral1_50</color>
+
+ <color name="home_settings_state_on_color">@android:color/system_accent1_100</color>
+ <color name="home_settings_state_off_color">@android:color/system_accent2_100</color>
+ <color name="home_settings_thumb_off_color">@android:color/system_neutral2_100</color>
+ <color name="home_settings_track_on_color">@android:color/system_accent1_600</color>
+ <color name="home_settings_track_off_color">@android:color/system_neutral2_600</color>
+
<color name="workspace_accent_color_light">@android:color/system_accent1_100</color>
<color name="workspace_accent_color_dark">@android:color/system_accent2_600</color>
</resources>
diff --git a/res/values-v31/config.xml b/res/values-v31/config.xml
new file mode 100644
index 0000000..afb9e6d
--- /dev/null
+++ b/res/values-v31/config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2021 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.
+ -->
+<resources>
+ <bool name="home_settings_icon_space_reserved">false</bool>
+ <bool name="home_settings_allow_divider">false</bool>
+</resources>
\ No newline at end of file
diff --git a/res/values-v31/styles.xml b/res/values-v31/styles.xml
new file mode 100644
index 0000000..0d2fce0
--- /dev/null
+++ b/res/values-v31/styles.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright (C) 2021 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.
+*/
+-->
+
+<resources>
+
+ <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
+ <item name="android:listPreferredItemPaddingEnd">16dp</item>
+ <item name="android:listPreferredItemPaddingStart">24dp</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:switchStyle">@style/HomeSettings.SwitchStyle</item>
+ <item name="android:textAppearanceListItem">@style/HomeSettings.PreferenceTitle</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
+ </style>
+
+ <style name="HomeSettings.PreferenceTheme" parent="@style/PreferenceThemeOverlay">
+ <item name="preferenceCategoryStyle">@style/HomeSettings.CategoryStyle</item>
+ <item name="preferenceCategoryTitleTextAppearance">@style/HomeSettings.CategoryTitle</item>
+ <item name="preferenceFragmentCompatStyle">@style/HomeSettings.FragmentCompatStyle</item>
+ <item name="preferenceScreenStyle">@style/HomeSettings.PreferenceScreenStyle</item>
+ <item name="preferenceStyle">@style/HomeSettings.PreferenceStyle</item>
+ <item name="switchPreferenceStyle">@style/HomeSettings.SwitchPreferenceStyle</item>
+ </style>
+
+ <style name="HomeSettings.CategoryStyle" parent="@style/Preference.Category.Material">
+ <item name="allowDividerAbove">@bool/home_settings_allow_divider</item>
+ <item name="allowDividerBelow">@bool/home_settings_allow_divider</item>
+ <item name="iconSpaceReserved">@bool/home_settings_icon_space_reserved</item>
+ </style>
+
+ <style name="HomeSettings.PreferenceStyle" parent="@style/Preference.Material">
+ <item name="iconSpaceReserved">@bool/home_settings_icon_space_reserved</item>
+ </style>
+
+ <style name="HomeSettings.PreferenceScreenStyle"
+ parent="@style/Preference.PreferenceScreen.Material">
+ <item name="iconSpaceReserved">@bool/home_settings_icon_space_reserved</item>
+ </style>
+
+ <style name="HomeSettings.SwitchPreferenceStyle"
+ parent="@style/Preference.SwitchPreference.Material">
+ <item name="iconSpaceReserved">@bool/home_settings_icon_space_reserved</item>
+ </style>
+
+ <style name="HomeSettings.SwitchStyle"
+ parent="@android:style/Widget.Material.CompoundButton.Switch">
+ <item name="android:switchMinWidth">52dp</item>
+ <item name="android:thumb">@drawable/home_settings_switch_thumb</item>
+ <item name="android:track">@drawable/home_settings_switch_track</item>
+ </style>
+
+ <style name="HomeSettings.PreferenceTitle"
+ parent="@android:style/TextAppearance.Material.Subhead">
+ <item name="android:fontFamily">google-sans</item>
+ <item name="android:textSize">20sp</item>
+ </style>
+
+ <style name="HomeSettings.CategoryTitle" parent="@android:style/TextAppearance.Material.Body2">
+ <item name="android:fontFamily">google-sans-text-medium</item>
+ </style>
+
+ <style name="HomeSettings.CollapsingToolbar" parent="@style/Theme.MaterialComponents.DayNight">
+ <item name="colorAccent">@color/home_settings_header_accent</item>
+ <item name="colorPrimary">@color/home_settings_header_expanded</item>
+ <item name="elevationOverlayColor">?attr/colorPrimary</item>
+ <item name="elevationOverlayEnabled">true</item>
+ </style>
+
+ <style name="HomeSettings.CollapsedToolbarTitle"
+ parent="@android:style/TextAppearance.DeviceDefault.Widget.ActionBar.Title">
+ <item name="android:fontFamily">google-sans</item>
+ </style>
+
+ <style name="HomeSettings.ExpandedToolbarTitle" parent="HomeSettings.CollapsedToolbarTitle">
+ <item name="android:textSize">36sp</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index f434644..46570f0 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -312,6 +312,10 @@
<!-- Taskbar related (placeholders to compile in Launcher3 without Quickstep) -->
<dimen name="taskbar_size">0dp</dimen>
+ <dimen name="qsb_widget_height">0dp</dimen>
+ <dimen name="taskbar_icon_size">44dp</dimen>
+ <!-- Note that this applies to both sides of all icons, so visible space is double this. -->
+ <dimen name="taskbar_icon_spacing">8dp</dimen>
<!-- Size of the maximum radius for the enforced rounded rectangles. -->
<dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
diff --git a/res/values/id.xml b/res/values/id.xml
index 1709c59..0e2dff0 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -20,4 +20,13 @@
<item type="id" name="view_type_widgets_list" />
<item type="id" name="view_type_widgets_header" />
<item type="id" name="view_type_widgets_search_header" />
+
+ <!-- Do not change, must be kept in sync with sysui navbar button IDs for tests! -->
+ <item type="id" name="home" />
+ <item type="id" name="recent_apps" />
+ <item type="id" name="back" />
+ <item type="id" name="ime_switcher" />
+ <item type="id" name="accessibility_button" />
+ <item type="id" name="rotate_suggestion" />
+ <!-- /Do not change, must be kept in sync with sysui navbar button IDs for tests! -->
</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d30b80c..e4a245a 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -147,18 +147,18 @@
<style name="AppTheme.Dark.DarkMainColor" parent="@style/LauncherTheme.Dark.DarkMainColor" />
<style name="AppTheme.Dark.DarkText" parent="@style/LauncherTheme.Dark.DarkText" />
- <style name="HomeSettingsTheme" parent="@android:style/Theme.DeviceDefault.Settings">
+ <style name="HomeSettings.Theme" parent="@android:style/Theme.DeviceDefault.Settings">
<item name="android:navigationBarColor">?android:colorPrimaryDark</item>
<item name="android:windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
- <item name="preferenceTheme">@style/HomeSettingsPreferenceTheme</item>
+ <item name="preferenceTheme">@style/HomeSettings.PreferenceTheme</item>
</style>
- <style name="HomeSettingsPreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material">
- <item name="preferenceFragmentCompatStyle">@style/HomeSettingsFragmentCompatStyle</item>
+ <style name="HomeSettings.PreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material">
+ <item name="preferenceFragmentCompatStyle">@style/HomeSettings.FragmentCompatStyle</item>
</style>
- <style name="HomeSettingsFragmentCompatStyle" parent="@style/PreferenceFragment.Material">
+ <style name="HomeSettings.FragmentCompatStyle" parent="@style/PreferenceFragment.Material">
<item name="android:layout">@layout/home_settings</item>
</style>
diff --git a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
index 8e49fae..d2051e0 100644
--- a/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
+++ b/robolectric_tests/src/com/android/launcher3/model/GridSizeMigrationTaskV2Test.java
@@ -131,7 +131,7 @@
mIdp.numDatabaseHotseatIcons);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
- task.migrate();
+ task.migrate(mIdp);
// Check hotseat items
Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
@@ -211,7 +211,7 @@
mIdp.numDatabaseHotseatIcons);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
- task.migrate();
+ task.migrate(mIdp);
// Check hotseat items
Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
@@ -259,7 +259,7 @@
mIdp.numDatabaseHotseatIcons);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(mContext, mDb, srcReader,
destReader, mIdp.numDatabaseHotseatIcons, new Point(mIdp.numColumns, mIdp.numRows));
- task.migrate();
+ task.migrate(mIdp);
// Check hotseat items
Cursor c = mContext.getContentResolver().query(LauncherSettings.Favorites.CONTENT_URI,
diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
index a2abfd5..07351fe 100644
--- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
+++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java
@@ -26,16 +26,16 @@
import android.os.Process;
-import com.android.launcher3.PagedView;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.shadows.ShadowLooperExecutor;
import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.LauncherLayoutBuilder;
import com.android.launcher3.util.LauncherModelHelper;
import com.android.launcher3.util.LooperExecutor;
-import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.util.RunnableList;
import org.junit.Before;
import org.junit.Test;
@@ -92,7 +92,7 @@
// Add a new callback
cb1.reset();
MyCallbacks cb2 = spy(MyCallbacks.class);
- cb2.mPageToBindSync = 2;
+ cb2.mPageToBindSync = IntSet.wrap(2);
mModelHelper.getModel().addCallbacksAndLoad(cb2);
waitForLoaderAndTempMainThread();
@@ -106,14 +106,14 @@
// No effect on callbacks when removing an callback
mModelHelper.getModel().removeCallbacks(cb2);
waitForLoaderAndTempMainThread();
- assertNull(cb1.mDeferredExecutor);
- assertNull(cb2.mDeferredExecutor);
+ assertNull(cb1.mPendingTasks);
+ assertNull(cb2.mPendingTasks);
// Reloading only loads registered callbacks
mModelHelper.getModel().startLoader();
waitForLoaderAndTempMainThread();
cb1.verifySynchronouslyBound(3);
- assertNull(cb2.mDeferredExecutor);
+ assertNull(cb2.mPendingTasks);
}
@Test
@@ -178,21 +178,17 @@
private abstract static class MyCallbacks implements Callbacks {
final List<ItemInfo> mItems = new ArrayList<>();
- int mPageToBindSync = 0;
- int mPageBoundSync = PagedView.INVALID_PAGE;
- ViewOnDrawExecutor mDeferredExecutor;
+ IntSet mPageToBindSync = IntSet.wrap(0);
+ IntSet mPageBoundSync = new IntSet();
+ RunnableList mPendingTasks;
AppInfo[] mAppInfos;
MyCallbacks() { }
@Override
- public void onPageBoundSynchronously(int page) {
- mPageBoundSync = page;
- }
-
- @Override
- public void executeOnNextDraw(ViewOnDrawExecutor executor) {
- mDeferredExecutor = executor;
+ public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+ mPageBoundSync = boundPages;
+ mPendingTasks = pendingTasks;
}
@Override
@@ -206,26 +202,26 @@
}
@Override
- public int getPageToBindSynchronously() {
+ public IntSet getPagesToBindSynchronously() {
return mPageToBindSync;
}
public void reset() {
mItems.clear();
- mPageBoundSync = PagedView.INVALID_PAGE;
- mDeferredExecutor = null;
+ mPageBoundSync = new IntSet();
+ mPendingTasks = null;
mAppInfos = null;
}
public void verifySynchronouslyBound(int totalItems) {
// Verify that the requested page is bound synchronously
- assertEquals(mPageBoundSync, mPageToBindSync);
+ assertEquals(mPageToBindSync, mPageBoundSync);
assertEquals(mItems.size(), 1);
- assertEquals(mItems.get(0).screenId, mPageBoundSync);
- assertNotNull(mDeferredExecutor);
+ assertEquals(IntSet.wrap(mItems.get(0).screenId), mPageBoundSync);
+ assertNotNull(mPendingTasks);
// Verify that all other pages are bound properly
- mDeferredExecutor.runAllTasks();
+ mPendingTasks.executeAllAndDestroy();
assertEquals(mItems.size(), totalItems);
}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherPageRestoreHelperTest.java b/robolectric_tests/src/com/android/launcher3/util/LauncherPageRestoreHelperTest.java
new file mode 100644
index 0000000..51f5851
--- /dev/null
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherPageRestoreHelperTest.java
@@ -0,0 +1,224 @@
+/**
+ * Copyright (C) 2021 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.launcher3.util;
+
+import android.os.Bundle;
+
+import com.android.launcher3.LauncherPageRestoreHelper;
+import com.android.launcher3.Workspace;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+@RunWith(RobolectricTestRunner.class)
+public class LauncherPageRestoreHelperTest {
+
+ // Type: int
+ private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen";
+ // Type: int
+ private static final String RUNTIME_STATE_CURRENT_SCREEN_COUNT =
+ "launcher.current_screen_count";
+
+ private LauncherPageRestoreHelper mPageRestoreHelper;
+ private Bundle mState;
+
+ @Mock
+ private Workspace mWorkspace;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mPageRestoreHelper = new LauncherPageRestoreHelper(mWorkspace);
+ mState = new Bundle();
+ }
+
+ @Test
+ public void givenNoChildrenInWorkspace_whenSavePages_thenNothingSaved() {
+ when(mWorkspace.getChildCount()).thenReturn(0);
+
+ mPageRestoreHelper.savePagesToRestore(mState);
+
+ assertFalse(mState.containsKey(RUNTIME_STATE_CURRENT_SCREEN_COUNT));
+ assertFalse(mState.containsKey(RUNTIME_STATE_CURRENT_SCREEN));
+ }
+
+ @Test
+ public void givenMultipleCurrentPages_whenSavePages_thenSavedCorrectly() {
+ when(mWorkspace.getChildCount()).thenReturn(5);
+ when(mWorkspace.getCurrentPage()).thenReturn(2);
+ givenPanelCount(2);
+
+ mPageRestoreHelper.savePagesToRestore(mState);
+
+ assertEquals(5, mState.getInt(RUNTIME_STATE_CURRENT_SCREEN_COUNT));
+ assertEquals(2, mState.getInt(RUNTIME_STATE_CURRENT_SCREEN));
+ }
+
+ @Test
+ public void givenNullSavedState_whenRestorePages_thenReturnEmptyIntSet() {
+ IntSet result = mPageRestoreHelper.getPagesToRestore(null);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void givenTotalPageCountMissing_whenRestorePages_thenReturnEmptyIntSet() {
+ givenSavedCurrentPage(1);
+ givenPanelCount(1);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void givenCurrentPageMissing_whenRestorePages_thenReturnEmptyIntSet() {
+ givenSavedPageCount(3);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void givenOnePanel_whenRestorePages_thenReturnThatPage() {
+ givenSavedCurrentPage(2);
+ givenSavedPageCount(5);
+ givenPanelCount(1);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(1, result.size());
+ assertEquals(2, result.getArray().get(0));
+ }
+
+ @Test
+ public void givenTwoPanelOnFirstPages_whenRestorePages_thenReturnThosePages() {
+ givenSavedCurrentPage(0, 1);
+ givenSavedPageCount(2);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(0, 1), result);
+ }
+
+ @Test
+ public void givenTwoPanelOnMiddlePages_whenRestorePages_thenReturnThosePages() {
+ givenSavedCurrentPage(2, 3);
+ givenSavedPageCount(5);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(2, 3), result);
+ }
+
+ @Test
+ public void givenTwoPanelOnLastPage_whenRestorePages_thenReturnOnlyLastPage() {
+ // The device has two panel home but the current page is the last page, so we don't have
+ // a right panel, only the left one.
+ givenSavedCurrentPage(2);
+ givenSavedPageCount(3);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(2), result);
+ }
+
+ @Test
+ public void givenOnlyOnePageAndPhoneFolding_whenRestorePages_thenReturnOnlyOnePage() {
+ givenSavedCurrentPage(0);
+ givenSavedPageCount(1);
+ givenPanelCount(1);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(0), result);
+ }
+
+ @Test
+ public void givenPhoneFolding_whenRestorePages_thenReturnOnlyTheFirstCurrentPage() {
+ givenSavedCurrentPage(2, 3);
+ givenSavedPageCount(4);
+ givenPanelCount(1);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(2), result);
+ }
+
+ @Test
+ public void givenPhoneUnfolding_whenRestorePages_thenReturnCurrentPagePlusTheNextOne() {
+ givenSavedCurrentPage(2);
+ givenSavedPageCount(4);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(2, 3), result);
+ }
+
+ @Test
+ public void givenPhoneUnfoldingOnLastPage_whenRestorePages_thenReturnOnlyLastPage() {
+ givenSavedCurrentPage(4);
+ givenSavedPageCount(5);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(4), result);
+ }
+
+ @Test
+ public void givenOnlyOnePageAndPhoneUnfolding_whenRestorePages_thenReturnOnlyOnePage() {
+ givenSavedCurrentPage(0);
+ givenSavedPageCount(1);
+ givenPanelCount(2);
+
+ IntSet result = mPageRestoreHelper.getPagesToRestore(mState);
+
+ assertEquals(IntSet.wrap(0), result);
+ }
+
+ private void givenPanelCount(int panelCount) {
+ when(mWorkspace.getPanelCount()).thenReturn(panelCount);
+ when(mWorkspace.getLeftmostVisiblePageForIndex(anyInt())).thenAnswer(invocation -> {
+ int pageIndex = invocation.getArgument(0);
+ return pageIndex * panelCount / panelCount;
+ });
+ }
+
+ private void givenSavedPageCount(int pageCount) {
+ mState.putInt(RUNTIME_STATE_CURRENT_SCREEN_COUNT, pageCount);
+ }
+
+ private void givenSavedCurrentPage(int... pages) {
+ mState.putInt(RUNTIME_STATE_CURRENT_SCREEN, pages[0]);
+ }
+}
diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
index fdddab4..caad40e 100644
--- a/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
+++ b/robolectric_tests/src/com/android/launcher3/util/LauncherUIHelper.java
@@ -81,7 +81,7 @@
doLayout(launcher);
ViewOnDrawExecutor executor = ReflectionHelpers.getField(launcher, "mPendingExecutor");
if (executor != null) {
- executor.runAllTasks();
+ executor.markCompleted();
}
return launcher;
}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ddbd425..f800cf6 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -407,7 +407,7 @@
}
}
- void clearPressedBackground() {
+ public void clearPressedBackground() {
setPressed(false);
setStayPressed(false);
}
@@ -853,8 +853,9 @@
switch (display) {
case DISPLAY_ALL_APPS:
return grid.allAppsIconSizePx;
- case DISPLAY_WORKSPACE:
case DISPLAY_FOLDER:
+ return grid.folderChildIconSizePx;
+ case DISPLAY_WORKSPACE:
default:
return grid.iconSizePx;
}
diff --git a/src/com/android/launcher3/ButtonDropTarget.java b/src/com/android/launcher3/ButtonDropTarget.java
index 61b5564..0d33b6f 100644
--- a/src/com/android/launcher3/ButtonDropTarget.java
+++ b/src/com/android/launcher3/ButtonDropTarget.java
@@ -211,11 +211,8 @@
}
final DragLayer dragLayer = mLauncher.getDragLayer();
final DragView dragView = d.dragView;
- final Rect from = new Rect();
- dragLayer.getViewRectRelativeToSelf(d.dragView, from);
-
final Rect to = getIconRect(d);
- final float scale = (float) to.width() / from.width();
+ final float scale = (float) to.width() / dragView.getMeasuredWidth();
dragView.disableColorExtraction();
dragView.detachContentView(/* reattachToPreviousParent= */ true);
mDropTargetBar.deferOnDragEnd();
@@ -229,9 +226,9 @@
post(dragView::resumeColorExtraction);
};
- dragLayer.animateView(d.dragView, from, to, scale, 1f, 1f, 0.1f, 0.1f,
+ dragLayer.animateView(d.dragView, to, scale, 0.1f, 0.1f,
DRAG_VIEW_DROP_DURATION,
- Interpolators.DEACCEL_2, Interpolators.LINEAR, onAnimationEndRunnable,
+ Interpolators.DEACCEL_2, onAnimationEndRunnable,
DragLayer.ANIMATION_END_DISAPPEAR, null);
}
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 00278e4..f4447b1 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -2147,7 +2147,7 @@
mShakeAnimators.clear();
}
- private void commitTempPlacement() {
+ private void commitTempPlacement(View dragView) {
mTmpOccupied.copyTo(mOccupied);
int screenId = Launcher.cast(mActivity).getWorkspace().getIdForScreen(this);
@@ -2165,7 +2165,7 @@
ItemInfo info = (ItemInfo) child.getTag();
// We do a null check here because the item info can be null in the case of the
// AllApps button in the hotseat.
- if (info != null) {
+ if (info != null && child != dragView) {
final boolean requiresDbUpdate = (info.cellX != lp.tmpCellX
|| info.cellY != lp.tmpCellY || info.spanX != lp.cellHSpan
|| info.spanY != lp.cellVSpan);
@@ -2327,7 +2327,7 @@
animateItemsToSolution(swapSolution, dragView, commit);
if (commit) {
- commitTempPlacement();
+ commitTempPlacement(null);
completeAndClearReorderPreviewAnimations();
setItemPlacementDirty(false);
} else {
@@ -2421,7 +2421,8 @@
if (!DESTRUCTIVE_REORDER &&
(mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) {
- commitTempPlacement();
+ // Since the temp solution didn't update dragView, don't commit it either
+ commitTempPlacement(dragView);
completeAndClearReorderPreviewAnimations();
setItemPlacementDirty(false);
} else {
@@ -2877,7 +2878,7 @@
directionVector, null, false, configuration).isSolution) {
if (commitConfig) {
copySolutionToTempState(configuration, null);
- commitTempPlacement();
+ commitTempPlacement(null);
// undo marking cells occupied since there is actually nothing being placed yet.
mOccupied.markCells(0, mCountY - 1, mCountX, 1, false);
}
diff --git a/src/com/android/launcher3/DeleteDropTarget.java b/src/com/android/launcher3/DeleteDropTarget.java
index 80ec192..ba55834 100644
--- a/src/com/android/launcher3/DeleteDropTarget.java
+++ b/src/com/android/launcher3/DeleteDropTarget.java
@@ -33,6 +33,7 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.views.Snackbar;
public class DeleteDropTarget extends ButtonDropTarget {
@@ -131,7 +132,7 @@
onAccessibilityDrop(null, item);
ModelWriter modelWriter = mLauncher.getModelWriter();
Runnable onUndoClicked = () -> {
- mLauncher.setPageToBindSynchronously(itemPage);
+ mLauncher.setPagesToBindSynchronously(IntSet.wrap(itemPage));
modelWriter.abortDelete();
mLauncher.getStatsLogManager().logger().log(LAUNCHER_UNDO);
};
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 5a3e7dd..222c7d6 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -45,6 +45,7 @@
import com.android.launcher3.icons.DotRenderer;
import com.android.launcher3.icons.GraphicsUtils;
import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.uioverrides.ApiWrapper;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.WindowBounds;
@@ -55,6 +56,8 @@
public class DeviceProfile {
private static final int DEFAULT_DOT_SIZE = 100;
+ // Ratio of empty space, qsb should take up to appear visually centered.
+ private static final float QSB_CENTER_FACTOR = .325f;
public final InvariantDeviceProfile inv;
private final Info mInfo;
@@ -159,6 +162,7 @@
// Start is the side next to the nav bar, end is the side next to the workspace
public final int hotseatBarSidePaddingStartPx;
public final int hotseatBarSidePaddingEndPx;
+ public final int hotseatQsbHeight;
public final float qsbBottomMarginOriginalPx;
public int qsbBottomMarginPx;
@@ -246,6 +250,7 @@
mMetrics = context.getResources().getDisplayMetrics();
final Resources res = context.getResources();
+ hotseatQsbHeight = res.getDimensionPixelSize(R.dimen.qsb_widget_height);
isTaskbarPresent = isTablet && FeatureFlags.ENABLE_TASKBAR.get();
if (isTaskbarPresent) {
// Taskbar will be added later, but provides bottom insets that we should subtract
@@ -758,7 +763,10 @@
}
}
- public Rect getHotseatLayoutPadding() {
+ /**
+ * Returns the padding for hotseat view
+ */
+ public Rect getHotseatLayoutPadding(Context context) {
if (isVerticalBarLayout()) {
if (isSeascape()) {
mHotseatPadding.set(mInsets.left + hotseatBarSidePaddingStartPx,
@@ -767,6 +775,30 @@
mHotseatPadding.set(hotseatBarSidePaddingEndPx, mInsets.top,
mInsets.right + hotseatBarSidePaddingStartPx, mInsets.bottom);
}
+ } else if (isTaskbarPresent) {
+ int hotseatHeight = workspacePadding.bottom + taskbarSize;
+ int taskbarOffset = getTaskbarOffsetY();
+ int hotseatTopDiff = hotseatHeight - taskbarSize - taskbarOffset;
+
+ int endOffset = ApiWrapper.getHotseatEndOffset(context);
+ int requiredWidth = iconSizePx * numShownHotseatIcons;
+
+ Resources res = context.getResources();
+ float taskbarIconSize = res.getDimension(R.dimen.taskbar_icon_size);
+ float taskbarIconSpacing = 2 * res.getDimension(R.dimen.taskbar_icon_spacing);
+ int maxSize = (int) (requiredWidth
+ * (taskbarIconSize + taskbarIconSpacing) / taskbarIconSize);
+ int hotseatSize = Math.min(maxSize, availableWidthPx - endOffset);
+ int sideSpacing = (availableWidthPx - hotseatSize) / 2;
+ mHotseatPadding.set(sideSpacing, hotseatTopDiff, sideSpacing, taskbarOffset);
+
+ if (endOffset > sideSpacing) {
+ int diff = Utilities.isRtl(context.getResources())
+ ? sideSpacing - endOffset
+ : endOffset - sideSpacing;
+ mHotseatPadding.left -= diff;
+ mHotseatPadding.right += diff;
+ }
} else {
// We want the edges of the hotseat to line up with the edges of the workspace, but the
// icons in the hotseat are a different size, and so don't line up perfectly. To account
@@ -788,6 +820,29 @@
}
/**
+ * Returns the number of pixels the QSB is translated from the bottom of the screen.
+ */
+ public int getQsbOffsetY() {
+ int freeSpace = isTaskbarPresent
+ ? workspacePadding.bottom
+ : hotseatBarSizePx - hotseatCellHeightPx - hotseatQsbHeight;
+
+ if (isScalableGrid) {
+ return Math.min(qsbBottomMarginPx, freeSpace);
+ } else {
+ return (int) (freeSpace * QSB_CENTER_FACTOR)
+ + (isTaskbarPresent ? taskbarSize : getInsets().bottom);
+ }
+ }
+
+ /**
+ * Returns the number of pixels the taskbar is translated from the bottom of the screen.
+ */
+ public int getTaskbarOffsetY() {
+ return (getQsbOffsetY() - taskbarSize) / 2;
+ }
+
+ /**
* @return the bounds for which the open folders should be contained within
*/
public Rect getAbsoluteOpenFolderBounds() {
diff --git a/src/com/android/launcher3/Hotseat.java b/src/com/android/launcher3/Hotseat.java
index 0a6f06c..b3ae15e 100644
--- a/src/com/android/launcher3/Hotseat.java
+++ b/src/com/android/launcher3/Hotseat.java
@@ -49,8 +49,6 @@
private final View mQsb;
private final int mQsbHeight;
- private final int mTaskbarViewHeight;
-
public Hotseat(Context context) {
this(context, null);
}
@@ -63,10 +61,9 @@
super(context, attrs, defStyle);
mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
- mQsbHeight = mQsb.getLayoutParams().height;
addView(mQsb);
- mTaskbarViewHeight = context.getResources().getDimensionPixelSize(R.dimen.taskbar_size);
+ mQsbHeight = getResources().getDimensionPixelSize(R.dimen.qsb_widget_height);
}
/**
@@ -114,18 +111,13 @@
lp.gravity = Gravity.BOTTOM;
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
lp.height = (grid.isTaskbarPresent
- ? grid.workspacePadding.bottom
+ ? grid.workspacePadding.bottom
: grid.hotseatBarSizePx)
+ (grid.isTaskbarPresent ? grid.taskbarSize : insets.bottom);
}
- if (!grid.isTaskbarPresent) {
- // When taskbar is present, we set the padding separately to ensure a seamless visual
- // handoff between taskbar and hotseat during drag and drop.
- Rect padding = grid.getHotseatLayoutPadding();
- setPadding(padding.left, padding.top, padding.right, padding.bottom);
- }
-
+ Rect padding = grid.getHotseatLayoutPadding(getContext());
+ setPadding(padding.left, padding.top, padding.right, padding.bottom);
setLayoutParams(lp);
InsettableFrameLayout.dispatchInsets(this, insets);
}
@@ -193,37 +185,12 @@
int left = (r - l - qsbWidth) / 2;
int right = left + qsbWidth;
- int bottom = b - t - getQsbOffsetY();
+ int bottom = b - t - mActivity.getDeviceProfile().getQsbOffsetY();
int top = bottom - mQsbHeight;
mQsb.layout(left, top, right, bottom);
}
/**
- * Returns the number of pixels the QSB is translated from the bottom of the screen.
- */
- private int getQsbOffsetY() {
- DeviceProfile dp = mActivity.getDeviceProfile();
- int freeSpace = dp.isTaskbarPresent
- ? dp.workspacePadding.bottom
- : dp.hotseatBarSizePx - dp.hotseatCellHeightPx - mQsbHeight;
-
- if (dp.isScalableGrid) {
- return Math.min(dp.qsbBottomMarginPx, freeSpace);
- } else {
- return (int) (freeSpace * QSB_CENTER_FACTOR) + (dp.isTaskbarPresent
- ? dp.taskbarSize
- : dp.getInsets().bottom);
- }
- }
-
- /**
- * Returns the number of pixels the taskbar is translated from the bottom of the screen.
- */
- public int getTaskbarOffsetY() {
- return (getQsbOffsetY() - mTaskbarViewHeight) / 2;
- }
-
- /**
* Sets the alpha value of just our ShortcutAndWidgetContainer.
*/
public void setIconsAlpha(float alpha) {
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 29a0223..a2bd201 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -36,7 +36,6 @@
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
-import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import android.util.Xml;
@@ -45,7 +44,6 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DisplayController.Info;
import com.android.launcher3.util.IntArray;
@@ -253,17 +251,10 @@
private String initGrid(Context context, String gridName) {
Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
- // Determine if we have split display
-
- boolean isTablet = false, isPhone = false;
- for (WindowBounds bounds : displayInfo.supportedBounds) {
- if (displayInfo.isTablet(bounds)) {
- isTablet = true;
- } else {
- isPhone = true;
- }
- }
- boolean isSplitDisplay = isPhone && isTablet && ENABLE_TWO_PANEL_HOME.get();
+ // Each screen has two profiles (portrait/landscape), so devices with four or more
+ // supported profiles implies two or more internal displays.
+ boolean isSplitDisplay =
+ displayInfo.supportedBounds.size() >= 4 && ENABLE_TWO_PANEL_HOME.get();
ArrayList<DisplayOption> allOptions =
getPredefinedDeviceProfiles(context, gridName, isSplitDisplay);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 099f256..78a8a97 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -48,7 +48,6 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_RECONFIGURED;
import static com.android.launcher3.model.ItemInstallQueue.FLAG_ACTIVITY_PAUSED;
import static com.android.launcher3.model.ItemInstallQueue.FLAG_DRAG_AND_DROP;
-import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
import static com.android.launcher3.popup.SystemShortcut.APP_INFO;
import static com.android.launcher3.popup.SystemShortcut.INSTALL;
import static com.android.launcher3.popup.SystemShortcut.WIDGETS;
@@ -106,6 +105,7 @@
import android.widget.Toast;
import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;
@@ -117,6 +117,7 @@
import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.DiscoveryBounce;
+import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
@@ -166,6 +167,7 @@
import com.android.launcher3.util.ActivityTracker;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
@@ -173,6 +175,7 @@
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.PendingRequestArgs;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.SystemUiController;
import com.android.launcher3.util.Themes;
@@ -247,8 +250,6 @@
protected static final int REQUEST_LAST = 100;
// Type: int
- private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen";
- // Type: int
private static final String RUNTIME_STATE = "launcher.state";
// Type: PendingRequestArgs
private static final String RUNTIME_STATE_PENDING_REQUEST_ARGS = "launcher.request_args";
@@ -286,6 +287,8 @@
private WidgetManagerHelper mAppWidgetManager;
private LauncherAppWidgetHost mAppWidgetHost;
+ private LauncherPageRestoreHelper mPageRestoreHelper;
+
private final int[] mTmpAddItemCellCoordinates = new int[2];
@Thunk
@@ -321,8 +324,8 @@
private PopupDataProvider mPopupDataProvider;
- private int mSynchronouslyBoundPage = PagedView.INVALID_PAGE;
- private int mPageToBindSynchronously = PagedView.INVALID_PAGE;
+ private IntSet mSynchronouslyBoundPages = new IntSet();
+ private IntSet mPagesToBindSynchronously = new IntSet();
// We only want to get the SharedPreferences once since it does an FS stat each time we get
// it from the context.
@@ -457,13 +460,10 @@
restoreState(savedInstanceState);
mStateManager.reapplyState();
- // We only load the page synchronously if the user rotates (or triggers a
- // configuration change) while launcher is in the foreground
- int currentScreen = PagedView.INVALID_PAGE;
+ mPageRestoreHelper = new LauncherPageRestoreHelper(mWorkspace);
if (savedInstanceState != null) {
- currentScreen = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, currentScreen);
+ mPagesToBindSynchronously = mPageRestoreHelper.getPagesToRestore(savedInstanceState);
}
- mPageToBindSynchronously = currentScreen;
if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
@@ -595,7 +595,7 @@
}
onDeviceProfileInitiated();
- mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true);
+ mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true, this);
}
public RotationHelper getRotationHelper() {
@@ -1188,6 +1188,7 @@
// Until the workspace is bound, ensure that we keep the wallpaper offset locked to the
// default state, otherwise we will update to the wrong offsets in RTL
mWorkspace.lockWallpaperToDefaultPage();
+ mWorkspace.bindAndInitLeftPanel();
mWorkspace.bindAndInitFirstWorkspaceScreen(null /* recycled qsb */);
mDragController.addDragListener(mWorkspace);
@@ -1308,7 +1309,8 @@
}
}
- public FolderIcon findFolderIcon(final int folderIconId) {
+ @Override
+ public @Nullable FolderIcon findFolderIcon(final int folderIconId) {
return (FolderIcon) mWorkspace.getHomescreenIconByItemId(folderIconId);
}
@@ -1583,18 +1585,17 @@
@Override
public void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
- mWorkspace.restoreInstanceStateForChild(mSynchronouslyBoundPage);
+ if (mSynchronouslyBoundPages != null) {
+ mSynchronouslyBoundPages.forEach(page -> mWorkspace.restoreInstanceStateForChild(page));
+ }
}
@Override
protected void onSaveInstanceState(Bundle outState) {
- if (mWorkspace.getChildCount() > 0) {
- outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getNextPage());
+ mPageRestoreHelper.savePagesToRestore(outState);
- }
outState.putInt(RUNTIME_STATE, mStateManager.getState().ordinal);
-
AbstractFloatingView widgets = AbstractFloatingView
.getOpenView(this, AbstractFloatingView.TYPE_WIDGETS_FULL_SHEET);
if (widgets != null) {
@@ -2073,24 +2074,24 @@
}
/**
- * Sets the next page to bind synchronously on next bind.
- * @param page
+ * Sets the next pages to bind synchronously on next bind.
+ * @param pages should not be null.
*/
- public void setPageToBindSynchronously(int page) {
- mPageToBindSynchronously = page;
+ public void setPagesToBindSynchronously(@NonNull IntSet pages) {
+ mPagesToBindSynchronously = pages;
}
/**
* Implementation of the method from LauncherModel.Callbacks.
*/
@Override
- public int getPageToBindSynchronously() {
- if (mPageToBindSynchronously != PagedView.INVALID_PAGE) {
- return mPageToBindSynchronously;
- } else if (mWorkspace != null) {
- return mWorkspace.getCurrentPage();
+ public IntSet getPagesToBindSynchronously() {
+ if (mPagesToBindSynchronously != null && !mPagesToBindSynchronously.isEmpty()) {
+ return mPagesToBindSynchronously;
+ } else if (mWorkspace != null) {
+ return mWorkspace.getVisiblePageIndices();
} else {
- return 0;
+ return new IntSet();
}
}
@@ -2101,7 +2102,7 @@
@Override
public void clearPendingBinds() {
if (mPendingExecutor != null) {
- mPendingExecutor.markCompleted();
+ mPendingExecutor.cancel();
mPendingExecutor = null;
// We might have set this flag previously and forgot to clear it.
@@ -2139,11 +2140,16 @@
@Override
public void bindScreens(IntArray orderedScreenIds) {
- // Make sure the first screen is always at the start.
+ // Make sure the first screen is at the start if there's no widget panel,
+ // or on the second place if the first is the widget panel
+ boolean isLeftPanelShown =
+ mWorkspace.mWorkspaceScreens.containsKey(Workspace.LEFT_PANEL_ID);
+ int firstScreenPosition = isLeftPanelShown && orderedScreenIds.size() > 1 ? 1 : 0;
+
if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
- orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != 0) {
+ orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != firstScreenPosition) {
orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
- orderedScreenIds.add(0, Workspace.FIRST_SCREEN_ID);
+ orderedScreenIds.add(firstScreenPosition, Workspace.FIRST_SCREEN_ID);
} else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
// If there are no screens, we need to have an empty screen
mWorkspace.addExtraEmptyScreen();
@@ -2160,10 +2166,17 @@
int count = orderedScreenIds.size();
for (int i = 0; i < count; i++) {
int screenId = orderedScreenIds.get(i);
- if (!FeatureFlags.QSB_ON_FIRST_SCREEN || screenId != Workspace.FIRST_SCREEN_ID) {
+ if (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID) {
// No need to bind the first screen, as its always bound.
- mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
+ continue;
}
+
+ if (screenId == Workspace.LEFT_PANEL_ID) {
+ // No need to bind the left panel, as its always bound.
+ continue;
+ }
+
+ mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
}
}
@@ -2238,6 +2251,11 @@
continue;
}
+ // Skip if the item is on the left widget panel but the panel is not shown
+ if (item.screenId == Workspace.LEFT_PANEL_ID && !getDeviceProfile().isTwoPanels) {
+ continue;
+ }
+
final View view;
switch (item.itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
@@ -2528,25 +2546,6 @@
return info;
}
- public void onPageBoundSynchronously(int page) {
- mSynchronouslyBoundPage = page;
- mWorkspace.setCurrentPage(page);
- mPageToBindSynchronously = PagedView.INVALID_PAGE;
- }
-
- @Override
- public void executeOnNextDraw(ViewOnDrawExecutor executor) {
- clearPendingBinds();
- mPendingExecutor = executor;
- if (!isInState(ALL_APPS)) {
- mAppsView.getAppsStore().enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW);
- mPendingExecutor.execute(() -> mAppsView.getAppsStore().disableDeferUpdates(
- AllAppsStore.DEFER_UPDATES_NEXT_DRAW));
- }
-
- executor.attachTo(this);
- }
-
public void clearPendingExecutor(ViewOnDrawExecutor executor) {
if (mPendingExecutor == executor) {
mPendingExecutor = null;
@@ -2554,22 +2553,31 @@
}
@Override
- public void finishFirstPageBind(final ViewOnDrawExecutor executor) {
+ public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+ mSynchronouslyBoundPages = boundPages;
+ if (!boundPages.isEmpty()) {
+ mWorkspace.setCurrentPage(boundPages.getArray().get(0));
+ }
+ mPagesToBindSynchronously = new IntSet();
+
+ clearPendingBinds();
+ ViewOnDrawExecutor executor = new ViewOnDrawExecutor(pendingTasks);
+ mPendingExecutor = executor;
+ if (!isInState(ALL_APPS)) {
+ mAppsView.getAppsStore().enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW);
+ pendingTasks.add(() -> mAppsView.getAppsStore().disableDeferUpdates(
+ AllAppsStore.DEFER_UPDATES_NEXT_DRAW));
+ }
+
AlphaProperty property = mDragLayer.getAlphaProperty(ALPHA_INDEX_LAUNCHER_LOAD);
if (property.getValue() < 1) {
ObjectAnimator anim = ObjectAnimator.ofFloat(property, MultiValueAlpha.VALUE, 1);
- if (executor != null) {
- anim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- executor.onLoadAnimationCompleted();
- }
- });
- }
+ anim.addListener(AnimatorListeners.forEndCallback(executor::onLoadAnimationCompleted));
anim.start();
- } else if (executor != null) {
+ } else {
executor.onLoadAnimationCompleted();
}
+ executor.attachTo(this);
}
/**
@@ -2577,7 +2585,7 @@
*
* Implementation of the method from LauncherModel.Callbacks.
*/
- public void finishBindingItems(int pageBoundFirst) {
+ public void finishBindingItems(IntSet pagesBoundFirst) {
Object traceToken = TraceHelper.INSTANCE.beginSection("finishBindingItems");
mWorkspace.restoreInstanceStateForRemainingPages();
@@ -2589,14 +2597,13 @@
mPendingActivityResult = null;
}
- ItemInstallQueue.INSTANCE.get(this)
- .resumeModelPush(FLAG_LOADER_RUNNING);
-
+ int currentPage = pagesBoundFirst != null && !pagesBoundFirst.isEmpty()
+ ? pagesBoundFirst.getArray().get(0) : PagedView.INVALID_PAGE;
// When undoing the removal of the last item on a page, return to that page.
// Since we are just resetting the current page without user interaction,
// override the previous page so we don't log the page switch.
- mWorkspace.setCurrentPage(pageBoundFirst, pageBoundFirst /* overridePrevPage */);
- mPageToBindSynchronously = PagedView.INVALID_PAGE;
+ mWorkspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */);
+ mPagesToBindSynchronously = new IntSet();
// Cache one page worth of icons
getViewCache().setCacheSize(R.layout.folder_application,
@@ -2666,7 +2673,7 @@
@Override
public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) {
if (!updated.isEmpty()) {
- mWorkspace.updateShortcuts(updated);
+ mWorkspace.updateWorkspaceItems(updated, this);
PopupContainerWithArrow.dismissInvalidPopup(this);
}
}
@@ -2678,7 +2685,7 @@
*/
@Override
public void bindRestoreItemsChange(HashSet<ItemInfo> updates) {
- mWorkspace.updateRestoreItems(updates);
+ mWorkspace.updateRestoreItems(updates, this);
}
/**
@@ -2880,13 +2887,6 @@
return new float[] {NO_SCALE, NO_OFFSET};
}
- /**
- * @see LauncherState#getTaskbarScale(Launcher)
- */
- public float getNormalTaskbarScale() {
- return 1f;
- }
-
public static Launcher getLauncher(Context context) {
return fromContext(context);
}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index eef3980..6966abf 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -144,9 +144,10 @@
enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList));
}
- public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) {
+ public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges,
+ @Nullable Callbacks owner) {
return new ModelWriter(mApp.getContext(), this, mBgDataModel,
- hasVerticalHotseat, verifyChanges);
+ hasVerticalHotseat, verifyChanges, owner);
}
@Override
@@ -329,7 +330,7 @@
public boolean addCallbacksAndLoad(Callbacks callbacks) {
synchronized (mLock) {
addCallbacks(callbacks);
- return startLoader();
+ return startLoader(new Callbacks[] { callbacks });
}
}
@@ -349,26 +350,32 @@
* @return true if the page could be bound synchronously.
*/
public boolean startLoader() {
+ return startLoader(new Callbacks[0]);
+ }
+
+ private boolean startLoader(Callbacks[] newCallbacks) {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING);
synchronized (mLock) {
- // Don't bother to start the thread if we know it's not going to do anything
- final Callbacks[] callbacksList = getCallbacks();
+ // If there is already one running, tell it to stop.
+ boolean wasRunning = stopLoader();
+ boolean bindDirectly = mModelLoaded && !mIsLoaderTaskRunning;
+ boolean bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.length == 0;
+ final Callbacks[] callbacksList = bindAllCallbacks ? getCallbacks() : newCallbacks;
+
if (callbacksList.length > 0) {
// Clear any pending bind-runnables from the synchronized load process.
for (Callbacks cb : callbacksList) {
MAIN_EXECUTOR.execute(cb::clearPendingBinds);
}
- // If there is already one running, tell it to stop.
- stopLoader();
LoaderResults loaderResults = new LoaderResults(
mApp, mBgDataModel, mBgAllAppsList, callbacksList);
- if (mModelLoaded && !mIsLoaderTaskRunning) {
+ if (bindDirectly) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
- loaderResults.bindWorkspace();
+ loaderResults.bindWorkspace(bindAllCallbacks);
// For now, continue posting the binding of AllApps as there are other
// issues that arise from that.
loaderResults.bindAllApps();
@@ -387,7 +394,7 @@
* If there is already a loader task running, tell it to stop.
* @return true if an existing loader was stopped.
*/
- public boolean stopLoader() {
+ private boolean stopLoader() {
synchronized (mLock) {
LoaderTask oldTask = mLoaderTask;
mLoaderTask = null;
diff --git a/src/com/android/launcher3/LauncherPageRestoreHelper.java b/src/com/android/launcher3/LauncherPageRestoreHelper.java
new file mode 100644
index 0000000..e679a12
--- /dev/null
+++ b/src/com/android/launcher3/LauncherPageRestoreHelper.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright (C) 2021 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.launcher3;
+
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.launcher3.util.IntSet;
+
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
+/**
+ * There's a logic which prioritizes the binding for the current page and defers the other pages'
+ * binding. If two panel home is enabled, we want to bind both pages together.
+ * LauncherPageRestoreHelper's purpose is to contain the logic for persisting, restoring and
+ * calculating which pages to load immediately.
+ */
+public class LauncherPageRestoreHelper {
+
+ public static final String TAG = "LauncherPageRestoreHelper";
+
+ // Type: int
+ private static final String RUNTIME_STATE_CURRENT_SCREEN = "launcher.current_screen";
+ // Type: int
+ private static final String RUNTIME_STATE_CURRENT_SCREEN_COUNT =
+ "launcher.current_screen_count";
+
+ private Workspace mWorkspace;
+
+ public LauncherPageRestoreHelper(Workspace workspace) {
+ this.mWorkspace = workspace;
+ }
+
+ /**
+ * Some configuration changes trigger Launcher to recreate itself, and we want to give more
+ * priority to the currently active pages in the restoration process.
+ */
+ @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+ public IntSet getPagesToRestore(Bundle savedInstanceState) {
+ IntSet pagesToRestore = new IntSet();
+
+ if (savedInstanceState == null) {
+ return pagesToRestore;
+ }
+
+ int currentPage = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN, -1);
+ int totalPageCount = savedInstanceState.getInt(RUNTIME_STATE_CURRENT_SCREEN_COUNT, -1);
+ int panelCount = mWorkspace.getPanelCount();
+
+ if (totalPageCount <= 0 || currentPage < 0) {
+ Log.e(TAG, "getPagesToRestore: Invalid input: " + totalPageCount + ", " + currentPage);
+ return pagesToRestore;
+ }
+
+ int newCurrentPage = mWorkspace.getLeftmostVisiblePageForIndex(currentPage);
+ for (int page = newCurrentPage; page < newCurrentPage + panelCount
+ && page < totalPageCount; page++) {
+ pagesToRestore.add(page);
+ }
+
+ return pagesToRestore;
+ }
+
+ /**
+ * This should be called from Launcher's onSaveInstanceState method to persist everything that
+ * is necessary to calculate later which pages need to be initialized first after a
+ * configuration change.
+ */
+ @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+ public void savePagesToRestore(Bundle outState) {
+ int pageCount = mWorkspace.getChildCount();
+ if (pageCount > 0) {
+ outState.putInt(RUNTIME_STATE_CURRENT_SCREEN, mWorkspace.getCurrentPage());
+ outState.putInt(RUNTIME_STATE_CURRENT_SCREEN_COUNT, pageCount);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index 3399ce9..7985ab5 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -181,14 +181,6 @@
return launcher.getNormalOverviewScaleAndOffset();
}
- public float getTaskbarScale(Launcher launcher) {
- return launcher.getNormalTaskbarScale();
- }
-
- public float getTaskbarTranslationY(Launcher launcher) {
- return -launcher.getHotseat().getTaskbarOffsetY();
- }
-
public float getOverviewFullscreenProgress() {
return 0;
}
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index b423871..20f5f9b 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -16,6 +16,8 @@
package com.android.launcher3;
+import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+
import static com.android.launcher3.anim.Interpolators.SCROLL;
import static com.android.launcher3.compat.AccessibilityManagerCompat.isAccessibilityEnabled;
import static com.android.launcher3.compat.AccessibilityManagerCompat.isObservedEventType;
@@ -48,6 +50,7 @@
import android.widget.ScrollView;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.config.FeatureFlags;
@@ -55,6 +58,7 @@
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.touch.PagedOrientationHandler.ChildBounds;
import com.android.launcher3.util.EdgeEffectCompat;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
@@ -100,6 +104,10 @@
@ViewDebug.ExportedProperty(category = "launcher")
protected int mCurrentPage;
+ // Difference between current scroll position and mCurrentPage's page scroll. Used to maintain
+ // relative scroll position unchanged in updateCurrentPageScroll. Cleared when snapping to a
+ // page.
+ protected int mCurrentPageScrollDiff;
@ViewDebug.ExportedProperty(category = "launcher")
protected int mNextPage = INVALID_PAGE;
@@ -243,7 +251,7 @@
// If the current page is invalid, just reset the scroll position to zero
int newPosition = 0;
if (0 <= mCurrentPage && mCurrentPage < getPageCount()) {
- newPosition = getScrollForPage(mCurrentPage);
+ newPosition = getScrollForPage(mCurrentPage) + mCurrentPageScrollDiff;
}
mOrientationHandler.set(this, VIEW_SCROLL_TO, newPosition);
mScroller.startScroll(mScroller.getCurrX(), 0, newPosition - mScroller.getCurrX(), 0);
@@ -291,9 +299,15 @@
return newPage;
}
- private int getLeftmostVisiblePageForIndex(int pageIndex) {
+ /**
+ * In most cases where panelCount is 1, this method will just return the page index that was
+ * passed in.
+ * But for example when two panel home is enabled we might need the leftmost visible page index
+ * because that page is the current page.
+ */
+ public int getLeftmostVisiblePageForIndex(int pageIndex) {
int panelCount = getPanelCount();
- return (pageIndex / panelCount) * panelCount;
+ return pageIndex - pageIndex % panelCount;
}
/**
@@ -304,16 +318,34 @@
}
/**
+ * Returns an IntSet with the indices of the currently visible pages
+ */
+ @VisibleForTesting(otherwise = PACKAGE_PRIVATE)
+ public IntSet getVisiblePageIndices() {
+ IntSet visiblePageIndices = new IntSet();
+ int panelCount = getPanelCount();
+ int pageCount = getPageCount();
+
+ // If a device goes from one panel to two panel (i.e. unfolding a foldable device) while
+ // an odd indexed page is the current page, then the new leftmost visible page will be
+ // different from the old mCurrentPage.
+ int currentPage = getLeftmostVisiblePageForIndex(mCurrentPage);
+ for (int page = currentPage; page < currentPage + panelCount && page < pageCount; page++) {
+ visiblePageIndices.add(page);
+ }
+ return visiblePageIndices;
+ }
+
+ /**
* Executes the callback against each visible page
*/
public void forEachVisiblePage(Consumer<View> callback) {
- int panelCount = getPanelCount();
- for (int i = mCurrentPage; i < mCurrentPage + panelCount; i++) {
- View page = getPageAt(i);
+ getVisiblePageIndices().forEach(pageIndex -> {
+ View page = getPageAt(pageIndex);
if (page != null) {
callback.accept(page);
}
- }
+ });
}
/**
@@ -424,6 +456,7 @@
* to provide custom behavior during animation.
*/
protected void onPageEndTransition() {
+ mCurrentPageScrollDiff = 0;
AccessibilityManagerCompat.sendScrollFinishedEventToTest(getContext());
AccessibilityManagerCompat.sendCustomAccessibilityEvent(getPageAt(mCurrentPage),
AccessibilityEvent.TYPE_VIEW_FOCUSED, null);
@@ -1054,26 +1087,25 @@
protected float getScrollProgress(int screenCenter, View v, int page) {
final int halfScreenSize = getMeasuredWidth() / 2;
-
int delta = screenCenter - (getScrollForPage(page) + halfScreenSize);
- int count = getChildCount();
+ int panelCount = getPanelCount();
+ int pageCount = getChildCount();
- final int totalDistance;
-
- int adjacentPage = page + 1;
+ int adjacentPage = page + panelCount;
if ((delta < 0 && !mIsRtl) || (delta > 0 && mIsRtl)) {
- adjacentPage = page - 1;
+ adjacentPage = page - panelCount;
}
- if (adjacentPage < 0 || adjacentPage > count - 1) {
- totalDistance = v.getMeasuredWidth() + mPageSpacing;
+ final int totalDistance;
+ if (adjacentPage < 0 || adjacentPage > pageCount - 1) {
+ totalDistance = (v.getMeasuredWidth() + mPageSpacing) * panelCount;
} else {
totalDistance = Math.abs(getScrollForPage(adjacentPage) - getScrollForPage(page));
}
float scrollProgress = delta / (totalDistance * 1.0f);
scrollProgress = Math.min(scrollProgress, MAX_SCROLL_PROGRESS);
- scrollProgress = Math.max(scrollProgress, - MAX_SCROLL_PROGRESS);
+ scrollProgress = Math.max(scrollProgress, -MAX_SCROLL_PROGRESS);
return scrollProgress;
}
@@ -1120,6 +1152,10 @@
mAllowOverScroll = enable;
}
+ protected float getSignificantMoveThreshold() {
+ return SIGNIFICANT_MOVE_THRESHOLD;
+ }
+
@Override
public boolean onTouchEvent(MotionEvent ev) {
// Skip touch handling if there are no pages to swipe
@@ -1191,6 +1227,7 @@
}
delta -= consumed;
}
+ delta /= mOrientationHandler.getPrimaryScale(this);
// Only scroll and update mLastMotionX if we have moved some discrete amount. We
// keep the remainder because we are actually testing if we've moved from the last
@@ -1243,11 +1280,12 @@
int velocity = (int) mOrientationHandler.getPrimaryVelocity(velocityTracker,
mActivePointerId);
- int delta = (int) (primaryDirection - mDownMotionPrimary);
+ float delta = primaryDirection - mDownMotionPrimary;
+ delta /= mOrientationHandler.getPrimaryScale(this);
int pageOrientedSize = mOrientationHandler.getMeasuredSize(getPageAt(mCurrentPage));
- boolean isSignificantMove = Math.abs(delta) > pageOrientedSize *
- SIGNIFICANT_MOVE_THRESHOLD;
+ boolean isSignificantMove = Math.abs(delta)
+ > pageOrientedSize * getSignificantMoveThreshold();
mTotalMotion += Math.abs(mLastMotion + mLastMotionRemainder - primaryDirection);
boolean passedSlop = mAllowEasyFling || mTotalMotion > mPageSlop;
@@ -1610,7 +1648,7 @@
public boolean scrollLeft() {
if (getNextPage() > 0) {
- snapToPage(getNextPage() - 1);
+ snapToPage(getNextPage() - getPanelCount());
return true;
}
return mAllowOverScroll;
@@ -1618,7 +1656,7 @@
public boolean scrollRight() {
if (getNextPage() < getChildCount() - 1) {
- snapToPage(getNextPage() + 1);
+ snapToPage(getNextPage() + getPanelCount());
return true;
}
return mAllowOverScroll;
diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
index 519b63d..bebbf4f 100644
--- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java
+++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java
@@ -31,7 +31,7 @@
import com.android.launcher3.CellLayout.ContainerType;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon.FolderIconParent {
static final String TAG = "ShortcutAndWidgetContainer";
@@ -104,9 +104,9 @@
public void setupLp(View child) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- if (child instanceof LauncherAppWidgetHostView) {
+ if (child instanceof NavigableAppWidgetHostView) {
DeviceProfile profile = mActivity.getDeviceProfile();
- ((LauncherAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
+ ((NavigableAppWidgetHostView) child).getWidgetInset(profile, mTempRect);
lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
profile.appWidgetScale.x, profile.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
@@ -129,8 +129,8 @@
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
final DeviceProfile dp = mActivity.getDeviceProfile();
- if (child instanceof LauncherAppWidgetHostView) {
- ((LauncherAppWidgetHostView) child).getWidgetInset(dp, mTempRect);
+ if (child instanceof NavigableAppWidgetHostView) {
+ ((NavigableAppWidgetHostView) child).getWidgetInset(dp, mTempRect);
lp.setup(mCellWidth, mCellHeight, invertLayoutHorizontally(), mCountX, mCountY,
dp.appWidgetScale.x, dp.appWidgetScale.y, mBorderSpacing, mTempRect);
} else {
@@ -178,16 +178,16 @@
*/
public void layoutChild(View child) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams();
- if (child instanceof LauncherAppWidgetHostView) {
- LauncherAppWidgetHostView lahv = (LauncherAppWidgetHostView) child;
+ if (child instanceof NavigableAppWidgetHostView) {
+ NavigableAppWidgetHostView nahv = (NavigableAppWidgetHostView) child;
// Scale and center the widget to fit within its cells.
DeviceProfile profile = mActivity.getDeviceProfile();
float scaleX = profile.appWidgetScale.x;
float scaleY = profile.appWidgetScale.y;
- lahv.setScaleToFit(Math.min(scaleX, scaleY));
- lahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
+ nahv.setScaleToFit(Math.min(scaleX, scaleY));
+ nahv.setTranslationForCentering(-(lp.width - (lp.width * scaleX)) / 2.0f,
-(lp.height - (lp.height * scaleY)) / 2.0f);
}
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 75f6278..3cabc87 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -85,6 +85,7 @@
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.PackageManagerHelper;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.PendingAddShortcutInfo;
@@ -659,25 +660,26 @@
* @param outObj this is set to the internal data associated with {@param info},
* eg {@link LauncherActivityInfo} or {@link ShortcutInfo}.
*/
- public static Drawable getFullDrawable(Launcher launcher, ItemInfo info, int width, int height,
+ public static Drawable getFullDrawable(Context context, ItemInfo info, int width, int height,
Object[] outObj) {
- Drawable icon = loadFullDrawableWithoutTheme(launcher, info, width, height, outObj);
+ Drawable icon = loadFullDrawableWithoutTheme(context, info, width, height, outObj);
if (icon instanceof BitmapInfo.Extender) {
- icon = ((BitmapInfo.Extender) icon).getThemedDrawable(launcher);
+ icon = ((BitmapInfo.Extender) icon).getThemedDrawable(context);
}
return icon;
}
- private static Drawable loadFullDrawableWithoutTheme(Launcher launcher, ItemInfo info,
+ private static Drawable loadFullDrawableWithoutTheme(Context context, ItemInfo info,
int width, int height, Object[] outObj) {
- LauncherAppState appState = LauncherAppState.getInstance(launcher);
+ ActivityContext activity = ActivityContext.lookupContext(context);
+ LauncherAppState appState = LauncherAppState.getInstance(context);
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
- LauncherActivityInfo activityInfo = launcher.getSystemService(LauncherApps.class)
+ LauncherActivityInfo activityInfo = context.getSystemService(LauncherApps.class)
.resolveActivity(info.getIntent(), info.user);
outObj[0] = activityInfo;
- return activityInfo == null ? null : LauncherAppState.getInstance(launcher)
+ return activityInfo == null ? null : LauncherAppState.getInstance(context)
.getIconProvider().getIcon(
- activityInfo, launcher.getDeviceProfile().inv.fillResIconDpi);
+ activityInfo, activity.getDeviceProfile().inv.fillResIconDpi);
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
if (info instanceof PendingAddShortcutInfo) {
ShortcutConfigActivityInfo activityInfo =
@@ -686,18 +688,18 @@
return activityInfo.getFullResIcon(appState.getIconCache());
}
List<ShortcutInfo> si = ShortcutKey.fromItemInfo(info)
- .buildRequest(launcher)
+ .buildRequest(context)
.query(ShortcutRequest.ALL);
if (si.isEmpty()) {
return null;
} else {
outObj[0] = si.get(0);
- return ShortcutCachingLogic.getIcon(launcher, si.get(0),
+ return ShortcutCachingLogic.getIcon(context, si.get(0),
appState.getInvariantDeviceProfile().fillResIconDpi);
}
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
FolderAdaptiveIcon icon = FolderAdaptiveIcon.createFolderAdaptiveIcon(
- launcher, info.id, new Point(width, height));
+ activity, info.id, new Point(width, height));
if (icon == null) {
return null;
}
@@ -715,8 +717,8 @@
* badge. When dragged from workspace or folder, it may contain app AND/OR work profile badge
**/
@TargetApi(Build.VERSION_CODES.O)
- public static Drawable getBadge(Launcher launcher, ItemInfo info, Object obj) {
- LauncherAppState appState = LauncherAppState.getInstance(launcher);
+ public static Drawable getBadge(Context context, ItemInfo info, Object obj) {
+ LauncherAppState appState = LauncherAppState.getInstance(context);
int iconSize = appState.getInvariantDeviceProfile().iconBitmapSize;
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
boolean iconBadged = (info instanceof ItemInfoWithIcon)
@@ -736,7 +738,7 @@
} else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
return ((FolderAdaptiveIcon) obj).getBadge();
} else {
- return launcher.getPackageManager()
+ return context.getPackageManager()
.getUserBadgedIcon(new FixedSizeEmptyDrawable(iconSize), info.user);
}
}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 5bdc402..a585be6 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -16,6 +16,8 @@
package com.android.launcher3;
+import static androidx.annotation.VisibleForTesting.PROTECTED;
+
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.LauncherState.ALL_APPS;
@@ -63,6 +65,8 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Toast;
+import androidx.annotation.VisibleForTesting;
+
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
import com.android.launcher3.accessibility.WorkspaceAccessibilityHelper;
import com.android.launcher3.anim.Interpolators;
@@ -79,7 +83,6 @@
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.folder.PreviewBackground;
import com.android.launcher3.graphics.DragPreviewProvider;
-import com.android.launcher3.graphics.PreloadIconDrawable;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.logger.LauncherAtom;
@@ -101,6 +104,7 @@
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LauncherBindableItemsContainer;
import com.android.launcher3.util.OverlayEdgeEffect;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.RunnableList;
@@ -119,7 +123,6 @@
import java.util.ArrayList;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -132,7 +135,7 @@
public class Workspace extends PagedView<WorkspacePageIndicator>
implements DropTarget, DragSource, View.OnTouchListener,
DragController.DragListener, Insettable, StateHandler<LauncherState>,
- WorkspaceLayoutManager {
+ WorkspaceLayoutManager, LauncherBindableItemsContainer {
/** The value that {@link #mTransitionProgress} must be greater than for
* {@link #transitionStateShouldAllowDrop()} to return true. */
@@ -312,18 +315,34 @@
// Increase our bottom insets so we don't overlap with the taskbar.
mInsets.bottom += grid.nonOverlappingTaskbarInset;
- if (isTwoPanelEnabled()) {
+ if (grid.isTwoPanels) {
setPageSpacing(0); // we have two pages and we don't want any spacing
- } else if (mWorkspaceFadeInAdjacentScreens) {
- // In landscape mode the page spacing is set to the default.
- setPageSpacing(grid.edgeMarginPx);
+
+ // Add left widget panel if it isn't already there
+ if (!mWorkspaceScreens.containsKey(LEFT_PANEL_ID)) {
+ int newCurrentPage = mCurrentPage + 1;
+ bindAndInitLeftPanel();
+ setCurrentPage(newCurrentPage);
+ }
} else {
- // In portrait, we want the pages spaced such that there is no
- // overhang of the previous / next page into the current page viewport.
- // We assume symmetrical padding in portrait mode.
- int maxInsets = Math.max(insets.left, insets.right);
- int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
- setPageSpacing(Math.max(maxInsets, maxPadding));
+ if (mWorkspaceFadeInAdjacentScreens) {
+ // In landscape mode the page spacing is set to the default.
+ setPageSpacing(grid.edgeMarginPx);
+ } else {
+ // In portrait, we want the pages spaced such that there is no
+ // overhang of the previous / next page into the current page viewport.
+ // We assume symmetrical padding in portrait mode.
+ int maxInsets = Math.max(insets.left, insets.right);
+ int maxPadding = Math.max(grid.edgeMarginPx, padding.left + 1);
+ setPageSpacing(Math.max(maxInsets, maxPadding));
+ }
+
+ // Remove left widget panel if it is present
+ if (mWorkspaceScreens.containsKey(LEFT_PANEL_ID)) {
+ int newCurrentPage = mCurrentPage - 1;
+ removeLeftPanel();
+ setCurrentPage(newCurrentPage);
+ }
}
int paddingLeftRight = grid.cellLayoutPaddingLeftRightPx;
@@ -462,7 +481,8 @@
}
@Override
- protected int getPanelCount() {
+ @VisibleForTesting(otherwise = PROTECTED)
+ public int getPanelCount() {
return isTwoPanelEnabled() ? 2 : super.getPanelCount();
}
@@ -553,7 +573,7 @@
return;
}
// Add the first page
- CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, 0);
+ CellLayout firstPage = insertNewWorkspaceScreen(Workspace.FIRST_SCREEN_ID, getChildCount());
// Always add a QSB on the first screen.
if (qsb == null) {
// In transposed layout, we add the QSB in the Grid. As workspace does not touch the
@@ -572,6 +592,19 @@
}
}
+ /**
+ * Initializes and binds the left panel
+ */
+ public void bindAndInitLeftPanel() {
+ if (!FeatureFlags.QSB_ON_FIRST_SCREEN || !isTwoPanelEnabled()
+ || mWorkspaceScreens.containsKey(Workspace.LEFT_PANEL_ID)) {
+ return;
+ }
+
+ insertNewWorkspaceScreen(Workspace.LEFT_PANEL_ID, getChildCount());
+ mLauncher.getModelWriter().setLeftPanelShown(true);
+ }
+
public void removeAllWorkspaceScreens() {
// Disable all layout transitions before removing all pages to ensure that we don't get the
// transition animations competing with us changing the scroll when we add pages
@@ -593,6 +626,7 @@
mLauncher.mHandler.removeCallbacksAndMessages(DeferredWidgetRefresh.class);
// Ensure that the first page is always present
+ bindAndInitLeftPanel();
bindAndInitFirstWorkspaceScreen(qsb);
// Re-enable the layout transitions
@@ -613,6 +647,18 @@
insertNewWorkspaceScreen(screenId, getChildCount());
}
+ private void removeLeftPanel() {
+ if (!mWorkspaceScreens.containsKey(LEFT_PANEL_ID)) {
+ return;
+ }
+ mLauncher.getModelWriter().setLeftPanelShown(false);
+ CellLayout leftPanel = mWorkspaceScreens.get(LEFT_PANEL_ID);
+ mWorkspaceScreens.remove(LEFT_PANEL_ID);
+ removeView(leftPanel);
+ mScreenOrder.removeValue(LEFT_PANEL_ID);
+ updatePageScrollValues();
+ }
+
public CellLayout insertNewWorkspaceScreen(int screenId, int insertIndex) {
if (mWorkspaceScreens.containsKey(screenId)) {
throw new RuntimeException("Screen id " + screenId + " already exists!");
@@ -2690,9 +2736,6 @@
public void animateWidgetDrop(ItemInfo info, CellLayout cellLayout, final DragView dragView,
final Runnable onCompleteRunnable, int animationType, final View finalView,
boolean external) {
- Rect from = new Rect();
- mLauncher.getDragLayer().getViewRectRelativeToSelf(dragView, from);
-
int[] finalPos = new int[2];
float scaleXY[] = new float[2];
boolean scalePreview = !(info instanceof PendingAddShortcutInfo);
@@ -2736,8 +2779,8 @@
}
}
};
- dragLayer.animateViewIntoPosition(dragView, from.left, from.top, finalPos[0],
- finalPos[1], 1, 1, 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
+ dragLayer.animateViewIntoPosition(dragView, finalPos[0],
+ finalPos[1], 1, scaleXY[0], scaleXY[1], onComplete, endStyle,
duration, this);
}
}
@@ -2965,9 +3008,9 @@
* @param user The user of the app to match.
*/
public View getFirstMatchForAppClose(int preferredItemId, String packageName, UserHandle user) {
- final Workspace.ItemOperator preferredItem = (ItemInfo info, View view) ->
+ final ItemOperator preferredItem = (ItemInfo info, View view) ->
info != null && info.id == preferredItemId;
- final Workspace.ItemOperator preferredItemInFolder = (info, view) -> {
+ final ItemOperator preferredItemInFolder = (info, view) -> {
if (info instanceof FolderInfo) {
FolderInfo folderInfo = (FolderInfo) info;
for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
@@ -2978,14 +3021,14 @@
}
return false;
};
- final Workspace.ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
+ final ItemOperator packageAndUserAndApp = (ItemInfo info, View view) ->
info != null
&& info.itemType == ITEM_TYPE_APPLICATION
&& info.user.equals(user)
&& info.getTargetComponent() != null
&& TextUtils.equals(info.getTargetComponent().getPackageName(),
packageName);
- final Workspace.ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
+ final ItemOperator packageAndUserAndAppInFolder = (info, view) -> {
if (info instanceof FolderInfo) {
FolderInfo folderInfo = (FolderInfo) info;
for (WorkspaceItemInfo shortcutInfo : folderInfo.contents) {
@@ -3104,22 +3147,7 @@
stripEmptyScreens();
}
- public interface ItemOperator {
- /**
- * Process the next itemInfo, possibly with side-effect on the next item.
- *
- * @param info info for the shortcut
- * @param view view for the shortcut
- * @return true if done, false to continue the map
- */
- boolean evaluate(ItemInfo info, View view);
- }
-
- /**
- * Map the operator over the shortcuts and widgets, return the first-non-null value.
- *
- * @param op the operator to map over the shortcuts
- */
+ @Override
public void mapOverItems(ItemOperator op) {
for (CellLayout layout : getWorkspaceAndHotseatCellLayouts()) {
if (mapOverCellLayout(layout, op) != null) {
@@ -3145,31 +3173,6 @@
return null;
}
- void updateShortcuts(List<WorkspaceItemInfo> shortcuts) {
- final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
- ItemOperator op = (info, v) -> {
- if (v instanceof BubbleTextView && updates.contains(info)) {
- WorkspaceItemInfo si = (WorkspaceItemInfo) info;
- BubbleTextView shortcut = (BubbleTextView) v;
- Drawable oldIcon = shortcut.getIcon();
- boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
- && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
- shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
- } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
- ((FolderIcon) v).updatePreviewItems(updates::contains);
- }
-
- // Iterate all items
- return false;
- };
-
- mapOverItems(op);
- Folder openFolder = Folder.getOpen(mLauncher);
- if (openFolder != null) {
- openFolder.iterateOverItems(op);
- }
- }
-
public void updateNotificationDots(Predicate<PackageUserKey> updatedDots) {
final PackageUserKey packageUserKey = new PackageUserKey(null, null);
Predicate<ItemInfo> matcher = info -> !packageUserKey.updateFromItemInfo(info)
@@ -3209,28 +3212,6 @@
removeItemsByMatcher(matcher);
}
- public void updateRestoreItems(final HashSet<ItemInfo> updates) {
- ItemOperator op = (info, v) -> {
- if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
- && updates.contains(info)) {
- ((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
- } else if (v instanceof PendingAppWidgetHostView
- && info instanceof LauncherAppWidgetInfo
- && updates.contains(info)) {
- ((PendingAppWidgetHostView) v).applyState();
- } else if (v instanceof FolderIcon && info instanceof FolderInfo) {
- ((FolderIcon) v).updatePreviewItems(updates::contains);
- }
- // process all the shortcuts
- return false;
- };
- mapOverItems(op);
- Folder folder = Folder.getOpen(mLauncher);
- if (folder != null) {
- folder.iterateOverItems(op);
- }
- }
-
public void widgetsRestored(final ArrayList<LauncherAppWidgetInfo> changedInfo) {
if (!changedInfo.isEmpty()) {
DeferredWidgetRefresh widgetRefresh = new DeferredWidgetRefresh(changedInfo,
diff --git a/src/com/android/launcher3/WorkspaceLayoutManager.java b/src/com/android/launcher3/WorkspaceLayoutManager.java
index d6302ce..326e3c3 100644
--- a/src/com/android/launcher3/WorkspaceLayoutManager.java
+++ b/src/com/android/launcher3/WorkspaceLayoutManager.java
@@ -32,6 +32,8 @@
int EXTRA_EMPTY_SCREEN_ID = -201;
// The is the first screen. It is always present, even if its empty.
int FIRST_SCREEN_ID = 0;
+ // This panel is shown on the first page if the panel count is greater than 1.
+ int LEFT_PANEL_ID = -777;
/**
* At bind time, we use the rank (screenId) to compute x and y for hotseat items.
diff --git a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
index 30c3417..97052b2 100644
--- a/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
+++ b/src/com/android/launcher3/compat/AccessibilityManagerCompat.java
@@ -89,6 +89,14 @@
sendEventToTest(accessibilityManager, context, TestProtocol.PAUSE_DETECTED_MESSAGE, null);
}
+ public static void sendDismissAnimationEndsEventToTest(Context context) {
+ final AccessibilityManager accessibilityManager = getAccessibilityManagerForTest(context);
+ if (accessibilityManager == null) return;
+
+ sendEventToTest(accessibilityManager, context, TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE,
+ null);
+ }
+
private static void sendEventToTest(
AccessibilityManager accessibilityManager,
Context context, String eventTag, Bundle data) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index aab6cb2..1779ddb 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -220,14 +220,14 @@
+ "predictions to be updated while they are visible to the user.");
public static final BooleanFlag ENABLE_TASKBAR = getDebugFlag(
- "ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
+ "ENABLE_TASKBAR", true, "Allows a system Taskbar to be shown on larger devices.");
public static final BooleanFlag ENABLE_OVERVIEW_GRID = getDebugFlag(
- "ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
+ "ENABLE_OVERVIEW_GRID", true, "Uses grid overview layout. "
+ "Only applicable on large screen devices.");
public static final BooleanFlag ENABLE_TWO_PANEL_HOME = getDebugFlag(
- "ENABLE_TWO_PANEL_HOME", false,
+ "ENABLE_TWO_PANEL_HOME", true,
"Uses two panel on home screen. Only applicable on large screen devices.");
public static final BooleanFlag ENABLE_SCRIM_FOR_APP_LAUNCH = getDebugFlag(
@@ -254,6 +254,10 @@
"ENABLE_WALLPAPER_SCRIM", false,
"Enables scrim over wallpaper for text protection.");
+ public static final BooleanFlag WIDGETS_IN_LAUNCHER_PREVIEW = getDebugFlag(
+ "WIDGETS_IN_LAUNCHER_PREVIEW", true,
+ "Enables widgets in Launcher preview for the Wallpaper app.");
+
public static void initialize(Context context) {
synchronized (sDebugFlags) {
for (DebugFlag flag : sDebugFlags) {
@@ -291,7 +295,7 @@
public static class BooleanFlag {
public final String key;
- public boolean defaultValue;
+ public final boolean defaultValue;
public BooleanFlag(String key, boolean defaultValue) {
this.key = key;
@@ -310,16 +314,12 @@
protected StringBuilder appendProps(StringBuilder src) {
return src.append(key).append(", defaultValue=").append(defaultValue);
}
-
- public void addChangeListener(Context context, Runnable r) { }
-
- public void removeChangeListener(Runnable r) {}
}
public static class DebugFlag extends BooleanFlag {
public final String description;
- private boolean mCurrentValue;
+ protected boolean mCurrentValue;
public DebugFlag(String key, boolean defaultValue, String description) {
super(key, defaultValue);
diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java
index 55be4a4..6bd6261 100644
--- a/src/com/android/launcher3/dragndrop/AddItemActivity.java
+++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java
@@ -189,10 +189,18 @@
if (appWidgetHostView != null) {
bounds = new Rect();
appWidgetHostView.getSourceVisualDragBounds(bounds);
- bounds.offset(appWidgetHostView.getLeft() - (int) mLastTouchPos.x,
- appWidgetHostView.getTop() - (int) mLastTouchPos.y);
- listener = new PinItemDragListener(mRequest, bounds,
- appWidgetHostView.getMeasuredWidth(), appWidgetHostView.getMeasuredWidth());
+ float appWidgetHostViewScale = mWidgetCell.getAppWidgetHostViewScale();
+ int xOffset =
+ appWidgetHostView.getLeft() - (int) (mLastTouchPos.x * appWidgetHostViewScale);
+ int yOffset = appWidgetHostView.getTop()
+ - (int) (mLastTouchPos.y * mWidgetCell.getAppWidgetHostViewScale());
+ bounds.offset(xOffset, yOffset);
+ listener = new PinItemDragListener(
+ mRequest,
+ bounds,
+ appWidgetHostView.getMeasuredWidth(),
+ appWidgetHostView.getMeasuredWidth(),
+ appWidgetHostView.getScaleX());
} else {
bounds = img.getBitmapBounds();
bounds.offset(img.getLeft() - (int) mLastTouchPos.x,
diff --git a/src/com/android/launcher3/dragndrop/DragController.java b/src/com/android/launcher3/dragndrop/DragController.java
index 5731db4..1e0edac 100644
--- a/src/com/android/launcher3/dragndrop/DragController.java
+++ b/src/com/android/launcher3/dragndrop/DragController.java
@@ -74,7 +74,7 @@
/** Coordinate for last touch event **/
protected final Point mLastTouch = new Point();
- private final Point mTmpPoint = new Point();
+ protected final Point mTmpPoint = new Point();
protected DropTarget.DragObject mDragObject;
@@ -317,7 +317,7 @@
mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration);
}
- private void callOnDragEnd() {
+ protected void callOnDragEnd() {
if (mIsInPreDrag && mOptions.preDragCondition != null) {
mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/);
}
@@ -343,7 +343,7 @@
/**
* Clamps the position to the drag layer bounds.
*/
- private Point getClampedDragLayerPos(float x, float y) {
+ protected Point getClampedDragLayerPos(float x, float y) {
mActivity.getDragLayer().getLocalVisibleRect(mRectTemp);
mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1));
mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1));
@@ -390,7 +390,7 @@
return false;
}
- Point dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
+ Point dragLayerPos = getClampedDragLayerPos(getX(ev), getY(ev));
mLastTouch.set(dragLayerPos.x, dragLayerPos.y);
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// Remember location of down touch
@@ -403,6 +403,14 @@
return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
}
+ protected float getX(MotionEvent ev) {
+ return ev.getX();
+ }
+
+ protected float getY(MotionEvent ev) {
+ return ev.getY();
+ }
+
/**
* Call this from a drag source view.
*/
diff --git a/src/com/android/launcher3/dragndrop/DragDriver.java b/src/com/android/launcher3/dragndrop/DragDriver.java
index d4ce308..72e47e5 100644
--- a/src/com/android/launcher3/dragndrop/DragDriver.java
+++ b/src/com/android/launcher3/dragndrop/DragDriver.java
@@ -165,8 +165,11 @@
* Class for driving an internal (i.e. not using framework) drag/drop operation.
*/
static class InternalDragDriver extends DragDriver {
+ private final DragController mDragController;
+
InternalDragDriver(DragController dragController, Consumer<MotionEvent> sec) {
super(dragController, sec);
+ mDragController = dragController;
}
@Override
@@ -176,11 +179,14 @@
switch (action) {
case MotionEvent.ACTION_MOVE:
- mEventListener.onDriverDragMove(ev.getX(), ev.getY());
+ mEventListener.onDriverDragMove(mDragController.getX(ev),
+ mDragController.getY(ev));
break;
case MotionEvent.ACTION_UP:
- mEventListener.onDriverDragMove(ev.getX(), ev.getY());
- mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+ mEventListener.onDriverDragMove(mDragController.getX(ev),
+ mDragController.getY(ev));
+ mEventListener.onDriverDragEnd(mDragController.getX(ev),
+ mDragController.getY(ev));
break;
case MotionEvent.ACTION_CANCEL:
mEventListener.onDriverDragCancel();
@@ -197,7 +203,8 @@
switch (action) {
case MotionEvent.ACTION_UP:
- mEventListener.onDriverDragEnd(ev.getX(), ev.getY());
+ mEventListener.onDriverDragEnd(mDragController.getX(ev),
+ mDragController.getY(ev));
break;
case MotionEvent.ACTION_CANCEL:
mEventListener.onDriverDragCancel();
diff --git a/src/com/android/launcher3/dragndrop/DragLayer.java b/src/com/android/launcher3/dragndrop/DragLayer.java
index 011325d..5ee4203 100644
--- a/src/com/android/launcher3/dragndrop/DragLayer.java
+++ b/src/com/android/launcher3/dragndrop/DragLayer.java
@@ -17,14 +17,19 @@
package com.android.launcher3.dragndrop;
+import static android.animation.ObjectAnimator.ofFloat;
+
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
+import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
+import static com.android.launcher3.Utilities.mapRange;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
-import android.animation.ValueAnimator;
-import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.animation.TypeEvaluator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
@@ -44,10 +49,11 @@
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Workspace;
+import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.anim.SpringProperty;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.graphics.Scrim;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
-import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.TouchController;
import com.android.launcher3.views.BaseDragLayer;
@@ -69,11 +75,9 @@
private DragController mDragController;
// Variables relating to animation of views after drop
- private ValueAnimator mDropAnim = null;
+ private Animator mDropAnim = null;
- @Thunk DragView mDropView = null;
- @Thunk int mAnchorViewInitialScrollX = 0;
- @Thunk View mAnchorView = null;
+ private DragView mDropView = null;
private boolean mHoverPointClosesFolder = false;
@@ -220,12 +224,7 @@
public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
int duration) {
- Rect r = new Rect();
- getViewRectRelativeToSelf(dragView, r);
- final int fromX = r.left;
- final int fromY = r.top;
-
- animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
+ animateViewIntoPosition(dragView, pos[0], pos[1], alpha, scaleX, scaleY,
onFinishRunnable, animationEndStyle, duration, null);
}
@@ -241,11 +240,6 @@
parentChildren.measureChild(child);
parentChildren.layoutChild(child);
- Rect dragViewBounds = new Rect();
- getViewRectRelativeToSelf(dragView, dragViewBounds);
- final int fromX = dragViewBounds.left;
- final int fromY = dragViewBounds.top;
-
float coord[] = new float[2];
float childScale = child.getScaleX();
@@ -288,51 +282,50 @@
child.setVisibility(INVISIBLE);
Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE);
- animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
+ animateViewIntoPosition(dragView, toX, toY, 1, toScale, toScale,
onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
}
- public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
- final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
- float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
- int animationEndStyle, int duration, View anchorView) {
- Rect from = new Rect(fromX, fromY, fromX +
- view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
- Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
- animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
- null, null, onCompleteRunnable, animationEndStyle, anchorView);
- }
-
/**
* This method animates a view at the end of a drag and drop animation.
- *
+ */
+ public void animateViewIntoPosition(final DragView view,
+ final int toX, final int toY, float finalAlpha,
+ float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
+ int animationEndStyle, int duration, View anchorView) {
+ Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
+ animateView(view, to, finalAlpha, finalScaleX, finalScaleY, duration,
+ null, onCompleteRunnable, animationEndStyle, anchorView);
+ }
+
+ /**
+ * This method animates a view at the end of a drag and drop animation.
* @param view The view to be animated. This view is drawn directly into DragLayer, and so
* doesn't need to be a child of DragLayer.
- * @param from The initial location of the view. Only the left and top parameters are used.
* @param to The final location of the view. Only the left and top parameters are used. This
- * location doesn't account for scaling, and so should be centered about the desired
- * final location (including scaling).
+* location doesn't account for scaling, and so should be centered about the desired
+* final location (including scaling).
* @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
* @param finalScaleX The final scale of the view. The view is scaled about its center.
* @param finalScaleY The final scale of the view. The view is scaled about its center.
* @param duration The duration of the animation.
* @param motionInterpolator The interpolator to use for the location of the view.
- * @param alphaInterpolator The interpolator to use for the alpha of the view.
* @param onCompleteRunnable Optional runnable to run on animation completion.
* @param animationEndStyle Whether or not to fade out the view once the animation completes.
- * {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
+* {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
* @param anchorView If not null, this represents the view which the animated view stays
- * anchored to in case scrolling is currently taking place. Note: currently this is
- * only used for the X dimension for the case of the workspace.
*/
- public void animateView(final DragView view, final Rect from, final Rect to,
- final float finalAlpha, final float initScaleX, final float initScaleY,
- final float finalScaleX, final float finalScaleY, int duration,
- final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
- final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
+ public void animateView(final DragView view, final Rect to,
+ final float finalAlpha, final float finalScaleX, final float finalScaleY, int duration,
+ final Interpolator motionInterpolator, final Runnable onCompleteRunnable,
+ final int animationEndStyle, View anchorView) {
+ view.cancelAnimation();
+ view.requestLayout();
+
+ final int[] from = getViewLocationRelativeToSelf(view);
// Calculate the duration of the animation based on the object's distance
- final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
+ final float dist = (float) Math.hypot(to.left - from[0], to.top - from[1]);
final Resources res = getResources();
final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
@@ -346,93 +339,45 @@
}
// Fall back to cubic ease out interpolator for the animation if none is specified
- TimeInterpolator interpolator = null;
- if (alphaInterpolator == null || motionInterpolator == null) {
- interpolator = DEACCEL_1_5;
- }
+ TimeInterpolator interpolator =
+ motionInterpolator == null ? DEACCEL_1_5 : motionInterpolator;
// Animate the view
- final float initAlpha = view.getAlpha();
- final float dropViewScale = view.getScaleX();
- AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- final float percent = (Float) animation.getAnimatedValue();
- final int width = view.getMeasuredWidth();
- final int height = view.getMeasuredHeight();
+ PendingAnimation anim = new PendingAnimation(duration);
+ anim.add(ofFloat(view, View.SCALE_X, finalScaleX), interpolator, SpringProperty.DEFAULT);
+ anim.add(ofFloat(view, View.SCALE_Y, finalScaleY), interpolator, SpringProperty.DEFAULT);
+ anim.setViewAlpha(view, finalAlpha, interpolator);
+ anim.setFloat(view, VIEW_TRANSLATE_Y, to.top, interpolator);
- float alphaPercent = alphaInterpolator == null ? percent :
- alphaInterpolator.getInterpolation(percent);
- float motionPercent = motionInterpolator == null ? percent :
- motionInterpolator.getInterpolation(percent);
-
- float initialScaleX = initScaleX * dropViewScale;
- float initialScaleY = initScaleY * dropViewScale;
- float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
- float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
- float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
-
- float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
- float fromTop = from.top + (initialScaleY - 1f) * height / 2;
-
- int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
- int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
-
- int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
- (mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
-
- int xPos = x - mDropView.getScrollX() + anchorAdjust;
- int yPos = y - mDropView.getScrollY();
-
- mDropView.setTranslationX(xPos);
- mDropView.setTranslationY(yPos);
- mDropView.setScaleX(scaleX);
- mDropView.setScaleY(scaleY);
- mDropView.setAlpha(alpha);
- }
- };
- animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
- anchorView);
+ ObjectAnimator xMotion = ofFloat(view, VIEW_TRANSLATE_X, to.left);
+ if (anchorView != null) {
+ final int startScroll = anchorView.getScrollX();
+ TypeEvaluator<Float> evaluator = (f, s, e) -> mapRange(f, s, e)
+ + (anchorView.getScaleX() * (startScroll - anchorView.getScrollX()));
+ xMotion.setEvaluator(evaluator);
+ }
+ anim.add(xMotion, interpolator, SpringProperty.DEFAULT);
+ if (onCompleteRunnable != null) {
+ anim.addListener(forEndCallback(onCompleteRunnable));
+ }
+ playDropAnimation(view, anim.buildAnim(), animationEndStyle);
}
- public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
- TimeInterpolator interpolator, final Runnable onCompleteRunnable,
- final int animationEndStyle, View anchorView) {
+ /**
+ * Runs a previously constructed drop animation
+ */
+ public void playDropAnimation(final DragView view, Animator animator, int animationEndStyle) {
// Clean up the previous animations
if (mDropAnim != null) mDropAnim.cancel();
// Show the drop view if it was previously hidden
mDropView = view;
- mDropView.cancelAnimation();
- mDropView.requestLayout();
-
- // Set the anchor view if the page is scrolling
- if (anchorView != null) {
- mAnchorViewInitialScrollX = anchorView.getScrollX();
- }
- mAnchorView = anchorView;
-
// Create and start the animation
- mDropAnim = new ValueAnimator();
- mDropAnim.setInterpolator(interpolator);
- mDropAnim.setDuration(duration);
- mDropAnim.setFloatValues(0f, 1f);
- mDropAnim.addUpdateListener(updateCb);
- mDropAnim.addListener(new AnimatorListenerAdapter() {
- public void onAnimationEnd(Animator animation) {
- if (onCompleteRunnable != null) {
- onCompleteRunnable.run();
- }
- switch (animationEndStyle) {
- case ANIMATION_END_DISAPPEAR:
- clearAnimatedView();
- break;
- case ANIMATION_END_REMAIN_VISIBLE:
- break;
- }
- mDropAnim = null;
- }
- });
+ mDropAnim = animator;
+ mDropAnim.addListener(forEndCallback(() -> mDropAnim = null));
+ if (animationEndStyle == ANIMATION_END_DISAPPEAR) {
+ mDropAnim.addListener(forEndCallback(this::clearAnimatedView));
+ }
mDropAnim.start();
}
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index 3fdb256..f2ab96c 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -53,23 +53,20 @@
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
-import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
-import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.statemanager.StateManager.StateListener;
import com.android.launcher3.util.RunnableList;
-import com.android.launcher3.util.Thunk;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
/** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
-public class DragView extends FrameLayout implements StateListener<LauncherState> {
+public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {
public static final int VIEW_ZOOM_DURATION = 150;
@@ -82,19 +79,18 @@
private final int mHeight;
private final int mBlurSizeOutline;
- private final int mRegistrationX;
- private final int mRegistrationY;
+ protected final int mRegistrationX;
+ protected final int mRegistrationY;
private final float mInitialScale;
- private final float mScaleOnDrop;
- private final int[] mTempLoc = new int[2];
+ protected final float mScaleOnDrop;
+ protected final int[] mTempLoc = new int[2];
private final RunnableList mOnDragStartCallback = new RunnableList();
private Point mDragVisualizeOffset = null;
private Rect mDragRegion = null;
- private final Launcher mLauncher;
- private final DragLayer mDragLayer;
- @Thunk final DragController mDragController;
+ protected final T mActivity;
+ private final BaseDragLayer<T> mDragLayer;
private boolean mHasDrawn = false;
final ValueAnimator mAnim;
@@ -110,7 +106,7 @@
private Path mScaledMaskPath;
private Drawable mBadge;
- public DragView(Launcher launcher, Drawable drawable, int registrationX,
+ public DragView(T launcher, Drawable drawable, int registrationX,
int registrationY, final float initialScale, final float scaleOnDrop,
final float finalScaleDps) {
this(launcher, getViewFromDrawable(launcher, drawable),
@@ -123,7 +119,7 @@
* <p>
* The registration point is the point inside our view that the touch events should
* be centered upon.
- * @param launcher The Launcher instance
+ * @param activity The Launcher instance/ActivityContext this DragView is in.
* @param content the view content that is attached to the drag view.
* @param width the width of the dragView
* @param height the height of the dragView
@@ -133,13 +129,12 @@
* @param scaleOnDrop the scale used in the drop animation.
* @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
*/
- public DragView(Launcher launcher, View content, int width, int height, int registrationX,
+ public DragView(T activity, View content, int width, int height, int registrationX,
int registrationY, final float initialScale, final float scaleOnDrop,
final float finalScaleDps) {
- super(launcher);
- mLauncher = launcher;
- mDragLayer = launcher.getDragLayer();
- mDragController = launcher.getDragController();
+ super(activity);
+ mActivity = activity;
+ mDragLayer = activity.getDragLayer();
mContent = content;
mWidth = width;
@@ -153,6 +148,12 @@
addView(content, new LayoutParams(width, height));
+ // If there is already a scale set on the content, we don't want to clip the children.
+ if (content.getScaleX() != 1 || content.getScaleY() != 1) {
+ setClipChildren(false);
+ setClipToPadding(false);
+ }
+
final float scale = (width + finalScaleDps) / width;
// Set the initial scale to avoid any jumps
@@ -188,24 +189,6 @@
setWillNotDraw(false);
}
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- mLauncher.getStateManager().addStateListener(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mLauncher.getStateManager().removeStateListener(this);
- }
-
- @Override
- public void onStateTransitionComplete(LauncherState finalState) {
- setVisibility((finalState == LauncherState.NORMAL
- || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE);
- }
-
/**
* Initialize {@code #mIconDrawable} if the item can be represented using
* an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
@@ -222,10 +205,10 @@
Object[] outObj = new Object[1];
int w = mWidth;
int h = mHeight;
- Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
+ Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, outObj);
if (dr instanceof AdaptiveIconDrawable) {
- int blurMargin = (int) mLauncher.getResources()
+ int blurMargin = (int) mActivity.getResources()
.getDimension(R.dimen.blur_size_medium_outline) / 2;
Rect bounds = new Rect(0, 0, w, h);
@@ -233,13 +216,13 @@
// Badge is applied after icon normalization so the bounds for badge should not
// be scaled down due to icon normalization.
Rect badgeBounds = new Rect(bounds);
- mBadge = getBadge(mLauncher, info, outObj[0]);
+ mBadge = getBadge(mActivity, info, outObj[0]);
mBadge.setBounds(badgeBounds);
// Do not draw the background in case of folder as its translucent
final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
- try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
+ try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
Drawable nDr; // drawable to be normalized
if (shouldDrawBackground) {
nDr = dr;
@@ -430,12 +413,11 @@
applyTranslation();
}
- public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) {
- mTempLoc[0] = toTouchX - mRegistrationX;
- mTempLoc[1] = toTouchY - mRegistrationY;
- mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop,
- DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
- }
+ /**
+ * Animate this DragView to the given DragLayer coordinates and then remove it.
+ */
+ public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable,
+ int duration);
public void animateShift(final int shiftX, final int shiftY) {
if (mAnim.isStarted()) {
@@ -471,7 +453,7 @@
Picture picture = new Picture();
mContent.draw(picture.beginRecording(mWidth, mHeight));
picture.endRecording();
- View view = new View(mLauncher);
+ View view = new View(mActivity);
view.setBackground(new PictureDrawable(picture));
view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
view.layout(mContent.getLeft(), mContent.getTop(),
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
index 98c0cfc..74d9a22 100644
--- a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -32,12 +32,12 @@
import androidx.annotation.Nullable;
-import com.android.launcher3.Launcher;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.folder.PreviewBackground;
import com.android.launcher3.graphics.ShiftedBitmapDrawable;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.util.Preconditions;
+import com.android.launcher3.views.ActivityContext;
/**
* {@link AdaptiveIconDrawable} representation of a {@link FolderIcon}
@@ -70,14 +70,14 @@
}
public static @Nullable FolderAdaptiveIcon createFolderAdaptiveIcon(
- Launcher launcher, int folderId, Point dragViewSize) {
+ ActivityContext activity, int folderId, Point dragViewSize) {
Preconditions.assertNonUiThread();
// Create the actual drawable on the UI thread to avoid race conditions with
// FolderIcon draw pass
try {
return MAIN_EXECUTOR.submit(() -> {
- FolderIcon icon = launcher.findFolderIcon(folderId);
+ FolderIcon icon = activity.findFolderIcon(folderId);
return icon == null ? null : createDrawableOnUiThread(icon, dragViewSize);
}).get();
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragController.java b/src/com/android/launcher3/dragndrop/LauncherDragController.java
index a98d70c..0e8b0a5 100644
--- a/src/com/android/launcher3/dragndrop/LauncherDragController.java
+++ b/src/com/android/launcher3/dragndrop/LauncherDragController.java
@@ -96,7 +96,7 @@
final float scaleDps = mIsInPreDrag
? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
final DragView dragView = mDragObject.dragView = drawable != null
- ? new DragView(
+ ? new LauncherDragView(
mActivity,
drawable,
registrationX,
@@ -104,7 +104,7 @@
initialDragViewScale,
dragViewScaleOnDrop,
scaleDps)
- : new DragView(
+ : new LauncherDragView(
mActivity,
view,
view.getMeasuredWidth(),
diff --git a/src/com/android/launcher3/dragndrop/LauncherDragView.java b/src/com/android/launcher3/dragndrop/LauncherDragView.java
new file mode 100644
index 0000000..cc68e2e
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/LauncherDragView.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 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.launcher3.dragndrop;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherState;
+import com.android.launcher3.statemanager.StateManager;
+
+/**
+ * A DragView drawn/used by the Launcher activity.
+ */
+public class LauncherDragView extends DragView<Launcher>
+ implements StateManager.StateListener<LauncherState> {
+
+
+ public LauncherDragView(Launcher launcher, Drawable drawable, int registrationX,
+ int registrationY, float initialScale, float scaleOnDrop, float finalScaleDps) {
+ super(launcher, drawable, registrationX, registrationY, initialScale, scaleOnDrop,
+ finalScaleDps);
+ }
+
+ public LauncherDragView(Launcher launcher, View content, int width, int height,
+ int registrationX, int registrationY, float initialScale, float scaleOnDrop,
+ float finalScaleDps) {
+ super(launcher, content, width, height, registrationX, registrationY, initialScale,
+ scaleOnDrop, finalScaleDps);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mActivity.getStateManager().addStateListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mActivity.getStateManager().removeStateListener(this);
+ }
+
+ @Override
+ public void onStateTransitionComplete(LauncherState finalState) {
+ setVisibility((finalState == LauncherState.NORMAL
+ || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE);
+ }
+
+ @Override
+ public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) {
+ mTempLoc[0] = toTouchX - mRegistrationX;
+ mTempLoc[1] = toTouchY - mRegistrationY;
+ mActivity.getDragLayer().animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop,
+ mScaleOnDrop, DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
+ }
+}
diff --git a/src/com/android/launcher3/dragndrop/PinItemDragListener.java b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
index 2bdf8a0..af43ae8 100644
--- a/src/com/android/launcher3/dragndrop/PinItemDragListener.java
+++ b/src/com/android/launcher3/dragndrop/PinItemDragListener.java
@@ -48,12 +48,19 @@
private final PinItemRequest mRequest;
private final CancellationSignal mCancelSignal;
+ private final float mPreviewScale;
public PinItemDragListener(PinItemRequest request, Rect previewRect,
int previewBitmapWidth, int previewViewWidth) {
+ this(request, previewRect, previewBitmapWidth, previewViewWidth, /* previewScale= */ 1f);
+ }
+
+ public PinItemDragListener(PinItemRequest request, Rect previewRect,
+ int previewBitmapWidth, int previewViewWidth, float previewScale) {
super(previewRect, previewBitmapWidth, previewViewWidth);
mRequest = request;
mCancelSignal = new CancellationSignal();
+ mPreviewScale = previewScale;
}
@Override
@@ -98,7 +105,7 @@
PendingItemDragHelper dragHelper = new PendingItemDragHelper(view);
if (mRequest.getRequestType() == PinItemRequest.REQUEST_TYPE_APPWIDGET) {
- dragHelper.setRemoteViewsPreview(getPreview(mRequest));
+ dragHelper.setRemoteViewsPreview(getPreview(mRequest), mPreviewScale);
}
return dragHelper;
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 22bb56c..7187188 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -75,7 +75,6 @@
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.accessibility.AccessibleDragListenerAdapter;
import com.android.launcher3.accessibility.FolderAccessibilityHelper;
import com.android.launcher3.anim.KeyboardInsetAnimationCallback;
@@ -94,6 +93,7 @@
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import com.android.launcher3.util.Executors;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
@@ -1196,8 +1196,7 @@
}
void replaceFolderWithFinalItem() {
- mLauncherDelegate.replaceFolderWithFinalItem(this);
- mDestroyed = true;
+ mDestroyed = mLauncherDelegate.replaceFolderWithFinalItem(this);
}
public boolean isDestroyed() {
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 96030f9..60d8cdb 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -336,8 +336,6 @@
if (animateView != null && mActivity instanceof Launcher) {
final Launcher launcher = (Launcher) mActivity;
DragLayer dragLayer = launcher.getDragLayer();
- Rect from = new Rect();
- dragLayer.getViewRectRelativeToSelf(animateView, from);
Rect to = finalRect;
if (to == null) {
to = new Rect();
@@ -403,13 +401,14 @@
}
final int finalIndex = index;
- dragLayer.animateView(animateView, from, to, finalAlpha,
- 1, 1, finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
- Interpolators.DEACCEL_2, Interpolators.ACCEL_2,
+ dragLayer.animateView(animateView, to, finalAlpha,
+ finalScale, finalScale, DROP_IN_ANIMATION_DURATION,
+ Interpolators.DEACCEL_2,
() -> {
mPreviewItemManager.hidePreviewItem(finalIndex, false);
mFolder.showItem(item);
- }, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ },
+ DragLayer.ANIMATION_END_DISAPPEAR, null);
mFolder.hideItem(item);
@@ -683,6 +682,7 @@
@Override
public void onAdd(WorkspaceItemInfo item, int rank) {
+ updatePreviewItems(false);
boolean wasDotted = mDotInfo.hasDot();
mDotInfo.addDotInfo(mActivity.getDotInfoForItem(item));
boolean isDotted = mDotInfo.hasDot();
@@ -694,6 +694,7 @@
@Override
public void onRemove(List<WorkspaceItemInfo> items) {
+ updatePreviewItems(false);
boolean wasDotted = mDotInfo.hasDot();
items.stream().map(mActivity::getDotInfoForItem).forEach(mDotInfo::subtractDotInfo);
boolean isDotted = mDotInfo.hasDot();
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 3d2884a..65991e4 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -41,12 +41,12 @@
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
import com.android.launcher3.Utilities;
-import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pageindicators.PageIndicatorDots;
import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.util.ViewCache;
import com.android.launcher3.views.ActivityContext;
diff --git a/src/com/android/launcher3/folder/LauncherDelegate.java b/src/com/android/launcher3/folder/LauncherDelegate.java
index f7d8e8c..e599e8c 100644
--- a/src/com/android/launcher3/folder/LauncherDelegate.java
+++ b/src/com/android/launcher3/folder/LauncherDelegate.java
@@ -93,7 +93,7 @@
}
}
- void replaceFolderWithFinalItem(Folder folder) {
+ boolean replaceFolderWithFinalItem(Folder folder) {
// Add the last remaining child to the workspace in place of the folder
Runnable onCompleteRunnable = new Runnable() {
@Override
@@ -147,6 +147,7 @@
} else {
onCompleteRunnable.run();
}
+ return true;
}
@@ -191,7 +192,7 @@
ModelWriter getModelWriter() {
if (mWriter == null) {
mWriter = LauncherAppState.getInstance((Context) mContext).getModel()
- .getWriter(false, false);
+ .getWriter(false, false, null);
}
return mWriter;
}
@@ -205,7 +206,9 @@
}
@Override
- void replaceFolderWithFinalItem(Folder folder) { }
+ boolean replaceFolderWithFinalItem(Folder folder) {
+ return false;
+ }
@Override
boolean interceptOutsideTouch(MotionEvent ev, BaseDragLayer dl, Folder folder) {
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index a549750..f027b33 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -32,13 +32,13 @@
import androidx.annotation.Nullable;
import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.icons.FastBitmapDrawable;
import com.android.launcher3.util.SafeCloseable;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import java.nio.ByteBuffer;
@@ -150,7 +150,7 @@
}
public float getScaleAndPosition(Drawable preview, int[] outPos) {
- float scale = Launcher.getLauncher(mView.getContext())
+ float scale = ActivityContext.lookupContext(mView.getContext())
.getDragLayer().getLocationInDragLayer(mView, outPos);
if (mView instanceof LauncherAppWidgetHostView) {
// App widgets are technically scaled, but are drawn at their expected size -- so the
@@ -167,7 +167,7 @@
/** Returns the scale and position of a given view for drag-n-drop. */
public float getScaleAndPosition(View view, int[] outPos) {
- float scale = Launcher.getLauncher(mView.getContext())
+ float scale = ActivityContext.lookupContext(mView.getContext())
.getDragLayer().getLocationInDragLayer(mView, outPos);
if (mView instanceof LauncherAppWidgetHostView) {
// App widgets are technically scaled, but are drawn at their expected size -- so the
@@ -201,7 +201,7 @@
public void run() {
Bitmap preview = convertPreviewToAlphaBitmap(mPreviewSnapshot);
if (mIsIcon) {
- int size = Launcher.getLauncher(mContext).getDeviceProfile().iconSizePx;
+ int size = ActivityContext.lookupContext(mContext).getDeviceProfile().iconSizePx;
preview = Bitmap.createScaledBitmap(preview, size, size, false);
}
//else case covers AppWidgetHost (doesn't drag/drop across different device profiles)
diff --git a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
index a387f04..94778a2 100644
--- a/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
+++ b/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
@@ -29,6 +29,7 @@
import android.app.Fragment;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
+import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
@@ -84,11 +85,15 @@
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
+import com.android.launcher3.widget.BaseLauncherAppWidgetHostView;
+import com.android.launcher3.widget.LauncherAppWidgetHost;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LocalColorExtractor;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.util.ArrayList;
@@ -205,7 +210,8 @@
private final LayoutInflater mHomeElementInflater;
private final InsettableFrameLayout mRootView;
private final Hotseat mHotseat;
- private final CellLayout mWorkspace;
+ private final Map<Integer, CellLayout> mWorkspaceScreens = new HashMap<>();
+ private final AppWidgetHost mAppWidgetHost;
private final SparseIntArray mWallpaperColorResources;
public LauncherPreviewRenderer(Context context,
@@ -250,19 +256,31 @@
new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
mHomeElementInflater.setFactory2(this);
+ int layoutRes = mDp.isTwoPanels ? R.layout.launcher_preview_two_panel_layout
+ : R.layout.launcher_preview_layout;
mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
- R.layout.launcher_preview_layout, null, false);
+ layoutRes, null, false);
mRootView.setInsets(mInsets);
measureView(mRootView, mDp.widthPx, mDp.heightPx);
mHotseat = mRootView.findViewById(R.id.hotseat);
mHotseat.resetLayout(false);
- mWorkspace = mRootView.findViewById(R.id.workspace);
- mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
+ if (mDp.isTwoPanels) {
+ CellLayout leftPanel = mRootView.findViewById(R.id.workspace_left);
+ leftPanel.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
+ mDp.workspacePadding.top,
+ mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
+ mDp.workspacePadding.bottom);
+ mWorkspaceScreens.put(LEFT_PANEL_ID, leftPanel);
+ }
+
+ CellLayout firstScreen = mRootView.findViewById(R.id.workspace);
+ firstScreen.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.top,
mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.bottom);
+ mWorkspaceScreens.put(FIRST_SCREEN_ID, firstScreen);
if (Utilities.ATLEAST_S) {
WallpaperColors wallpaperColors = wallpaperColorsOverride != null
@@ -273,6 +291,9 @@
} else {
mWallpaperColorResources = null;
}
+ mAppWidgetHost = FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()
+ ? new LauncherPreviewAppWidgetHost(context)
+ : null;
}
/** Populate preview and render it. */
@@ -330,18 +351,20 @@
@Override
public CellLayout getScreenWithId(int screenId) {
- return mWorkspace;
+ return mWorkspaceScreens.get(screenId);
}
private void inflateAndAddIcon(WorkspaceItemInfo info) {
+ CellLayout screen = mWorkspaceScreens.get(info.screenId);
BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
- R.layout.app_icon, mWorkspace, false);
+ R.layout.app_icon, screen, false);
icon.applyFromWorkspaceItem(info);
addInScreenFromBind(icon, info);
}
private void inflateAndAddFolder(FolderInfo info) {
- FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
+ CellLayout screen = mWorkspaceScreens.get(info.screenId);
+ FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
info);
addInScreenFromBind(folderIcon, info);
}
@@ -372,20 +395,31 @@
private void inflateAndAddWidgets(
LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
- AppWidgetHostView view = new AppWidgetHostView(mContext);
- view.setAppWidget(-1, providerInfo);
- view.updateAppWidget(null);
- view.setTag(info);
+ AppWidgetHostView view;
+ if (FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()) {
+ view = mAppWidgetHost.createView(mContext, info.appWidgetId, providerInfo);
+ } else {
+ view = new NavigableAppWidgetHostView(this) {
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ };
+ view.setAppWidget(-1, providerInfo);
+ view.updateAppWidget(null);
+ }
if (mWallpaperColorResources != null) {
view.setColorResources(mWallpaperColorResources);
}
+ view.setTag(info);
addInScreenFromBind(view, info);
}
private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
- View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
+ CellLayout screen = mWorkspaceScreens.get(info.screenId);
+ View view = PredictedAppIconInflater.inflate(mHomeElementInflater, screen, info);
if (view != null) {
addInScreenFromBind(view, info);
}
@@ -416,11 +450,13 @@
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
- filterCurrentWorkspaceItems(0 /* currentScreenId */,
- dataModel.workspaceItems, currentWorkspaceItems,
- otherWorkspaceItems);
- filterCurrentWorkspaceItems(0 /* currentScreenId */, dataModel.appWidgets,
- currentAppWidgets, otherAppWidgets);
+
+ IntSet currentScreenIds = IntSet.wrap(mWorkspaceScreens.keySet());
+ filterCurrentWorkspaceItems(currentScreenIds, dataModel.workspaceItems,
+ currentWorkspaceItems, otherWorkspaceItems);
+ filterCurrentWorkspaceItems(currentScreenIds, dataModel.appWidgets, currentAppWidgets,
+ otherAppWidgets);
+
sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
for (ItemInfo itemInfo : currentWorkspaceItems) {
switch (itemInfo.itemType) {
@@ -473,12 +509,13 @@
// Add first page QSB
if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
+ CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
View qsb = mHomeElementInflater.inflate(
- R.layout.search_container_workspace, mWorkspace, false);
+ R.layout.search_container_workspace, firstScreen, false);
CellLayout.LayoutParams lp =
- new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1);
+ new CellLayout.LayoutParams(0, 0, firstScreen.getCountX(), 1);
lp.canReorder = false;
- mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
+ firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
}
measureView(mRootView, mDp.widthPx, mDp.heightPx);
@@ -493,6 +530,32 @@
view.layout(0, 0, width, height);
}
+ private class LauncherPreviewAppWidgetHost extends AppWidgetHost {
+
+ private LauncherPreviewAppWidgetHost(Context context) {
+ super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID);
+ }
+
+ @Override
+ protected AppWidgetHostView onCreateView(
+ Context context,
+ int appWidgetId,
+ AppWidgetProviderInfo appWidget) {
+ return new LauncherPreviewAppWidgetHostView(LauncherPreviewRenderer.this);
+ }
+ }
+
+ private static class LauncherPreviewAppWidgetHostView extends BaseLauncherAppWidgetHostView {
+ private LauncherPreviewAppWidgetHostView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean shouldAllowDirectClick() {
+ return false;
+ }
+ }
+
/** Root layout for launcher preview that intercepts all touch events. */
public static class LauncherPreviewLayout extends InsettableFrameLayout {
public LauncherPreviewLayout(Context context, AttributeSet attrs) {
diff --git a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
index df49359..13c83be 100644
--- a/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
+++ b/src/com/android/launcher3/graphics/PreviewSurfaceRenderer.java
@@ -38,11 +38,13 @@
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
import com.android.launcher3.graphics.LauncherPreviewRenderer.PreviewContext;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.GridSizeMigrationTask;
@@ -163,11 +165,17 @@
@Override
public void run() {
+ DeviceProfile deviceProfile = mIdp.getDeviceProfile(mContext);
+ String query = (deviceProfile.isTwoPanels ? LauncherSettings.Favorites.SCREEN
+ + " = " + Workspace.LEFT_PANEL_ID + " or " : "")
+ + LauncherSettings.Favorites.SCREEN + " = " + Workspace.FIRST_SCREEN_ID
+ + " or " + LauncherSettings.Favorites.CONTAINER + " = "
+ + LauncherSettings.Favorites.CONTAINER_HOTSEAT;
loadWorkspace(new ArrayList<>(), LauncherSettings.Favorites.PREVIEW_CONTENT_URI,
- LauncherSettings.Favorites.SCREEN + " = 0 or "
- + LauncherSettings.Favorites.CONTAINER + " = "
- + LauncherSettings.Favorites.CONTAINER_HOTSEAT);
+ query);
+
MAIN_EXECUTOR.execute(() -> {
+ mBgDataModel.isLeftPanelShown = deviceProfile.isTwoPanels;
renderView(previewContext, mBgDataModel, mWidgetProvidersMap);
mOnDestroyCallbacks.add(previewContext::onDestroy);
});
diff --git a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
index 01b3e6e..b2b0010 100644
--- a/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
+++ b/src/com/android/launcher3/model/AddWorkspaceItemsTask.java
@@ -287,28 +287,19 @@
// Find appropriate space for the item.
int screenId = 0;
- int[] cordinates = new int[2];
+ int[] coordinates = new int[2];
boolean found = false;
int screenCount = workspaceScreens.size();
- // First check the preferred screen.
- int preferredScreenIndex = workspaceScreens.isEmpty() ? 0 : 1;
- if (preferredScreenIndex < screenCount) {
- screenId = workspaceScreens.get(preferredScreenIndex);
- found = findNextAvailableIconSpaceInScreen(
- app, screenItems.get(screenId), cordinates, spanX, spanY);
- }
-
- if (!found) {
- // Search on any of the screens starting from the first screen.
- for (int screen = 1; screen < screenCount; screen++) {
- screenId = workspaceScreens.get(screen);
- if (findNextAvailableIconSpaceInScreen(
- app, screenItems.get(screenId), cordinates, spanX, spanY)) {
- // We found a space for it
- found = true;
- break;
- }
+ int firstScreenToCheck = dataModel.isLeftPanelShown ? 2 : 1;
+ // Search on the screens for empty space
+ for (int screen = firstScreenToCheck; screen < screenCount; screen++) {
+ screenId = workspaceScreens.get(screen);
+ if (findNextAvailableIconSpaceInScreen(
+ app, screenItems.get(screenId), coordinates, spanX, spanY)) {
+ // We found a space for it
+ found = true;
+ break;
}
}
@@ -324,11 +315,11 @@
// If we still can't find an empty space, then God help us all!!!
if (!findNextAvailableIconSpaceInScreen(
- app, screenItems.get(screenId), cordinates, spanX, spanY)) {
+ app, screenItems.get(screenId), coordinates, spanX, spanY)) {
throw new RuntimeException("Can't find space to add the item");
}
}
- return new int[] {screenId, cordinates[0], cordinates[1]};
+ return new int[] {screenId, coordinates[0], coordinates[1]};
}
private boolean findNextAvailableIconSpaceInScreen(
diff --git a/src/com/android/launcher3/model/BaseLoaderResults.java b/src/com/android/launcher3/model/BaseLoaderResults.java
index 5c85bab..c202d8d 100644
--- a/src/com/android/launcher3/model/BaseLoaderResults.java
+++ b/src/com/android/launcher3/model/BaseLoaderResults.java
@@ -16,24 +16,27 @@
package com.android.launcher3.model;
+import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.os.Process;
import android.util.Log;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
-import com.android.launcher3.PagedView;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.IntArray;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.LooperIdleLock;
-import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.util.RunnableList;
import java.util.ArrayList;
import java.util.Collections;
@@ -71,7 +74,7 @@
/**
* Binds all loaded data to actual views on the main thread.
*/
- public void bindWorkspace() {
+ public void bindWorkspace(boolean incrementBindId) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
@@ -83,7 +86,9 @@
appWidgets.addAll(mBgDataModel.appWidgets);
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
mBgDataModel.extraItems.forEach(extraItems::add);
- mBgDataModel.lastBindId++;
+ if (incrementBindId) {
+ mBgDataModel.lastBindId++;
+ }
mMyBindingId = mBgDataModel.lastBindId;
}
@@ -160,20 +165,25 @@
}
private void bind() {
- final int currentScreen;
+ IntSet currentScreenIndices;
{
// Create an anonymous scope to calculate currentScreen as it has to be a
// final variable.
- int currScreen = mCallbacks.getPageToBindSynchronously();
- if (currScreen >= mOrderedScreenIds.size()) {
- // There may be no workspace screens (just hotseat items and an empty page).
- currScreen = PagedView.INVALID_PAGE;
+ IntSet screenIndices = mCallbacks.getPagesToBindSynchronously();
+ if (screenIndices == null || screenIndices.isEmpty()
+ || screenIndices.getArray().get(screenIndices.size() - 1)
+ >= mOrderedScreenIds.size()) {
+ // There maybe no workspace screens (just hotseat items and an empty page).
+ // Also we want to prevent IndexOutOfBoundsExceptions.
+ screenIndices = new IntSet();
}
- currentScreen = currScreen;
+ currentScreenIndices = screenIndices;
}
- final boolean validFirstPage = currentScreen >= 0;
- final int currentScreenId =
- validFirstPage ? mOrderedScreenIds.get(currentScreen) : INVALID_SCREEN_ID;
+
+
+ IntSet currentScreenIds = new IntSet();
+ currentScreenIndices.forEach(
+ index -> currentScreenIds.add(mOrderedScreenIds.get(index)));
// Separate the items that are on the current screen, and all the other remaining items
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
@@ -181,9 +191,9 @@
ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
- filterCurrentWorkspaceItems(currentScreenId, mWorkspaceItems, currentWorkspaceItems,
+ filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
- filterCurrentWorkspaceItems(currentScreenId, mAppWidgets, currentAppWidgets,
+ filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets,
otherAppWidgets);
final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
@@ -198,40 +208,29 @@
// Bind workspace screens
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
- Executor mainExecutor = mUiExecutor;
// Load items on the current page.
- bindWorkspaceItems(currentWorkspaceItems, mainExecutor);
- bindAppWidgets(currentAppWidgets, mainExecutor);
+ bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
+ bindAppWidgets(currentAppWidgets, mUiExecutor);
mExtraItems.forEach(item ->
- executeCallbacksTask(c -> c.bindExtraContainerItems(item), mainExecutor));
+ executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
- // In case of validFirstPage, only bind the first screen, and defer binding the
- // remaining screens after first onDraw (and an optional the fade animation whichever
- // happens later).
- // This ensures that the first screen is immediately visible (eg. during rotation)
- // In case of !validFirstPage, bind all pages one after other.
+ RunnableList pendingTasks = new RunnableList();
+ Executor pendingExecutor = pendingTasks::add;
+ bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
+ bindAppWidgets(otherAppWidgets, pendingExecutor);
+ executeCallbacksTask(c -> c.finishBindingItems(currentScreenIndices), pendingExecutor);
+ pendingExecutor.execute(
+ () -> {
+ MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
+ ItemInstallQueue.INSTANCE.get(mApp.getContext())
+ .resumeModelPush(FLAG_LOADER_RUNNING);
+ });
- final Executor deferredExecutor =
- validFirstPage ? new ViewOnDrawExecutor() : mainExecutor;
-
- executeCallbacksTask(c -> c.finishFirstPageBind(
- validFirstPage ? (ViewOnDrawExecutor) deferredExecutor : null), mainExecutor);
-
- bindWorkspaceItems(otherWorkspaceItems, deferredExecutor);
- bindAppWidgets(otherAppWidgets, deferredExecutor);
- // Tell the workspace that we're done binding items
- executeCallbacksTask(c -> c.finishBindingItems(currentScreen), deferredExecutor);
-
- if (validFirstPage) {
- executeCallbacksTask(c -> {
- // We are loading synchronously, which means, some of the pages will be
- // bound after first draw. Inform the mCallbacks that page binding is
- // not complete, and schedule the remaining pages.
- c.onPageBoundSynchronously(currentScreen);
- c.executeOnNextDraw((ViewOnDrawExecutor) deferredExecutor);
-
- }, mUiExecutor);
- }
+ executeCallbacksTask(
+ c -> {
+ MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ c.onInitialBindComplete(currentScreenIndices, pendingTasks);
+ }, mUiExecutor);
}
private void bindWorkspaceItems(
diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java
index ad553d5..a3a4717 100644
--- a/src/com/android/launcher3/model/BaseModelUpdateTask.java
+++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java
@@ -90,7 +90,7 @@
public ModelWriter getModelWriter() {
// Updates from model task, do not deal with icon position in hotseat. Also no need to
// verify changes as the ModelTasks always push the changes to callbacks
- return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */);
+ return mModel.getWriter(false /* hasVerticalHotseat */, false /* verifyChanges */, null);
}
public void bindUpdatedWorkspaceItems(List<WorkspaceItemInfo> allUpdates) {
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 1d7d1a2..dd4c3c3 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -49,7 +49,7 @@
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
import com.android.launcher3.util.ItemInfoMatcher;
-import com.android.launcher3.util.ViewOnDrawExecutor;
+import com.android.launcher3.util.RunnableList;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import java.io.FileDescriptor;
@@ -117,6 +117,11 @@
public int lastBindId = 0;
/**
+ * Value that indicates if left widget panel is shown or not.
+ */
+ public boolean isLeftPanelShown = false;
+
+ /**
* Clears all the data
*/
public synchronized void clear() {
@@ -141,6 +146,14 @@
if (FeatureFlags.QSB_ON_FIRST_SCREEN || screenSet.isEmpty()) {
screenSet.add(Workspace.FIRST_SCREEN_ID);
}
+
+ if (isLeftPanelShown) {
+ // We should add it even though there are no items on it.
+ screenSet.add(Workspace.LEFT_PANEL_ID);
+ } else {
+ // We should NOT add it even though there are items on it.
+ screenSet.remove(Workspace.LEFT_PANEL_ID);
+ }
return screenSet.getArray();
}
@@ -446,37 +459,49 @@
int FLAG_QUIET_MODE_CHANGE_PERMISSION = 1 << 2;
/**
- * Returns the page number to bind first, synchronously if possible or -1
+ * Returns an IntSet of page numbers to bind first, synchronously if possible
+ * or an empty IntSet
*/
- int getPageToBindSynchronously();
- void clearPendingBinds();
- void startBinding();
- void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons);
- void bindScreens(IntArray orderedScreenIds);
- void finishFirstPageBind(ViewOnDrawExecutor executor);
- void finishBindingItems(int pageBoundFirst);
- void preAddApps();
- void bindAppsAdded(IntArray newScreens,
- ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated);
+ default IntSet getPagesToBindSynchronously() {
+ return new IntSet();
+ }
+
+ default void clearPendingBinds() { }
+ default void startBinding() { }
+
+ default void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+ default void bindScreens(IntArray orderedScreenIds) { }
+ default void finishBindingItems(IntSet pagesBoundFirst) { }
+ default void preAddApps() { }
+ default void bindAppsAdded(IntArray newScreens,
+ ArrayList<ItemInfo> addNotAnimated, ArrayList<ItemInfo> addAnimated) { }
+
+ /**
+ * Called when some persistent property of an item is modified
+ */
+ default void bindItemsModified(List<ItemInfo> items) { }
/**
* Binds updated incremental download progress
*/
- void bindIncrementalDownloadProgressUpdated(AppInfo app);
- void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated);
- void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets);
- void bindRestoreItemsChange(HashSet<ItemInfo> updates);
- void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher);
- void bindAllWidgets(List<WidgetsListBaseEntry> widgets);
- void onPageBoundSynchronously(int page);
- void executeOnNextDraw(ViewOnDrawExecutor executor);
- void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap);
+ default void bindIncrementalDownloadProgressUpdated(AppInfo app) { }
+ default void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
+ default void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
+ default void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
+ default void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
+ default void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
+
+ default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks) {
+ pendingTasks.executeAllAndDestroy();
+ }
+
+ default void bindDeepShortcutMap(HashMap<ComponentKey, Integer> deepShortcutMap) { }
/**
* Binds extra item provided any external source
*/
default void bindExtraContainerItems(FixedContainerItems item) { }
- void bindAllApplications(AppInfo[] apps, int flags);
+ default void bindAllApplications(AppInfo[] apps, int flags) { }
}
}
diff --git a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
index 8a1d73e..3935bcf 100644
--- a/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
+++ b/src/com/android/launcher3/model/GridSizeMigrationTaskV2.java
@@ -41,6 +41,7 @@
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Utilities;
+import com.android.launcher3.Workspace;
import com.android.launcher3.graphics.LauncherPreviewRenderer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
@@ -183,7 +184,7 @@
Point targetSize = new Point(idp.numColumns, idp.numRows);
GridSizeMigrationTaskV2 task = new GridSizeMigrationTaskV2(context, t.getDb(),
srcReader, destReader, idp.numDatabaseHotseatIcons, targetSize);
- task.migrate();
+ task.migrate(idp);
if (!migrateForPreview) {
dropTable(t.getDb(), LauncherSettings.Favorites.TMP_TABLE);
@@ -210,7 +211,7 @@
}
@VisibleForTesting
- protected boolean migrate() {
+ protected boolean migrate(InvariantDeviceProfile idp) {
if (mHotseatDiff.isEmpty() && mWorkspaceDiff.isEmpty()) {
return false;
}
@@ -224,7 +225,17 @@
Collections.sort(mWorkspaceDiff);
// Migrate workspace.
+ // First we create a collection of the screens
+ List<Integer> screens = new ArrayList<>();
+ if (idp.getDeviceProfile(mContext).isTwoPanels) {
+ screens.add(Workspace.LEFT_PANEL_ID);
+ }
for (int screenId = 0; screenId <= mDestReader.mLastScreenId; screenId++) {
+ screens.add(screenId);
+ }
+
+ // Then we place the items on the screens
+ for (int screenId : screens) {
if (DEBUG) {
Log.d(TAG, "Migrating " + screenId);
}
@@ -236,6 +247,8 @@
}
}
+ // In case the new grid is smaller, there might be some leftover items that don't fit on
+ // any of the screens, in this case we add them to new screens until all of them are placed.
int screenId = mDestReader.mLastScreenId + 1;
while (!mWorkspaceDiff.isEmpty()) {
GridPlacementSolution workspaceSolution = new GridPlacementSolution(mDb, mSrcReader,
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 28fd389..31ca6e7 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -86,6 +86,7 @@
import com.android.launcher3.shortcuts.ShortcutRequest.QueryResult;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IOUtils;
+import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
@@ -176,8 +177,9 @@
ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
// Screen set is never empty
final int firstScreen = mBgDataModel.collectWorkspaceScreens().get(0);
+ // TODO(b/185515153): support two panel home.
- filterCurrentWorkspaceItems(firstScreen, allItems, firstScreenItems,
+ filterCurrentWorkspaceItems(IntSet.wrap(firstScreen), allItems, firstScreenItems,
new ArrayList<>() /* otherScreenItems are ignored */);
mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
}
@@ -208,7 +210,7 @@
}
verifyNotStopped();
- mResults.bindWorkspace();
+ mResults.bindWorkspace(true /* incrementBindId */);
logASplit(logger, "bindWorkspace");
mModelDelegate.workspaceLoadComplete();
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index 9b5fac8..58aa9e5 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -51,7 +51,8 @@
* Filters the set of items who are directly or indirectly (via another container) on the
* specified screen.
*/
- public static <T extends ItemInfo> void filterCurrentWorkspaceItems(int currentScreenId,
+ public static <T extends ItemInfo> void filterCurrentWorkspaceItems(
+ IntSet currentScreenIds,
ArrayList<T> allWorkspaceItems,
ArrayList<T> currentScreenItems,
ArrayList<T> otherScreenItems) {
@@ -65,7 +66,7 @@
(lhs, rhs) -> Integer.compare(lhs.container, rhs.container));
for (T info : allWorkspaceItems) {
if (info.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
- if (info.screenId == currentScreenId) {
+ if (currentScreenIds.contains(info.screenId)) {
currentScreenItems.add(info);
itemsOnScreen.add(info.id);
} else {
diff --git a/src/com/android/launcher3/model/ModelWriter.java b/src/com/android/launcher3/model/ModelWriter.java
index 080ce20..55edfd4 100644
--- a/src/com/android/launcher3/model/ModelWriter.java
+++ b/src/com/android/launcher3/model/ModelWriter.java
@@ -23,12 +23,13 @@
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel;
+import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherProvider;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -36,19 +37,22 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.util.ContentWriter;
+import com.android.launcher3.util.Executors;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.widget.LauncherAppWidgetHost;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
-import java.util.concurrent.Executor;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
@@ -63,7 +67,10 @@
private final Context mContext;
private final LauncherModel mModel;
private final BgDataModel mBgDataModel;
- private final Handler mUiHandler;
+ private final LooperExecutor mUiExecutor;
+
+ @Nullable
+ private final Callbacks mOwner;
private final boolean mHasVerticalHotseat;
private final boolean mVerifyChanges;
@@ -73,13 +80,15 @@
private boolean mPreparingToUndo;
public ModelWriter(Context context, LauncherModel model, BgDataModel dataModel,
- boolean hasVerticalHotseat, boolean verifyChanges) {
+ boolean hasVerticalHotseat, boolean verifyChanges,
+ @Nullable Callbacks owner) {
mContext = context;
mModel = model;
mBgDataModel = dataModel;
mHasVerticalHotseat = hasVerticalHotseat;
mVerifyChanges = verifyChanges;
- mUiHandler = new Handler(Looper.getMainLooper());
+ mOwner = owner;
+ mUiExecutor = Executors.MAIN_EXECUTOR;
}
private void updateItemInfoProps(
@@ -112,6 +121,13 @@
}
}
+ /**
+ * Sets the value that indicates if left widget panel is shown or not.
+ */
+ public void setLeftPanelShown(boolean value) {
+ mBgDataModel.isLeftPanelShown = value;
+ }
+
private void checkItemInfoLocked(int itemId, ItemInfo item, StackTraceElement[] stackTrace) {
ItemInfo modelItem = mBgDataModel.itemsIdMap.get(itemId);
if (modelItem != null && item != modelItem) {
@@ -155,6 +171,8 @@
public void moveItemInDatabase(final ItemInfo item,
int container, int screenId, int cellX, int cellY) {
updateItemInfoProps(item, container, screenId, cellX, cellY);
+ notifyItemModified(item);
+
enqueueDeleteRunnable(new UpdateItemRunnable(item, () ->
new ContentWriter(mContext)
.put(Favorites.CONTAINER, item.container)
@@ -171,6 +189,7 @@
public void moveItemsInDatabase(final ArrayList<ItemInfo> items, int container, int screen) {
ArrayList<ContentValues> contentValues = new ArrayList<>();
int count = items.size();
+ notifyOtherCallbacks(c -> c.bindItemsModified(items));
for (int i = 0; i < count; i++) {
ItemInfo item = items.get(i);
@@ -196,8 +215,9 @@
updateItemInfoProps(item, container, screenId, cellX, cellY);
item.spanX = spanX;
item.spanY = spanY;
+ notifyItemModified(item);
- ((Executor) MODEL_EXECUTOR).execute(new UpdateItemRunnable(item, () ->
+ MODEL_EXECUTOR.execute(new UpdateItemRunnable(item, () ->
new ContentWriter(mContext)
.put(Favorites.CONTAINER, item.container)
.put(Favorites.CELLX, item.cellX)
@@ -212,13 +232,18 @@
* Update an item to the database in a specified container.
*/
public void updateItemInDatabase(ItemInfo item) {
- ((Executor) MODEL_EXECUTOR).execute(new UpdateItemRunnable(item, () -> {
+ notifyItemModified(item);
+ MODEL_EXECUTOR.execute(new UpdateItemRunnable(item, () -> {
ContentWriter writer = new ContentWriter(mContext);
item.onAddToDatabase(writer);
return writer;
}));
}
+ private void notifyItemModified(ItemInfo item) {
+ notifyOtherCallbacks(c -> c.bindItemsModified(Collections.singletonList(item)));
+ }
+
/**
* Add an item to the database in a specified container. Sets the container, screen, cellX and
* cellY fields of the item. Also assigns an ID to the item.
@@ -229,10 +254,11 @@
final ContentResolver cr = mContext.getContentResolver();
item.id = Settings.call(cr, Settings.METHOD_NEW_ITEM_ID).getInt(Settings.EXTRA_VALUE);
+ notifyOtherCallbacks(c -> c.bindItems(Collections.singletonList(item), false));
ModelVerifier verifier = new ModelVerifier();
final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
- ((Executor) MODEL_EXECUTOR).execute(() -> {
+ MODEL_EXECUTOR.execute(() -> {
// Write the item on background thread, as some properties might have been updated in
// the background.
final ContentWriter writer = new ContentWriter(mContext);
@@ -274,6 +300,7 @@
(item) -> item.getTargetComponent() == null ? ""
: item.getTargetComponent().getPackageName()).collect(
Collectors.joining(",")), new Exception());
+ notifyDelete(items);
enqueueDeleteRunnable(() -> {
for (ItemInfo item : items) {
final Uri uri = Favorites.getContentUri(item.id);
@@ -290,6 +317,7 @@
*/
public void deleteFolderAndContentsFromDatabase(final FolderInfo info) {
ModelVerifier verifier = new ModelVerifier();
+ notifyDelete(Collections.singleton(info));
enqueueDeleteRunnable(() -> {
ContentResolver cr = mContext.getContentResolver();
@@ -308,6 +336,7 @@
* Deletes the widget info and the widget id.
*/
public void deleteWidgetInfo(final LauncherAppWidgetInfo info, LauncherAppWidgetHost host) {
+ notifyDelete(Collections.singleton(info));
if (host != null && !info.isCustomWidget() && info.isWidgetIdAllocated()) {
// Deleting an app widget ID is a void call but writes to disk before returning
// to the caller...
@@ -316,6 +345,10 @@
deleteItemFromDatabase(info);
}
+ private void notifyDelete(Collection<? extends ItemInfo> items) {
+ notifyOtherCallbacks(c -> c.bindWorkspaceComponentsRemoved(ItemInfoMatcher.ofItems(items)));
+ }
+
/**
* Delete operations tracked using {@link #enqueueDeleteRunnable} will only be called
* if {@link #commitDelete} is called. Note that one of {@link #commitDelete()} or
@@ -341,14 +374,14 @@
if (mPreparingToUndo) {
mDeleteRunnables.add(r);
} else {
- ((Executor) MODEL_EXECUTOR).execute(r);
+ MODEL_EXECUTOR.execute(r);
}
}
public void commitDelete() {
mPreparingToUndo = false;
for (Runnable runnable : mDeleteRunnables) {
- ((Executor) MODEL_EXECUTOR).execute(runnable);
+ MODEL_EXECUTOR.execute(runnable);
}
mDeleteRunnables.clear();
}
@@ -364,6 +397,20 @@
mModel.forceReload();
}
+ private void notifyOtherCallbacks(CallbackTask task) {
+ if (mOwner == null) {
+ // If the call is happening from a model, it will take care of updating the callbacks
+ return;
+ }
+ mUiExecutor.execute(() -> {
+ for (Callbacks c : mModel.getCallbacks()) {
+ if (c != mOwner) {
+ task.execute(c);
+ }
+ }
+ });
+ }
+
private class UpdateItemRunnable extends UpdateItemBaseRunnable {
private final ItemInfo mItem;
private final Supplier<ContentWriter> mWriter;
@@ -484,7 +531,7 @@
int executeId = mBgDataModel.lastBindId;
- mUiHandler.post(() -> {
+ mUiExecutor.post(() -> {
int currentId = mBgDataModel.lastBindId;
if (currentId > executeId) {
// Model was already bound after job was executed.
diff --git a/src/com/android/launcher3/popup/SystemShortcut.java b/src/com/android/launcher3/popup/SystemShortcut.java
index d3f4909..e5424cf 100644
--- a/src/com/android/launcher3/popup/SystemShortcut.java
+++ b/src/com/android/launcher3/popup/SystemShortcut.java
@@ -50,8 +50,6 @@
*/
private boolean isEnabled = true;
- private boolean mHasFinishRecentsInAction = false;
-
public SystemShortcut(int iconResId, int labelResId, T target, ItemInfo itemInfo) {
mIconResId = iconResId;
mLabelResId = labelResId;
@@ -102,14 +100,6 @@
return mAccessibilityActionId == action;
}
- public void setHasFinishRecentsInAction(boolean hasFinishRecentsInAction) {
- mHasFinishRecentsInAction = hasFinishRecentsInAction;
- }
-
- public boolean hasFinishRecentsInAction() {
- return mHasFinishRecentsInAction;
- }
-
public interface Factory<T extends BaseDraggingActivity> {
@Nullable SystemShortcut<T> getShortcut(T activity, ItemInfo itemInfo);
diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
index 5999091..1a96c23 100644
--- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
+++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java
@@ -35,22 +35,13 @@
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
-import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.PopupContainerWithArrow;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.util.ComponentKey;
-import com.android.launcher3.util.IntArray;
-import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.util.Themes;
-import com.android.launcher3.util.ViewOnDrawExecutor;
import com.android.launcher3.views.BaseDragLayer;
-import com.android.launcher3.widget.model.WidgetsListBaseEntry;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
/**
* Launcher activity for secondary displays
@@ -175,67 +166,10 @@
}
@Override
- public int getPageToBindSynchronously() {
- return 0;
- }
-
- @Override
- public void clearPendingBinds() { }
-
- @Override
- public void startBinding() { }
-
- @Override
- public void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
-
- @Override
- public void bindScreens(IntArray orderedScreenIds) { }
-
- @Override
- public void finishFirstPageBind(ViewOnDrawExecutor executor) {
- if (executor != null) {
- executor.onLoadAnimationCompleted();
- }
- }
-
- @Override
- public void finishBindingItems(int pageBoundFirst) { }
-
- @Override
- public void preAddApps() { }
-
- @Override
- public void bindAppsAdded(IntArray newScreens, ArrayList<ItemInfo> addNotAnimated,
- ArrayList<ItemInfo> addAnimated) { }
-
- @Override
public void bindIncrementalDownloadProgressUpdated(AppInfo app) {
mAppsView.getAppsStore().updateProgressBar(app);
}
- @Override
- public void bindWorkspaceItemsChanged(List<WorkspaceItemInfo> updated) { }
-
- @Override
- public void bindWidgetsRestored(ArrayList<LauncherAppWidgetInfo> widgets) { }
-
- @Override
- public void bindRestoreItemsChange(HashSet<ItemInfo> updates) { }
-
- @Override
- public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { }
-
- @Override
- public void bindAllWidgets(List<WidgetsListBaseEntry> widgets) { }
-
- @Override
- public void onPageBoundSynchronously(int page) { }
-
- @Override
- public void executeOnNextDraw(ViewOnDrawExecutor executor) {
- executor.attachTo(getDragLayer(), false, null);
- }
-
/**
* Called when apps-button is clicked
*/
diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java
index 4261d08..944a41f 100644
--- a/src/com/android/launcher3/testing/TestInformationHandler.java
+++ b/src/com/android/launcher3/testing/TestInformationHandler.java
@@ -117,6 +117,15 @@
TestProtocol.sDisableSensorRotation = true;
return response;
+ case TestProtocol.REQUEST_IS_TABLET:
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD, mDeviceProfile.isTablet);
+ return response;
+
+ case TestProtocol.REQUEST_IS_TWO_PANELS:
+ response.putBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD,
+ mDeviceProfile.isTwoPanels);
+ return response;
+
default:
return null;
}
diff --git a/src/com/android/launcher3/testing/TestProtocol.java b/src/com/android/launcher3/testing/TestProtocol.java
index b6da7fc..65bec25 100644
--- a/src/com/android/launcher3/testing/TestProtocol.java
+++ b/src/com/android/launcher3/testing/TestProtocol.java
@@ -24,6 +24,7 @@
public static final String SWITCHED_TO_STATE_MESSAGE = "TAPL_SWITCHED_TO_STATE";
public static final String SCROLL_FINISHED_MESSAGE = "TAPL_SCROLL_FINISHED";
public static final String PAUSE_DETECTED_MESSAGE = "TAPL_PAUSE_DETECTED";
+ public static final String DISMISS_ANIMATION_ENDS_MESSAGE = "TAPL_DISMISS_ANIMATION_ENDS";
public static final int NORMAL_STATE_ORDINAL = 0;
public static final int SPRING_LOADED_STATE_ORDINAL = 1;
public static final int OVERVIEW_STATE_ORDINAL = 2;
@@ -94,6 +95,8 @@
public static final String REQUEST_GET_TEST_EVENTS = "get-test-events";
public static final String REQUEST_STOP_EVENT_LOGGING = "stop-event-logging";
public static final String REQUEST_CLEAR_DATA = "clear-data";
+ public static final String REQUEST_IS_TABLET = "is-tablet";
+ public static final String REQUEST_IS_TWO_PANELS = "is-two-panel";
public static boolean sDebugTracing = false;
public static final String REQUEST_ENABLE_DEBUG_TRACING = "enable-debug-tracing";
diff --git a/src/com/android/launcher3/touch/BaseSwipeDetector.java b/src/com/android/launcher3/touch/BaseSwipeDetector.java
index 1276ece..cfd3153 100644
--- a/src/com/android/launcher3/touch/BaseSwipeDetector.java
+++ b/src/com/android/launcher3/touch/BaseSwipeDetector.java
@@ -17,6 +17,8 @@
import static android.view.MotionEvent.INVALID_POINTER_ID;
+import static com.android.launcher3.Utilities.IS_RUNNING_IN_TEST_HARNESS;
+
import android.graphics.PointF;
import android.util.Log;
import android.view.MotionEvent;
@@ -41,7 +43,8 @@
*/
public abstract class BaseSwipeDetector {
- private static final boolean DBG = false;
+ // b/193440212: Debug swipe gesture in tests.
+ private static final boolean DBG = IS_RUNNING_IN_TEST_HARNESS;
private static final String TAG = "BaseSwipeDetector";
private static final float ANIMATION_DURATION = 1200;
/** The minimum release velocity in pixels per millisecond that triggers fling.*/
diff --git a/src/com/android/launcher3/util/FlingAnimation.java b/src/com/android/launcher3/util/FlingAnimation.java
index c9aa51c..ac864e9 100644
--- a/src/com/android/launcher3/util/FlingAnimation.java
+++ b/src/com/android/launcher3/util/FlingAnimation.java
@@ -1,12 +1,14 @@
package com.android.launcher3.util;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.graphics.PointF;
import android.graphics.Rect;
+import android.graphics.RectF;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
@@ -35,7 +37,7 @@
protected final float mUX, mUY;
protected Rect mIconRect;
- protected Rect mFrom;
+ protected RectF mFrom;
protected int mDuration;
protected float mAnimationTimeFraction;
@@ -55,17 +57,17 @@
@Override
public void run() {
mIconRect = mDropTarget.getIconRect(mDragObject);
+ mDragObject.dragView.cancelAnimation();
+ mDragObject.dragView.requestLayout();
// Initiate from
- mFrom = new Rect();
- mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, mFrom);
- float scale = mDragObject.dragView.getScaleX();
- float xOffset = ((scale - 1f) * mDragObject.dragView.getMeasuredWidth()) / 2f;
- float yOffset = ((scale - 1f) * mDragObject.dragView.getMeasuredHeight()) / 2f;
- mFrom.left += xOffset;
- mFrom.right -= xOffset;
- mFrom.top += yOffset;
- mFrom.bottom -= yOffset;
+ Rect from = new Rect();
+ mDragLayer.getViewRectRelativeToSelf(mDragObject.dragView, from);
+
+ mFrom = new RectF(from);
+ mFrom.inset(
+ ((1 - mDragObject.dragView.getScaleX()) * from.width()) / 2f,
+ ((1 - mDragObject.dragView.getScaleY()) * from.height()) / 2f);
mDuration = Math.abs(mUY) > Math.abs(mUX) ? initFlingUpDuration() : initFlingLeftDuration();
mAnimationTimeFraction = ((float) mDuration) / (mDuration + DRAG_END_DELAY);
@@ -95,17 +97,15 @@
}
};
- Runnable onAnimationEndRunnable = new Runnable() {
- @Override
- public void run() {
- mLauncher.getStateManager().goToState(NORMAL);
- mDropTarget.completeDrop(mDragObject);
- }
- };
-
mDropTarget.onDrop(mDragObject, mDragOptions);
- mDragLayer.animateView(mDragObject.dragView, this, duration, tInterpolator,
- onAnimationEndRunnable, DragLayer.ANIMATION_END_DISAPPEAR, null);
+ ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+ anim.setDuration(duration).setInterpolator(tInterpolator);
+ anim.addUpdateListener(this);
+ anim.addListener(forEndCallback(() -> {
+ mLauncher.getStateManager().goToState(NORMAL);
+ mDropTarget.completeDrop(mDragObject);
+ }));
+ mDragLayer.playDropAnimation(mDragObject.dragView, anim, DragLayer.ANIMATION_END_DISAPPEAR);
}
/**
@@ -129,7 +129,7 @@
}
double t = (-mUY - Math.sqrt(d)) / mAY;
- float sX = -mFrom.exactCenterX() + mIconRect.exactCenterX();
+ float sX = -mFrom.centerX() + mIconRect.exactCenterX();
// Find horizontal acceleration such that: u*t + a*t*t/2 = s
mAX = (float) ((sX - t * mUX) * 2 / (t * t));
@@ -157,7 +157,7 @@
}
double t = (-mUX - Math.sqrt(d)) / mAX;
- float sY = -mFrom.exactCenterY() + mIconRect.exactCenterY();
+ float sY = -mFrom.centerY() + mIconRect.exactCenterY();
// Find vertical acceleration such that: u*t + a*t*t/2 = s
mAY = (float) ((sY - t * mUY) * 2 / (t * t));
diff --git a/src/com/android/launcher3/util/IntArray.java b/src/com/android/launcher3/util/IntArray.java
index 7252f7a..e7235e7 100644
--- a/src/com/android/launcher3/util/IntArray.java
+++ b/src/com/android/launcher3/util/IntArray.java
@@ -17,13 +17,14 @@
package com.android.launcher3.util;
import java.util.Arrays;
+import java.util.Iterator;
import java.util.StringTokenizer;
/**
* Copy of the platform hidden implementation of android.util.IntArray.
* Implements a growing array of int primitives.
*/
-public class IntArray implements Cloneable {
+public class IntArray implements Cloneable, Iterable<Integer> {
private static final int MIN_CAPACITY_INCREMENT = 12;
private static final int[] EMPTY_INT = new int[0];
@@ -272,4 +273,30 @@
throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index);
}
}
+
+ @Override
+ public Iterator<Integer> iterator() {
+ return new ValueIterator();
+ }
+
+ @Thunk
+ class ValueIterator implements Iterator<Integer> {
+
+ private int mNextIndex = 0;
+
+ @Override
+ public boolean hasNext() {
+ return mNextIndex < size();
+ }
+
+ @Override
+ public Integer next() {
+ return get(mNextIndex++);
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/com/android/launcher3/util/IntSet.java b/src/com/android/launcher3/util/IntSet.java
index 851f129..e5b4f59 100644
--- a/src/com/android/launcher3/util/IntSet.java
+++ b/src/com/android/launcher3/util/IntSet.java
@@ -16,11 +16,13 @@
package com.android.launcher3.util;
import java.util.Arrays;
+import java.util.Iterator;
/**
* A wrapper over IntArray implementing a growing set of int primitives.
+ * The elements in the array are sorted in ascending order.
*/
-public class IntSet {
+public class IntSet implements Iterable<Integer> {
final IntArray mArray = new IntArray();
@@ -34,6 +36,16 @@
}
}
+ /**
+ * Removes the specified value from the set if it exist.
+ */
+ public void remove(int value) {
+ int index = Arrays.binarySearch(mArray.mValues, 0, mArray.mSize, value);
+ if (index >= 0) {
+ mArray.removeIndex(index);
+ }
+ }
+
public boolean contains(int value) {
return Arrays.binarySearch(mArray.mValues, 0, mArray.mSize, value) >= 0;
}
@@ -61,6 +73,9 @@
return (obj instanceof IntSet) && ((IntSet) obj).mArray.equals(mArray);
}
+ /**
+ * Returns the wrapped IntArray. The elements in the array are sorted in ascending order.
+ */
public IntArray getArray() {
return mArray;
}
@@ -78,4 +93,30 @@
Arrays.sort(set.mArray.mValues, 0, set.mArray.mSize);
return set;
}
+
+ /**
+ * Returns an IntSet with the given values.
+ */
+ public static IntSet wrap(int... array) {
+ return wrap(IntArray.wrap(array));
+ }
+
+ /**
+ * Returns an IntSet with the given values.
+ */
+ public static IntSet wrap(Iterable<Integer> iterable) {
+ IntSet set = new IntSet();
+ iterable.forEach(set::add);
+ return set;
+ }
+
+ @Override
+ public Iterator<Integer> iterator() {
+ return mArray.iterator();
+ }
+
+ @Override
+ public String toString() {
+ return "IntSet{" + mArray.toConcatString() + '}';
+ }
}
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index 354609d..e8ba28f 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -23,6 +23,7 @@
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.shortcuts.ShortcutKey;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
@@ -89,4 +90,13 @@
static ItemInfoMatcher ofItemIds(IntSet ids) {
return (info, cn) -> ids.contains(info.id);
}
+
+ /**
+ * Returns a matcher for items with provided items
+ */
+ static ItemInfoMatcher ofItems(Collection<? extends ItemInfo> items) {
+ IntSet ids = new IntSet();
+ items.forEach(item -> ids.add(item.id));
+ return ofItemIds(ids);
+ }
}
diff --git a/src/com/android/launcher3/util/LauncherBindableItemsContainer.java b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
new file mode 100644
index 0000000..a4cb30a
--- /dev/null
+++ b/src/com/android/launcher3/util/LauncherBindableItemsContainer.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2021 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.launcher3.util;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.folder.Folder;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.graphics.PreloadIconDrawable;
+import com.android.launcher3.model.data.FolderInfo;
+import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.LauncherAppWidgetInfo;
+import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.widget.PendingAppWidgetHostView;
+
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Interface representing a container which can bind Launcher items with some utility methods
+ */
+public interface LauncherBindableItemsContainer {
+
+ /**
+ * Called to update workspace items as a result of
+ * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindWorkspaceItemsChanged(List)}
+ */
+ default void updateWorkspaceItems(List<WorkspaceItemInfo> shortcuts, ActivityContext context) {
+ final HashSet<WorkspaceItemInfo> updates = new HashSet<>(shortcuts);
+ ItemOperator op = (info, v) -> {
+ if (v instanceof BubbleTextView && updates.contains(info)) {
+ WorkspaceItemInfo si = (WorkspaceItemInfo) info;
+ BubbleTextView shortcut = (BubbleTextView) v;
+ Drawable oldIcon = shortcut.getIcon();
+ boolean oldPromiseState = (oldIcon instanceof PreloadIconDrawable)
+ && ((PreloadIconDrawable) oldIcon).hasNotCompleted();
+ shortcut.applyFromWorkspaceItem(si, si.isPromise() != oldPromiseState);
+ } else if (info instanceof FolderInfo && v instanceof FolderIcon) {
+ ((FolderIcon) v).updatePreviewItems(updates::contains);
+ }
+
+ // Iterate all items
+ return false;
+ };
+
+ mapOverItems(op);
+ Folder openFolder = Folder.getOpen(context);
+ if (openFolder != null) {
+ openFolder.iterateOverItems(op);
+ }
+ }
+
+ /**
+ * Called to update restored items as a result of
+ * {@link com.android.launcher3.model.BgDataModel.Callbacks#bindRestoreItemsChange(HashSet)}}
+ */
+ default void updateRestoreItems(final HashSet<ItemInfo> updates, ActivityContext context) {
+ ItemOperator op = (info, v) -> {
+ if (info instanceof WorkspaceItemInfo && v instanceof BubbleTextView
+ && updates.contains(info)) {
+ ((BubbleTextView) v).applyLoadingState(false /* promiseStateChanged */);
+ } else if (v instanceof PendingAppWidgetHostView
+ && info instanceof LauncherAppWidgetInfo
+ && updates.contains(info)) {
+ ((PendingAppWidgetHostView) v).applyState();
+ } else if (v instanceof FolderIcon && info instanceof FolderInfo) {
+ ((FolderIcon) v).updatePreviewItems(updates::contains);
+ }
+ // process all the shortcuts
+ return false;
+ };
+
+ mapOverItems(op);
+ Folder folder = Folder.getOpen(context);
+ if (folder != null) {
+ folder.iterateOverItems(op);
+ }
+ }
+
+ /**
+ * Map the operator over the shortcuts and widgets.
+ *
+ * @param op the operator to map over the shortcuts
+ */
+ void mapOverItems(ItemOperator op);
+
+ interface ItemOperator {
+ /**
+ * Process the next itemInfo, possibly with side-effect on the next item.
+ *
+ * @param info info for the shortcut
+ * @param view view for the shortcut
+ * @return true if done, false to continue the map
+ */
+ boolean evaluate(ItemInfo info, View view);
+ }
+}
diff --git a/src/com/android/launcher3/util/MultiValueAlpha.java b/src/com/android/launcher3/util/MultiValueAlpha.java
index 5be9529..8591872 100644
--- a/src/com/android/launcher3/util/MultiValueAlpha.java
+++ b/src/com/android/launcher3/util/MultiValueAlpha.java
@@ -16,6 +16,8 @@
package com.android.launcher3.util;
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
import android.util.FloatProperty;
import android.view.View;
@@ -121,5 +123,12 @@
public String toString() {
return Float.toString(mValue);
}
+
+ /**
+ * Creates and returns an Animator from the current value to the given value.
+ */
+ public Animator animateToValue(float value) {
+ return ObjectAnimator.ofFloat(this, VALUE, value);
+ }
}
}
diff --git a/src/com/android/launcher3/util/ViewOnDrawExecutor.java b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
index 82e24c2..5d90291 100644
--- a/src/com/android/launcher3/util/ViewOnDrawExecutor.java
+++ b/src/com/android/launcher3/util/ViewOnDrawExecutor.java
@@ -16,28 +16,21 @@
package com.android.launcher3.util;
-import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
-
-import android.os.Process;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewTreeObserver.OnDrawListener;
-import androidx.annotation.VisibleForTesting;
-
import com.android.launcher3.Launcher;
-import java.util.ArrayList;
-import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* An executor which runs all the tasks after the first onDraw is called on the target view.
*/
-public class ViewOnDrawExecutor implements Executor, OnDrawListener, Runnable,
+public class ViewOnDrawExecutor implements OnDrawListener, Runnable,
OnAttachStateChangeListener {
- private final ArrayList<Runnable> mTasks = new ArrayList<>();
+ private final RunnableList mTasks;
private Consumer<ViewOnDrawExecutor> mOnClearCallback;
private View mAttachedView;
@@ -46,22 +39,16 @@
private boolean mLoadAnimationCompleted;
private boolean mFirstDrawCompleted;
- public void attachTo(Launcher launcher) {
- attachTo(launcher.getWorkspace(), true /* waitForLoadAnimation */,
- launcher::clearPendingExecutor);
+ private boolean mCancelled;
+
+ public ViewOnDrawExecutor(RunnableList tasks) {
+ mTasks = tasks;
}
- /**
- * Attached the executor to the existence of the view
- */
- public void attachTo(View attachedView, boolean waitForLoadAnimation,
- Consumer<ViewOnDrawExecutor> onClearCallback) {
- mOnClearCallback = onClearCallback;
- mAttachedView = attachedView;
+ public void attachTo(Launcher launcher) {
+ mOnClearCallback = launcher::clearPendingExecutor;
+ mAttachedView = launcher.getWorkspace();
mAttachedView.addOnAttachStateChangeListener(this);
- if (!waitForLoadAnimation) {
- mLoadAnimationCompleted = true;
- }
if (mAttachedView.isAttachedToWindow()) {
attachObserver();
@@ -75,12 +62,6 @@
}
@Override
- public void execute(Runnable command) {
- mTasks.add(command);
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- }
-
- @Override
public void onViewAttachedToWindow(View v) {
attachObserver();
}
@@ -105,12 +86,17 @@
public void run() {
// Post the pending tasks after both onDraw and onLoadAnimationCompleted have been called.
if (mLoadAnimationCompleted && mFirstDrawCompleted && !mCompleted) {
- runAllTasks();
+ markCompleted();
}
}
+ /**
+ * Executes all tasks immediately
+ */
public void markCompleted() {
- mTasks.clear();
+ if (!mCancelled) {
+ mTasks.executeAllAndDestroy();
+ }
mCompleted = true;
if (mAttachedView != null) {
mAttachedView.getViewTreeObserver().removeOnDrawListener(this);
@@ -119,21 +105,10 @@
if (mOnClearCallback != null) {
mOnClearCallback.accept(this);
}
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
}
- protected boolean isCompleted() {
- return mCompleted;
- }
-
- /**
- * Executes all tasks immediately
- */
- @VisibleForTesting
- public void runAllTasks() {
- for (final Runnable r : mTasks) {
- r.run();
- }
+ public void cancel() {
+ mCancelled = true;
markCompleted();
}
}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 646b669..b95904e 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -21,9 +21,12 @@
import android.view.LayoutInflater;
import android.view.View.AccessibilityDelegate;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.dragndrop.DragController;
+import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.util.ViewCache;
@@ -100,6 +103,13 @@
}
/**
+ * Returns the FolderIcon with the given item id, if it exists.
+ */
+ default @Nullable FolderIcon findFolderIcon(final int folderIconId) {
+ return null;
+ }
+
+ /**
* Returns the ActivityContext associated with the given Context.
*/
static <T extends Context & ActivityContext> T lookupContext(Context context) {
diff --git a/src/com/android/launcher3/views/BaseDragLayer.java b/src/com/android/launcher3/views/BaseDragLayer.java
index 01c0b56..76dfb3c 100644
--- a/src/com/android/launcher3/views/BaseDragLayer.java
+++ b/src/com/android/launcher3/views/BaseDragLayer.java
@@ -430,18 +430,20 @@
}
public void getViewRectRelativeToSelf(View v, Rect r) {
+ int[] loc = getViewLocationRelativeToSelf(v);
+ r.set(loc[0], loc[1], loc[0] + v.getMeasuredWidth(), loc[1] + v.getMeasuredHeight());
+ }
+
+ protected int[] getViewLocationRelativeToSelf(View v) {
int[] loc = new int[2];
getLocationInWindow(loc);
int x = loc[0];
int y = loc[1];
v.getLocationInWindow(loc);
- int vX = loc[0];
- int vY = loc[1];
-
- int left = vX - x;
- int top = vY - y;
- r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
+ loc[0] -= x;
+ loc[1] -= y;
+ return loc;
}
@Override
diff --git a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
index 1cc7f53..b92c476 100644
--- a/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/AddItemWidgetsBottomSheet.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.Utilities.ATLEAST_R;
import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
+import static com.android.launcher3.widget.BaseWidgetSheet.MAX_WIDTH_SCALE_FOR_LARGER_SCREEN;
import android.animation.PropertyValuesHolder;
import android.annotation.SuppressLint;
@@ -25,10 +26,12 @@
import android.graphics.Insets;
import android.graphics.Rect;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.WindowInsets;
+import android.widget.ScrollView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
@@ -44,6 +47,7 @@
private static final int DEFAULT_CLOSE_DURATION = 200;
private final Rect mInsets;
+ private ScrollView mWidgetPreviewScrollView;
public AddItemWidgetsBottomSheet(Context context, AttributeSet attrs) {
this(context, attrs, 0);
@@ -68,6 +72,19 @@
}
@Override
+ public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ mNoIntercept = false;
+ // Suppress drag to dismiss gesture if the scroll view is being scrolled.
+ if (getPopupContainer().isEventOverView(mWidgetPreviewScrollView, ev)
+ && mWidgetPreviewScrollView.getScrollY() > 0) {
+ mNoIntercept = true;
+ }
+ }
+ return super.onControllerInterceptTouchEvent(ev);
+ }
+
+ @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int width = r - l;
int height = b - t;
@@ -93,6 +110,15 @@
2 * (mInsets.left + mInsets.right));
}
+ if (deviceProfile.isTablet || deviceProfile.isTwoPanels) {
+ // In large screen devices, we restrict the width of the widgets picker to show part of
+ // the home screen. Let's ensure the minimum width used is at least the minimum width
+ // that isn't taken by the widgets picker.
+ int minUsedWidth = (int) (deviceProfile.availableWidthPx
+ * (1 - MAX_WIDTH_SCALE_FOR_LARGER_SCREEN));
+ widthUsed = Math.max(widthUsed, minUsedWidth);
+ }
+
int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
measureChildWithMargins(mContent, widthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
@@ -104,6 +130,7 @@
protected void onFinishInflate() {
super.onFinishInflate();
mContent = findViewById(R.id.add_item_bottom_sheet_content);
+ mWidgetPreviewScrollView = findViewById(R.id.widget_preview_scroll_view);
}
private void animateOpen() {
diff --git a/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
new file mode 100644
index 0000000..2742882
--- /dev/null
+++ b/src/com/android/launcher3/widget/BaseLauncherAppWidgetHostView.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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.launcher3.widget;
+
+import android.content.Context;
+import android.graphics.Outline;
+import android.graphics.Rect;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.RemoteViews;
+
+import androidx.annotation.UiThread;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Executors;
+
+/**
+ * Launcher AppWidgetHostView with support for rounded corners and a fallback View.
+ */
+public abstract class BaseLauncherAppWidgetHostView extends NavigableAppWidgetHostView {
+
+ protected final LayoutInflater mInflater;
+
+ private final Rect mEnforcedRectangle = new Rect();
+ private final float mEnforcedCornerRadius;
+ private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
+ outline.setEmpty();
+ } else {
+ outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
+ }
+ }
+ };
+
+ public BaseLauncherAppWidgetHostView(Context context) {
+ super(context);
+
+ setExecutor(Executors.THREAD_POOL_EXECUTOR);
+
+ mInflater = LayoutInflater.from(context);
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
+ }
+
+ @Override
+ protected View getErrorView() {
+ return mInflater.inflate(R.layout.appwidget_error, this, false);
+ }
+
+ /**
+ * Fall back to error layout instead of showing widget.
+ */
+ public void switchToErrorView() {
+ // Update the widget with 0 Layout id, to reset the view to error view.
+ updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ try {
+ super.onLayout(changed, left, top, right, bottom);
+ } catch (final RuntimeException e) {
+ post(this::switchToErrorView);
+ }
+
+ enforceRoundedCorners();
+ }
+
+ @UiThread
+ private void resetRoundedCorners() {
+ setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ setClipToOutline(false);
+ }
+
+ @UiThread
+ private void enforceRoundedCorners() {
+ if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
+ resetRoundedCorners();
+ return;
+ }
+ View background = RoundedCornerEnforcement.findBackground(this);
+ if (background == null
+ || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
+ resetRoundedCorners();
+ return;
+ }
+ RoundedCornerEnforcement.computeRoundedRectangle(this,
+ background,
+ mEnforcedRectangle);
+ setOutlineProvider(mCornerRadiusEnforcementOutline);
+ setClipToOutline(true);
+ }
+
+ /** Returns the corner radius currently enforced, in pixels. */
+ public float getEnforcedCornerRadius() {
+ return mEnforcedCornerRadius;
+ }
+
+ /** Returns true if the corner radius are enforced for this App Widget. */
+ public boolean hasEnforcedCornerRadius() {
+ return getClipToOutline();
+ }
+}
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 9f0b9d9..ee44174 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -51,6 +51,13 @@
public abstract class BaseWidgetSheet extends AbstractSlideInView<Launcher>
implements OnClickListener, OnLongClickListener, DragSource,
PopupDataProvider.PopupDataChangeListener, Insettable {
+ /** The default number of cells that can fit horizontally in a widget sheet. */
+ protected static final int DEFAULT_MAX_HORIZONTAL_SPANS = 4;
+ /**
+ * The maximum scale, [0, 1], of the device screen width that the widgets picker can consume
+ * on large screen devices.
+ */
+ protected static final float MAX_WIDTH_SCALE_FOR_LARGER_SCREEN = 0.89f;
protected static final String KEY_WIDGETS_EDUCATION_TIP_SEEN =
"launcher.widgets_education_tip_seen";
@@ -131,6 +138,15 @@
2 * (mInsets.left + mInsets.right));
}
+ if (deviceProfile.isTablet || deviceProfile.isTwoPanels) {
+ // In large screen devices, we restrict the width of the widgets picker to show part of
+ // the home screen. Let's ensure the minimum width used is at least the minimum width
+ // that isn't taken by the widgets picker.
+ int minUsedWidth = (int) (deviceProfile.availableWidthPx
+ * (1 - MAX_WIDTH_SCALE_FOR_LARGER_SCREEN));
+ widthUsed = Math.max(widthUsed, minUsedWidth);
+ }
+
int heightUsed = mInsets.top + deviceProfile.edgeMarginPx;
measureChildWithMargins(mContent, widthMeasureSpec,
widthUsed, heightMeasureSpec, heightUsed);
@@ -138,6 +154,17 @@
MeasureSpec.getSize(heightMeasureSpec));
}
+ /** Returns the number of cells that can fit horizontally in a given {@code content}. */
+ protected int computeMaxHorizontalSpans(View content, int contentHorizontalPaddingPx) {
+ DeviceProfile deviceProfile = mActivityContext.getDeviceProfile();
+ int availableWidth = content.getMeasuredWidth() - contentHorizontalPaddingPx;
+ Point cellSize = deviceProfile.getCellSize();
+ if (cellSize.x > 0) {
+ return availableWidth / cellSize.x;
+ }
+ return DEFAULT_MAX_HORIZONTAL_SPANS;
+ }
+
private boolean beginDraggingWidget(WidgetCell v) {
// Get the widget preview as the drag representation
WidgetImageView image = v.getWidgetView();
@@ -149,7 +176,9 @@
}
PendingItemDragHelper dragHelper = new PendingItemDragHelper(v);
- dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview());
+ // RemoteViews are being rendered in AppWidgetHostView in WidgetCell. And thus, the scale of
+ // RemoteViews is equivalent to the AppWidgetHostView scale.
+ dragHelper.setRemoteViewsPreview(v.getRemoteViewsPreview(), v.getAppWidgetHostViewScale());
dragHelper.setAppWidgetHostViewPreview(v.getAppWidgetHostViewPreview());
if (image.getDrawable() != null) {
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index 63bc416..fb6de9f 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -20,19 +20,16 @@
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
-import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.os.SystemClock;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
-import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewDebug;
import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.AdapterView;
import android.widget.Advanceable;
@@ -40,7 +37,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.UiThread;
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Launcher;
@@ -51,7 +47,6 @@
import com.android.launcher3.keyboard.ViewGroupFocusHelper;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
-import com.android.launcher3.util.Executors;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
@@ -61,7 +56,7 @@
/**
* {@inheritDoc}
*/
-public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
+public class LauncherAppWidgetHostView extends BaseLauncherAppWidgetHostView
implements TouchCompleteListener, View.OnLongClickListener,
LocalColorExtractor.Listener {
@@ -76,8 +71,6 @@
// Maximum duration for which updates can be deferred.
private static final long UPDATE_LOCK_TIMEOUT_MILLIS = 1000;
- protected final LayoutInflater mInflater;
-
private final CheckLongPressHelper mLongPressHelper;
protected final Launcher mLauncher;
private final Workspace mWorkspace;
@@ -101,18 +94,6 @@
private final Rect mWidgetSizeAtDrag = new Rect();
private final RectF mTempRectF = new RectF();
- private final Rect mEnforcedRectangle = new Rect();
- private final float mEnforcedCornerRadius;
- private final ViewOutlineProvider mCornerRadiusEnforcementOutline = new ViewOutlineProvider() {
- @Override
- public void getOutline(View view, Outline outline) {
- if (mEnforcedRectangle.isEmpty() || mEnforcedCornerRadius <= 0) {
- outline.setEmpty();
- } else {
- outline.setRoundRect(mEnforcedRectangle, mEnforcedCornerRadius);
- }
- }
- };
private final Object mUpdateLock = new Object();
private final ViewGroupFocusHelper mDragLayerRelativeCoordinateHelper;
private long mDeferUpdatesUntilMillis = 0;
@@ -126,18 +107,15 @@
mLauncher = Launcher.getLauncher(context);
mWorkspace = mLauncher.getWorkspace();
mLongPressHelper = new CheckLongPressHelper(this, this);
- mInflater = LayoutInflater.from(context);
setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
setBackgroundResource(R.drawable.widget_internal_focus_bg);
- setExecutor(Executors.THREAD_POOL_EXECUTOR);
if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
setOnLightBackground(true);
}
mColorExtractor = LocalColorExtractor.newInstance(getContext());
mColorExtractor.setListener(this);
- mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext());
mDragLayerRelativeCoordinateHelper = new ViewGroupFocusHelper(mLauncher.getDragLayer());
}
@@ -169,11 +147,6 @@
}
@Override
- protected View getErrorView() {
- return mInflater.inflate(R.layout.appwidget_error, this, false);
- }
-
- @Override
public void updateAppWidget(RemoteViews remoteViews) {
synchronized (mUpdateLock) {
if (isDeferringUpdates()) {
@@ -329,28 +302,12 @@
}
}
- public void switchToErrorView() {
- // Update the widget with 0 Layout id, to reset the view to error view.
- updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
- }
-
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- try {
- super.onLayout(changed, left, top, right, bottom);
- } catch (final RuntimeException e) {
- post(new Runnable() {
- @Override
- public void run() {
- switchToErrorView();
- }
- });
- }
+ super.onLayout(changed, left, top, right, bottom);
mIsScrollable = checkScrollableRecursively(this);
updateColorExtraction();
-
- enforceRoundedCorners();
}
/** Starts the drag mode. */
@@ -564,40 +521,4 @@
}
return false;
}
-
- @UiThread
- private void resetRoundedCorners() {
- setOutlineProvider(ViewOutlineProvider.BACKGROUND);
- setClipToOutline(false);
- }
-
- @UiThread
- private void enforceRoundedCorners() {
- if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled()) {
- resetRoundedCorners();
- return;
- }
- View background = RoundedCornerEnforcement.findBackground(this);
- if (background == null
- || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) {
- resetRoundedCorners();
- return;
- }
- RoundedCornerEnforcement.computeRoundedRectangle(this,
- background,
- mEnforcedRectangle);
- setOutlineProvider(mCornerRadiusEnforcementOutline);
- setClipToOutline(true);
- }
-
- /** Returns the corner radius currently enforced, in pixels. */
- public float getEnforcedCornerRadius() {
- return mEnforcedCornerRadius;
- }
-
- /** Returns true if the corner radius are enforced for this App Widget. */
- public boolean hasEnforcedCornerRadius() {
- return getClipToOutline();
- }
-
}
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index 6163b51..d12fe74 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -26,7 +26,6 @@
import android.view.ViewDebug;
import android.view.ViewGroup;
-import com.android.launcher3.BaseActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Reorderable;
import com.android.launcher3.dragndrop.DraggableView;
@@ -59,7 +58,7 @@
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mChildrenFocused;
- protected final BaseActivity mActivity;
+ protected final ActivityContext mActivity;
public NavigableAppWidgetHostView(Context context) {
super(context);
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index cea4de7..a4003d4 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -22,6 +22,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.util.Size;
import android.view.View;
import android.view.View.MeasureSpec;
import android.widget.RemoteViews;
@@ -41,6 +42,7 @@
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.icons.RoundDrawableWrapper;
import com.android.launcher3.widget.dragndrop.AppWidgetHostViewDragListener;
+import com.android.launcher3.widget.util.WidgetSizes;
/**
* Extension of {@link DragPreviewProvider} with logic specific to pending widgets/shortcuts
@@ -54,6 +56,7 @@
private int[] mEstimatedCellSize;
@Nullable private RemoteViews mRemoteViewsPreview;
+ private float mRemoteViewsPreviewScale = 1f;
@Nullable private NavigableAppWidgetHostView mAppWidgetHostViewPreview;
private final float mEnforcedRoundedCornersForWidget;
@@ -68,8 +71,10 @@
* Sets a {@link RemoteViews} which shows an app widget preview provided by app developers in
* the pin widget flow.
*/
- public void setRemoteViewsPreview(@Nullable RemoteViews remoteViewsPreview) {
+ public void setRemoteViewsPreview(@Nullable RemoteViews remoteViewsPreview,
+ float previewScale) {
mRemoteViewsPreview = remoteViewsPreview;
+ mRemoteViewsPreviewScale = previewScale;
}
/** Sets a {@link NavigableAppWidgetHostView} which shows a preview layout of an app widget. */
@@ -120,13 +125,14 @@
mAppWidgetHostViewPreview.setPadding(padding.left, padding.top, padding.right,
padding.bottom);
mAppWidgetHostViewPreview.updateAppWidget(/* remoteViews= */ mRemoteViewsPreview);
- int width =
- deviceProfile.cellWidthPx * mAddInfo.spanX + padding.left + padding.right;
- int height =
- deviceProfile.cellHeightPx * mAddInfo.spanY + padding.top + padding.bottom;
+ Size widgetSizes = WidgetSizes.getWidgetPaddedSizePx(launcher,
+ mAddInfo.componentName, deviceProfile, mAddInfo.spanX, mAddInfo.spanY);
mAppWidgetHostViewPreview.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ MeasureSpec.makeMeasureSpec(widgetSizes.getWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(widgetSizes.getHeight(), MeasureSpec.EXACTLY));
+ mAppWidgetHostViewPreview.setClipChildren(false);
+ mAppWidgetHostViewPreview.setClipToPadding(false);
+ mAppWidgetHostViewPreview.setScaleToFit(mRemoteViewsPreviewScale);
}
if (mAppWidgetHostViewPreview != null) {
previewSizeBeforeScale[0] = mAppWidgetHostViewPreview.getMeasuredWidth();
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 5769ba0..d3f4528 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,12 +16,14 @@
package com.android.launcher3.widget;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
import static com.android.launcher3.Utilities.ATLEAST_S;
import android.content.Context;
import android.graphics.Bitmap;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.CancellationSignal;
import android.util.AttributeSet;
@@ -31,6 +33,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
@@ -72,11 +75,36 @@
/** Widget preview width is calculated by multiplying this factor to the widget cell width. */
private static final float PREVIEW_SCALE = 0.8f;
- protected int mPreviewWidth;
- protected int mPreviewHeight;
+ /**
+ * The maximum dimension that can be used as the size in
+ * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int)}.
+ *
+ * <p>This is equal to (1 << MeasureSpec.MODE_SHIFT) - 1.
+ */
+ private static final int MAX_MEASURE_SPEC_DIMENSION = (1 << 30) - 1;
+
+ /**
+ * The target preview width, in pixels, of a widget or a shortcut.
+ *
+ * <p>The actual preview width may be smaller than or equal to this value subjected to scaling.
+ */
+ protected int mTargetPreviewWidth;
+
+ /**
+ * The target preview height, in pixels, of a widget or a shortcut.
+ *
+ * <p>The actual preview height may be smaller than or equal to this value subjected to scaling.
+ */
+ protected int mTargetPreviewHeight;
+
protected int mPresetPreviewSize;
+
private int mCellSize;
- private float mPreviewScale = 1f;
+
+ /**
+ * The scale of the preview container.
+ */
+ private float mPreviewContainerScale = 1f;
private FrameLayout mWidgetImageContainer;
private WidgetImageView mWidgetImage;
@@ -97,10 +125,10 @@
protected final BaseActivity mActivity;
private final CheckLongPressHelper mLongPressHelper;
private final float mEnforcedCornerRadius;
- private final int mShortcutPreviewPadding;
private RemoteViews mRemoteViewsPreview;
private NavigableAppWidgetHostView mAppWidgetHostViewPreview;
+ private float mAppWidgetHostViewScale = 1f;
private int mSourceContainer = CONTAINER_WIDGETS_TRAY;
public WidgetCell(Context context) {
@@ -123,14 +151,12 @@
setClipToPadding(false);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
- mShortcutPreviewPadding =
- 2 * getResources().getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
}
private void setContainerWidth() {
mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE);
mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
- mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
+ mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
}
@Override
@@ -153,6 +179,11 @@
return mRemoteViewsPreview;
}
+ /** Returns the app widget host view scale, which is a value between [0f, 1f]. */
+ public float getAppWidgetHostViewScale() {
+ return mAppWidgetHostViewScale;
+ }
+
/**
* Called to clear the view and free attached resources. (e.g., {@link Bitmap}
*/
@@ -167,7 +198,7 @@
mWidgetDims.setText(null);
mWidgetDescription.setText(null);
mWidgetDescription.setVisibility(GONE);
- mPreviewWidth = mPreviewHeight = mPresetPreviewSize;
+ mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
if (mActiveRequest != null) {
mActiveRequest.cancel();
@@ -178,6 +209,7 @@
mWidgetImageContainer.removeView(mAppWidgetHostViewPreview);
}
mAppWidgetHostViewPreview = null;
+ mAppWidgetHostViewScale = 1f;
mItem = null;
}
@@ -249,16 +281,6 @@
@Nullable RemoteViews remoteViews) {
appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo);
- Rect padding;
- DeviceProfile deviceProfile = mActivity.getDeviceProfile();
- if (deviceProfile.shouldInsetWidgets()) {
- padding = new Rect();
- appWidgetHostViewPreview.getWidgetInset(deviceProfile, padding);
- } else {
- padding = deviceProfile.inv.defaultWidgetPadding;
- }
- appWidgetHostViewPreview.setPadding(padding.left, padding.top, padding.right,
- padding.bottom);
appWidgetHostViewPreview.updateAppWidget(remoteViews);
}
@@ -306,12 +328,12 @@
if (getWidth() > 0 && getHeight() > 0) {
// Scale down the preview size if it's wider than the cell.
float maxWidth = getWidth();
- float previewWidth = drawable.getIntrinsicWidth() * mPreviewScale;
+ float previewWidth = drawable.getIntrinsicWidth() * mPreviewContainerScale;
scale = Math.min(maxWidth / previewWidth, 1);
}
setContainerSize(
- Math.round(drawable.getIntrinsicWidth() * scale),
- Math.round(drawable.getIntrinsicHeight() * scale));
+ Math.round(drawable.getIntrinsicWidth() * scale * mPreviewContainerScale),
+ Math.round(drawable.getIntrinsicHeight() * scale * mPreviewContainerScale));
mWidgetImage.setDrawable(drawable);
mWidgetImage.setVisibility(View.VISIBLE);
if (mAppWidgetHostViewPreview != null) {
@@ -330,16 +352,32 @@
private void setContainerSize(int width, int height) {
LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams();
- layoutParams.width = (int) (width * mPreviewScale);
- layoutParams.height = (int) (height * mPreviewScale);
+ layoutParams.width = width;
+ layoutParams.height = height;
mWidgetImageContainer.setLayoutParams(layoutParams);
}
public void ensurePreview() {
if (mAppWidgetHostViewPreview != null) {
- setContainerSize(mPreviewWidth, mPreviewHeight);
+ int containerWidth = (int) (mTargetPreviewWidth * mPreviewContainerScale);
+ int containerHeight = (int) (mTargetPreviewHeight * mPreviewContainerScale);
+ setContainerSize(containerWidth, containerHeight);
+ if (mAppWidgetHostViewPreview.getChildCount() == 1) {
+ View widgetContent = mAppWidgetHostViewPreview.getChildAt(0);
+ ViewGroup.LayoutParams layoutParams = widgetContent.getLayoutParams();
+ // We only scale preview if both the width & height of the outermost view group are
+ // not set to MATCH_PARENT.
+ boolean shouldScale =
+ layoutParams.width != MATCH_PARENT && layoutParams.height != MATCH_PARENT;
+ if (shouldScale) {
+ setNoClip(mWidgetImageContainer);
+ setNoClip(mAppWidgetHostViewPreview);
+ mAppWidgetHostViewScale = computeWidgetPreviewScale();
+ mAppWidgetHostViewPreview.setScaleToFit(mAppWidgetHostViewScale);
+ }
+ }
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
- mPreviewWidth, mPreviewHeight, Gravity.FILL);
+ containerWidth, containerHeight, Gravity.FILL);
mAppWidgetHostViewPreview.setLayoutParams(params);
mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
mWidgetImage.setVisibility(View.GONE);
@@ -351,7 +389,7 @@
}
mActiveRequest = mWidgetPreviewLoader.loadPreview(
BaseActivity.fromContext(getContext()), mItem,
- new Size(mPreviewWidth, mPreviewHeight),
+ new Size(mTargetPreviewWidth, mTargetPreviewHeight),
this::applyPreview);
}
@@ -364,9 +402,9 @@
public Size setPreviewSize(WidgetItem widgetItem, float previewScale) {
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, widgetItem);
- mPreviewWidth = widgetSize.getWidth();
- mPreviewHeight = widgetSize.getHeight();
- mPreviewScale = previewScale;
+ mTargetPreviewWidth = widgetSize.getWidth();
+ mTargetPreviewHeight = widgetSize.getHeight();
+ mPreviewContainerScale = previewScale;
return widgetSize;
}
@@ -429,4 +467,35 @@
super.onInitializeAccessibilityNodeInfo(info);
info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
}
+
+ private static void setNoClip(ViewGroup view) {
+ view.setClipChildren(false);
+ view.setClipToPadding(false);
+ }
+
+ private float computeWidgetPreviewScale() {
+ if (mAppWidgetHostViewPreview.getChildCount() != 1) {
+ return 1f;
+ }
+
+ // Measure the largest possible width & height that the app widget wants to display.
+ mAppWidgetHostViewPreview.measure(
+ makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED),
+ makeMeasureSpec(MAX_MEASURE_SPEC_DIMENSION, MeasureSpec.UNSPECIFIED));
+ int appWidgetContentWidth = mAppWidgetHostViewPreview.getChildAt(0).getMeasuredWidth();
+ int appWidgetContentHeight = mAppWidgetHostViewPreview.getChildAt(0).getMeasuredHeight();
+ if (appWidgetContentWidth == 0 || appWidgetContentHeight == 0) {
+ return 1f;
+ }
+
+ int horizontalPadding = mAppWidgetHostViewPreview.getPaddingStart()
+ + mAppWidgetHostViewPreview.getPaddingEnd();
+ int verticalPadding = mAppWidgetHostViewPreview.getPaddingTop()
+ + mAppWidgetHostViewPreview.getPaddingBottom();
+ return Math.min(
+ (mTargetPreviewWidth - horizontalPadding) * mPreviewContainerScale
+ / appWidgetContentWidth,
+ (mTargetPreviewHeight - verticalPadding) * mPreviewContainerScale
+ / appWidgetContentHeight);
+ }
}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index dedcc65..406de10 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -70,9 +70,11 @@
private static final int DEFAULT_CLOSE_DURATION = 200;
private static final long EDUCATION_TIP_DELAY_MS = 300;
+ private final int mWidgetSheetContentHorizontalPadding;
+
private ItemInfo mOriginalItemInfo;
private final int mMaxTableHeight;
- private int mMaxHorizontalSpan = 4;
+ private int mMaxHorizontalSpan = DEFAULT_MAX_HORIZONTAL_SPANS;
private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
new OnLayoutChangeListener() {
@@ -117,6 +119,9 @@
if (!hasSeenEducationTip()) {
addOnLayoutChangeListener(mLayoutChangeListenerToShowTips);
}
+
+ mWidgetSheetContentHorizontalPadding = getResources().getDimensionPixelSize(
+ R.dimen.widget_list_horizontal_margin);
}
@Override
@@ -137,10 +142,8 @@
private boolean updateMaxSpansPerRow() {
if (getMeasuredWidth() == 0) return false;
- int paddingPx = 2 * getResources().getDimensionPixelOffset(
- R.dimen.widget_cell_horizontal_padding);
- int maxHorizontalSpan = findViewById(R.id.widgets_table).getMeasuredWidth()
- / (mActivityContext.getDeviceProfile().cellWidthPx + paddingPx);
+ int maxHorizontalSpan = computeMaxHorizontalSpans(mContent,
+ mWidgetSheetContentHorizontalPadding);
if (mMaxHorizontalSpan != maxHorizontalSpan) {
// Ensure the table layout is showing widgets in the right column after measure.
mMaxHorizontalSpan = maxHorizontalSpan;
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 6c2cca6..5e1a534 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -25,6 +25,7 @@
import android.content.Context;
import android.content.pm.LauncherApps;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Process;
import android.os.UserHandle;
@@ -148,13 +149,13 @@
private final int mTabsHeight;
private final int mViewPagerTopPadding;
private final int mSearchAndRecommendationContainerBottomMargin;
- private final int mWidgetCellHorizontalPadding;
+ private final int mWidgetSheetContentHorizontalPadding;
@Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
@Nullable private PersonalWorkPagedView mViewPager;
private boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
- private int mMaxSpansPerRow = 4;
+ private int mMaxSpansPerRow = DEFAULT_MAX_HORIZONTAL_SPANS;
private View mTabsView;
private TextView mNoWidgetsView;
private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder;
@@ -166,19 +167,20 @@
mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH));
+
+ Resources resources = getResources();
mTabsHeight = mHasWorkProfile
- ? getContext().getResources()
- .getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
+ ? resources.getDimensionPixelSize(R.dimen.all_apps_header_pill_height)
: 0;
mViewPagerTopPadding = mHasWorkProfile
? getContext().getResources()
.getDimensionPixelSize(R.dimen.widget_picker_view_pager_top_padding)
: 0;
- mSearchAndRecommendationContainerBottomMargin = getContext().getResources()
- .getDimensionPixelSize(mHasWorkProfile
+ mSearchAndRecommendationContainerBottomMargin = resources.getDimensionPixelSize(
+ mHasWorkProfile
? R.dimen.search_and_recommended_widgets_container_small_bottom_margin
: R.dimen.search_and_recommended_widgets_container_bottom_margin);
- mWidgetCellHorizontalPadding = 2 * getResources().getDimensionPixelOffset(
+ mWidgetSheetContentHorizontalPadding = 2 * resources.getDimensionPixelSize(
R.dimen.widget_cell_horizontal_padding);
}
@@ -375,11 +377,10 @@
private boolean updateMaxSpansPerRow() {
if (getMeasuredWidth() == 0) return false;
- int previousMaxSpansPerRow = mMaxSpansPerRow;
- mMaxSpansPerRow = getMeasuredWidth()
- / (mActivityContext.getDeviceProfile().cellWidthPx + mWidgetCellHorizontalPadding);
-
- if (previousMaxSpansPerRow != mMaxSpansPerRow) {
+ int maxHorizontalSpans = computeMaxHorizontalSpans(mContent,
+ mWidgetSheetContentHorizontalPadding);
+ if (mMaxSpansPerRow != maxHorizontalSpans) {
+ mMaxSpansPerRow = maxHorizontalSpans;
mAdapters.get(AdapterHolder.PRIMARY).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
mMaxSpansPerRow);
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setMaxHorizontalSpansPerRow(
diff --git a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
index 4407fe1..cc90e6c 100644
--- a/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/src_ui_overrides/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -17,6 +17,7 @@
package com.android.launcher3.uioverrides;
import android.app.Person;
+import android.content.Context;
import android.content.pm.ShortcutInfo;
import android.view.Display;
@@ -36,4 +37,11 @@
public static boolean isInternalDisplay(Display display) {
return display.getDisplayId() == Display.DEFAULT_DISPLAY;
}
+
+ /**
+ * Returns the minimum space that should be left empty at the end of hotseat
+ */
+ public static int getHotseatEndOffset(Context context) {
+ return 0;
+ }
}
diff --git a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
index 33066e4..032a7b4 100644
--- a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
@@ -25,8 +25,8 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
-import com.android.launcher3.Workspace;
import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import org.junit.After;
import org.junit.Test;
@@ -75,7 +75,7 @@
@Test
public void testPromiseIcon_addedFromEligibleSession() throws Throwable {
final String appLabel = "Test Promise App " + UUID.randomUUID().toString();
- final Workspace.ItemOperator findPromiseApp = (info, view) ->
+ final ItemOperator findPromiseApp = (info, view) ->
info != null && TextUtils.equals(info.title, appLabel);
// Create and add test session
@@ -97,7 +97,7 @@
@Test
public void testPromiseIcon_notAddedFromIneligibleSession() throws Throwable {
final String appLabel = "Test Promise App " + UUID.randomUUID().toString();
- final Workspace.ItemOperator findPromiseApp = (info, view) ->
+ final ItemOperator findPromiseApp = (info, view) ->
info != null && TextUtils.equals(info.title, appLabel);
// Create and add test session without icon or label
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 4dd44f4..4beb617 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -80,8 +80,12 @@
assertTrue(message, failed);
}
+ private int pagesPerScreen() {
+ return mLauncher.isTwoPanels() ? 2 : 1;
+ }
+
private boolean isWorkspaceScrollable(Launcher launcher) {
- return launcher.getWorkspace().getPageCount() > 1;
+ return launcher.getWorkspace().getPageCount() > pagesPerScreen();
}
private int getCurrentWorkspacePage(Launcher launcher) {
@@ -195,8 +199,9 @@
workspace.ensureWorkspaceIsScrollable();
executeOnLauncher(
- launcher -> assertEquals("Ensuring workspace scrollable didn't switch to page #1",
- 1, getCurrentWorkspacePage(launcher)));
+ launcher -> assertEquals(
+ "Ensuring workspace scrollable didn't switch to next screen",
+ pagesPerScreen(), getCurrentWorkspacePage(launcher)));
executeOnLauncher(
launcher -> assertTrue("ensureScrollable didn't make workspace scrollable",
isWorkspaceScrollable(launcher)));
@@ -212,8 +217,8 @@
workspace.flingForward();
executeOnLauncher(
- launcher -> assertEquals("Flinging forward didn't switch workspace to page #1",
- 1, getCurrentWorkspacePage(launcher)));
+ launcher -> assertEquals("Flinging forward didn't switch workspace to next screen",
+ pagesPerScreen(), getCurrentWorkspacePage(launcher)));
assertTrue("Launcher internal state is not Home", isInState(() -> LauncherState.NORMAL));
// Test starting a workspace app.
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
index 4978c01..5ea5d65 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
@@ -28,13 +28,13 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.launcher3.Workspace;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.testcomponent.WidgetConfigActivity;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.Wait.Condition;
import com.android.launcher3.util.rule.ShellCommandRule;
@@ -121,7 +121,7 @@
/**
* Condition for searching widget id
*/
- private class WidgetSearchCondition implements Condition, Workspace.ItemOperator {
+ private class WidgetSearchCondition implements Condition, ItemOperator {
@Override
public boolean isTrue() throws Throwable {
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
index 745dc22..641e53a 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
@@ -30,7 +30,6 @@
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.LauncherSettings.Favorites;
-import com.android.launcher3.Workspace.ItemOperator;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
@@ -40,6 +39,7 @@
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
import com.android.launcher3.testcomponent.RequestPinItemActivity;
import com.android.launcher3.ui.AbstractLauncherUiTest;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.Wait.Condition;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
diff --git a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
index 6a6ec3e..2093682 100644
--- a/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
+++ b/tests/src/com/android/launcher3/util/rule/LauncherActivityRule.java
@@ -18,7 +18,7 @@
import android.app.Activity;
import com.android.launcher3.Launcher;
-import com.android.launcher3.Workspace.ItemOperator;
+import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java
index 1cb6b2d..78301e4 100644
--- a/tests/tapl/com/android/launcher3/tapl/AllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java
@@ -135,6 +135,7 @@
.collect(Collectors.toList()),
mLauncher.getVisibleBounds(searchBox).bottom
- mLauncher.getVisibleBounds(allAppsContainer).top);
+ verifyActiveContainer();
final int newScroll = getAllAppsScroll();
mLauncher.assertTrue(
"Scrolled in a wrong direction in AllApps: from " + scroll + " to "
@@ -144,7 +145,6 @@
mLauncher.assertTrue(
"Exceeded max scroll attempts: " + MAX_SCROLL_ATTEMPTS,
++attempts <= MAX_SCROLL_ATTEMPTS);
- verifyActiveContainer();
scroll = newScroll;
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java
index 4b1610e..b290bb1 100644
--- a/tests/tapl/com/android/launcher3/tapl/Background.java
+++ b/tests/tapl/com/android/launcher3/tapl/Background.java
@@ -68,6 +68,10 @@
}
protected boolean zeroButtonToOverviewGestureStartsInLauncher() {
+ return mLauncher.isTablet();
+ }
+
+ protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
return false;
}
@@ -90,21 +94,32 @@
mLauncher.sendPointer(
downTime, downTime, MotionEvent.ACTION_DOWN, start, gestureScope);
- mLauncher.executeAndWaitForLauncherEvent(
- () -> mLauncher.movePointer(
- downTime,
- downTime,
- ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
- start,
- end,
- gestureScope),
- event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(event.getClassName()),
- () -> "Pause wasn't detected", "swiping and holding");
- mLauncher.runToState(
- () -> mLauncher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
- gestureScope),
- OVERVIEW_STATE_ORDINAL, "sending UP event");
+ Runnable swipeAndHold = () -> mLauncher.movePointer(
+ downTime,
+ downTime,
+ ZERO_BUTTON_SWIPE_UP_GESTURE_DURATION,
+ start,
+ end,
+ gestureScope);
+ String swipeAndHoldAction = "swiping and holding";
+ Runnable up = () -> mLauncher.sendPointer(
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, end,
+ gestureScope);
+ String upAction = "sending UP event";
+ if (zeroButtonToOverviewGestureStateTransitionWhileHolding()) {
+ mLauncher.runToState(swipeAndHold, OVERVIEW_STATE_ORDINAL, swipeAndHoldAction);
+ try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(upAction)) {
+ up.run();
+ }
+ } else {
+ mLauncher.executeAndWaitForLauncherEvent(
+ swipeAndHold,
+ event -> TestProtocol.PAUSE_DETECTED_MESSAGE.equals(
+ event.getClassName()),
+ () -> "Pause wasn't detected",
+ swipeAndHoldAction);
+ mLauncher.runToState(up, OVERVIEW_STATE_ORDINAL, upAction);
+ }
break;
}
@@ -134,9 +149,15 @@
}
case THREE_BUTTON:
+ if (mLauncher.isTablet()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_UP);
+ }
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.runToState(
- () -> mLauncher.waitForSystemUiObject("recent_apps").click(),
+ () -> mLauncher.waitForNavigationUiObject("recent_apps").click(),
OVERVIEW_STATE_ORDINAL, "clicking Recents button");
break;
}
@@ -224,11 +245,23 @@
case THREE_BUTTON:
// Double press the recents button.
- UiObject2 recentsButton = mLauncher.waitForSystemUiObject("recent_apps");
+ UiObject2 recentsButton = mLauncher.waitForNavigationUiObject("recent_apps");
+ if (mLauncher.isTablet()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_UP);
+ }
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.runToState(() -> recentsButton.click(), OVERVIEW_STATE_ORDINAL,
"clicking Recents button for the first time");
mLauncher.getOverview();
+ if (mLauncher.isTablet()) {
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_DOWN);
+ mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN,
+ LauncherInstrumentation.EVENT_TOUCH_UP);
+ }
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, SQUARE_BUTTON_EVENT);
mLauncher.executeAndWaitForEvent(
() -> recentsButton.click(),
diff --git a/tests/tapl/com/android/launcher3/tapl/Home.java b/tests/tapl/com/android/launcher3/tapl/Home.java
index 0060844..ee9dd1a 100644
--- a/tests/tapl/com/android/launcher3/tapl/Home.java
+++ b/tests/tapl/com/android/launcher3/tapl/Home.java
@@ -63,4 +63,8 @@
return true;
}
+ @Override
+ protected boolean zeroButtonToOverviewGestureStateTransitionWhileHolding() {
+ return true;
+ }
}
\ No newline at end of file
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index 19094f8..1f64131 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -97,8 +97,8 @@
private static final int ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME = 20;
private static final int GESTURE_STEP_MS = 16;
- private static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
- private static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
+ static final Pattern EVENT_TOUCH_DOWN = getTouchEventPattern("ACTION_DOWN");
+ static final Pattern EVENT_TOUCH_UP = getTouchEventPattern("ACTION_UP");
private static final Pattern EVENT_TOUCH_CANCEL = getTouchEventPattern("ACTION_CANCEL");
private static final Pattern EVENT_PILFER_POINTERS = Pattern.compile("pilferPointers");
static final Pattern EVENT_START = Pattern.compile("start:");
@@ -207,6 +207,11 @@
public LauncherInstrumentation(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
mDevice = UiDevice.getInstance(instrumentation);
+ try {
+ mDevice.executeShellCommand("am wait-for-broadcast-idle");
+ } catch (IOException e) {
+ log("Failed to wait for broadcast idle");
+ }
// Launcher should run in test harness so that custom accessibility protocol between
// Launcher and TAPL is enabled. In-process tests enable this protocol with a direct call
@@ -283,6 +288,16 @@
.getParcelable(TestProtocol.TEST_INFO_RESPONSE_FIELD);
}
+ public boolean isTablet() {
+ return getTestInfo(TestProtocol.REQUEST_IS_TABLET)
+ .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
+ public boolean isTwoPanels() {
+ return getTestInfo(TestProtocol.REQUEST_IS_TWO_PANELS)
+ .getBoolean(TestProtocol.TEST_INFO_RESPONSE_FIELD);
+ }
+
void setActiveContainer(VisibleContainer container) {
sActiveContainer = new WeakReference<>(container);
}
@@ -499,9 +514,8 @@
void fail(String message) {
checkForAnomaly();
Assert.fail(formatSystemHealthMessage(formatErrorWithEvents(
- "http://go/tapl test failure:\nContext: " + getContextDescription()
- + " - visible state is " + getVisibleStateMessage()
- + ";\nDetails: " + message, true)));
+ "http://go/tapl test failure: " + message + ";\nContext: " + getContextDescription()
+ + "; now visible state is " + getVisibleStateMessage(), true)));
}
private String getContextDescription() {
@@ -554,29 +568,35 @@
public String getNavigationModeMismatchError(boolean waitForCorrectState) {
final int waitTime = waitForCorrectState ? WAIT_TIME_MS : 0;
final NavigationModel navigationModel = getNavigationModel();
-
+ String resPackage = getNavigationButtonResPackage();
if (navigationModel == NavigationModel.THREE_BUTTON) {
- if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ if (!mDevice.wait(Until.hasObject(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button not present in 3-button mode";
}
} else {
- if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "recent_apps")), waitTime)) {
+ if (!mDevice.wait(Until.gone(By.res(resPackage, "recent_apps")), waitTime)) {
return "Recents button is present in non-3-button mode";
}
}
if (navigationModel == NavigationModel.ZERO_BUTTON) {
- if (!mDevice.wait(Until.gone(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ if (!mDevice.wait(Until.gone(By.res(resPackage, "home")), waitTime)) {
return "Home button is present in gestural mode";
}
} else {
- if (!mDevice.wait(Until.hasObject(By.res(SYSTEMUI_PACKAGE, "home")), waitTime)) {
+ if (!mDevice.wait(Until.hasObject(By.res(resPackage, "home")), waitTime)) {
return "Home button not present in non-gestural mode";
}
}
return null;
}
+ private String getNavigationButtonResPackage() {
+ return isTablet() && getNavigationModel() == NavigationModel.THREE_BUTTON ?
+ getLauncherPackageName() :
+ SYSTEMUI_PACKAGE;
+ }
+
private UiObject2 verifyContainerType(ContainerType containerType) {
waitForLauncherInitialized();
@@ -688,7 +708,8 @@
* @return the Workspace object.
*/
public Workspace pressHome() {
- try (LauncherInstrumentation.Closable e = eventsCheck()) {
+ try (LauncherInstrumentation.Closable e = eventsCheck();
+ LauncherInstrumentation.Closable c = addContextLayer("want to switch to home")) {
waitForLauncherInitialized();
// Click home, then wait for any accessibility event, then wait until accessibility
// events stop.
@@ -708,7 +729,7 @@
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME,
false, GestureScope.INSIDE_TO_OUTSIDE);
- try (LauncherInstrumentation.Closable c = addContextLayer(
+ try (LauncherInstrumentation.Closable c1 = addContextLayer(
"Swiped up from context menu to home")) {
waitUntilLauncherObjectGone(CONTEXT_MENU_RES_ID);
// Swiping up can temporarily bring Nexus Launcher if the current
@@ -728,7 +749,7 @@
displaySize.x / 2, displaySize.y - 1,
displaySize.x / 2, 0,
ZERO_BUTTON_STEPS_FROM_BACKGROUND_TO_HOME, NORMAL_STATE_ORDINAL,
- launcherWasVisible
+ launcherWasVisible || isTablet()
? GestureScope.INSIDE_TO_OUTSIDE
: GestureScope.OUTSIDE_WITH_PILFER);
}
@@ -740,16 +761,20 @@
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_DOWN_TIS);
expectEvent(TestProtocol.SEQUENCE_TIS, EVENT_TOUCH_UP_TIS);
}
+ if (isTablet()) {
+ expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_DOWN);
+ expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_TOUCH_UP);
+ }
runToState(
- waitForSystemUiObject("home")::click,
+ waitForNavigationUiObject("home")::click,
NORMAL_STATE_ORDINAL,
!hasLauncherObject(WORKSPACE_RES_ID)
&& (hasLauncherObject(APPS_RES_ID)
|| hasLauncherObject(OVERVIEW_RES_ID)),
action);
}
- try (LauncherInstrumentation.Closable c = addContextLayer(
+ try (LauncherInstrumentation.Closable c1 = addContextLayer(
"performed action to switch to Home - " + action)) {
return getWorkspace();
}
@@ -892,6 +917,15 @@
return object;
}
+ @NonNull
+ UiObject2 waitForNavigationUiObject(String resId) {
+ String resPackage = getNavigationButtonResPackage();
+ final UiObject2 object = mDevice.wait(
+ Until.findObject(By.res(resPackage, resId)), WAIT_TIME_MS);
+ assertNotNull("Can't find a navigation UI object with id: " + resId, object);
+ return object;
+ }
+
@Nullable
UiObject2 findObjectInContainer(UiObject2 container, BySelector selector) {
try {
@@ -930,7 +964,7 @@
void waitForObjectEnabled(UiObject2 object, String waitReason) {
try {
assertTrue("Timed out waiting for object to be enabled for " + waitReason + " "
- + object.getResourceName(),
+ + object.getResourceName(),
object.wait(Until.enabled(true), WAIT_TIME_MS));
} catch (StaleObjectException e) {
fail("The object disappeared from screen");
@@ -1049,7 +1083,7 @@
() -> "Failed to receive an event for the state change: expected ["
+ TestProtocol.stateOrdinalToString(expectedState)
+ "], actual: " + eventListToString(actualEvents),
- actionName);
+ actionName);
}
private boolean isSwitchToStateEvent(
@@ -1176,7 +1210,7 @@
event -> TestProtocol.SCROLL_FINISHED_MESSAGE.equals(event.getClassName()),
() -> "Didn't receive a scroll end message: " + startX + ", " + startY
+ ", " + endX + ", " + endY,
- "scrolling");
+ "scrolling");
}
// Inject a swipe gesture. Inject exactly 'steps' motion points, incrementing event time by a
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 657b74d..71c0abb 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -59,9 +59,14 @@
final Rect taskBounds = mLauncher.getVisibleBounds(mTask);
final int centerX = taskBounds.centerX();
final int centerY = taskBounds.centerY();
- mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
- LauncherInstrumentation.GestureScope.INSIDE);
- mLauncher.waitForIdle();
+ mLauncher.executeAndWaitForLauncherEvent(
+ () -> mLauncher.linearGesture(centerX, centerY, centerX, 0, 10, false,
+ LauncherInstrumentation.GestureScope.INSIDE),
+ event -> TestProtocol.DISMISS_ANIMATION_ENDS_MESSAGE.equals(
+ event.getClassName()),
+ () -> "Didn't receive a dismiss animation ends message: " + centerX + ", "
+ + centerY,
+ "swiping to dismiss");
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 43134d9..2acf7b4 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -63,7 +63,7 @@
/**
* Swipes up to All Apps.
*
- * @return the App Apps object.
+ * @return the All Apps object.
*/
@NonNull
public AllApps switchToAllApps() {
@@ -163,7 +163,7 @@
}
private boolean isWorkspaceScrollable(UiObject2 workspace) {
- return workspace.getChildCount() > 1;
+ return workspace.getChildCount() > (mLauncher.isTwoPanels() ? 2 : 1);
}
@NonNull