Files
Flexera-Launcher/flexera/src/app/flexera/widget/FlexeraWidgets.kt
T
TMCandClaude Opus 5.5 f5d7cf07ed Line up every widget with the icons, as the Pictures widget does
Widgets of other apps used to fill their cells minus Launcher3's widget
padding, which is a few pixels wider than the icons' edges and reaches down
into the room kept for icon labels; on the owner's phone they hung about
13 px past the icons sideways and 40 px below. Now the room a widget keeps in
its cells is the distance from the cell edges to the icon edges, worked out
as IconFrame does for the launcher's own widgets, so every widget's edges
meet the edges of the icons around it.

The one function (FlexeraWidgets.appWidgetPadding) is used for the layout,
for the size the app is told, for the drop animation and through
WidgetSizes for previews. The cells a widget needs are still worked out
with Launcher3's padding, so no widget changes its cell count. A new switch
under Settings, Home screen, Widgets ("Line up with icons", on by default)
turns it off; Lawnchair's Widget padding slider shows only then.

Checked on the emulator against lines drawn at the icon edges: Google
Calendar's widgets meet them within 2 px; Google Clock's sits about 20 px
inside, because it draws an empty margin of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:19:33 +01:00

120 lines
5.2 KiB
Kotlin

package app.flexera.widget
import android.appwidget.AppWidgetProviderInfo
import android.content.ComponentName
import android.graphics.Rect
import android.view.View
import android.widget.FrameLayout
import com.android.launcher3.DeviceProfile
import com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR
import com.android.launcher3.model.data.LauncherAppWidgetInfo
import com.android.launcher3.widget.LauncherAppWidgetHostView
import com.android.launcher3.widget.PendingAppWidgetHostView
import kotlin.math.max
import kotlin.math.roundToInt
/**
* The widgets that come with the launcher: Pictures ([PictureWidgetProvider]) and Clock
* ([ClockWidgetProvider]).
*
* Android knows each as an ordinary app widget, so they are added, moved, resized and removed
* like any other. What they show is not sent through Android, though: the launcher draws them
* itself with a view of its own, which is what lets them line up with the icons ([IconFrame]).
* This is where Launcher3's widget code asks about them, and about the room every other widget
* keeps in its cells, which by default lines those up with the icons as well.
*/
object FlexeraWidgets {
private val noInset = Rect()
private fun isOwn(provider: ComponentName?): Boolean =
provider == PictureWidgetProvider.componentName || provider == ClockWidgetProvider.componentName
/**
* The room a widget's cells keep free around it, on each side; one cell is [cellWidth] by
* [cellHeight]. The launcher's own widgets take none, because they place what they draw
* against the icons of their cells by themselves ([IconFrame]); every other widget gets
* [appWidgetPadding].
*/
@JvmStatic
fun cellInset(child: View, profile: DeviceProfile, cellWidth: Int, cellHeight: Int): Rect {
val info = child.tag as? LauncherAppWidgetInfo
if (info != null && isOwn(info.providerName)) return noInset
return appWidgetPadding(profile, cellWidth, cellHeight)
}
/** [appWidgetPadding] for the cells of [profile]'s home screen. */
@JvmStatic
fun appWidgetPadding(profile: DeviceProfile): Rect {
val cell = profile.cellSize
return appWidgetPadding(profile, cell.x, cell.y)
}
/**
* The room a widget of another app keeps free inside its cells, on each side, where one
* cell is [cellWidth] by [cellHeight]. Lined up with the icons (the default), it is the
* distance from the cells' edges to the edges of the icons that would sit in them: the
* same box the launcher's own widgets draw in ([IconFrame]), so that every widget's edges
* meet the edges of the icons above, below and beside it. Otherwise it is Launcher3's own,
* which the Widget padding setting scales.
*/
@JvmStatic
fun appWidgetPadding(profile: DeviceProfile, cellWidth: Int, cellHeight: Int): Rect {
if (!profile.alignWidgetsWithIcons || cellWidth <= 0 || cellHeight <= 0) {
return profile.widgetPadding
}
// As in IconFrame: the icon is centred sideways in its cell and sits a set distance
// below the cell's top; what is seen of it leaves a little air inside its square.
val icon = profile.iconSizePx.toFloat()
val air = icon * (1f - ICON_VISIBLE_AREA_FACTOR) / 2f
val iconTop = if (profile.cellYPaddingPx >= 0) {
profile.cellYPaddingPx.toFloat()
} else {
max(0f, (cellHeight - profile.cellHeightPx) / 2f)
}
val side = max(0, ((cellWidth - icon) / 2f + air).roundToInt())
return Rect(
side,
max(0, (iconTop + air).roundToInt()),
side,
max(0, (cellHeight - iconTop - icon + air).roundToInt()),
)
}
/**
* Puts the launcher's own view into a widget host view when the widget is one of the
* launcher's, and returns it; returns null for every other widget. Launcher3's host view
* calls this in place of asking Android for the widget's views.
*/
@JvmStatic
fun attachIfOwn(host: LauncherAppWidgetHostView, appWidgetId: Int, info: AppWidgetProviderInfo?): View? {
val provider = info?.provider
if (!isOwn(provider)) return null
// A widget that still waits to be set up draws its own "tap to set up".
if (host is PendingAppWidgetHostView) return null
val pictures = provider == PictureWidgetProvider.componentName
var view: View? = null
for (i in host.childCount - 1 downTo 0) {
val child = host.getChildAt(i)
val wanted = if (pictures) child is PictureWidgetView else child is ClockWidgetView
if (wanted && view == null) view = child else host.removeViewAt(i)
}
if (view == null) {
view = if (pictures) PictureWidgetView(host.context) else ClockWidgetView(host.context)
host.addView(
view,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
),
)
}
when (view) {
is PictureWidgetView -> view.bind(appWidgetId)
is ClockWidgetView -> view.bind(appWidgetId)
}
return view
}
}