@livx.cc/appwrap 0.51.0 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/app/shell/custom-webview.android.ts +39 -4
- package/runtime/modules-native/widget/App_Resources/Android/src/main/java/cc/livx/appwrap/AppwrapWidgetProvider.kt +45 -6
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/layout/appwrap_widget.xml +77 -24
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_colors.xml +10 -0
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_styles.xml +39 -12
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values-night/appwrap_widget_colors.xml +9 -0
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/xml/appwrap_widget_info.xml +5 -1
package/package.json
CHANGED
|
@@ -107,6 +107,14 @@ export class CustomWebView extends WebView {
|
|
|
107
107
|
// object, robust to NS re-wrapping the arg passed into the ChromeClient callback). WeakRef
|
|
108
108
|
// so a destroyed view can be GC'd; entry pruned on dispose.
|
|
109
109
|
private static registry = new Map<number, WeakRef<CustomWebView>>();
|
|
110
|
+
// Retired asset-serving WebViewClients (app/file loader ONLY — see disposeNativeView), kept
|
|
111
|
+
// process-alive so a shouldInterceptRequest already in flight on a background thread when the view is
|
|
112
|
+
// disposed can't fire against a GC'd JS peer → "Cannot find runtime for instance". Holding the (JS-peer)
|
|
113
|
+
// object here keeps NS's per-object runtime mapping resolvable. Server mode installs no interceptor, so
|
|
114
|
+
// its clients are NOT parked (all their callbacks are main-thread → safe to GC). Growth is bounded by
|
|
115
|
+
// the number of interceptor-bearing WebViews that dispose — note handlers-background can spin up an
|
|
116
|
+
// offscreen WebView per background task, so this is per-disposed-interceptor-view, not strictly one.
|
|
117
|
+
private static retiredClients = new Set<android.webkit.WebViewClient>();
|
|
110
118
|
static forNative(view: android.webkit.WebView): CustomWebView | undefined {
|
|
111
119
|
const owner = CustomWebView.registry.get(view.hashCode())?.deref();
|
|
112
120
|
// identityHashCode is not unique: on a (rare) collision the map holds the LAST writer —
|
|
@@ -155,9 +163,20 @@ export class CustomWebView extends WebView {
|
|
|
155
163
|
disposeNativeView(): void {
|
|
156
164
|
const wv = this.android as android.webkit.WebView | undefined;
|
|
157
165
|
if (wv) CustomWebView.registry.delete(wv.hashCode());
|
|
158
|
-
//
|
|
159
|
-
this.webViewClient = null;
|
|
166
|
+
// WebChromeClient (onJsPrompt) only fires on the main thread → safe to release for GC now.
|
|
160
167
|
this.webChromeClient = null;
|
|
168
|
+
// WebViewClient is different: shouldInterceptRequest can be dispatched on a BACKGROUND thread, so a
|
|
169
|
+
// request already in flight when the view tears down would fire AFTER dispose. Releasing the client's
|
|
170
|
+
// JS peer here would let that late off-thread call crash with "Cannot find runtime for instance".
|
|
171
|
+
// Park it in a process-lived set so the peer stays registered for any straggler callback — but ONLY
|
|
172
|
+
// when the interceptor is actually installed (app/file loader). Server mode installs no
|
|
173
|
+
// shouldInterceptRequest (see createAssetServingClient), so its client has no off-thread callback to
|
|
174
|
+
// outlive dispose: parking it would be pure dead retention (an accumulating leak across background-
|
|
175
|
+
// task WebViews), so just release it for GC.
|
|
176
|
+
if (this.webViewClient) {
|
|
177
|
+
if (SHELL_CONFIG.loader !== 'server') CustomWebView.retiredClients.add(this.webViewClient);
|
|
178
|
+
this.webViewClient = null;
|
|
179
|
+
}
|
|
161
180
|
super.disposeNativeView();
|
|
162
181
|
}
|
|
163
182
|
|
|
@@ -307,11 +326,27 @@ function createAssetServingClient(): android.webkit.WebViewClient {
|
|
|
307
326
|
return new android.webkit.WebResourceResponse(mimeFor(ext, rel), 'utf-8', 200, 'OK', outHeaders, pin);
|
|
308
327
|
};
|
|
309
328
|
|
|
329
|
+
// WebViewClient callbacks. All are dispatched on the UI/main thread EXCEPT shouldInterceptRequest,
|
|
330
|
+
// which Android runs on a BACKGROUND thread (ShouldInterceptRequestMediator). NativeScript resolves
|
|
331
|
+
// the JS peer for a callback's instance via a per-object runtime lookup (Runtime.getObjectRuntime);
|
|
332
|
+
// if that peer is momentarily unregistered — freshly created and not yet mapped on boot, or disposed
|
|
333
|
+
// and GC'd on relaunch / RESTART-from-crash — the off-thread call throws
|
|
334
|
+
// `NativeScriptException: Cannot find runtime for instance` (the red NS crash screen). NS marshals a
|
|
335
|
+
// background-thread call to the main thread fine ONCE the peer is mapped, so this is a registration/
|
|
336
|
+
// lifetime race, not a plain GC one — which is why the earlier strong-JS-ref fix didn't cover the boot path.
|
|
337
|
+
//
|
|
338
|
+
// In server mode (loader:'server') the page loads from a real https `serverUrl`: there are NO
|
|
339
|
+
// https://appwrap.local requests and `backendOrigin` is same-origin, so the asset interceptor does
|
|
340
|
+
// ZERO work — yet it is the ONLY off-main-thread callback, i.e. the only one exposed to that race.
|
|
341
|
+
// So OMIT it in server mode (the conditional spread expands to nothing): Android's default pure-Java
|
|
342
|
+
// interception runs, NS is never crossed into JS off-thread, and the crash is structurally impossible.
|
|
343
|
+
// app/file loaders MUST serve bundled www/ assets, so they keep it (and retain the client for the
|
|
344
|
+
// process lifetime — see CustomWebView.disposeNativeView — so a late in-flight call can't hit a dead peer).
|
|
310
345
|
// NS runtime adds `.extend` on Java classes; it's not in the static typings, hence the cast.
|
|
311
346
|
assetClientClass = (android.webkit.WebViewClient as any).extend({
|
|
312
347
|
// Modern (API 21+) overload passes a WebResourceRequest; the legacy overload passed a string URL.
|
|
313
348
|
// We defensively handle both, so the param is the union.
|
|
314
|
-
shouldInterceptRequest(
|
|
349
|
+
...(SHELL_CONFIG.loader === 'server' ? {} : { shouldInterceptRequest(
|
|
315
350
|
_view: android.webkit.WebView,
|
|
316
351
|
request: android.webkit.WebResourceRequest | string
|
|
317
352
|
): android.webkit.WebResourceResponse | null {
|
|
@@ -343,7 +378,7 @@ function createAssetServingClient(): android.webkit.WebViewClient {
|
|
|
343
378
|
return new android.webkit.WebResourceResponse(
|
|
344
379
|
'text/plain', 'utf-8', 404, 'Not Found', new java.util.HashMap<string, string>(), emptyStream()
|
|
345
380
|
);
|
|
346
|
-
},
|
|
381
|
+
} }),
|
|
347
382
|
|
|
348
383
|
// External URL schemes (mailto:/tel:/sms:/geo:/intent:/webcal:/market:/…) can't be rendered by the
|
|
349
384
|
// WebView — without this override it tries to LOAD them and dumps a full-page net::ERR_UNKNOWN_URL_SCHEME
|
|
@@ -5,7 +5,13 @@ import android.appwidget.AppWidgetManager
|
|
|
5
5
|
import android.appwidget.AppWidgetProvider
|
|
6
6
|
import android.content.Context
|
|
7
7
|
import android.content.Intent
|
|
8
|
+
import android.graphics.Bitmap
|
|
8
9
|
import android.graphics.BitmapFactory
|
|
10
|
+
import android.graphics.BitmapShader
|
|
11
|
+
import android.graphics.Canvas
|
|
12
|
+
import android.graphics.Paint
|
|
13
|
+
import android.graphics.RectF
|
|
14
|
+
import android.graphics.Shader
|
|
9
15
|
import android.net.Uri
|
|
10
16
|
import android.view.View
|
|
11
17
|
import android.widget.RemoteViews
|
|
@@ -15,12 +21,14 @@ import java.io.File
|
|
|
15
21
|
/**
|
|
16
22
|
* Home-screen launcher widget (Android parity of the iOS WidgetKit launcher). Reads the payload the
|
|
17
23
|
* `widget.publish` handler wrote into SharedPreferences ("appwrap.widget" / key "appwrap.widget") and
|
|
18
|
-
* renders up to
|
|
19
|
-
* are PNGs the handler saved into the app files dir. Resource ids are looked up by
|
|
20
|
-
* needs no compile-time reference to the host app's R class.
|
|
24
|
+
* renders up to 12 icon-grid tiles across 4 columns; each tile deep-links back into the app via its
|
|
25
|
+
* entry's URL. Icons are PNGs the handler saved into the app files dir. Resource ids are looked up by
|
|
26
|
+
* name so the module needs no compile-time reference to the host app's R class.
|
|
21
27
|
*/
|
|
22
28
|
class AppwrapWidgetProvider : AppWidgetProvider() {
|
|
23
|
-
private val
|
|
29
|
+
private val columns = 4
|
|
30
|
+
private val tiles = (0 until 12).map { "tile$it" }
|
|
31
|
+
private val rows = (0 until 3).map { "row$it" }
|
|
24
32
|
|
|
25
33
|
override fun onUpdate(ctx: Context, mgr: AppWidgetManager, ids: IntArray) {
|
|
26
34
|
for (id in ids) render(ctx, mgr, id)
|
|
@@ -29,6 +37,27 @@ class AppwrapWidgetProvider : AppWidgetProvider() {
|
|
|
29
37
|
private fun res(ctx: Context, type: String, name: String) =
|
|
30
38
|
ctx.resources.getIdentifier(name, type, ctx.packageName)
|
|
31
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Round an app icon to look native on the home screen (iOS-parity: iOS clips to a
|
|
42
|
+
* RoundedRectangle ~24% of the icon size). RemoteViews ImageViews can't clip via an outline, so we
|
|
43
|
+
* center-crop the source to a square and redraw it into a rounded bitmap at full source resolution
|
|
44
|
+
* — the widget host then downscales a crisp, antialiased image rather than a hard-edged square.
|
|
45
|
+
*/
|
|
46
|
+
private fun rounded(src: Bitmap): Bitmap {
|
|
47
|
+
val size = minOf(src.width, src.height)
|
|
48
|
+
val sq = if (src.width != src.height)
|
|
49
|
+
Bitmap.createBitmap(src, (src.width - size) / 2, (src.height - size) / 2, size, size)
|
|
50
|
+
else src
|
|
51
|
+
val out = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
|
|
52
|
+
val canvas = Canvas(out)
|
|
53
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
54
|
+
shader = BitmapShader(sq, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
|
|
55
|
+
}
|
|
56
|
+
val r = size * 0.22f
|
|
57
|
+
canvas.drawRoundRect(RectF(0f, 0f, size.toFloat(), size.toFloat()), r, r, paint)
|
|
58
|
+
return out
|
|
59
|
+
}
|
|
60
|
+
|
|
32
61
|
private fun render(ctx: Context, mgr: AppWidgetManager, id: Int) {
|
|
33
62
|
val rv = RemoteViews(ctx.packageName, res(ctx, "layout", "appwrap_widget"))
|
|
34
63
|
val raw = ctx.getSharedPreferences("appwrap.widget", Context.MODE_PRIVATE)
|
|
@@ -39,10 +68,20 @@ class AppwrapWidgetProvider : AppWidgetProvider() {
|
|
|
39
68
|
rv.setViewVisibility(res(ctx, "id", "appwrap_widget_title"),
|
|
40
69
|
if (title.isEmpty()) View.GONE else View.VISIBLE)
|
|
41
70
|
val entries = payload?.optJSONArray("entries")
|
|
71
|
+
val count = entries?.length() ?: 0
|
|
72
|
+
|
|
73
|
+
// A row with no entries at all is GONE so it contributes no height (the grid stays top-packed
|
|
74
|
+
// and leftover widget height falls to the bottom). A row that is only PARTLY filled stays
|
|
75
|
+
// VISIBLE and hides its trailing tiles as INVISIBLE, so a short final row keeps its tiles in
|
|
76
|
+
// the leading columns rather than centring them (iOS Grid's short-final-row behaviour).
|
|
77
|
+
for (r in rows.indices) {
|
|
78
|
+
rv.setViewVisibility(res(ctx, "id", rows[r]),
|
|
79
|
+
if (r * columns < count) View.VISIBLE else View.GONE)
|
|
80
|
+
}
|
|
42
81
|
|
|
43
82
|
for (i in tiles.indices) {
|
|
44
83
|
val tileId = res(ctx, "id", tiles[i])
|
|
45
|
-
val e = if (entries != null && i <
|
|
84
|
+
val e = if (entries != null && i < count) entries.optJSONObject(i) else null
|
|
46
85
|
if (e == null) { rv.setViewVisibility(tileId, View.INVISIBLE); continue }
|
|
47
86
|
rv.setViewVisibility(tileId, View.VISIBLE)
|
|
48
87
|
rv.setTextViewText(res(ctx, "id", "${tiles[i]}_label"), e.optString("label"))
|
|
@@ -57,7 +96,7 @@ class AppwrapWidgetProvider : AppWidgetProvider() {
|
|
|
57
96
|
if (icon.isNotEmpty()) {
|
|
58
97
|
val f = File(ctx.filesDir, icon)
|
|
59
98
|
if (f.exists()) BitmapFactory.decodeFile(f.absolutePath)
|
|
60
|
-
?.let { rv.setImageViewBitmap(res(ctx, "id", "${tiles[i]}_icon"), it) }
|
|
99
|
+
?.let { rv.setImageViewBitmap(res(ctx, "id", "${tiles[i]}_icon"), rounded(it)) }
|
|
61
100
|
}
|
|
62
101
|
val link = e.optString("deepLink", "")
|
|
63
102
|
if (link.isNotEmpty()) {
|
package/runtime/modules-native/widget/App_Resources/Android/src/main/res/layout/appwrap_widget.xml
CHANGED
|
@@ -1,32 +1,36 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<!-- AppWrap launcher widget: title + a
|
|
3
|
-
AppwrapWidgetProvider from the published payload
|
|
4
|
-
|
|
2
|
+
<!-- AppWrap launcher widget: title + a 4-column icon grid of deep-linked tiles (up to 3 rows / 12
|
|
3
|
+
tiles), populated by AppwrapWidgetProvider from the published payload.
|
|
4
|
+
|
|
5
|
+
LAYOUT CONTRACT (iOS parity — see AppwrapWidget.swift `gridView`):
|
|
6
|
+
* Rows are CONTENT-HEIGHT (wrap_content, NO layout_weight). The root is a vertical
|
|
7
|
+
LinearLayout at match_parent, so rows stack from the TOP and any leftover widget height
|
|
8
|
+
falls to the BOTTOM — the "app folder" feel. Giving rows `0dp + weight=1` (as this layout
|
|
9
|
+
once did) makes them greedily split the full height and float each icon to the centre of
|
|
10
|
+
its band, which strands the last row at the bottom behind a large dead gap on a
|
|
11
|
+
vertically-resized widget. Don't reintroduce weights here.
|
|
12
|
+
* A partially-filled row keeps its trailing tiles INVISIBLE (not GONE) so the short final row
|
|
13
|
+
stays aligned to the LEADING columns instead of centring. A fully-empty row is set GONE by
|
|
14
|
+
the provider so it consumes no height. -->
|
|
5
15
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
6
16
|
android:layout_width="match_parent"
|
|
7
17
|
android:layout_height="match_parent"
|
|
8
18
|
android:orientation="vertical"
|
|
9
|
-
android:background="
|
|
19
|
+
android:background="@color/appwrap_widget_bg"
|
|
10
20
|
android:padding="10dp">
|
|
11
21
|
|
|
12
22
|
<TextView
|
|
13
23
|
android:id="@+id/appwrap_widget_title"
|
|
14
24
|
android:layout_width="wrap_content"
|
|
15
25
|
android:layout_height="wrap_content"
|
|
16
|
-
android:textColor="
|
|
26
|
+
android:textColor="@color/appwrap_widget_title"
|
|
17
27
|
android:textStyle="bold"
|
|
18
28
|
android:textSize="12sp"
|
|
19
|
-
android:layout_marginBottom="
|
|
29
|
+
android:layout_marginBottom="8dp"
|
|
20
30
|
android:maxLines="1"
|
|
21
31
|
android:ellipsize="end" />
|
|
22
32
|
|
|
23
|
-
<LinearLayout
|
|
24
|
-
android:layout_width="match_parent"
|
|
25
|
-
android:layout_height="0dp"
|
|
26
|
-
android:layout_weight="1"
|
|
27
|
-
android:orientation="horizontal"
|
|
28
|
-
android:weightSum="2">
|
|
29
|
-
|
|
33
|
+
<LinearLayout android:id="@+id/row0" style="@style/AppwrapWidgetRow">
|
|
30
34
|
<LinearLayout android:id="@+id/tile0" style="@style/AppwrapWidgetTile">
|
|
31
35
|
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
32
36
|
<ImageView android:id="@+id/tile0_icon" style="@style/AppwrapWidgetIcon" />
|
|
@@ -34,7 +38,6 @@
|
|
|
34
38
|
</FrameLayout>
|
|
35
39
|
<TextView android:id="@+id/tile0_label" style="@style/AppwrapWidgetLabel" />
|
|
36
40
|
</LinearLayout>
|
|
37
|
-
|
|
38
41
|
<LinearLayout android:id="@+id/tile1" style="@style/AppwrapWidgetTile">
|
|
39
42
|
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
40
43
|
<ImageView android:id="@+id/tile1_icon" style="@style/AppwrapWidgetIcon" />
|
|
@@ -42,15 +45,6 @@
|
|
|
42
45
|
</FrameLayout>
|
|
43
46
|
<TextView android:id="@+id/tile1_label" style="@style/AppwrapWidgetLabel" />
|
|
44
47
|
</LinearLayout>
|
|
45
|
-
</LinearLayout>
|
|
46
|
-
|
|
47
|
-
<LinearLayout
|
|
48
|
-
android:layout_width="match_parent"
|
|
49
|
-
android:layout_height="0dp"
|
|
50
|
-
android:layout_weight="1"
|
|
51
|
-
android:orientation="horizontal"
|
|
52
|
-
android:weightSum="2">
|
|
53
|
-
|
|
54
48
|
<LinearLayout android:id="@+id/tile2" style="@style/AppwrapWidgetTile">
|
|
55
49
|
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
56
50
|
<ImageView android:id="@+id/tile2_icon" style="@style/AppwrapWidgetIcon" />
|
|
@@ -58,7 +52,6 @@
|
|
|
58
52
|
</FrameLayout>
|
|
59
53
|
<TextView android:id="@+id/tile2_label" style="@style/AppwrapWidgetLabel" />
|
|
60
54
|
</LinearLayout>
|
|
61
|
-
|
|
62
55
|
<LinearLayout android:id="@+id/tile3" style="@style/AppwrapWidgetTile">
|
|
63
56
|
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
64
57
|
<ImageView android:id="@+id/tile3_icon" style="@style/AppwrapWidgetIcon" />
|
|
@@ -67,4 +60,64 @@
|
|
|
67
60
|
<TextView android:id="@+id/tile3_label" style="@style/AppwrapWidgetLabel" />
|
|
68
61
|
</LinearLayout>
|
|
69
62
|
</LinearLayout>
|
|
63
|
+
<LinearLayout android:id="@+id/row1" style="@style/AppwrapWidgetRow">
|
|
64
|
+
<LinearLayout android:id="@+id/tile4" style="@style/AppwrapWidgetTile">
|
|
65
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
66
|
+
<ImageView android:id="@+id/tile4_icon" style="@style/AppwrapWidgetIcon" />
|
|
67
|
+
<TextView android:id="@+id/tile4_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
68
|
+
</FrameLayout>
|
|
69
|
+
<TextView android:id="@+id/tile4_label" style="@style/AppwrapWidgetLabel" />
|
|
70
|
+
</LinearLayout>
|
|
71
|
+
<LinearLayout android:id="@+id/tile5" style="@style/AppwrapWidgetTile">
|
|
72
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
73
|
+
<ImageView android:id="@+id/tile5_icon" style="@style/AppwrapWidgetIcon" />
|
|
74
|
+
<TextView android:id="@+id/tile5_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
75
|
+
</FrameLayout>
|
|
76
|
+
<TextView android:id="@+id/tile5_label" style="@style/AppwrapWidgetLabel" />
|
|
77
|
+
</LinearLayout>
|
|
78
|
+
<LinearLayout android:id="@+id/tile6" style="@style/AppwrapWidgetTile">
|
|
79
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
80
|
+
<ImageView android:id="@+id/tile6_icon" style="@style/AppwrapWidgetIcon" />
|
|
81
|
+
<TextView android:id="@+id/tile6_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
82
|
+
</FrameLayout>
|
|
83
|
+
<TextView android:id="@+id/tile6_label" style="@style/AppwrapWidgetLabel" />
|
|
84
|
+
</LinearLayout>
|
|
85
|
+
<LinearLayout android:id="@+id/tile7" style="@style/AppwrapWidgetTile">
|
|
86
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
87
|
+
<ImageView android:id="@+id/tile7_icon" style="@style/AppwrapWidgetIcon" />
|
|
88
|
+
<TextView android:id="@+id/tile7_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
89
|
+
</FrameLayout>
|
|
90
|
+
<TextView android:id="@+id/tile7_label" style="@style/AppwrapWidgetLabel" />
|
|
91
|
+
</LinearLayout>
|
|
92
|
+
</LinearLayout>
|
|
93
|
+
<LinearLayout android:id="@+id/row2" style="@style/AppwrapWidgetRow">
|
|
94
|
+
<LinearLayout android:id="@+id/tile8" style="@style/AppwrapWidgetTile">
|
|
95
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
96
|
+
<ImageView android:id="@+id/tile8_icon" style="@style/AppwrapWidgetIcon" />
|
|
97
|
+
<TextView android:id="@+id/tile8_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
98
|
+
</FrameLayout>
|
|
99
|
+
<TextView android:id="@+id/tile8_label" style="@style/AppwrapWidgetLabel" />
|
|
100
|
+
</LinearLayout>
|
|
101
|
+
<LinearLayout android:id="@+id/tile9" style="@style/AppwrapWidgetTile">
|
|
102
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
103
|
+
<ImageView android:id="@+id/tile9_icon" style="@style/AppwrapWidgetIcon" />
|
|
104
|
+
<TextView android:id="@+id/tile9_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
105
|
+
</FrameLayout>
|
|
106
|
+
<TextView android:id="@+id/tile9_label" style="@style/AppwrapWidgetLabel" />
|
|
107
|
+
</LinearLayout>
|
|
108
|
+
<LinearLayout android:id="@+id/tile10" style="@style/AppwrapWidgetTile">
|
|
109
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
110
|
+
<ImageView android:id="@+id/tile10_icon" style="@style/AppwrapWidgetIcon" />
|
|
111
|
+
<TextView android:id="@+id/tile10_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
112
|
+
</FrameLayout>
|
|
113
|
+
<TextView android:id="@+id/tile10_label" style="@style/AppwrapWidgetLabel" />
|
|
114
|
+
</LinearLayout>
|
|
115
|
+
<LinearLayout android:id="@+id/tile11" style="@style/AppwrapWidgetTile">
|
|
116
|
+
<FrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="4dp">
|
|
117
|
+
<ImageView android:id="@+id/tile11_icon" style="@style/AppwrapWidgetIcon" />
|
|
118
|
+
<TextView android:id="@+id/tile11_badge" style="@style/AppwrapWidgetBadge" android:visibility="gone" />
|
|
119
|
+
</FrameLayout>
|
|
120
|
+
<TextView android:id="@+id/tile11_label" style="@style/AppwrapWidgetLabel" />
|
|
121
|
+
</LinearLayout>
|
|
122
|
+
</LinearLayout>
|
|
70
123
|
</LinearLayout>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Widget chrome colors. Split into values/ + values-night/ so the tile follows the host's theme
|
|
3
|
+
(iOS parity: containerBackground(.fill.tertiary) + .primary label adapt automatically; a
|
|
4
|
+
hardcoded white slab with #333 text stayed stark-white on a dark home screen). The widget host
|
|
5
|
+
inflates RemoteViews with its own configuration, so -night resolves without any provider code. -->
|
|
6
|
+
<resources>
|
|
7
|
+
<color name="appwrap_widget_bg">#F2FFFFFF</color>
|
|
8
|
+
<color name="appwrap_widget_title">#222222</color>
|
|
9
|
+
<color name="appwrap_widget_label">#333333</color>
|
|
10
|
+
</resources>
|
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<resources>
|
|
3
|
-
<!-- One
|
|
3
|
+
<!-- One grid row: 4 equal columns, CONTENT-HEIGHT. The absence of a layout_weight is load-bearing
|
|
4
|
+
(see the layout's contract comment) — a weighted row splits the widget height and floats its
|
|
5
|
+
icons mid-band, leaving a dead gap on a tall widget. -->
|
|
6
|
+
<style name="AppwrapWidgetRow">
|
|
7
|
+
<item name="android:layout_width">match_parent</item>
|
|
8
|
+
<item name="android:layout_height">wrap_content</item>
|
|
9
|
+
<item name="android:orientation">horizontal</item>
|
|
10
|
+
<item name="android:weightSum">4</item>
|
|
11
|
+
<item name="android:layout_marginBottom">10dp</item>
|
|
12
|
+
</style>
|
|
13
|
+
<!-- One icon-grid tile: a centered icon over a single-line label, equal-weight in its row.
|
|
14
|
+
Height is wrap_content so the row measures to content, not to the widget's full height. -->
|
|
4
15
|
<style name="AppwrapWidgetTile">
|
|
5
16
|
<item name="android:layout_width">0dp</item>
|
|
6
|
-
<item name="android:layout_height">
|
|
17
|
+
<item name="android:layout_height">wrap_content</item>
|
|
7
18
|
<item name="android:layout_weight">1</item>
|
|
8
19
|
<item name="android:orientation">vertical</item>
|
|
9
|
-
<item name="android:gravity">
|
|
20
|
+
<item name="android:gravity">center_horizontal</item>
|
|
10
21
|
<item name="android:padding">4dp</item>
|
|
11
22
|
</style>
|
|
23
|
+
<!-- The provider rounds the bitmap itself (RemoteViews ImageView can't clip via outline), so the
|
|
24
|
+
source is drawn crisp at full res and just scaled to fit here — no fitCenter squashing. -->
|
|
25
|
+
<!-- The icon carries a top margin + SYMMETRIC start/end margins so the wrap_content badge-container
|
|
26
|
+
FrameLayout measures LARGER than the 44dp icon. The badge (pinned top|end of that padded
|
|
27
|
+
container) lands in the end-margin region — visually BEYOND the icon's top-right corner (iOS
|
|
28
|
+
offset(x:6,y:-6) parity) — yet stays WITHIN the container bounds, so RemoteViews' default
|
|
29
|
+
child-clipping never cuts it. marginStart mirrors marginEnd so the icon stays HORIZONTALLY
|
|
30
|
+
CENTERED within the FrameLayout (and thus under its label) — an end-only margin shifts the
|
|
31
|
+
icon left, leaving an asymmetric right gap.
|
|
32
|
+
SIZE: 44dp + 14dp of margins = 58dp, + 8dp tile padding = 66dp per column. At 4 columns that
|
|
33
|
+
needs ~284dp of widget width, which is why appwrap_widget_info declares minResizeWidth 280dp
|
|
34
|
+
— shrinking below that would start clipping the icons. -->
|
|
12
35
|
<style name="AppwrapWidgetIcon">
|
|
13
|
-
<item name="android:layout_width">
|
|
14
|
-
<item name="android:layout_height">
|
|
15
|
-
<item name="android:
|
|
36
|
+
<item name="android:layout_width">44dp</item>
|
|
37
|
+
<item name="android:layout_height">44dp</item>
|
|
38
|
+
<item name="android:layout_marginTop">7dp</item>
|
|
39
|
+
<item name="android:layout_marginStart">7dp</item>
|
|
40
|
+
<item name="android:layout_marginEnd">7dp</item>
|
|
41
|
+
<item name="android:scaleType">fitXY</item>
|
|
16
42
|
</style>
|
|
17
|
-
<!-- Unread-count bubble
|
|
43
|
+
<!-- Unread-count bubble overlapping the icon's top-right corner (iOS-parity: small, circular for
|
|
44
|
+
single digits, red fill, white ring, bold white number). ~40% of the 44dp icon. -->
|
|
18
45
|
<style name="AppwrapWidgetBadge">
|
|
19
46
|
<item name="android:layout_width">wrap_content</item>
|
|
20
|
-
<item name="android:layout_height">
|
|
47
|
+
<item name="android:layout_height">17dp</item>
|
|
21
48
|
<item name="android:layout_gravity">top|end</item>
|
|
22
|
-
<item name="android:minWidth">
|
|
23
|
-
<item name="android:paddingLeft">
|
|
24
|
-
<item name="android:paddingRight">
|
|
49
|
+
<item name="android:minWidth">17dp</item>
|
|
50
|
+
<item name="android:paddingLeft">4dp</item>
|
|
51
|
+
<item name="android:paddingRight">4dp</item>
|
|
25
52
|
<item name="android:background">@drawable/appwrap_badge_bg</item>
|
|
26
53
|
<item name="android:gravity">center</item>
|
|
27
54
|
<item name="android:textColor">#FFFFFF</item>
|
|
@@ -32,7 +59,7 @@
|
|
|
32
59
|
<style name="AppwrapWidgetLabel">
|
|
33
60
|
<item name="android:layout_width">wrap_content</item>
|
|
34
61
|
<item name="android:layout_height">wrap_content</item>
|
|
35
|
-
<item name="android:textColor"
|
|
62
|
+
<item name="android:textColor">@color/appwrap_widget_label</item>
|
|
36
63
|
<item name="android:textSize">11sp</item>
|
|
37
64
|
<item name="android:maxLines">1</item>
|
|
38
65
|
<item name="android:ellipsize">end</item>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Dark-theme counterpart of values/appwrap_widget_colors.xml (same names, resolved by the host's
|
|
3
|
+
night configuration). Translucent near-black slab + light text, mirroring the light variant's
|
|
4
|
+
alpha so the tile reads as the same material on either theme. -->
|
|
5
|
+
<resources>
|
|
6
|
+
<color name="appwrap_widget_bg">#E61C1C1E</color>
|
|
7
|
+
<color name="appwrap_widget_title">#FFFFFF</color>
|
|
8
|
+
<color name="appwrap_widget_label">#E6FFFFFF</color>
|
|
9
|
+
</resources>
|
package/runtime/modules-native/widget/App_Resources/Android/src/main/res/xml/appwrap_widget_info.xml
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<!-- Launcher widget: ~4×2 cells (medium), resizable, initial layout = the icon-grid. updatePeriodMillis
|
|
3
|
-
0 → the app drives refreshes itself via widget.publish (AppWidgetManager broadcast).
|
|
3
|
+
0 → the app drives refreshes itself via widget.publish (AppWidgetManager broadcast).
|
|
4
|
+
minResizeWidth 280dp: the grid is 4 fixed-width columns (~66dp each — see AppwrapWidgetIcon),
|
|
5
|
+
so ~284dp is the narrowest width that renders without clipping the icons. minWidth stays 250dp
|
|
6
|
+
(it only gates where the widget may be PLACED); minResizeWidth is what bounds a user drag. -->
|
|
4
7
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
5
8
|
android:minWidth="250dp"
|
|
6
9
|
android:minHeight="110dp"
|
|
10
|
+
android:minResizeWidth="280dp"
|
|
7
11
|
android:targetCellWidth="4"
|
|
8
12
|
android:targetCellHeight="2"
|
|
9
13
|
android:updatePeriodMillis="0"
|