1 /* 2 * Copyright (C) 2024 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.systemui.communal.widgets 18 19 import android.annotation.IdRes 20 import android.annotation.Nullable 21 import android.content.Context 22 import android.content.res.Resources 23 import android.graphics.Rect 24 import android.view.View 25 import android.view.ViewGroup 26 import androidx.core.os.BuildCompat.isAtLeastS 27 import com.android.systemui.res.R 28 import kotlin.math.min 29 30 /** 31 * Utilities to compute the enforced use of rounded corners on App Widgets. This is a fork of the 32 * Launcher3 source code to enforce the same visual treatment on communal hub. 33 */ 34 internal object RoundedCornerEnforcement { 35 /** 36 * Find the background view for a widget. 37 * 38 * @param appWidget the view containing the App Widget (typically the instance of 39 * [CommunalAppWidgetHostView]). 40 */ findBackgroundnull41 fun findBackground(appWidget: View): View? { 42 val backgrounds = findViewsWithId(appWidget, R.id.background) 43 if (backgrounds.size == 1) { 44 return backgrounds[0] 45 } 46 // Really, the argument should contain the widget, so it cannot be the background. 47 if (appWidget is ViewGroup) { 48 val vg = appWidget 49 if (vg.childCount > 0) { 50 return findUndefinedBackground(vg.getChildAt(0)) 51 } 52 } 53 return appWidget 54 } 55 56 /** Check whether the app widget has opted out of the enforcement. */ hasAppWidgetOptedOutnull57 fun hasAppWidgetOptedOut(appWidget: View?, background: View): Boolean { 58 return background.id == R.id.background && background.clipToOutline 59 } 60 61 /** 62 * Computes the rounded rectangle needed for this app widget. 63 * 64 * @param appWidget View onto which the rounded rectangle will be applied. 65 * @param background Background view. This must be either `appWidget` or a descendant of 66 * `appWidget`. 67 * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame of 68 * `appWidget`. 69 */ computeRoundedRectanglenull70 fun computeRoundedRectangle(appWidget: View, background: View, outRect: Rect) { 71 var background = background 72 outRect.left = 0 73 outRect.right = background.width 74 outRect.top = 0 75 outRect.bottom = background.height 76 while (background !== appWidget) { 77 outRect.offset(background.left, background.top) 78 background = background.parent as View 79 } 80 } 81 82 /** Get the radius of the rounded rectangle defined in the host's resource. */ getOwnedEnforcedRadiusnull83 private fun getOwnedEnforcedRadius(context: Context): Float { 84 val res: Resources = context.resources 85 return res.getDimension(R.dimen.communal_enforced_rounded_corner_max_radius) 86 } 87 88 /** 89 * Computes the radius of the rounded rectangle that should be applied to a widget expanded in 90 * the given context. 91 */ computeEnforcedRadiusnull92 fun computeEnforcedRadius(context: Context): Float { 93 if (!isAtLeastS()) { 94 return 0f 95 } 96 val res: Resources = context.resources 97 val systemRadius: Float = 98 res.getDimension(android.R.dimen.system_app_widget_background_radius) 99 val defaultRadius = getOwnedEnforcedRadius(context) 100 return min(defaultRadius, systemRadius) 101 } 102 findViewsWithIdnull103 private fun findViewsWithId(view: View, @IdRes viewId: Int): List<View> { 104 val output: MutableList<View> = ArrayList() 105 accumulateViewsWithId(view, viewId, output) 106 return output 107 } 108 109 // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. accumulateViewsWithIdnull110 private fun accumulateViewsWithId(view: View, @IdRes viewId: Int, output: MutableList<View>) { 111 if (view.id == viewId) { 112 output.add(view) 113 return 114 } 115 if (view is ViewGroup) { 116 val vg = view 117 for (i in 0 until vg.childCount) { 118 accumulateViewsWithId(vg.getChildAt(i), viewId, output) 119 } 120 } 121 } 122 isViewVisiblenull123 private fun isViewVisible(view: View): Boolean { 124 return if (view.visibility != View.VISIBLE) { 125 false 126 } else !view.willNotDraw() || view.foreground != null || view.background != null 127 } 128 129 @Nullable findUndefinedBackgroundnull130 private fun findUndefinedBackground(current: View): View? { 131 if (current.visibility != View.VISIBLE) { 132 return null 133 } 134 if (isViewVisible(current)) { 135 return current 136 } 137 var lastVisibleView: View? = null 138 // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw 139 // something, or a ViewGroup that contains more than one view. 140 if (current is ViewGroup) { 141 val vg = current 142 for (i in 0 until vg.childCount) { 143 val visibleView = findUndefinedBackground(vg.getChildAt(i)) 144 if (visibleView != null) { 145 if (lastVisibleView != null) { 146 return current // At least two visible children 147 } 148 lastVisibleView = visibleView 149 } 150 } 151 } 152 return lastVisibleView 153 } 154 } 155