From 7a183e40516abf58288b26ba79325695a4ee5ded Mon Sep 17 00:00:00 2001 From: Shamali P Date: Tue, 28 May 2024 13:14:15 +0000 Subject: [PATCH] Adjust previews in a row to align with each other and remove whitespace. - Introduces a WidgetTableRow that identifies preview height for all of its children while keeping them aligned with minimum whitespace - The fullSheet's move / change animations can conflict with the resize so, we wait for them before performing resize of the row. Bug: 335715046 Test: Manual Flag: NONE BUGFIX Change-Id: Id843430c7adfc228c219ba54d504baddba792df0 --- .../launcher3/dragndrop/AddItemActivity.java | 11 ++- .../android/launcher3/widget/WidgetCell.java | 86 ++++++++++++++++-- .../launcher3/widget/WidgetTableRow.java | 90 +++++++++++++++++++ .../launcher3/widget/WidgetsBottomSheet.java | 7 +- .../WidgetsListTableViewHolderBinder.java | 29 +++--- .../WidgetsRecommendationTableLayout.java | 9 +- 6 files changed, 208 insertions(+), 24 deletions(-) create mode 100644 src/com/android/launcher3/widget/WidgetTableRow.java diff --git a/src/com/android/launcher3/dragndrop/AddItemActivity.java b/src/com/android/launcher3/dragndrop/AddItemActivity.java index ec0a222a8a..85eb39bab8 100644 --- a/src/com/android/launcher3/dragndrop/AddItemActivity.java +++ b/src/com/android/launcher3/dragndrop/AddItemActivity.java @@ -91,7 +91,8 @@ import java.util.function.Supplier; */ @TargetApi(Build.VERSION_CODES.O) public class AddItemActivity extends BaseActivity - implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener { + implements OnLongClickListener, OnTouchListener, AbstractSlideInView.OnCloseListener, + WidgetCell.PreviewReadyListener { private static final int SHADOW_SIZE = 10; @@ -142,6 +143,7 @@ public class AddItemActivity extends BaseActivity mDragLayer = findViewById(R.id.add_item_drag_layer); mDragLayer.recreateControllers(); mWidgetCell = findViewById(R.id.widget_cell); + mWidgetCell.addPreviewReadyListener(this); mAccessibilityManager = getApplicationContext().getSystemService(AccessibilityManager.class); @@ -454,4 +456,11 @@ public class AddItemActivity extends BaseActivity .withItemInfo((ItemInfo) mWidgetCell.getWidgetView().getTag()) .log(command); } + + @Override + public void onPreviewAvailable() { + // Set the preview height based on "the only" widget's preview. + mWidgetCell.setParentAlignedPreviewHeight(mWidgetCell.getPreviewContentHeight()); + mWidgetCell.post(mWidgetCell::requestLayout); + } } diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java index eac2ce716a..2bb485aafa 100644 --- a/src/com/android/launcher3/widget/WidgetCell.java +++ b/src/com/android/launcher3/widget/WidgetCell.java @@ -103,6 +103,8 @@ public class WidgetCell extends LinearLayout { private Size mWidgetSize; private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader; + @Nullable + private PreviewReadyListener mPreviewReadyListener = null; protected CancellableTask mActiveRequest; private boolean mAnimatePreview = true; @@ -118,7 +120,8 @@ public class WidgetCell extends LinearLayout { private CancellableTask mIconLoadRequest; private boolean mIsShowingAddButton = false; - + // Height enforced by the parent to align all widget cells displayed by it. + private int mParentAlignedPreviewHeight; public WidgetCell(Context context) { this(context, null); } @@ -190,6 +193,8 @@ public class WidgetCell extends LinearLayout { mWidgetDims.setText(null); mWidgetDescription.setText(null); mWidgetDescription.setVisibility(GONE); + mPreviewReadyListener = null; + mParentAlignedPreviewHeight = 0; showDescription(true); showDimensions(true); @@ -338,8 +343,8 @@ public class WidgetCell extends LinearLayout { private void updateAppWidgetHostScale(NavigableAppWidgetHostView view) { // Scale the content such that all of the content is visible - int contentWidth = view.getWidth(); - int contentHeight = view.getHeight(); + float contentWidth = view.getWidth(); + float contentHeight = view.getHeight(); if (view.getChildCount() == 1) { View content = view.getChildAt(0); @@ -359,6 +364,12 @@ public class WidgetCell extends LinearLayout { mAppWidgetHostViewScale = Math.min(pWidth / contentWidth, pHeight / contentHeight); } view.setScaleToFit(mAppWidgetHostViewScale); + + // layout based previews maybe ready at this point to inspect their inner height. + if (mPreviewReadyListener != null) { + mPreviewReadyListener.onPreviewAvailable(); + mPreviewReadyListener = null; + } } public WidgetImageView getWidgetView() { @@ -384,6 +395,12 @@ public class WidgetCell extends LinearLayout { removeView(mAppWidgetHostViewPreview); mAppWidgetHostViewPreview = null; } + + // Drawables of the image previews are available at this point to measure. + if (mPreviewReadyListener != null) { + mPreviewReadyListener.onPreviewAvailable(); + mPreviewReadyListener = null; + } } if (mAnimatePreview) { @@ -489,14 +506,20 @@ public class WidgetCell extends LinearLayout { // mPreviewContainerScale ensures the needed scaling with respect to original widget size. mAppWidgetHostViewScale = mPreviewContainerScale; containerLp.width = mPreviewContainerSize.getWidth(); - containerLp.height = mPreviewContainerSize.getHeight(); + int height = mPreviewContainerSize.getHeight(); // If we don't have enough available width, scale the preview container to fit. if (containerLp.width > maxWidth) { containerLp.width = maxWidth; mAppWidgetHostViewScale = (float) containerLp.width / mPreviewContainerSize.getWidth(); - containerLp.height = Math.round( - mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale); + height = Math.round(mPreviewContainerSize.getHeight() * mAppWidgetHostViewScale); + } + + // Use parent aligned height in set. + if (mParentAlignedPreviewHeight > 0) { + containerLp.height = Math.min(height, mParentAlignedPreviewHeight); + } else { + containerLp.height = height; } // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass @@ -512,6 +535,42 @@ public class WidgetCell extends LinearLayout { } } + /** + * Sets the height of the preview as adjusted by the parent to have this cell's content aligned + * with other cells displayed by the parent. + */ + public void setParentAlignedPreviewHeight(int previewHeight) { + mParentAlignedPreviewHeight = previewHeight; + } + + /** + * Returns the height of the preview without any empty space. + * In case of appwidget host views, it returns the height of first child. This way, if preview + * view provided by an app doesn't fill bounds, this will return actual height without white + * space. + */ + public int getPreviewContentHeight() { + // By default assume scaled height. + int height = Math.round(mPreviewContainerScale * mWidgetSize.getHeight()); + + if (mWidgetImage != null && mWidgetImage.getDrawable() != null) { + // getBitmapBounds returns the scaled bounds. + Rect bitmapBounds = mWidgetImage.getBitmapBounds(); + height = bitmapBounds.height(); + } else if (mAppWidgetHostViewPreview != null + && mAppWidgetHostViewPreview.getChildCount() == 1) { + int contentHeight = Math.round( + mPreviewContainerScale * mWidgetSize.getHeight()); + int previewInnerHeight = Math.round( + mAppWidgetHostViewScale * mAppWidgetHostViewPreview.getChildAt( + 0).getMeasuredHeight()); + // Use either of the inner scaled height or the scaled widget height + height = Math.min(contentHeight, previewInnerHeight); + } + + return height; + } + /** * Loads a high resolution package icon to show next to the widget title. */ @@ -651,4 +710,19 @@ public class WidgetCell extends LinearLayout { } return false; } + + /** + * Listener to notify when previews are available. + */ + public void addPreviewReadyListener(PreviewReadyListener previewReadyListener) { + mPreviewReadyListener = previewReadyListener; + } + + /** + * Listener interface for subscribers to listen to preview's availability. + */ + public interface PreviewReadyListener { + /** Handler on to invoke when previews are available. */ + void onPreviewAvailable(); + } } diff --git a/src/com/android/launcher3/widget/WidgetTableRow.java b/src/com/android/launcher3/widget/WidgetTableRow.java new file mode 100644 index 0000000000..a5312ce4b7 --- /dev/null +++ b/src/com/android/launcher3/widget/WidgetTableRow.java @@ -0,0 +1,90 @@ +/* + * 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.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.TableRow; + +/** + * A row of {@link WidgetCell}s that can be displayed in a table. + */ +public class WidgetTableRow extends TableRow implements WidgetCell.PreviewReadyListener { + private int mNumOfReadyCells; + private int mNumOfCells; + private int mResizeDelay; + + public WidgetTableRow(Context context) { + super(context); + } + public WidgetTableRow(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public void onPreviewAvailable() { + mNumOfReadyCells++; + + // Once all previews are loaded, find max visible height and adjust the preview containers. + if (mNumOfReadyCells == mNumOfCells) { + resize(); + } + } + + private void resize() { + int previewHeight = 0; + // get the maximum height of each widget preview + for (int i = 0; i < getChildCount(); i++) { + WidgetCell widgetCell = (WidgetCell) getChildAt(i); + previewHeight = Math.max(widgetCell.getPreviewContentHeight(), previewHeight); + } + if (mResizeDelay > 0) { + postDelayed(() -> setAlpha(1f), mResizeDelay); + } + if (previewHeight > 0) { + for (int i = 0; i < getChildCount(); i++) { + WidgetCell widgetCell = (WidgetCell) getChildAt(i); + widgetCell.setParentAlignedPreviewHeight(previewHeight); + widgetCell.postDelayed(widgetCell::requestLayout, mResizeDelay); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + } + + /** + * Sets up the row to display the provided number of numOfCells. + * + * @param numOfCells number of numOfCells in the row + * @param resizeDelayMs time to wait in millis before making any layout size adjustments e.g. we + * want to wait for list expand collapse animation before resizing the + * cell previews. + */ + public void setupRow(int numOfCells, int resizeDelayMs) { + mNumOfCells = numOfCells; + mNumOfReadyCells = 0; + + mResizeDelay = resizeDelayMs; + // For delayed resize, reveal contents only after resize is done. + if (mResizeDelay > 0) { + setAlpha(0); + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java index 4ea24265c3..894099de74 100644 --- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java +++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java @@ -31,7 +31,6 @@ import android.view.ViewParent; import android.view.animation.Interpolator; import android.widget.ScrollView; import android.widget.TableLayout; -import android.widget.TableRow; import android.widget.TextView; import androidx.annotation.Px; @@ -137,8 +136,9 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { mActivityContext.getDeviceProfile(), mMaxHorizontalSpan, mWidgetCellHorizontalPadding) .forEach(row -> { - TableRow tableRow = new TableRow(getContext()); + WidgetTableRow tableRow = new WidgetTableRow(getContext()); tableRow.setGravity(Gravity.TOP); + tableRow.setupRow(row.size(), /*resizeDelayMs=*/ 0); row.forEach(widgetItem -> { WidgetCell widget = addItemCell(tableRow); widget.applyFromCellItem(widgetItem); @@ -163,9 +163,10 @@ public class WidgetsBottomSheet extends BaseWidgetSheet { return super.onControllerInterceptTouchEvent(ev); } - protected WidgetCell addItemCell(ViewGroup parent) { + protected WidgetCell addItemCell(WidgetTableRow parent) { WidgetCell widget = (WidgetCell) LayoutInflater.from(getContext()) .inflate(R.layout.widget_cell, parent, false); + widget.addPreviewReadyListener(parent); widget.setOnClickListener(this); View previewContainer = widget.findViewById(R.id.widget_preview_container); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java index 56352ccc9f..45d733a3a5 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java @@ -15,6 +15,11 @@ */ package com.android.launcher3.widget.picker; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.CHANGE_DURATION_MS; +import static com.android.launcher3.widget.picker.WidgetsListItemAnimator.MOVE_DURATION_MS; + +import static android.animation.ValueAnimator.areAnimatorsEnabled; + import android.content.Context; import android.graphics.Bitmap; import android.util.Log; @@ -26,7 +31,6 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.TableLayout; -import android.widget.TableRow; import androidx.annotation.NonNull; import androidx.annotation.Px; @@ -36,6 +40,7 @@ import com.android.launcher3.model.WidgetItem; import com.android.launcher3.recyclerview.ViewHolderBinder; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetTableRow; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.util.WidgetsTableUtils; @@ -111,17 +116,15 @@ public final class WidgetsListTableViewHolderBinder for (int i = 0; i < widgetItemsTable.size(); i++) { List widgetItemsPerRow = widgetItemsTable.get(i); for (int j = 0; j < widgetItemsPerRow.size(); j++) { - TableRow row = (TableRow) table.getChildAt(i); + WidgetTableRow row = (WidgetTableRow) table.getChildAt(i); row.setVisibility(View.VISIBLE); WidgetCell widget = (WidgetCell) row.getChildAt(j); widget.clear(); + widget.addPreviewReadyListener(row); WidgetItem widgetItem = widgetItemsPerRow.get(j); widget.setVisibility(View.VISIBLE); - // When preview loads, notify adapter to rebind the item and possibly animate - widget.applyFromCellItem(widgetItem, - bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)), - holder.previewCache.get(widgetItem)); + widget.applyFromCellItem(widgetItem); widget.requestLayout(); } } @@ -143,14 +146,19 @@ public final class WidgetsListTableViewHolderBinder for (int i = 0; i < widgetItemsTable.size(); i++) { List widgetItems = widgetItemsTable.get(i); - TableRow tableRow; + WidgetTableRow tableRow; if (i < table.getChildCount()) { - tableRow = (TableRow) table.getChildAt(i); + tableRow = (WidgetTableRow) table.getChildAt(i); } else { - tableRow = new TableRow(table.getContext()); + tableRow = new WidgetTableRow(table.getContext()); tableRow.setGravity(Gravity.TOP); table.addView(tableRow); } + // Pass resize delay to let the "move" and "change" animations run before resizing the + // row. + tableRow.setupRow(widgetItems.size(), + /*resizeDelayMs=*/ + areAnimatorsEnabled() ? (CHANGE_DURATION_MS + MOVE_DURATION_MS) : 0); if (tableRow.getChildCount() > widgetItems.size()) { for (int j = widgetItems.size(); j < tableRow.getChildCount(); j++) { tableRow.getChildAt(j).setVisibility(View.GONE); @@ -161,6 +169,7 @@ public final class WidgetsListTableViewHolderBinder R.layout.widget_cell, tableRow, false); // set up touch. widget.setOnClickListener(mIconClickListener); + widget.addPreviewReadyListener(tableRow); View preview = widget.findViewById(R.id.widget_preview_container); preview.setOnClickListener(mIconClickListener); preview.setOnLongClickListener(mIconLongClickListener); @@ -176,7 +185,7 @@ public final class WidgetsListTableViewHolderBinder int numOfRows = holder.tableContainer.getChildCount(); holder.previewCache.clear(); for (int i = 0; i < numOfRows; i++) { - TableRow tableRow = (TableRow) holder.tableContainer.getChildAt(i); + WidgetTableRow tableRow = (WidgetTableRow) holder.tableContainer.getChildAt(i); int numOfCols = tableRow.getChildCount(); for (int j = 0; j < numOfCols; j++) { WidgetCell widget = (WidgetCell) tableRow.getChildAt(j); diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java index 6dbad5c7d2..1ed3d884c4 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java @@ -27,9 +27,7 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.TableLayout; -import android.widget.TableRow; import androidx.annotation.Nullable; import androidx.annotation.Px; @@ -38,6 +36,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.model.WidgetItem; import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetTableRow; import com.android.launcher3.widget.picker.util.WidgetPreviewContainerSize; import java.util.ArrayList; @@ -105,7 +104,8 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { for (int i = 0; i < recommendationTable.size(); i++) { List widgetItems = recommendationTable.get(i); - TableRow tableRow = new TableRow(getContext()); + WidgetTableRow tableRow = new WidgetTableRow(getContext()); + tableRow.setupRow(widgetItems.size(), /*resizeDelayMs=*/ 0); tableRow.setGravity(Gravity.TOP); for (WidgetItem widgetItem : widgetItems) { WidgetCell widgetCell = addItemCell(tableRow); @@ -121,9 +121,10 @@ public final class WidgetsRecommendationTableLayout extends TableLayout { setVisibility(VISIBLE); } - private WidgetCell addItemCell(ViewGroup parent) { + private WidgetCell addItemCell(WidgetTableRow parent) { WidgetCell widget = (WidgetCell) LayoutInflater.from( getContext()).inflate(R.layout.widget_cell, parent, false); + widget.addPreviewReadyListener(parent); widget.setOnClickListener(mWidgetCellOnClickListener); View previewContainer = widget.findViewById(R.id.widget_preview_container);