1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3.widget; 18 19 import android.appwidget.AppWidgetHostView; 20 import android.content.Context; 21 import android.content.res.Resources; 22 import android.graphics.Rect; 23 import android.view.View; 24 import android.view.ViewGroup; 25 26 import androidx.annotation.IdRes; 27 import androidx.annotation.NonNull; 28 import androidx.annotation.Nullable; 29 30 import com.android.launcher3.R; 31 import com.android.launcher3.Utilities; 32 import com.android.launcher3.config.FeatureFlags; 33 34 import java.util.ArrayList; 35 import java.util.List; 36 37 /** 38 * Utilities to compute the enforced the use of rounded corners on App Widgets. 39 */ 40 public class RoundedCornerEnforcement { 41 // This class is only a namespace and not meant to be instantiated. RoundedCornerEnforcement()42 private RoundedCornerEnforcement() { 43 } 44 45 /** 46 * Find the background view for a widget. 47 * 48 * @param appWidget the view containing the App Widget (typically the instance of 49 * {@link AppWidgetHostView}). 50 */ 51 @Nullable findBackground(@onNull View appWidget)52 public static View findBackground(@NonNull View appWidget) { 53 List<View> backgrounds = findViewsWithId(appWidget, android.R.id.background); 54 if (backgrounds.size() == 1) { 55 return backgrounds.get(0); 56 } 57 // Really, the argument should contain the widget, so it cannot be the background. 58 if (appWidget instanceof ViewGroup) { 59 ViewGroup vg = (ViewGroup) appWidget; 60 if (vg.getChildCount() > 0) { 61 return findUndefinedBackground(vg.getChildAt(0)); 62 } 63 } 64 return appWidget; 65 } 66 67 /** 68 * Check whether the app widget has opted out of the enforcement. 69 */ hasAppWidgetOptedOut(@onNull View appWidget, @NonNull View background)70 public static boolean hasAppWidgetOptedOut(@NonNull View appWidget, @NonNull View background) { 71 return background.getId() == android.R.id.background && background.getClipToOutline(); 72 } 73 74 /** Check if the app widget is in the deny list. */ isRoundedCornerEnabled()75 public static boolean isRoundedCornerEnabled() { 76 return Utilities.ATLEAST_S && FeatureFlags.ENABLE_ENFORCED_ROUNDED_CORNERS.get(); 77 } 78 79 /** 80 * Computes the rounded rectangle needed for this app widget. 81 * 82 * @param appWidget View onto which the rounded rectangle will be applied. 83 * @param background Background view. This must be either {@code appWidget} or a descendant 84 * of {@code appWidget}. 85 * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame 86 * of {@code appWidget}. 87 */ computeRoundedRectangle(@onNull View appWidget, @NonNull View background, @NonNull Rect outRect)88 public static void computeRoundedRectangle(@NonNull View appWidget, @NonNull View background, 89 @NonNull Rect outRect) { 90 outRect.left = 0; 91 outRect.right = background.getWidth(); 92 outRect.top = 0; 93 outRect.bottom = background.getHeight(); 94 while (background != appWidget) { 95 outRect.offset(background.getLeft(), background.getTop()); 96 background = (View) background.getParent(); 97 } 98 } 99 100 /** 101 * Computes the radius of the rounded rectangle that should be applied to a widget expanded 102 * in the given context. 103 */ computeEnforcedRadius(@onNull Context context)104 public static float computeEnforcedRadius(@NonNull Context context) { 105 if (!Utilities.ATLEAST_S) { 106 return 0; 107 } 108 Resources res = context.getResources(); 109 float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius); 110 float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius); 111 return Math.min(defaultRadius, systemRadius); 112 } 113 findViewsWithId(View view, @IdRes int viewId)114 private static List<View> findViewsWithId(View view, @IdRes int viewId) { 115 List<View> output = new ArrayList<>(); 116 accumulateViewsWithId(view, viewId, output); 117 return output; 118 } 119 120 // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. accumulateViewsWithId(View view, @IdRes int viewId, List<View> output)121 private static void accumulateViewsWithId(View view, @IdRes int viewId, List<View> output) { 122 if (view.getId() == viewId) { 123 output.add(view); 124 return; 125 } 126 if (view instanceof ViewGroup) { 127 ViewGroup vg = (ViewGroup) view; 128 for (int i = 0; i < vg.getChildCount(); i++) { 129 accumulateViewsWithId(vg.getChildAt(i), viewId, output); 130 } 131 } 132 } 133 isViewVisible(View view)134 private static boolean isViewVisible(View view) { 135 if (view.getVisibility() != View.VISIBLE) { 136 return false; 137 } 138 return !view.willNotDraw() || view.getForeground() != null || view.getBackground() != null; 139 } 140 141 @Nullable findUndefinedBackground(View current)142 private static View findUndefinedBackground(View current) { 143 if (current.getVisibility() != View.VISIBLE) { 144 return null; 145 } 146 if (isViewVisible(current)) { 147 return current; 148 } 149 View lastVisibleView = null; 150 // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw 151 // something, or a ViewGroup that contains more than one view. 152 if (current instanceof ViewGroup) { 153 ViewGroup vg = (ViewGroup) current; 154 for (int i = 0; i < vg.getChildCount(); i++) { 155 View visibleView = findUndefinedBackground(vg.getChildAt(i)); 156 if (visibleView != null) { 157 if (lastVisibleView != null) { 158 return current; // At least two visible children 159 } 160 lastVisibleView = visibleView; 161 } 162 } 163 } 164 return lastVisibleView; 165 } 166 } 167