Skip to content

Commit 25efa5d

Browse files
committed
feat: CheckBox, RadioButton and Switch
Fix: ensure widget reload triggers on app update & cold start (device reboot)
1 parent 14d13d6 commit 25efa5d

28 files changed

+787
-50
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ tools/assets/App_Resources/Android/src/main/assets/ionicWebStart
5151
tools/assets/App_Resources/iOS/ionicWebModal
5252
tools/assets/App_Resources/Android/src/main/assets/ionicWebModal
5353
.nx/cache
54-
.nx/workspace-data
54+
.nx/workspace-data
55+
packages/widgets/platforms/android/widgets.aar

apps/demo/src/app.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Application, Http, ImageSource } from '@nativescript/core';
2-
import { registerWidgetListener, LinearLayout, ImageView, Image, Button, VStack, List, Text, updateWidget, Root, HStack, ButtonView, Flipper, Chronometer, Clock, Grid, ProgressBar, Stack, Spacer } from '@nativescript/widgets';
2+
import { registerWidgetListener, LinearLayout, ImageView, Image, Button, VStack, List, Text, updateWidget, Root, HStack, ButtonView, Flipper, Chronometer, Clock, Grid, ProgressBar, Stack, Spacer, Switch, CheckBox, RadioButton } from '@nativescript/widgets';
33
// uncomment to test Flutter
44
// import { init } from '@nativescript/flutter';
55
// init();
@@ -126,6 +126,45 @@ function gridWidget(provider: string, ids: number[]) {
126126
for (const id of ids) updateWidget(provider, root, id);
127127
}
128128

129+
// 6b. Toggle widget (single Switch)
130+
function toggleWidget(provider: string, ids: number[]) {
131+
const root = Root(() => VStack(() => [HStack(() => [Text('🔔 Notifications').setTextSize(16), Spacer(), Text('On').setTextSize(12)]), Spacer(12), HStack(() => [Text('Push alerts').setTextSize(14), Spacer(), Switch(true)]), Spacer(8), Text('Tap the switch to toggle').setColor('#95a5a6').setTextSize(11)]))
132+
.setBackgroundColor('#ffffff')
133+
.setPadding(12);
134+
135+
for (const id of ids) updateWidget(provider, root, id);
136+
}
137+
138+
// 6c. Checklist widget (multiple checkboxes)
139+
function checklistWidget(provider: string, ids: number[]) {
140+
const items = [
141+
{ label: 'Buy groceries', checked: true },
142+
{ label: 'Send invoices', checked: false },
143+
{ label: 'Workout', checked: false },
144+
];
145+
146+
const root = Root(() => VStack(() => [Text('🗒️ Today').setTextSize(18), Spacer(8), ...items.map((it) => HStack(() => [CheckBox(it.checked).onCheck('toggle', { label: it.label }), Spacer(8), Text(it.label).setTextSize(14)]).setMargin(6, 8, 6, 8))]))
147+
.setBackgroundColor('#ffffff')
148+
.setPadding(12);
149+
150+
for (const id of ids) updateWidget(provider, root, id);
151+
}
152+
153+
// 6d. Radio group widget (select one)
154+
function radioWidget(provider: string, ids: number[]) {
155+
const options = [
156+
{ label: 'Home', checked: true },
157+
{ label: 'Work', checked: false },
158+
{ label: 'Travel', checked: false },
159+
];
160+
161+
const root = Root(() => VStack(() => [Text('📍 Mode').setTextSize(18), Spacer(8), ...options.map((opt) => HStack(() => [RadioButton(opt.checked), Spacer(8), Text(opt.label).setTextSize(14)]).setMargin(6, 6, 6, 6))]))
162+
.setBackgroundColor('#ffffff')
163+
.setPadding(12);
164+
165+
for (const id of ids) updateWidget(provider, root, id);
166+
}
167+
129168
// 7. StackView (swipeable cards)
130169
function stackWidget(provider: string, ids: number[]) {
131170
const cards = [
@@ -196,15 +235,21 @@ registerWidgetListener('org.nativescript.plugindemo.PluginDemoWidgetProvider', {
196235
onClick(event) {
197236
console.log('Widget clicked', event);
198237
},
238+
onCheck(event) {
239+
console.log('Check toggled', event);
240+
},
199241
onUpdate: (event) => {
200242
// dashboardWidget(event.provider, event.appWidgetIds);
201243
//countdownWidget(event.provider, event.appWidgetIds);
202244
// clockWidget(event.provider, event.appWidgetIds);
203245
// progressWidget(event.provider, event.appWidgetIds);
204246
// listWidget(event.provider, event.appWidgetIds);
205247
// slideshowWidget(event.provider, event.appWidgetIds);
206-
gridWidget(event.provider, event.appWidgetIds);
248+
//gridWidget(event.provider, event.appWidgetIds);
207249
//stackWidget(event.provider, event.appWidgetIds);
250+
toggleWidget(event.provider, event.appWidgetIds);
251+
// checklistWidget(event.provider, event.appWidgetIds);
252+
// radioWidget(event.provider, event.appWidgetIds);
208253
/* const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
209254
const list = List(
210255
data.length,

packages/widgets/index.android.ts

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Utils, CoreTypes, Color, Length, ImageSource } from '@nativescript/core
22
import { layout } from '@nativescript/core/utils';
33

44
const native_ = Symbol('[[_native_]]');
5+
const ACTION_CLICK = 'org.nativescript.widgets.ACTION_CLICK';
6+
const ACTION_CHECK = 'org.nativescript.widgets.ACTION_CHECK';
57
export class WidgetManager {
68
[native_]: org.nativescript.widgets.AppWidgetManager;
79

@@ -51,6 +53,7 @@ interface IWidgetListener {
5153
onDeleted?: (event: { provider: string; appWidgetIds: number[] }) => void;
5254
onDisabled?: (provider: string) => void;
5355
onClick?: (event: { action: string; extras: Record<string, any>; provider: string }) => void;
56+
onCheck?: (event: { action: string; extras: Record<string, any>; provider: string }) => void;
5457
onResize?: (event: { provider: string; appWidgetId: number; minWidth: number; minHeight: number; maxWidth: number; maxHeight: number; manager: WidgetManager; widgetManager: PlatformWidgetManager }) => void;
5558
}
5659

@@ -145,13 +148,7 @@ const toBundle = (extras: Record<string, BundleValue | null | undefined>): andro
145148
const fromBundle = (bundle: android.os.Bundle): Record<string, any> => {
146149
const result: Record<string, any> = {};
147150
if (!bundle) return result;
148-
const keys = bundle.keySet();
149-
const iter = keys.iterator();
150-
while (iter.hasNext()) {
151-
const key = iter.next() as string;
152-
result[key] = bundle.get(key);
153-
}
154-
return result;
151+
return Utils.dataDeserialize(bundle);
155152
};
156153

157154
const toPxValue = (value: CoreTypes.FixedLengthType) => {
@@ -297,6 +294,11 @@ export class RemoteViews {
297294
return this;
298295
}
299296

297+
onCheck(action: string, extras?: Record<string, string | number | boolean>): this {
298+
this.native.onCheck(action, extras ? toBundle(extras) : null);
299+
return this;
300+
}
301+
300302
onItemClick(action: string, extras?: Record<string, string | number | boolean>): this {
301303
this.native.onItemClick(action, extras ? toBundle(extras) : null);
302304
return this;
@@ -621,6 +623,24 @@ export function Grid(columns: number, spacing?: number, content?: ViewBuilder):
621623
return container;
622624
}
623625

626+
export function Switch(checked: boolean) {
627+
const ret = new SwitchView();
628+
ret.setChecked(checked);
629+
return ret;
630+
}
631+
632+
export function CheckBox(checked: boolean) {
633+
const ret = new CheckBoxView();
634+
ret.setChecked(checked);
635+
return ret;
636+
}
637+
638+
export function RadioButton(checked: boolean) {
639+
const ret = new RadioButtonView();
640+
ret.setChecked(checked);
641+
return ret;
642+
}
643+
624644
export class RootLayoutView extends RemoteViews {
625645
constructor(id?: string) {
626646
super();
@@ -1052,6 +1072,54 @@ export class TextClockView extends RemoteViews {
10521072
}
10531073
}
10541074

1075+
export class CheckBoxView extends RemoteViews {
1076+
constructor(id?: string) {
1077+
super();
1078+
this[native_] = new org.nativescript.widgets.RemoteViews.CheckBox(id ?? null);
1079+
}
1080+
1081+
get native() {
1082+
return this[native_] as org.nativescript.widgets.RemoteViews.CheckBox;
1083+
}
1084+
1085+
setChecked(checked: boolean): this {
1086+
this.native.setChecked(checked);
1087+
return this;
1088+
}
1089+
}
1090+
1091+
export class RadioButtonView extends RemoteViews {
1092+
constructor(id?: string) {
1093+
super();
1094+
this[native_] = new org.nativescript.widgets.RemoteViews.RadioButton(id ?? null);
1095+
}
1096+
1097+
get native() {
1098+
return this[native_] as org.nativescript.widgets.RemoteViews.RadioButton;
1099+
}
1100+
1101+
setChecked(checked: boolean): this {
1102+
this.native.setChecked(checked);
1103+
return this;
1104+
}
1105+
}
1106+
1107+
export class SwitchView extends RemoteViews {
1108+
constructor(id?: string) {
1109+
super();
1110+
this[native_] = new org.nativescript.widgets.RemoteViews.Switch(id ?? null);
1111+
}
1112+
1113+
get native() {
1114+
return this[native_] as org.nativescript.widgets.RemoteViews.Switch;
1115+
}
1116+
1117+
setChecked(checked: boolean): this {
1118+
this.native.setChecked(checked);
1119+
return this;
1120+
}
1121+
}
1122+
10551123
function toJSArray(array: androidNative.Array<number>) {
10561124
const jsArray: number[] = [];
10571125
for (let i = 0; i < array.length; i++) {
@@ -1094,9 +1162,12 @@ export function registerWidgetListener(provider: string, listener: IWidgetListen
10941162
}
10951163
},
10961164
onAction(context, provider, action, extras) {
1097-
if (listener.onClick) {
1165+
if (action === ACTION_CLICK && listener.onClick) {
10981166
listener.onClick({ action, extras: extras ? fromBundle(extras) : {}, provider });
10991167
}
1168+
if (action === ACTION_CHECK && listener.onCheck) {
1169+
listener.onCheck({ action, extras: extras ? fromBundle(extras) : {}, provider });
1170+
}
11001171
},
11011172
onOptionsChanged(context, provider, appWidgetId, newOptions, manager, widgetManager) {
11021173
if (listener.onResize) {

packages/widgets/index.d.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CoreTypes, Color, ImageSource } from '@nativescript/core';
1+
import { CoreTypes, Color, ImageSource, Switch } from '@nativescript/core';
22

33
export class PlatformRemoteViews {
44
readonly native: any;
@@ -28,6 +28,7 @@ export class RemoteViews {
2828
setOnClickFillInIntent(intent: any): this;
2929

3030
onClick(action: string, extras?: Record<string, string | number | boolean>): this;
31+
onCheck(action: string, extras?: Record<string, string | number | boolean>): this;
3132

3233
onItemClick(action: string, extras?: Record<string, string | number | boolean>): this;
3334

@@ -49,6 +50,7 @@ interface IWidgetListener {
4950
onDeleted?: (event: { provider: string; appWidgetIds: number[] }) => void;
5051
onDisabled?: (provider: string) => void;
5152
onClick?: (event: { action: string; extras: Record<string, any>; provider: string }) => void;
53+
onCheck?: (event: { action: string; extras: Record<string, any>; provider: string }) => void;
5254
onResize?: (event: { provider: string; appWidgetId: number; minWidth: number; minHeight: number; maxWidth: number; maxHeight: number; manager: WidgetManager; widgetManager: PlatformWidgetManager }) => void;
5355
}
5456

@@ -174,6 +176,12 @@ export class SpacerView extends RemoteViews {
174176
setSize(size: CoreTypes.FixedLengthType): this;
175177
}
176178

179+
export class RadioButtonView extends RemoteViews {}
180+
181+
export class SwitchView extends RemoteViews {}
182+
183+
export class CheckBoxView extends RemoteViews {}
184+
177185
export class RootLayoutView extends RemoteViews {}
178186

179187
export function Root(content?: ViewBuilder): RemoteViews;
@@ -208,6 +216,12 @@ export function Stack(count?: number, content?: (index: number) => RemoteViews):
208216

209217
export function ForEach<T>(items: T[], id?: keyof T | ((item: T) => string | number), content?: (item: T, index: number) => RemoteViews): RemoteViews[];
210218

219+
export function Switch(checked: boolean): SwitchView;
220+
221+
export function CheckBox(checked: boolean): CheckBoxView;
222+
223+
export function RadioButton(checked: boolean): RadioButtonView;
224+
211225
export interface ViewModifers {
212226
padding?: {
213227
left?: number;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
33

4+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
5+
46
<application>
57
<service
68
android:name="org.nativescript.widgets.RemoteViewsListService"
79
android:exported="false"
810
android:permission="android.permission.BIND_REMOTEVIEWS" />
11+
12+
<receiver
13+
android:name="org.nativescript.widgets.AppWidgetReceiver"
14+
android:exported="true">
15+
<intent-filter>
16+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
17+
</intent-filter>
18+
</receiver>
919
</application>
1020

1121
</manifest>

0 commit comments

Comments
 (0)