From 298526e812d347ca3c6a10061faa90899dabf56d Mon Sep 17 00:00:00 2001 From: Pierre Barbier de Reuille Date: Thu, 18 Mar 2021 08:49:47 +0000 Subject: [PATCH 1/2] Enforce rounded corners on App Widgets. The feature is controlled by the ENABLE_ENFORCED_ROUNDED_CORNERS flag (currently set to false). If does not yet handle P/H flags to control its behavior. Bug: 183097166 Test: Manual tests with top 1P App Widgets (See bug for results) Change-Id: I56fca1b717f37ad518588115409f2144a71d4b98 --- res/values/dimens.xml | 3 + .../launcher3/config/FeatureFlags.java | 3 + .../dragndrop/AppWidgetHostViewDrawable.java | 20 +++ .../widget/LauncherAppWidgetHostView.java | 55 +++++- .../widget/RoundedCornerEnforcement.java | 170 ++++++++++++++++++ 5 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 src/com/android/launcher3/widget/RoundedCornerEnforcement.java diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 9d6c936384..c378a806a1 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -277,4 +277,7 @@ 0dp + + 16dp + 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..9b971dbc8a 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,38 @@ 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; + } +} From 452df2d5bb8fbc210909fff80536a223472d88c1 Mon Sep 17 00:00:00 2001 From: Pierre Barbier de Reuille Date: Fri, 19 Mar 2021 23:16:54 +0000 Subject: [PATCH 2/2] Update the resize frame to use the forced radius. When forcing the rounded corners, the radius may be smaller than the default one. When this happens, we should change how the frame is drawn to only use that reduced radius, or this looks weird. Bug: 183097166 Test: Manual tests with top 1P App Widgets Change-Id: I6ce232515ce14059e1168e16735719ea2370132c --- res/layout/app_widget_resize_frame.xml | 1 + src/com/android/launcher3/AppWidgetResizeFrame.java | 12 ++++++++++++ .../launcher3/widget/LauncherAppWidgetHostView.java | 1 + 3 files changed, 14 insertions(+) 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 @@