From fb16517fea11051e37edbcd999b84c3a41b9eed7 Mon Sep 17 00:00:00 2001 From: Fengjiang Li Date: Mon, 4 Mar 2024 15:36:08 -0800 Subject: [PATCH] [2/n] Avoid flicker to drop a widget that needs a config activity 1. Don't show background in PendingAppWidgetHostView if we are showing preview bitmap 2. Extract bitmap from DropView instead of LauncherAppHostView 3. Letterbox bitmap into PendingAppWidgetHostView's canvas if canvas has different aspect ratio Fix: 284236964 Flag: aconfig launcher.enable_add_app_widget_via_config_activity_v2 DISABLED Test: manual Change-Id: I76de215186b96ffe65c909b28155fb19ac90d4f1 --- src/com/android/launcher3/Launcher.java | 9 +- src/com/android/launcher3/RectUtils.kt | 60 ++++++++ .../widget/PendingAppWidgetHostView.java | 37 +++-- .../com/android/launcher3/RectUtilsTest.kt | 138 ++++++++++++++++++ 4 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 src/com/android/launcher3/RectUtils.kt create mode 100644 tests/src/com/android/launcher3/RectUtilsTest.kt diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index c1ebbe58a2..7267e637f4 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -1483,11 +1483,10 @@ public class Launcher extends StatefulActivity if (showPendingWidget) { launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY; PendingAppWidgetHostView pendingAppWidgetHostView = new PendingAppWidgetHostView( - this, mAppWidgetHolder, launcherInfo, appWidgetInfo); - pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap); + this, mAppWidgetHolder, launcherInfo, appWidgetInfo, widgetPreviewBitmap); hostView = pendingAppWidgetHostView; } else if (hostView instanceof PendingAppWidgetHostView) { - ((PendingAppWidgetHostView) hostView).setPreviewBitmap(null); + ((PendingAppWidgetHostView) hostView).setPreviewBitmapAndUpdateBackground(null); // User has selected a widget config and exited the config activity, we can trigger // re-inflation of PendingAppWidgetHostView to replace it with // LauncherAppWidgetHostView in workspace. @@ -1822,7 +1821,9 @@ public class Launcher extends StatefulActivity if (isActivityStarted) { DragView dropView = getDragLayer().clearAnimatedView(); if (dropView != null && dropView.containsAppWidgetHostView()) { - widgetPreviewBitmap = getBitmapFromView(dropView.getContentView()); + // Extracting Bitmap from dropView instead of its content view produces the correct + // bitmap. + widgetPreviewBitmap = getBitmapFromView(dropView); } } diff --git a/src/com/android/launcher3/RectUtils.kt b/src/com/android/launcher3/RectUtils.kt new file mode 100644 index 0000000000..68d2eaf9ab --- /dev/null +++ b/src/com/android/launcher3/RectUtils.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2024 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.graphics.Rect + +/** + * Fit [this] into [targetRect] with letter boxing. After calling this method, [this] will be + * modified to be letter boxed. + * + * @param targetRect target [Rect] that [this] should be fitted into + */ +fun Rect.letterBox(targetRect: Rect) { + letterBox(targetRect, this) +} + +/** + * Fit [this] into [targetRect] with letter boxing. After calling this method, [resultRect] will be + * modified to be letter boxed. + * + * @param targetRect target [Rect] that [this] should be fitted into + * @param resultRect the letter boxed [Rect] + */ +fun Rect.letterBox(targetRect: Rect, resultRect: Rect) { + val widthRatio: Float = 1f * targetRect.width() / width() + val heightRatio: Float = 1f * targetRect.height() / height() + if (widthRatio < heightRatio) { + val scaledHeight: Int = (widthRatio * height()).toInt() + val verticalPadding: Int = (targetRect.height() - scaledHeight) / 2 + resultRect.set( + targetRect.left, + targetRect.top + verticalPadding, + targetRect.right, + targetRect.bottom - verticalPadding + ) + } else { + val scaledWidth: Int = (heightRatio * width()).toInt() + val horizontalPadding: Int = (targetRect.width() - scaledWidth) / 2 + resultRect.set( + targetRect.left + horizontalPadding, + targetRect.top, + targetRect.right - horizontalPadding, + targetRect.bottom + ) + } +} diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java index 86400baaf2..7f9a1fc4c2 100644 --- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java @@ -53,6 +53,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.RectUtilsKt; import com.android.launcher3.icons.FastBitmapDrawable; import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver; import com.android.launcher3.model.data.ItemInfoWithIcon; @@ -76,6 +77,10 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView private final Rect mRect = new Rect(); + private final Rect mPreviewBitmapRect = new Rect(); + private final Rect mCanvasRect = new Rect(); + private final Rect mLetterBoxedPreviewBitmapRect = new Rect(); + private final LauncherWidgetHolder mWidgetHolder; private final LauncherAppWidgetProviderInfo mAppwidget; private final LauncherAppWidgetInfo mInfo; @@ -103,9 +108,14 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) { - this(context, widgetHolder, info, appWidget, - context.getResources().getText(R.string.gadget_complete_setup_text)); + this(context, widgetHolder, info, appWidget, null); + } + public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, + LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget, + @Nullable Bitmap previewBitmap) { + this(context, widgetHolder, info, appWidget, + context.getResources().getText(R.string.gadget_complete_setup_text), previewBitmap); super.updateAppWidget(null); setOnClickListener(mActivityContext.getItemOnClickListener()); @@ -123,7 +133,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView Context context, LauncherWidgetHolder widgetHolder, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) { this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider), - appWidget, appWidget.label); + appWidget, appWidget.label, null); getBackground().mutate().setAlpha(DEFERRED_ALPHA); mCenterDrawable = new ColorDrawable(Color.TRANSPARENT); @@ -132,8 +142,12 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView mIsDeferredWidget = true; } - /** Set {@link Bitmap} of widget preview. */ - public void setPreviewBitmap(@Nullable Bitmap previewBitmap) { + /** + * Set {@link Bitmap} of widget preview and update background drawable. When showing preview + * bitmap, we shouldn't draw background. + */ + public void setPreviewBitmapAndUpdateBackground(@Nullable Bitmap previewBitmap) { + setBackgroundResource(previewBitmap != null ? 0 : R.drawable.pending_widget_bg); if (this.mPreviewBitmap == previewBitmap) { return; } @@ -143,7 +157,8 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView private PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info, - LauncherAppWidgetProviderInfo appwidget, CharSequence label) { + LauncherAppWidgetProviderInfo appwidget, CharSequence label, + @Nullable Bitmap previewBitmap) { super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme)); mWidgetHolder = widgetHolder; mAppwidget = appwidget; @@ -161,7 +176,7 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG); setWillNotDraw(false); - setBackgroundResource(R.drawable.pending_widget_bg); + setPreviewBitmapAndUpdateBackground(previewBitmap); } @Override @@ -440,7 +455,12 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView protected void onDraw(Canvas canvas) { if (mPreviewBitmap != null && (mInfo.restoreStatus & LauncherAppWidgetInfo.FLAG_UI_NOT_READY) != 0) { - canvas.drawBitmap(mPreviewBitmap, 0, 0, mPreviewPaint); + mPreviewBitmapRect.set(0, 0, mPreviewBitmap.getWidth(), mPreviewBitmap.getHeight()); + mCanvasRect.set(0, 0, getWidth(), getHeight()); + + RectUtilsKt.letterBox(mPreviewBitmapRect, mCanvasRect, mLetterBoxedPreviewBitmapRect); + canvas.drawBitmap(mPreviewBitmap, mPreviewBitmapRect, mLetterBoxedPreviewBitmapRect, + mPreviewPaint); return; } if (mCenterDrawable == null) { @@ -463,7 +483,6 @@ public class PendingAppWidgetHostView extends LauncherAppWidgetHostView mSetupTextLayout.draw(canvas); canvas.restore(); } - } /** diff --git a/tests/src/com/android/launcher3/RectUtilsTest.kt b/tests/src/com/android/launcher3/RectUtilsTest.kt new file mode 100644 index 0000000000..f0d22eb9d4 --- /dev/null +++ b/tests/src/com/android/launcher3/RectUtilsTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2024 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.graphics.Rect +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class RectUtilsTest { + + private val srcRect = Rect() + private val destRect = Rect() + private val letterBoxedRect = Rect() + + @Test + fun letterBoxSelf_toSameRect_noScale() { + srcRect.set(0, 0, 100, 100) + destRect.set(0, 0, 100, 100) + + srcRect.letterBox(destRect) + + assertThat(srcRect).isEqualTo(Rect(0, 0, 100, 100)) + } + + @Test + fun letterBox_toSameRect_noScale() { + srcRect.set(0, 0, 100, 100) + destRect.set(0, 0, 100, 100) + + srcRect.letterBox(destRect, letterBoxedRect) + + assertThat(letterBoxedRect).isEqualTo(Rect(0, 0, 100, 100)) + assertThat(srcRect).isEqualTo(Rect(0, 0, 100, 100)) + } + + @Test + fun letterBoxSelf_toSmallHeight_scaleDownHorizontally() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(0, 0, 939, 520) + + srcRect.letterBox(destRect) + + assertThat(srcRect).isEqualTo(Rect(114, 0, 825, 520)) + } + + @Test + fun letterBoxRect_toSmallHeight_scaleDownHorizontally() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(0, 0, 939, 520) + + srcRect.letterBox(destRect, letterBoxedRect) + + assertThat(letterBoxedRect).isEqualTo(Rect(114, 0, 825, 520)) + assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114)) + } + + @Test + fun letterBoxSelf_toSmallHeightWithOffset_scaleDownHorizontally() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(10, 20, 949, 540) + + srcRect.letterBox(destRect) + + assertThat(srcRect).isEqualTo(Rect(124, 20, 835, 540)) + } + + @Test + fun letterBoxRect_toSmallHeightWithOffset_scaleDownHorizontally() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(10, 20, 949, 540) + + srcRect.letterBox(destRect, letterBoxedRect) + + assertThat(letterBoxedRect).isEqualTo(Rect(124, 20, 835, 540)) + assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114)) + } + + @Test + fun letterBoxSelf_toSmallWidth_scaleDownVertically() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(0, 0, 520, 939) + + srcRect.letterBox(destRect) + + assertThat(srcRect).isEqualTo(Rect(0, 280, 520, 659)) + } + + @Test + fun letterBoxRect_toSmallWidth_scaleDownVertically() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(0, 0, 520, 939) + + srcRect.letterBox(destRect, letterBoxedRect) + + assertThat(letterBoxedRect).isEqualTo(Rect(0, 280, 520, 659)) + assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114)) + } + + @Test + fun letterBoxSelf_toSmallWidthWithOffset_scaleDownVertically() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(40, 60, 560, 999) + + srcRect.letterBox(destRect) + + assertThat(srcRect).isEqualTo(Rect(40, 340, 560, 719)) + } + + @Test + fun letterBoxRect_toSmallWidthWithOffset_scaleDownVertically() { + srcRect.set(0, 0, 2893, 2114) + destRect.set(40, 60, 560, 999) + + srcRect.letterBox(destRect, letterBoxedRect) + + assertThat(letterBoxedRect).isEqualTo(Rect(40, 340, 560, 719)) + assertThat(srcRect).isEqualTo(Rect(0, 0, 2893, 2114)) + } +}