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>
120 lines
5.2 KiB
Kotlin
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
|
|
}
|
|
}
|