Author SHA1 Message Date
MrBlakeandClaude Opus 5.5 4b5c9bdf85 widgets: reload data on update, clock widget, picker previews; README screenshots; 0.2.0
- Glance update() only recomposed the running session, so saved styles (opacity, colours)
  and new forecasts never reached placed widgets; a state version now triggers a reload
- new minimal clock widget (live TextClock, date, current weather), transparent by default
- static preview images for the widget picker, generated previews on Android 15+
- debug builds install next to the release app (app.wetter.debug)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:51:16 +02:00
MrBlakeandClaude Opus 5.5 d1d8d01dd0 build: release script for Obtainium-compatible Gitea releases
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 14:41:38 +02:00
29 changed files with 251 additions and 22 deletions
+19 -2
View File
@@ -20,9 +20,26 @@ A weather app with the look and feel of Pixel Weather: an animated sky that chan
- **No account, no tracking, no analytics.** Everything stays on the device. Coordinates are rounded to two decimals (about 1 km) before they reach the weather service.
- **Open data sources only:** [Open-Meteo](https://open-meteo.com) for forecasts, 15-minute precipitation, air quality, pollen and place search (CC BY 4.0), and [Nominatim / OpenStreetMap](https://nominatim.org) for naming your current location.
- **Features today:** current location and saved cities (swipe between them), search with a full preview before saving, precipitation-in-the-next-hours card, a rule-based daily summary, day detail view, units and theme settings, light and dark mode.
- **Five home-screen widgets:** a weather card that grows from 3×2 to 4×5 (hourly panel, then a daily list), a pill, a tilted pill, a one-line bar with a cookie badge, and a classic card. Each one can be customised when added or later: location, system/light/dark, colours (wallpaper Material You, current weather, or five fixed palettes, all WCAG AA checked) and background opacity from 100 % down to fully transparent.
- **Six home-screen widgets:** a minimal clock with date and current weather (transparent by default), a weather card that grows from 3×2 to 4×5 (hourly panel, then a daily list), a pill, a tilted pill, a one-line bar with a cookie badge, and a classic card. The widget picker shows real previews. Each one can be customised when added or later: location, system/light/dark, colours (wallpaper Material You, current weather, or five fixed palettes, all WCAG AA checked) and background opacity from 100 % down to fully transparent.
- **Coming next:** severe weather alerts (DWD / MeteoAlarm), precipitation radar map, card reordering, notifications.
<p align="center">
<img src="docs/screenshots/S01-clear-day.png" width="200" alt="Home, sunny">
<img src="docs/screenshots/S01-clear-night-dark.png" width="200" alt="Home, clear night, dark mode">
<img src="docs/screenshots/S01-rain-nowcast.png" width="200" alt="Rain with the next-hours precipitation card">
</p>
<p align="center">
<img src="docs/screenshots/S01-full-light.png" width="200" alt="Full forecast with detail tiles">
<img src="docs/screenshots/S04-day-dark.png" width="200" alt="Day detail">
<img src="docs/screenshots/S08-settings.png" width="200" alt="Settings">
</p>
<p align="center">
<img src="docs/screenshots/widgets.png" width="380" alt="Home-screen widgets: clock, weather card, pill, bar, classic, tilted pill">
</p>
<sub>Screenshots are rendered from a real Open-Meteo response (Berlin) by the app's screenshot tests; widgets are rendered on a Pixel with the wallpaper colours of that phone.</sub>
Requires Android 10 (API 29) or newer.
## Install with Obtainium
@@ -93,7 +110,7 @@ cd wetter
./gradlew :app:recordRoborazziDebug # re-render replica/clone-screens/
```
Local release builds are signed with the debug key so they install over each other. Published releases will use a dedicated signing key.
Release builds are signed with the project key from `~/.android/nogoogleapps-release.properties` (kept outside the repository); without it they fall back to the debug key for local testing. `scripts/release.sh <app>` builds, signs and publishes a release in the format Obtainium expects.
## Repository layout
Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Build, sign and publish one app as a Gitea release that Obtainium can find.
# Usage: scripts/release.sh wetter (version comes from app/build.gradle.kts)
# Needs: .giteatoken in the repo root (git-ignored) and ~/.android/nogoogleapps-release.properties
set -euo pipefail
cd "$(dirname "$0")/.."
app="${1:?app folder, e.g. wetter}"
title="$(tr '[:lower:]' '[:upper:]' <<< "${app:0:1}")${app:1}"
version="$(sed -nE 's/.*versionName = "([^"]+)".*/\1/p' "$app/app/build.gradle.kts")"
tag="$app-v$version"
api="https://git.mrblake.cc/api/v1/repos/MrBlake/NoGooleApps"
token="$(tr -d ' \n' < .giteatoken)"
[ -f ~/.android/nogoogleapps-release.properties ] || { echo "release key missing" >&2; exit 1; }
(cd "$app" && ./gradlew -q :app:assembleRelease)
apk="$app/app/build/outputs/apk/release/app-release.apk"
"${ANDROID_HOME:-$HOME/Android/Sdk}"/build-tools/36.0.0/apksigner verify "$apk"
git -c credential.helper= push -q "https://MrBlake:$token@git.mrblake.cc/MrBlake/NoGooleApps.git" HEAD:main
body="$(printf '{"tag_name":"%s","target_commitish":"%s","name":"%s %s","prerelease":true}' "$tag" "$(git rev-parse HEAD)" "$title" "$version")"
id="$(curl -sf -X POST -H "Authorization: token $token" -H "Content-Type: application/json" -d "$body" "$api/releases" \
| python3 -I -c 'import sys,json; print(json.load(sys.stdin)["id"])')"
curl -sf -X POST -H "Authorization: token $token" \
-F "attachment=@$apk;type=application/vnd.android.package-archive" \
"$api/releases/$id/assets?name=$app-$version.apk" > /dev/null
echo "published $title $version ($tag)"
+8 -2
View File
@@ -17,8 +17,8 @@ android {
applicationId = "app.wetter"
minSdk = 29
targetSdk = 36
versionCode = 1
versionName = "0.1.0"
versionCode = 2
versionName = "0.2.0"
}
// Release key lives outside the repo; without it, release builds fall back to the debug key (local only).
@@ -36,7 +36,13 @@ android {
}
buildTypes {
debug {
// installs next to the release app from Obtainium
applicationIdSuffix = ".debug"
resValue("string", "app_name", "Wetter (Debug)")
}
release {
resValue("string", "app_name", "Wetter")
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug")
@@ -50,6 +50,7 @@ class WidgetGalleryActivity : androidx.activity.ComponentActivity() {
PillWidget() to DpSize(170.dp, 64.dp),
),
)
if (intent.getBooleanExtra("export", false)) { exportPreviews(column); return }
lifecycleScope.launch {
for ((widget, size) in pages[page.coerceIn(0, pages.lastIndex)]) {
val views = widget.compose(this@WidgetGalleryActivity, size = size)
@@ -60,6 +61,48 @@ class WidgetGalleryActivity : androidx.activity.ComponentActivity() {
}
}
/** Renders each widget at its default size and writes preview_<key>.png to external files. */
private fun exportPreviews(column: LinearLayout) {
val items = listOf(
"card" to (WeatherCardWidget() to DpSize(300.dp, 230.dp)),
"pill" to (PillWidget() to DpSize(280.dp, 120.dp)),
"tilt" to (TiltWidget() to DpSize(160.dp, 160.dp)),
"bar" to (BarWidget() to DpSize(300.dp, 80.dp)),
"classic" to (ClassicWidget() to DpSize(300.dp, 170.dp)),
"clock" to (app.wetter.widget.ClockWidget() to DpSize(300.dp, 140.dp)),
)
lifecycleScope.launch {
val frames = mutableListOf<Pair<String, FrameLayout>>()
for ((key, pair) in items) {
val (widget, size) = pair
val frame = FrameLayout(this@WidgetGalleryActivity)
if (key == "clock") {
// the clock is transparent: show it over a soft wallpaper-like backdrop
app.wetter.widget.WidgetPreviewOverride.config =
app.wetter.data.WidgetConfigEntity(-1, null, "wallpaper", "dark", 0)
frame.background = android.graphics.drawable.GradientDrawable(
android.graphics.drawable.GradientDrawable.Orientation.TL_BR,
intArrayOf(Color.rgb(74, 98, 120), Color.rgb(150, 128, 112)),
).apply { cornerRadius = px(28).toFloat() }
}
val views = widget.compose(this@WidgetGalleryActivity, size = size)
app.wetter.widget.WidgetPreviewOverride.config = null
frame.addView(views.apply(this@WidgetGalleryActivity, frame))
column.addView(frame, LinearLayout.LayoutParams(px(size.width.value), px(size.height.value)).apply { bottomMargin = px(12) })
frames += key to frame
}
column.postDelayed({
val dir = getExternalFilesDir(null)!!
for ((key, frame) in frames) {
val bmp = android.graphics.Bitmap.createBitmap(frame.width, frame.height, android.graphics.Bitmap.Config.ARGB_8888)
frame.draw(android.graphics.Canvas(bmp))
java.io.File(dir, "preview_$key.png").outputStream().use { bmp.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, it) }
}
android.util.Log.i("WidgetGallery", "exported ${frames.size}")
}, 1500)
}
}
private fun px(dp: Float) = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics).toInt()
private fun px(dp: Int) = px(dp.toFloat())
}
+4
View File
@@ -29,6 +29,10 @@
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /></intent-filter>
</activity>
<receiver android:name=".widget.ClockReceiver" android:exported="true" android:label="@string/widget_clock_name">
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_clock" />
</receiver>
<receiver android:name=".widget.WeatherCardReceiver" android:exported="true" android:label="@string/widget_card_name">
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/widget_card" />
@@ -100,7 +100,7 @@ class WidgetConfigActivity : ComponentActivity() {
ConfigScreen(appWidgetId, widget, size) { config ->
lifecycleScope.launch {
Graph.repo.saveWidgetConfig(config)
runCatching { widget.update(this@WidgetConfigActivity, GlanceAppWidgetManager(this@WidgetConfigActivity).getGlanceIdBy(appWidgetId)) }
runCatching { refreshWidget(this@WidgetConfigActivity, widget, GlanceAppWidgetManager(this@WidgetConfigActivity).getGlanceIdBy(appWidgetId)) }
setResult(RESULT_OK, Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId))
finish()
}
@@ -114,6 +114,7 @@ class WidgetConfigActivity : ComponentActivity() {
providerClass.endsWith("PillReceiver") -> PillWidget() to DpSize(300.dp, 130.dp)
providerClass.endsWith("TiltReceiver") -> TiltWidget() to DpSize(170.dp, 170.dp)
providerClass.endsWith("BarReceiver") -> BarWidget() to DpSize(320.dp, 84.dp)
providerClass.endsWith("ClockReceiver") -> ClockWidget() to DpSize(320.dp, 150.dp)
providerClass.endsWith("ClassicReceiver") -> ClassicWidget() to DpSize(320.dp, 160.dp)
else -> WeatherCardWidget() to DpSize(320.dp, 240.dp)
}
@@ -125,7 +126,10 @@ class WidgetConfigActivity : ComponentActivity() {
private fun ConfigScreen(appWidgetId: Int, widget: GlanceAppWidget, size: DpSize, onDone: (WidgetConfigEntity) -> Unit) {
val context = LocalContext.current
val locations by Graph.repo.locations.collectAsState(initial = emptyList())
var config by remember { mutableStateOf(WidgetConfigEntity(appWidgetId, null)) }
var config by remember {
val d = (widget as? WeatherWidget)?.defaultStyle ?: WidgetStyle()
mutableStateOf(WidgetConfigEntity(appWidgetId, null, d.palette, d.theme, d.opacity))
}
var loaded by remember { mutableStateOf(false) }
LaunchedEffect(appWidgetId) {
Graph.repo.widgetConfig(appWidgetId)?.let { config = it }
@@ -54,22 +54,46 @@ import app.wetter.data.conditionOf
import app.wetter.ui.components.conditionLabel
import kotlinx.coroutines.flow.first
import kotlin.reflect.KClass
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.glance.appwidget.state.updateAppWidgetState
import androidx.glance.currentState
private val DATA_VERSION = longPreferencesKey("data_version")
/** All widget types, paired with their receivers. */
val ALL_WIDGETS: List<Pair<() -> WeatherWidget, KClass<out GlanceAppWidgetReceiver>>> = listOf(
::WeatherCardWidget to WeatherCardReceiver::class,
::PillWidget to PillReceiver::class,
::TiltWidget to TiltReceiver::class,
::BarWidget to BarReceiver::class,
::ClassicWidget to ClassicReceiver::class,
::ClockWidget to ClockReceiver::class,
)
/**
* Glance's update() only recomposes a running session, it does not run provideGlance again.
* Bumping a value in the widget's state makes the composition reload settings and forecast.
*/
suspend fun refreshWidget(context: Context, widget: GlanceAppWidget, id: GlanceId) {
updateAppWidgetState(context, id) { it[DATA_VERSION] = System.nanoTime() }
widget.update(context, id)
}
suspend fun updateAllWidgets(context: Context) {
WeatherCardWidget().updateAll(context)
PillWidget().updateAll(context)
TiltWidget().updateAll(context)
BarWidget().updateAll(context)
ClassicWidget().updateAll(context)
val manager = GlanceAppWidgetManager(context)
for ((make, receiver) in ALL_WIDGETS) {
val widget = make()
manager.getGlanceIds(widget.javaClass).forEach { runCatching { refreshWidget(context, widget, it) } }
}
}
/** Picker previews (Android 15+). The system rate-limits this, so failures are ignored. */
suspend fun publishWidgetPreviews(context: Context) {
if (Build.VERSION.SDK_INT < 35) return
val manager = GlanceAppWidgetManager(context)
listOf<KClass<out GlanceAppWidgetReceiver>>(
WeatherCardReceiver::class, PillReceiver::class, TiltReceiver::class, BarReceiver::class, ClassicReceiver::class,
).forEach { runCatching { manager.setWidgetPreviews(it) } }
ALL_WIDGETS.forEach { (_, receiver) -> runCatching { manager.setWidgetPreviews(receiver) } }
}
class WidgetData(val location: LocationEntity?, val forecast: Forecast?, val units: Units, val palette: WidgetPalette) {
@@ -81,7 +105,7 @@ object WidgetPreviewOverride {
@Volatile var config: app.wetter.data.WidgetConfigEntity? = null
}
suspend fun loadWidgetData(context: Context, id: GlanceId?): WidgetData {
suspend fun loadWidgetData(context: Context, id: GlanceId?, defaultStyle: WidgetStyle = WidgetStyle()): WidgetData {
Graph.init(context)
// ids that are not real widgets (previews, the debug gallery) fall back to the current location
val appWidgetId = id?.let { runCatching { GlanceAppWidgetManager(context).getAppWidgetId(it) }.getOrNull() } ?: -1
@@ -89,19 +113,28 @@ suspend fun loadWidgetData(context: Context, id: GlanceId?): WidgetData {
val location = config?.locationId?.let { Graph.repo.location(it) } ?: Graph.repo.widgetLocation(appWidgetId)
val forecast = location?.let { Graph.repo.forecastOnce(it.id) }
val settings = runCatching { Graph.settings.settings.first() }.getOrDefault(Settings())
val style = config?.let { WidgetStyle(it.style, it.theme, it.opacity) } ?: WidgetStyle()
val style = config?.let { WidgetStyle(it.style, it.theme, it.opacity) } ?: defaultStyle
return WidgetData(location, forecast, Units(settings), resolvePalette(context, style, forecast))
}
/** Base: loads data once per update, renders [Content], and offers the same content as picker preview. */
abstract class WeatherWidget : GlanceAppWidget() {
/** Style used until the person configures this widget. */
open val defaultStyle: WidgetStyle get() = WidgetStyle()
override suspend fun provideGlance(context: Context, id: GlanceId) {
val data = loadWidgetData(context, id)
provideContent { Root(data) }
val first = loadWidgetData(context, id, defaultStyle)
provideContent {
val version = currentState(DATA_VERSION) ?: 0L
val data by produceState(first, version) { if (version != 0L) value = loadWidgetData(context, id, defaultStyle) }
Root(data)
}
}
override suspend fun providePreview(context: Context, widgetCategory: Int) {
val data = loadWidgetData(context, null)
// a transparent widget would vanish against the picker, so previews always get a background
val style = defaultStyle.let { if (it.opacity < 100) it.copy(opacity = 100) else it }
val data = loadWidgetData(context, null, style)
provideContent { Root(data) }
}
@@ -404,4 +437,37 @@ class WeatherCardReceiver : GlanceAppWidgetReceiver() { override val glanceAppWi
class PillReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = PillWidget() }
class TiltReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = TiltWidget() }
class BarReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = BarWidget() }
class ClockReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = ClockWidget() }
class ClassicReceiver : GlanceAppWidgetReceiver() { override val glanceAppWidget: GlanceAppWidget = ClassicWidget() }
// ---------------------------------------------------------------------------------------------
// 6. Clock: big live time, date and the current weather. Transparent by default, for a clean home screen
// ---------------------------------------------------------------------------------------------
class ClockWidget : WeatherWidget() {
override val sizeMode = SizeMode.Exact
override val defaultStyle = WidgetStyle(palette = WidgetStyle.WALLPAPER, theme = WidgetStyle.DARK, opacity = 0)
@Composable
override fun Content(data: WidgetData, f: Forecast) {
val ctx = LocalContext.current
val p = data.palette
val u = data.units
val size = LocalSize.current
// on a bare wallpaper white text with a soft shadow reads best; with a background use the palette
val fg = if (p.bg.alpha < .3f) Color.White else p.onBg
val timeSp = (size.height.value * .42f).coerceIn(40f, 96f)
val views = android.widget.RemoteViews(ctx.packageName, R.layout.widget_clock).apply {
val c = android.graphics.Color.argb((fg.alpha * 255).toInt(), (fg.red * 255).toInt(), (fg.green * 255).toInt(), (fg.blue * 255).toInt())
listOf(R.id.clock_time, R.id.clock_date, R.id.clock_sep, R.id.clock_temp).forEach { setTextColor(it, c) }
setTextViewTextSize(R.id.clock_time, android.util.TypedValue.COMPLEX_UNIT_SP, timeSp)
setImageViewBitmap(R.id.clock_icon, WidgetArt.icon(conditionOf(f.current.code), f.current.isDay, 96))
setTextViewText(R.id.clock_temp, u.temp(f.current.temp))
setContentDescription(R.id.clock_temp, "${u.temp(f.current.temp)} ${conditionText(f)}")
}
Box(
GlanceModifier.fillMaxSize().cornerRadius(28.dp).background(p.bg).clickable(open(data)),
contentAlignment = Alignment.Center,
) { androidx.glance.appwidget.AndroidRemoteViews(views) }
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:gravity="center" android:padding="12dp">
<TextClock
android:id="@+id/clock_time"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:format24Hour="HH:mm" android:format12Hour="h:mm"
android:fontFamily="sans-serif-light" android:textSize="72sp" android:includeFontPadding="false"
android:letterSpacing="-0.02"
android:shadowColor="#66000000" android:shadowRadius="8" android:shadowDy="2" />
<LinearLayout
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:gravity="center_vertical" android:layout_marginTop="4dp">
<TextClock
android:id="@+id/clock_date"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:format24Hour="EEE, d. MMM" android:format12Hour="EEE, MMM d"
android:fontFamily="sans-serif-medium" android:textSize="16sp"
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
<TextView
android:id="@+id/clock_sep"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text=" · " android:textSize="16sp"
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
<ImageView
android:id="@+id/clock_icon"
android:layout_width="22dp" android:layout_height="22dp" android:importantForAccessibility="no" />
<TextView
android:id="@+id/clock_temp"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:fontFamily="sans-serif-medium" android:textSize="16sp"
android:shadowColor="#66000000" android:shadowRadius="6" android:shadowDy="1" />
</LinearLayout>
</LinearLayout>
+2 -1
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Wetter</string>
<string name="cond_clear_day">Sonnig</string>
<string name="cond_clear_night">Klar</string>
@@ -133,6 +132,8 @@
<string-array name="widget_palettes">
<item>Wallpaper</item><item>Wetterlage</item><item>Ozean</item><item>Wald</item><item>Sonne</item><item>Lavendel</item><item>Graphit</item>
</string-array>
<string name="widget_clock_name">Uhr &amp; Wetter</string>
<string name="widget_clock_desc">Große Uhrzeit, Datum und das aktuelle Wetter</string>
<string name="widget_source">Quelle: Open-Meteo</string>
<string name="widget_pick">Ort wählen</string>
<string name="widget_follow">Mein Standort (folgt dir)</string>
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_bar_desc"
android:previewImage="@drawable/preview_bar"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="250dp" android:minHeight="60dp"
android:targetCellWidth="4" android:targetCellHeight="1"
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_card_desc"
android:previewImage="@drawable/preview_card"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="250dp" android:minHeight="180dp"
android:targetCellWidth="4" android:targetCellHeight="2"
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_classic_desc"
android:previewImage="@drawable/preview_classic"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="250dp" android:minHeight="110dp"
android:minWidth="250dp" android:minHeight="150dp"
android:targetCellWidth="4" android:targetCellHeight="2"
android:minResizeWidth="140dp" android:minResizeHeight="110dp"
android:minResizeWidth="140dp" android:minResizeHeight="150dp"
android:maxResizeWidth="400dp" android:maxResizeHeight="250dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="3600000"
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_clock_desc"
android:initialLayout="@layout/glance_default_loading_layout"
android:previewImage="@drawable/preview_clock"
android:minWidth="250dp" android:minHeight="110dp"
android:targetCellWidth="4" android:targetCellHeight="2"
android:minResizeWidth="180dp" android:minResizeHeight="80dp"
android:maxResizeWidth="600dp" android:maxResizeHeight="300dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="3600000"
android:configure="app.wetter.widget.WidgetConfigActivity"
android:widgetFeatures="reconfigurable|configuration_optional"
android:widgetCategory="home_screen|keyguard" />
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_pill_desc"
android:previewImage="@drawable/preview_pill"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="200dp" android:minHeight="110dp"
android:targetCellWidth="4" android:targetCellHeight="2"
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/widget_tilt_desc"
android:previewImage="@drawable/preview_tilt"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="110dp" android:minHeight="110dp"
android:targetCellWidth="2" android:targetCellHeight="2"