diff --git a/res/layout/app_widget_resize_frame.xml b/res/layout/app_widget_resize_frame.xml index 2e476df6c9..671dbc63c0 100644 --- a/res/layout/app_widget_resize_frame.xml +++ b/res/layout/app_widget_resize_frame.xml @@ -26,6 +26,7 @@ 0dp + + 16dp + diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index d02efb0100..ab9178556e 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -14,12 +14,15 @@ import android.appwidget.AppWidgetProviderInfo; import android.content.Context; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; import android.util.AttributeSet; import android.util.SizeF; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.widget.ImageView; import androidx.annotation.Nullable; @@ -166,6 +169,15 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O DragLayer dl = launcher.getDragLayer(); AppWidgetResizeFrame frame = (AppWidgetResizeFrame) launcher.getLayoutInflater() .inflate(R.layout.app_widget_resize_frame, dl, false); + if (widget.hasEnforcedCornerRadius()) { + float enforcedCornerRadius = widget.getEnforcedCornerRadius(); + ImageView imageView = frame.findViewById(R.id.widget_resize_frame); + Drawable d = imageView.getDrawable(); + if (d instanceof GradientDrawable) { + GradientDrawable gd = (GradientDrawable) d.mutate(); + gd.setCornerRadius(enforcedCornerRadius); + } + } frame.setupForWidget(widget, cellLayout, dl); ((DragLayer.LayoutParams) frame.getLayoutParams()).customPosition = true; diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java index 96251f000c..7f76d27564 100644 --- a/src/com/android/launcher3/config/FeatureFlags.java +++ b/src/com/android/launcher3/config/FeatureFlags.java @@ -215,6 +215,9 @@ public final class FeatureFlags { public static final BooleanFlag ENABLE_SPLIT_SELECT = getDebugFlag( "ENABLE_SPLIT_SELECT", false, "Uses new split screen selection overview UI"); + public static final BooleanFlag ENABLE_ENFORCED_ROUNDED_CORNERS = new DeviceFlag( + "ENABLE_ENFORCED_ROUNDED_CORNERS", true, "Enforce rounded corners on all App Widgets"); + public static void initialize(Context context) { synchronized (sDebugFlags) { for (DebugFlag flag : sDebugFlags) { diff --git a/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java b/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java index 92ae670e52..5cd95dc413 100644 --- a/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java +++ b/src/com/android/launcher3/dragndrop/AppWidgetHostViewDrawable.java @@ -17,8 +17,12 @@ package com.android.launcher3.dragndrop; import android.graphics.Canvas; import android.graphics.ColorFilter; +import android.graphics.Outline; import android.graphics.Paint; +import android.graphics.Path; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.Drawable; import com.android.launcher3.widget.LauncherAppWidgetHostView; @@ -28,14 +32,30 @@ public final class AppWidgetHostViewDrawable extends Drawable { private final LauncherAppWidgetHostView mAppWidgetHostView; private Paint mPaint = new Paint(); + private final Path mClipPath; public AppWidgetHostViewDrawable(LauncherAppWidgetHostView appWidgetHostView) { mAppWidgetHostView = appWidgetHostView; + Path clipPath = null; + if (appWidgetHostView.getClipToOutline()) { + Outline outline = new Outline(); + mAppWidgetHostView.getOutlineProvider().getOutline(mAppWidgetHostView, outline); + Rect rect = new Rect(); + if (outline.getRect(rect)) { + float radius = outline.getRadius(); + clipPath = new Path(); + clipPath.addRoundRect(new RectF(rect), radius, radius, Path.Direction.CCW); + } + } + mClipPath = clipPath; } @Override public void draw(Canvas canvas) { int saveCount = canvas.saveLayer(0, 0, getIntrinsicWidth(), getIntrinsicHeight(), mPaint); + if (mClipPath != null) { + canvas.clipPath(mClipPath); + } mAppWidgetHostView.draw(canvas); canvas.restoreToCount(saveCount); } diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java index 5c18faf213..2e542edf76 100644 --- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java +++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetProviderInfo; 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; @@ -32,12 +33,14 @@ 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; import android.widget.RemoteViews; import androidx.annotation.Nullable; +import androidx.annotation.UiThread; import com.android.launcher3.CheckLongPressHelper; import com.android.launcher3.Launcher; @@ -95,6 +98,18 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView private final Rect mWidgetSizeAtDrag = new Rect(); private final RectF mTempRectF = new RectF(); private final boolean mIsRtl; + 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 LauncherAppWidgetHostView(Context context) { super(context); @@ -112,6 +127,8 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView mIsRtl = Utilities.isRtl(context.getResources()); mColorExtractor = LocalColorExtractor.newInstance(getContext()); mColorExtractor.setListener(this); + + mEnforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(getContext()); } @Override @@ -169,7 +186,7 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView if (viewGroup instanceof AdapterView) { return true; } else { - for (int i=0; i < viewGroup.getChildCount(); i++) { + for (int i = 0; i < viewGroup.getChildCount(); i++) { View child = viewGroup.getChildAt(i); if (child instanceof ViewGroup) { if (checkScrollableRecursively((ViewGroup) child)) { @@ -272,6 +289,8 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView int pageId = mWorkspace.getPageIndexForScreenId(info.screenId); updateColorExtraction(mCurrentWidgetSize, pageId); } + + enforceRoundedCorners(); } /** Starts the drag mode. */ @@ -469,4 +488,39 @@ public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView } return false; } + + @UiThread + private void resetRoundedCorners() { + setOutlineProvider(ViewOutlineProvider.BACKGROUND); + setClipToOutline(false); + } + + @UiThread + private void enforceRoundedCorners() { + if (mEnforcedCornerRadius <= 0 || !RoundedCornerEnforcement.isRoundedCornerEnabled(this)) { + resetRoundedCorners(); + return; + } + View background = RoundedCornerEnforcement.findBackground(this); + if (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/RoundedCornerEnforcement.java b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java new file mode 100644 index 0000000000..99eccd1303 --- /dev/null +++ b/src/com/android/launcher3/widget/RoundedCornerEnforcement.java @@ -0,0 +1,170 @@ +/* + * 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.appwidget.AppWidgetHostView; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.config.FeatureFlags; + +import java.util.ArrayList; +import java.util.List; + +/** + * Utilities to compute the enforced the use of rounded corners on App Widgets. + */ +public class RoundedCornerEnforcement { + // This class is only a namespace and not meant to be instantiated. + private RoundedCornerEnforcement() { + } + + /** + * Find the background view for a widget. + * + * @param appWidget the view containing the App Widget (typically the instance of + * {@link AppWidgetHostView}). + */ + @Nullable + public static View findBackground(@NonNull View appWidget) { + List backgrounds = findViewsWithId(appWidget, android.R.id.background); + if (backgrounds.size() == 1) { + return backgrounds.get(0); + } + // Really, the argument should contain the widget, so it cannot be the background. + if (appWidget instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) appWidget; + if (vg.getChildCount() > 0) { + return findUndefinedBackground(vg.getChildAt(0)); + } + } + return appWidget; + } + + /** + * Check whether the app widget has opted out of the enforcement. + */ + public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) { + return background.getId() == android.R.id.background && background.getClipToOutline(); + } + + /** Check if the app widget is in the deny list. */ + public static boolean isRoundedCornerEnabled(@NonNull View view) { + if (!Utilities.ATLEAST_S || !FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get()) { + return false; + } + // Here we need to test if the view's component is in the (to be created) deny list. + return true; + } + + /** + * Computes the rounded rectangle needed for this app widget. + * + * @param appWidget View onto which the rounded rectangle will be applied. + * @param background Background view. This must be either {@code appWidget} or a descendant + * of {@code appWidget}. + * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame + * of {@code appWidget}. + */ + public static void computeRoundedRectangle(@NonNull View appWidget, @NonNull View background, + @NonNull Rect outRect) { + outRect.left = 0; + outRect.right = background.getWidth(); + outRect.top = 0; + outRect.bottom = background.getHeight(); + while (background != appWidget) { + outRect.offset(background.getLeft(), background.getTop()); + background = (View) background.getParent(); + } + } + + /** + * Computes the radius of the rounded rectangle that should be applied to a widget expanded + * in the given context. + */ + public static float computeEnforcedRadius(@NonNull Context context) { + if (!Utilities.ATLEAST_S) { + return 0; + } + Resources res = context.getResources(); + float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius); + float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius); + return Math.min(defaultRadius, systemRadius); + } + + private static List findViewsWithId(View view, @IdRes int viewId) { + List output = new ArrayList<>(); + accumulateViewsWithId(view, viewId, output); + return output; + } + + // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. + private static void accumulateViewsWithId(View view, @IdRes int viewId, List output) { + if (view.getId() == viewId) { + output.add(view); + return; + } + if (view instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) view; + for (int i = 0; i < vg.getChildCount(); i++) { + accumulateViewsWithId(vg.getChildAt(i), viewId, output); + } + } + } + + private static boolean isViewVisible(View view) { + if (view.getVisibility() != View.VISIBLE) { + return false; + } + return !view.willNotDraw() || view.getForeground() != null || view.getBackground() != null; + } + + @Nullable + private static View findUndefinedBackground(View current) { + if (current.getVisibility() != View.VISIBLE) { + return null; + } + if (isViewVisible(current)) { + return current; + } + View lastVisibleView = null; + // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw + // something, or a ViewGroup that contains more than one view. + if (current instanceof ViewGroup) { + ViewGroup vg = (ViewGroup) current; + for (int i = 0; i < vg.getChildCount(); i++) { + View visibleView = findUndefinedBackground(vg.getChildAt(i)); + if (visibleView != null) { + if (lastVisibleView != null) { + return current; // At least two visible children + } + lastVisibleView = visibleView; + } + } + } + return lastVisibleView; + } +}