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 static com.android.launcher3.Flags.useSystemRadiusForAppWidgets; 20 21 import android.appwidget.AppWidgetHostView; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Rect; 25 import android.view.View; 26 import android.view.ViewGroup; 27 28 import androidx.annotation.IdRes; 29 import androidx.annotation.NonNull; 30 import androidx.annotation.Nullable; 31 32 import com.android.launcher3.R; 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 background)70 public static boolean hasAppWidgetOptedOut(@NonNull View background) { 71 return background.getId() == android.R.id.background && background.getClipToOutline(); 72 } 73 74 /** 75 * Computes the rounded rectangle needed for this app widget. 76 * 77 * @param appWidget View onto which the rounded rectangle will be applied. 78 * @param background Background view. This must be either {@code appWidget} or a descendant 79 * of {@code appWidget}. 80 * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame 81 * of {@code appWidget}. 82 */ computeRoundedRectangle(@onNull View appWidget, @NonNull View background, @NonNull Rect outRect)83 public static void computeRoundedRectangle(@NonNull View appWidget, @NonNull View background, 84 @NonNull Rect outRect) { 85 outRect.left = 0; 86 outRect.right = background.getWidth(); 87 outRect.top = 0; 88 outRect.bottom = background.getHeight(); 89 while (background != appWidget) { 90 outRect.offset(background.getLeft(), background.getTop()); 91 background = (View) background.getParent(); 92 } 93 } 94 95 /** 96 * Computes the radius of the rounded rectangle that should be applied to a widget expanded 97 * in the given context. 98 */ computeEnforcedRadius(@onNull Context context)99 public static float computeEnforcedRadius(@NonNull Context context) { 100 Resources res = context.getResources(); 101 float systemRadius = res.getDimension(android.R.dimen.system_app_widget_background_radius); 102 if (useSystemRadiusForAppWidgets()) { 103 return systemRadius; 104 } 105 106 float defaultRadius = res.getDimension(R.dimen.enforced_rounded_corner_max_radius); 107 return Math.min(defaultRadius, systemRadius); 108 } 109 findViewsWithId(View view, @IdRes int viewId)110 private static List<View> findViewsWithId(View view, @IdRes int viewId) { 111 List<View> output = new ArrayList<>(); 112 accumulateViewsWithId(view, viewId, output); 113 return output; 114 } 115 116 // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. accumulateViewsWithId(View view, @IdRes int viewId, List<View> output)117 private static void accumulateViewsWithId(View view, @IdRes int viewId, List<View> output) { 118 if (view.getId() == viewId) { 119 output.add(view); 120 return; 121 } 122 if (view instanceof ViewGroup) { 123 ViewGroup vg = (ViewGroup) view; 124 for (int i = 0; i < vg.getChildCount(); i++) { 125 accumulateViewsWithId(vg.getChildAt(i), viewId, output); 126 } 127 } 128 } 129 isViewVisible(View view)130 private static boolean isViewVisible(View view) { 131 if (view.getVisibility() != View.VISIBLE) { 132 return false; 133 } 134 return !view.willNotDraw() || view.getForeground() != null || view.getBackground() != null; 135 } 136 137 @Nullable findUndefinedBackground(View current)138 private static View findUndefinedBackground(View current) { 139 if (current.getVisibility() != View.VISIBLE) { 140 return null; 141 } 142 if (isViewVisible(current)) { 143 return current; 144 } 145 View lastVisibleView = null; 146 // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw 147 // something, or a ViewGroup that contains more than one view. 148 if (current instanceof ViewGroup) { 149 ViewGroup vg = (ViewGroup) current; 150 for (int i = 0; i < vg.getChildCount(); i++) { 151 View visibleView = findUndefinedBackground(vg.getChildAt(i)); 152 if (visibleView != null) { 153 if (lastVisibleView != null) { 154 return current; // At least two visible children 155 } 156 lastVisibleView = visibleView; 157 } 158 } 159 } 160 return lastVisibleView; 161 } 162 } 163