Simplifying widget cell layout
Calculating the preview scale during onMeasure to ensure the
content is never clipped.
Bug: 268715418
Test: Verified on device
Change-Id: Ia0fd5ca3ccaae9644e043d75f810e91909a2aea0
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 3f61aaa..6db4fb5 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -24,6 +24,7 @@
android:layout_weight="1"
android:importantForAccessibility="noHideDescendants"
android:hapticFeedbackEnabled="false"
+ android:clipChildren="false"
android:layout_marginVertical="8dp">
<!-- The image of the widget. This view does not support padding. Any placement adjustment
should be done using margins. Width & height are set at runtime after scaling the
diff --git a/src/com/android/launcher3/widget/PendingItemDragHelper.java b/src/com/android/launcher3/widget/PendingItemDragHelper.java
index 67d21f7..f269434 100644
--- a/src/com/android/launcher3/widget/PendingItemDragHelper.java
+++ b/src/com/android/launcher3/widget/PendingItemDragHelper.java
@@ -157,8 +157,15 @@
previewBounds.right -= padding;
}
if (mAppWidgetHostViewPreview != null) {
- previewWidth = mAppWidgetHostViewPreview.getMeasuredWidth();
- previewHeight = mAppWidgetHostViewPreview.getMeasuredHeight();
+ float previewScale = mAppWidgetHostViewPreview.getScaleX();
+ int widgetWidth = mAppWidgetHostViewPreview.getMeasuredWidth();
+ int widgetHeight = mAppWidgetHostViewPreview.getMeasuredHeight();
+ previewWidth = Math.round(widgetWidth * previewScale);
+ previewHeight = Math.round(widgetHeight * previewScale);
+
+ previewBounds.offset(
+ Math.round(widgetWidth * (previewScale - 1) / 2),
+ Math.round(widgetHeight * (previewScale - 1) / 2));
} else {
previewWidth = preview.getIntrinsicWidth();
previewHeight = preview.getIntrinsicHeight();
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index 80bc1a7..43218a1 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -16,13 +16,10 @@
package com.android.launcher3.widget;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
-import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
-
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
import static com.android.launcher3.Utilities.ATLEAST_S;
-import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_WIDGET_CENTERING;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
+import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
import android.content.Context;
import android.graphics.Bitmap;
@@ -47,7 +44,6 @@
import androidx.annotation.Nullable;
import com.android.launcher3.CheckLongPressHelper;
-import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.icons.FastBitmapDrawable;
@@ -55,7 +51,6 @@
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.views.ActivityContext;
-import com.android.launcher3.widget.util.WidgetSizes;
import java.util.function.Consumer;
@@ -75,40 +70,8 @@
private static final int FADE_IN_DURATION_MS = 90;
- /** Widget cell width is calculated by multiplying this factor to grid cell width. */
- private static final float WIDTH_SCALE = 3f;
-
- /** Widget preview width is calculated by multiplying this factor to the widget cell width. */
- private static final float PREVIEW_SCALE = 0.8f;
-
/**
- * 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;
-
- /**
- * The scale of the preview container.
+ * The requested scale of the preview container. It can be lower than this as well.
*/
private float mPreviewContainerScale = 1f;
@@ -119,7 +82,8 @@
private TextView mWidgetDims;
private TextView mWidgetDescription;
- protected WidgetItem mItem;
+ private WidgetItem mItem;
+ private Size mWidgetSize;
private final DatabaseWidgetPreviewLoader mWidgetPreviewLoader;
@@ -150,18 +114,11 @@
mWidgetPreviewLoader = new DatabaseWidgetPreviewLoader(context);
mLongPressHelper = new CheckLongPressHelper(this);
mLongPressHelper.setLongPressTimeoutFactor(1);
+ mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
+ mWidgetSize = new Size(0, 0);
- setContainerWidth();
- setWillNotDraw(false);
setClipToPadding(false);
setAccessibilityDelegate(mActivity.getAccessibilityDelegate());
- mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
- }
-
- private void setContainerWidth() {
- mCellSize = (int) (mActivity.getDeviceProfile().allAppsIconSizePx * WIDTH_SCALE);
- mPresetPreviewSize = (int) (mCellSize * PREVIEW_SCALE);
- mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
}
@Override
@@ -206,7 +163,6 @@
mWidgetDims.setText(null);
mWidgetDescription.setText(null);
mWidgetDescription.setVisibility(GONE);
- mTargetPreviewWidth = mTargetPreviewHeight = mPresetPreviewSize;
if (mActiveRequest != null) {
mActiveRequest.cancel();
@@ -218,7 +174,9 @@
}
mAppWidgetHostViewPreview = null;
mAppWidgetHostViewScale = 1f;
+ mPreviewContainerScale = 1f;
mItem = null;
+ mWidgetSize = new Size(0, 0);
}
public void setSourceContainer(int sourceContainer) {
@@ -248,17 +206,12 @@
*/
public void applyFromCellItem(WidgetItem item, float previewScale,
@NonNull Consumer<Bitmap> callback, @Nullable Bitmap cachedPreview) {
- // setPreviewSize
- DeviceProfile deviceProfile = mActivity.getDeviceProfile();
- Size widgetSize = WidgetSizes.getWidgetItemSizePx(getContext(), deviceProfile, item);
- mTargetPreviewWidth = widgetSize.getWidth();
- mTargetPreviewHeight = widgetSize.getHeight();
mPreviewContainerScale = previewScale;
- applyPreviewOnAppWidgetHostView(item);
-
Context context = getContext();
mItem = item;
+ mWidgetSize = getWidgetItemSizePx(getContext(), mActivity.getDeviceProfile(), mItem);
+
mWidgetName.setText(mItem.label);
mWidgetName.setContentDescription(
context.getString(R.string.widget_preview_context_description, mItem.label));
@@ -282,68 +235,31 @@
setTag(new PendingAddWidgetInfo(item.widgetInfo, mSourceContainer));
}
- ensurePreviewWithCallback(callback, cachedPreview);
- }
-
- private static class ScaledAppWidgetHostView extends LauncherAppWidgetHostView {
- private boolean mKeepOrigForDragging = true;
-
- ScaledAppWidgetHostView(Context context) {
- super(context);
- }
-
- /**
- * Set if the view will keep its original scale when dragged
- * @param isKeepOrig True if keep original scale when dragged, false otherwise
- */
- public void setKeepOrigForDragging(boolean isKeepOrig) {
- mKeepOrigForDragging = isKeepOrig;
- }
-
- /**
- * @return True if the view is set to preserve original scale when dragged, false otherwise
- */
- public boolean isKeepOrigForDragging() {
- return mKeepOrigForDragging;
- }
-
- @Override
- public void startDrag() {
- super.startDrag();
- if (!isKeepOrigForDragging()) {
- // restore to original scale when being dragged, if set to do so
- setScaleToFit(1.0f);
- }
- // When the drag start, translations need to be set to zero to center the view
- getTranslateDelegate().setTranslation(INDEX_WIDGET_CENTERING, 0f, 0f);
- }
- }
-
- private void applyPreviewOnAppWidgetHostView(WidgetItem item) {
if (mRemoteViewsPreview != null) {
- mAppWidgetHostViewPreview = createAppWidgetHostView(getContext());
+ mAppWidgetHostViewPreview = createAppWidgetHostView(context);
setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, item.widgetInfo,
mRemoteViewsPreview);
- return;
+ } else if (item.hasPreviewLayout()) {
+ // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview
+ // as a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView,
+ // which supports applying local color extraction during drag & drop.
+ mAppWidgetHostViewPreview = isLauncherContext(context)
+ ? new LauncherAppWidgetHostView(context)
+ : createAppWidgetHostView(context);
+ LauncherAppWidgetProviderInfo providerInfo =
+ fromProviderInfo(context, item.widgetInfo.clone());
+ // A hack to force the initial layout to be the preview layout since there is no API for
+ // rendering a preview layout for work profile apps yet. For non-work profile layout, a
+ // proper solution is to use RemoteViews(PackageName, LayoutId).
+ providerInfo.initialLayout = item.widgetInfo.previewLayout;
+ setAppWidgetHostViewPreview(mAppWidgetHostViewPreview, providerInfo, null);
+ } else if (cachedPreview != null) {
+ applyPreview(cachedPreview);
+ } else {
+ if (mActiveRequest == null) {
+ mActiveRequest = mWidgetPreviewLoader.loadPreview(mItem, mWidgetSize, callback);
+ }
}
-
- if (!item.hasPreviewLayout()) return;
-
- Context context = getContext();
- // If the context is a Launcher activity, DragView will show mAppWidgetHostViewPreview as
- // a preview during drag & drop. And thus, we should use LauncherAppWidgetHostView, which
- // supports applying local color extraction during drag & drop.
- mAppWidgetHostViewPreview = isLauncherContext(context)
- ? new ScaledAppWidgetHostView(context)
- : createAppWidgetHostView(context);
- LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo =
- LauncherAppWidgetProviderInfo.fromProviderInfo(context, item.widgetInfo.clone());
- // A hack to force the initial layout to be the preview layout since there is no API for
- // rendering a preview layout for work profile apps yet. For non-work profile layout, a
- // proper solution is to use RemoteViews(PackageName, LayoutId).
- launcherAppWidgetProviderInfo.initialLayout = item.widgetInfo.previewLayout;
- setAppWidgetHostViewPreview(mAppWidgetHostViewPreview,
- launcherAppWidgetProviderInfo, /* remoteViews= */ null);
}
private void setAppWidgetHostViewPreview(
@@ -353,6 +269,43 @@
appWidgetHostViewPreview.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
appWidgetHostViewPreview.setAppWidget(/* appWidgetId= */ -1, providerInfo);
appWidgetHostViewPreview.updateAppWidget(remoteViews);
+ appWidgetHostViewPreview.setClipToPadding(false);
+ appWidgetHostViewPreview.setClipChildren(false);
+
+ FrameLayout.LayoutParams widgetHostLP = new FrameLayout.LayoutParams(
+ mWidgetSize.getWidth(), mWidgetSize.getHeight(), Gravity.CENTER);
+ mWidgetImageContainer.addView(appWidgetHostViewPreview, /* index= */ 0, widgetHostLP);
+ mWidgetImage.setVisibility(View.GONE);
+ applyPreview(null);
+
+ appWidgetHostViewPreview.addOnLayoutChangeListener(
+ (v, l, t, r, b, ol, ot, or, ob) ->
+ updateAppWidgetHostScale(appWidgetHostViewPreview));
+ }
+
+ private void updateAppWidgetHostScale(NavigableAppWidgetHostView view) {
+ // Scale the content such that all of the content is visible
+ int contentWidth = view.getWidth();
+ int contentHeight = view.getHeight();
+
+ if (view.getChildCount() == 1) {
+ View content = view.getChildAt(0);
+ // Take the content width based on the edge furthest from the center, so that when
+ // scaling the hostView, the farthest edge is still visible.
+ contentWidth = 2 * Math.max(contentWidth / 2 - content.getLeft(),
+ content.getRight() - contentWidth / 2);
+ contentHeight = 2 * Math.max(contentHeight / 2 - content.getTop(),
+ content.getBottom() - contentHeight / 2);
+ }
+
+ if (contentWidth <= 0 || contentHeight <= 0) {
+ mAppWidgetHostViewScale = 1;
+ } else {
+ float pWidth = mWidgetImageContainer.getWidth();
+ float pHeight = mWidgetImageContainer.getHeight();
+ mAppWidgetHostViewScale = Math.min(pWidth / contentWidth, pHeight / contentHeight);
+ }
+ view.setScaleToFit(mAppWidgetHostViewScale);
}
public WidgetImageView getWidgetView() {
@@ -372,17 +325,6 @@
if (bitmap != null) {
Drawable drawable = new RoundDrawableWrapper(
new FastBitmapDrawable(bitmap), mEnforcedCornerRadius);
-
- // Scale down the preview size if it's wider than the cell.
- float scale = 1f;
- if (mTargetPreviewWidth > 0) {
- float maxWidth = mTargetPreviewWidth;
- float previewWidth = drawable.getIntrinsicWidth() * mPreviewContainerScale;
- scale = Math.min(maxWidth / previewWidth, 1);
- }
- setContainerSize(
- Math.round(drawable.getIntrinsicWidth() * scale * mPreviewContainerScale),
- Math.round(drawable.getIntrinsicHeight() * scale * mPreviewContainerScale));
mWidgetImage.setDrawable(drawable);
mWidgetImage.setVisibility(View.VISIBLE);
if (mAppWidgetHostViewPreview != null) {
@@ -415,76 +357,6 @@
}
}
- private void setContainerSize(int width, int height) {
- LayoutParams layoutParams = (LayoutParams) mWidgetImageContainer.getLayoutParams();
- layoutParams.width = width;
- layoutParams.height = height;
- mWidgetImageContainer.setLayoutParams(layoutParams);
- }
-
- /**
- * Ensures that the preview is already loaded or being loaded. If the preview is not loaded,
- * it applies the provided cachedPreview. If that is null, it starts a loader and notifies the
- * callback on successful load.
- */
- private void ensurePreviewWithCallback(Consumer<Bitmap> callback,
- @Nullable Bitmap cachedPreview) {
- if (mAppWidgetHostViewPreview != null) {
- int containerWidth = (int) (mTargetPreviewWidth * mPreviewContainerScale);
- int containerHeight = (int) (mTargetPreviewHeight * mPreviewContainerScale);
- setContainerSize(containerWidth, containerHeight);
- boolean shouldMeasureAndScale = false;
- 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.
- shouldMeasureAndScale =
- layoutParams.width != MATCH_PARENT && layoutParams.height != MATCH_PARENT;
- if (shouldMeasureAndScale) {
- setNoClip(mWidgetImageContainer);
- setNoClip(mAppWidgetHostViewPreview);
- mAppWidgetHostViewScale = measureAndComputeWidgetPreviewScale();
- }
- }
-
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
- mTargetPreviewWidth, mTargetPreviewHeight, Gravity.FILL);
- mAppWidgetHostViewPreview.setLayoutParams(params);
-
- if (!shouldMeasureAndScale
- && mAppWidgetHostViewPreview instanceof ScaledAppWidgetHostView) {
- // If the view is not measured & scaled, at least one side will match the grid size,
- // so it should be safe to restore the original scale once it is dragged.
- ScaledAppWidgetHostView tempView =
- (ScaledAppWidgetHostView) mAppWidgetHostViewPreview;
- tempView.setKeepOrigForDragging(false);
- tempView.setScaleToFit(mPreviewContainerScale);
- } else if (!shouldMeasureAndScale) {
- mAppWidgetHostViewPreview.setScaleToFit(mPreviewContainerScale);
- } else {
- mAppWidgetHostViewPreview.setScaleToFit(mAppWidgetHostViewScale);
- }
- mAppWidgetHostViewPreview.getTranslateDelegate().setTranslation(
- INDEX_WIDGET_CENTERING,
- -(params.width - (params.width * mPreviewContainerScale)) / 2.0f,
- -(params.height - (params.height * mPreviewContainerScale)) / 2.0f);
- mWidgetImageContainer.addView(mAppWidgetHostViewPreview, /* index= */ 0);
- mWidgetImage.setVisibility(View.GONE);
- applyPreview(null);
- return;
- }
- if (cachedPreview != null) {
- applyPreview(cachedPreview);
- return;
- }
- if (mActiveRequest != null) {
- return;
- }
- mActiveRequest = mWidgetPreviewLoader.loadPreview(
- mItem, new Size(mTargetPreviewWidth, mTargetPreviewHeight), callback);
- }
-
@Override
public boolean onTouchEvent(MotionEvent ev) {
super.onTouchEvent(ev);
@@ -522,61 +394,20 @@
info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK);
}
- private static void setNoClip(ViewGroup view) {
- view.setClipChildren(false);
- view.setClipToPadding(false);
- }
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ ViewGroup.LayoutParams containerLp = mWidgetImageContainer.getLayoutParams();
- private float measureAndComputeWidgetPreviewScale() {
- if (mAppWidgetHostViewPreview.getChildCount() != 1) {
- return 1f;
+ mAppWidgetHostViewScale = mPreviewContainerScale;
+ int maxWidth = MeasureSpec.getSize(widthMeasureSpec);
+ containerLp.width = Math.round(mWidgetSize.getWidth() * mAppWidgetHostViewScale);
+ if (containerLp.width > maxWidth) {
+ containerLp.width = maxWidth;
+ mAppWidgetHostViewScale = (float) containerLp.width / mWidgetSize.getWidth();
}
+ containerLp.height = Math.round(mWidgetSize.getHeight() * mAppWidgetHostViewScale);
+ // No need to call mWidgetImageContainer.setLayoutParams as we are in measure pass
- // 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));
- if (mRemoteViewsPreview != null) {
- // If RemoteViews contains multiple sizes, the best fit sized RemoteViews will be
- // selected in onLayout. To work out the right measurement, let's layout and then
- // measure again.
- mAppWidgetHostViewPreview.layout(
- /* left= */ 0,
- /* top= */ 0,
- /* right= */ mTargetPreviewWidth,
- /* bottom= */ mTargetPreviewHeight);
- mAppWidgetHostViewPreview.measure(
- makeMeasureSpec(mTargetPreviewWidth, MeasureSpec.UNSPECIFIED),
- makeMeasureSpec(mTargetPreviewHeight, MeasureSpec.UNSPECIFIED));
-
- }
- View widgetContent = mAppWidgetHostViewPreview.getChildAt(0);
- int appWidgetContentWidth = widgetContent.getMeasuredWidth();
- int appWidgetContentHeight = widgetContent.getMeasuredHeight();
- if (appWidgetContentWidth == 0 || appWidgetContentHeight == 0) {
- return 1f;
- }
-
- // If the width / height of the widget content is set to wrap content, overrides the width /
- // height with the measured dimension. This avoids incorrect measurement after scaling.
- FrameLayout.LayoutParams layoutParam =
- (FrameLayout.LayoutParams) widgetContent.getLayoutParams();
- if (layoutParam.width == WRAP_CONTENT) {
- layoutParam.width = widgetContent.getMeasuredWidth();
- }
- if (layoutParam.height == WRAP_CONTENT) {
- layoutParam.height = widgetContent.getMeasuredHeight();
- }
- widgetContent.setLayoutParams(layoutParam);
-
- int horizontalPadding = mAppWidgetHostViewPreview.getPaddingStart()
- + mAppWidgetHostViewPreview.getPaddingEnd();
- int verticalPadding = mAppWidgetHostViewPreview.getPaddingTop()
- + mAppWidgetHostViewPreview.getPaddingBottom();
- return Math.min(
- (mTargetPreviewWidth - horizontalPadding) * mPreviewContainerScale
- / appWidgetContentWidth,
- (mTargetPreviewHeight - verticalPadding) * mPreviewContainerScale
- / appWidgetContentHeight);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}