├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── docs.yml ├── .gitignore ├── .watchmanconfig ├── .yarn └── releases │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle.properties └── src │ ├── main │ ├── AndroidManifest.xml │ ├── AndroidManifestNew.xml │ ├── java │ │ └── com │ │ │ └── reactnativeandroidwidget │ │ │ ├── AndroidWidgetModule.java │ │ │ ├── AndroidWidgetPackage.java │ │ │ ├── RNWidget.java │ │ │ ├── RNWidgetBackgroundTaskWorker.java │ │ │ ├── RNWidgetCollectionService.java │ │ │ ├── RNWidgetConfigurationActivity.java │ │ │ ├── RNWidgetJsCommunication.java │ │ │ ├── RNWidgetProvider.java │ │ │ ├── RNWidgetUtil.java │ │ │ ├── builder │ │ │ ├── ClickableView.java │ │ │ ├── CollectionView.java │ │ │ ├── CollectionViewItem.java │ │ │ ├── WidgetFactory.java │ │ │ ├── WidgetWithViews.java │ │ │ └── widget │ │ │ │ ├── BaseLayoutWidget.java │ │ │ │ ├── BaseWidget.java │ │ │ │ ├── FrameLayoutWidget.java │ │ │ │ ├── IconWidget.java │ │ │ │ ├── ImageWidget.java │ │ │ │ ├── LinearLayoutWidget.java │ │ │ │ ├── ListWidget.java │ │ │ │ ├── RootWidget.java │ │ │ │ ├── SvgWidget.java │ │ │ │ ├── TextWidget.java │ │ │ │ └── utils │ │ │ │ └── ResourceUtils.java │ │ │ └── oss │ │ │ └── HeadlessJsTaskWorker.java │ └── res │ │ └── layout │ │ ├── rn_widget.xml │ │ ├── rn_widget_clickable.xml │ │ ├── rn_widget_list.xml │ │ └── rn_widget_list_item.xml │ ├── newarch │ └── AndroidWidgetSpec.java │ └── oldarch │ └── AndroidWidgetSpec.java ├── app.plugin.ts ├── babel.config.js ├── docs ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api │ │ ├── _category_.json │ │ ├── register-widget-configuration-screen.md │ │ ├── register-widget-task-handler.md │ │ ├── request-widget-update-by-id.md │ │ ├── request-widget-update.md │ │ └── widget-preview.md │ ├── demo.md │ ├── handling-clicks.md │ ├── index.md │ ├── limitations.md │ ├── primitives │ │ ├── _category_.json │ │ ├── flex-widget.md │ │ ├── icon-widget.md │ │ ├── image-widget.md │ │ ├── list-widget.md │ │ ├── overlap-widget.md │ │ ├── svg-widget.md │ │ └── text-widget.md │ ├── tutorial │ │ ├── _category_.json │ │ ├── congratulations.md │ │ ├── make-widget-configurable.md │ │ ├── register-task-handler.md │ │ ├── register-widget-expo.md │ │ ├── register-widget.md │ │ ├── try-it-our.md │ │ ├── widget-design.md │ │ └── widget-preview.md │ └── update-widget.md ├── docusaurus.config.ts ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── index.tsx ├── static │ ├── .nojekyll │ ├── img │ │ ├── HelloWidgetPreview.png │ │ ├── favicon.png │ │ ├── hello_preview.png │ │ ├── logo.png │ │ ├── og-image.png │ │ ├── undraw_android_jr64.svg │ │ ├── undraw_journey_re_ec5q.svg │ │ └── undraw_mobile_prototyping_grmd.svg │ └── video.mp4 ├── tsconfig.json └── yarn.lock ├── example-expo ├── .gitignore ├── .yarn │ └── releases │ │ └── yarn-3.6.4.cjs ├── .yarnrc.yml ├── App.tsx ├── app.config.ts ├── assets │ ├── adaptive-icon.png │ ├── fonts │ │ ├── Ndot-55.otf │ │ ├── material.ttf │ │ └── material_outlined.otf │ ├── icon.png │ ├── splash.png │ └── widget-preview │ │ ├── clickdemo.png │ │ ├── counter.png │ │ ├── debugevents.png │ │ ├── fitness.png │ │ ├── list.png │ │ ├── resizable.png │ │ ├── rotated.png │ │ └── shopify.png ├── babel.config.js ├── eas.json ├── index.ts ├── metro.config.js ├── package.json ├── tsconfig.json └── yarn.lock ├── example ├── .bundle │ └── config ├── .watchmanconfig ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ ├── release.keystore │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── assets │ │ │ └── fonts │ │ │ │ ├── Ndot-55.otf │ │ │ │ ├── material.ttf │ │ │ │ └── material_outlined.otf │ │ │ ├── java │ │ │ └── com │ │ │ │ └── androidwidgetexample │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainApplication.kt │ │ │ │ ├── WidgetConfigurationActivity.java │ │ │ │ └── widget │ │ │ │ ├── ClickDemo.java │ │ │ │ ├── Configurable.kt │ │ │ │ ├── Counter.java │ │ │ │ ├── DebugEvents.java │ │ │ │ ├── Fitness.java │ │ │ │ ├── List.java │ │ │ │ ├── Resizable.java │ │ │ │ ├── Rotated.java │ │ │ │ └── Shopify.java │ │ │ └── res │ │ │ ├── drawable │ │ │ ├── clickdemo_preview.png │ │ │ ├── counter_preview.png │ │ │ ├── debugevents_preview.png │ │ │ ├── fitness_preview.png │ │ │ ├── list_preview.png │ │ │ ├── resizable_preview.png │ │ │ ├── rn_edit_text_material.xml │ │ │ ├── rotated_preview.png │ │ │ └── shopify_preview.png │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ │ └── xml │ │ │ ├── widgetprovider_clickdemo.xml │ │ │ ├── widgetprovider_configurable.xml │ │ │ ├── widgetprovider_counter.xml │ │ │ ├── widgetprovider_debugevents.xml │ │ │ ├── widgetprovider_fitness.xml │ │ │ ├── widgetprovider_list.xml │ │ │ ├── widgetprovider_resizable.xml │ │ │ ├── widgetprovider_rotated.xml │ │ │ └── widgetprovider_shopify.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── assets │ ├── SVG_Logo.svg │ └── music │ │ ├── Electric_Ladyland.jpg │ │ ├── FMacRumours.png │ │ ├── believe.png │ │ └── sinatra.jpg ├── babel.config.js ├── index.js ├── metro.config.js ├── package.json ├── react-native.config.js ├── src │ ├── App.tsx │ ├── WidgetConfigurationScreen.tsx │ ├── index.ts │ ├── linking.config.ts │ ├── screens │ │ ├── BorderScreen.tsx │ │ ├── CounterScreen.tsx │ │ ├── FlexScreen.tsx │ │ ├── ListScreen.tsx │ │ ├── SvgScreen.tsx │ │ ├── TextScreen.tsx │ │ └── widget-preview │ │ │ ├── ClickDemoWidgetPreviewScreen.tsx │ │ │ ├── DebugEventsWidgetPreviewScreen.tsx │ │ │ ├── FitnessWidgetPreviewScreen.tsx │ │ │ ├── ListDemoWidgetPreviewDeepLinkScreen.tsx │ │ │ ├── ListDemoWidgetPreviewScreen.tsx │ │ │ ├── ResizableMusicWidgetPreviewScreen.tsx │ │ │ ├── RotatedWidgetPreviewScreen.tsx │ │ │ └── ShopifyWidgetPreviewScreen.tsx │ ├── widgetTaskHandler.tsx │ └── widgets │ │ ├── ClickDemoWidget.tsx │ │ ├── ConfigurableWidget.tsx │ │ ├── CounterWidget.tsx │ │ ├── DebugEventsWidget.tsx │ │ ├── FitnessWidget.tsx │ │ ├── ListDemoWidget.tsx │ │ ├── ResizableMusicWidget.tsx │ │ ├── RotatedWidget.tsx │ │ └── ShopifyWidget.tsx ├── tsconfig.json └── yarn.lock ├── lefthook.yml ├── package.json ├── scripts └── bootstrap.js ├── src ├── AndroidWidget.ts ├── NativeAndroidWidget.ts ├── __tests__ │ ├── index.test.tsx │ └── style.utils.test.ts ├── api │ ├── WidgetPreview.tsx │ ├── build-widget-tree.ts │ ├── get-widget-info.ts │ ├── private.types.ts │ ├── register-widget-configuration-screen.tsx │ ├── register-widget-task-handler.tsx │ ├── request-widget-update-by-id.tsx │ ├── request-widget-update.tsx │ └── types.ts ├── config-plugin.type.ts ├── index.ts └── widgets │ ├── FlexWidget.tsx │ ├── IconWidget.tsx │ ├── ImageWidget.tsx │ ├── ListWidget.tsx │ ├── OverlapWidget.tsx │ ├── SvgWidget.tsx │ ├── TextWidget.tsx │ └── utils │ ├── click-action.ts │ ├── common-internal.props.ts │ ├── style.props.ts │ └── style.utils.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.{java,xml}] 18 | 19 | indent_style = space 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to gh-pages 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v3 13 | 14 | - name: Install and Build 🔧 15 | run: | 16 | yarn install 17 | yarn install --cwd example 18 | yarn install --cwd docs 19 | yarn --cwd docs build 20 | 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@v4 23 | with: 24 | folder: docs/build 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | .kotlin/ 43 | 44 | # Ruby 45 | example/vendor/ 46 | 47 | # node.js 48 | # 49 | node_modules/ 50 | npm-debug.log 51 | yarn-debug.log 52 | yarn-error.log 53 | 54 | # BUCK 55 | buck-out/ 56 | \.buckd/ 57 | android/app/libs 58 | android/keystores/debug.keystore 59 | 60 | # Expo 61 | .expo/* 62 | 63 | # generated by bob 64 | lib/ 65 | app.plugin.js 66 | 67 | # docs 68 | docs/docs/public-api/ 69 | 70 | # Temporary files created by Metro to check the health of the file watcher 71 | .metro-health-check* 72 | 73 | # testing 74 | /coverage 75 | 76 | # Yarn 77 | **/.yarn/* 78 | !**/.yarn/patches 79 | !**/.yarn/plugins 80 | !**/.yarn/releases 81 | !**/.yarn/sdks 82 | !**/.yarn/versions 83 | 84 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stefan Aleksovski 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-android-widget 2 | 3 | Build Android Widgets with React Native 4 | 5 | ## Installation 6 | 7 | npm: 8 | 9 | ```sh 10 | npm install --save react-native-android-widget 11 | ``` 12 | 13 | yarn: 14 | 15 | ```sh 16 | yarn add react-native-android-widget 17 | ``` 18 | 19 | ## Usage 20 | 21 | For usage see the [documentation](https://saleksovski.github.io/react-native-android-widget/). 22 | 23 | ## Demo 24 | 25 | https://github.com/sAleksovski/react-native-android-widget/assets/7473800/68a0afc7-27a1-4d27-bcdd-4f15f4894313 26 | 27 | You can also download the demo app from the [Releases](https://github.com/sAleksovski/react-native-android-widget/releases) page. 28 | 29 | ## Contributing 30 | 31 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 32 | 33 | ## License 34 | 35 | MIT 36 | 37 | --- 38 | 39 | Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) 40 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:7.2.1" 9 | } 10 | } 11 | 12 | def isNewArchitectureEnabled() { 13 | return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true" 14 | } 15 | 16 | apply plugin: 'com.android.library' 17 | 18 | if (isNewArchitectureEnabled()) { 19 | apply plugin: 'com.facebook.react' 20 | } 21 | 22 | def getExtOrDefault(name) { 23 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['AndroidWidget_' + name] 24 | } 25 | 26 | def getExtOrIntegerDefault(name) { 27 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['AndroidWidget_' + name]).toInteger() 28 | } 29 | 30 | def supportsNamespace() { 31 | def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.') 32 | def major = parsed[0].toInteger() 33 | def minor = parsed[1].toInteger() 34 | 35 | // Namespace support was added in 7.3.0 36 | if (major == 7 && minor >= 3) { 37 | return true 38 | } 39 | 40 | return major >= 8 41 | } 42 | 43 | android { 44 | if (supportsNamespace()) { 45 | namespace "com.reactnativeandroidwidget" 46 | 47 | sourceSets { 48 | main { 49 | manifest.srcFile "src/main/AndroidManifestNew.xml" 50 | } 51 | } 52 | } 53 | 54 | compileSdkVersion getExtOrIntegerDefault('compileSdkVersion') 55 | 56 | defaultConfig { 57 | minSdkVersion getExtOrIntegerDefault('minSdkVersion') 58 | targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') 59 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() 60 | } 61 | buildTypes { 62 | release { 63 | minifyEnabled false 64 | } 65 | } 66 | 67 | lintOptions { 68 | disable 'GradleCompatible' 69 | } 70 | 71 | compileOptions { 72 | sourceCompatibility JavaVersion.VERSION_1_8 73 | targetCompatibility JavaVersion.VERSION_1_8 74 | } 75 | 76 | sourceSets { 77 | main { 78 | if (isNewArchitectureEnabled()) { 79 | java.srcDirs += [ 80 | "src/newarch", 81 | // This is needed to build Kotlin project with NewArch enabled 82 | "${project.buildDir}/generated/source/codegen/java" 83 | ] 84 | } else { 85 | java.srcDirs += ["src/oldarch"] 86 | } 87 | } 88 | } 89 | } 90 | 91 | repositories { 92 | mavenCentral() 93 | google() 94 | } 95 | 96 | 97 | dependencies { 98 | // For < 0.71, this will be from the local maven repo 99 | // For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin 100 | //noinspection GradleDynamicVersion 101 | implementation "com.facebook.react:react-native:+" 102 | // From node_modules 103 | implementation 'androidx.concurrent:concurrent-futures:1.1.0' 104 | implementation 'androidx.work:work-runtime:2.8.1' 105 | implementation 'com.caverock:androidsvg-aar:1.4' 106 | } 107 | 108 | if (isNewArchitectureEnabled()) { 109 | react { 110 | jsRootDir = file("../src/") 111 | libraryName = "AndroidWidget" 112 | codegenJavaPackageName = "com.reactnativeandroidwidget" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | AndroidWidget_kotlinVersion=2.0.21 2 | AndroidWidget_minSdkVersion=21 3 | AndroidWidget_targetSdkVersion=33 4 | AndroidWidget_compileSdkVersion=33 5 | AndroidWidget_ndkversion=27.1.12297006 6 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifestNew.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/AndroidWidgetModule.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import android.app.Activity; 4 | import android.appwidget.AppWidgetManager; 5 | import android.content.Intent; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import com.facebook.react.bridge.Promise; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.bridge.ReactMethod; 12 | import com.facebook.react.bridge.ReadableMap; 13 | import com.facebook.react.bridge.WritableArray; 14 | import com.facebook.react.bridge.WritableMap; 15 | 16 | public class AndroidWidgetModule extends com.reactnativeandroidwidget.AndroidWidgetSpec { 17 | public static final String NAME = "AndroidWidget"; 18 | 19 | AndroidWidgetModule(ReactApplicationContext context) { 20 | super(context); 21 | } 22 | 23 | @Override 24 | @NonNull 25 | public String getName() { 26 | return NAME; 27 | } 28 | 29 | @ReactMethod 30 | public void drawWidget(ReadableMap config, String widgetName) { 31 | try { 32 | new RNWidget(getReactApplicationContext(), config, widgetName).drawWidgets(); 33 | } catch (Exception e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | @ReactMethod 39 | public void drawWidgetById(ReadableMap config, String widgetName, double widgetId) { 40 | try { 41 | new RNWidget(getReactApplicationContext(), config, widgetName).drawWidget((int) widgetId); 42 | } catch (Exception e) { 43 | e.printStackTrace(); 44 | } 45 | } 46 | 47 | @ReactMethod 48 | public void createPreview(ReadableMap config, double width, double height, Promise promise) { 49 | try { 50 | WritableMap preview = new RNWidget(getReactApplicationContext(), config).createPreview((int) width, (int) height); 51 | promise.resolve(preview); 52 | } catch (Exception e) { 53 | e.printStackTrace(); 54 | promise.reject(e); 55 | } 56 | } 57 | 58 | @ReactMethod 59 | public void getWidgetInfo(String widgetName, Promise promise) { 60 | WritableArray widgetInfo = RNWidgetUtil.getWidgetInfo(getReactApplicationContext(), widgetName); 61 | promise.resolve(widgetInfo); 62 | } 63 | 64 | @ReactMethod 65 | public void finishWidgetConfiguration(double appWidgetId, String result) { 66 | Activity activity = getReactApplicationContext().getCurrentActivity(); 67 | Intent intent = new Intent(); 68 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, (int) appWidgetId); 69 | activity.setResult("ok".equals(result) ? Activity.RESULT_OK : Activity.RESULT_CANCELED, intent); 70 | activity.finish(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/AndroidWidgetPackage.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import com.facebook.react.TurboReactPackage; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.module.model.ReactModuleInfo; 9 | import com.facebook.react.module.model.ReactModuleInfoProvider; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class AndroidWidgetPackage extends TurboReactPackage { 15 | 16 | @Nullable 17 | @Override 18 | public NativeModule getModule(String name, ReactApplicationContext reactContext) { 19 | if (name.equals(AndroidWidgetModule.NAME)) { 20 | return new AndroidWidgetModule(reactContext); 21 | } else { 22 | return null; 23 | } 24 | } 25 | 26 | @Override 27 | public ReactModuleInfoProvider getReactModuleInfoProvider() { 28 | return () -> { 29 | final Map moduleInfos = new HashMap<>(); 30 | boolean isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 31 | moduleInfos.put( 32 | AndroidWidgetModule.NAME, 33 | new ReactModuleInfo( 34 | AndroidWidgetModule.NAME, 35 | AndroidWidgetModule.NAME, 36 | false, // canOverrideExistingModule 37 | false, // needsEagerInit 38 | true, // hasConstants 39 | false, // isCxxModule 40 | isTurboModule // isTurboModule 41 | ) 42 | ); 43 | return moduleInfos; 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/RNWidgetBackgroundTaskWorker.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.work.Data; 8 | import androidx.work.WorkerParameters; 9 | 10 | import com.facebook.react.bridge.Arguments; 11 | import com.facebook.react.jstasks.HeadlessJsTaskConfig; 12 | import com.reactnativeandroidwidget.oss.HeadlessJsTaskWorker; 13 | 14 | import java.util.HashMap; 15 | import java.util.Map; 16 | 17 | public class RNWidgetBackgroundTaskWorker extends HeadlessJsTaskWorker { 18 | 19 | public RNWidgetBackgroundTaskWorker( 20 | @NonNull Context context, 21 | @NonNull WorkerParameters params) { 22 | super(context, params); 23 | } 24 | 25 | @Nullable 26 | @Override 27 | protected HeadlessJsTaskConfig getTaskConfig(Data data) { 28 | Map arguments = new HashMap<>(data.getKeyValueMap()); 29 | arguments.put("screenInfo", RNWidgetUtil.getScreenInfo(getApplicationContext()).toHashMap()); 30 | 31 | return new HeadlessJsTaskConfig( 32 | "RNWidgetBackgroundTask", 33 | Arguments.makeNativeMap(arguments), 34 | 30 * 1000, 35 | true 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/RNWidgetConfigurationActivity.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import android.appwidget.AppWidgetManager; 4 | import android.appwidget.AppWidgetProviderInfo; 5 | import android.os.Bundle; 6 | 7 | import com.facebook.react.ReactActivity; 8 | import com.facebook.react.ReactActivityDelegate; 9 | import com.facebook.react.bridge.Arguments; 10 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 11 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 12 | 13 | public class RNWidgetConfigurationActivity extends ReactActivity { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | 19 | // Set the result to CANCELED. This will cause the widget host to cancel 20 | // out of the widget placement if the user presses the back button. 21 | setResult(RESULT_CANCELED); 22 | } 23 | 24 | /** 25 | * Returns the name of the widget configuration component registered from JavaScript. This is used to schedule 26 | * rendering of the component. 27 | */ 28 | @Override 29 | protected String getMainComponentName() { 30 | return "RNWidgetConfigurationScreen"; 31 | } 32 | 33 | /** 34 | * Returns the instance of the {@link ReactActivityDelegate}. Here we create a {@link 35 | * ReactActivityDelegate} instead of {@link DefaultReactActivityDelegate} which is in the default 36 | * MainActivity since we need to override `getLaunchOptions` to provide initial props containing 37 | * the widget info. 38 | */ 39 | @Override 40 | protected ReactActivityDelegate createReactActivityDelegate() { 41 | return new DefaultReactActivityDelegate( 42 | this, 43 | getMainComponentName(), 44 | DefaultNewArchitectureEntryPoint.getFabricEnabled()) { 45 | 46 | @Override 47 | protected Bundle getLaunchOptions() { 48 | Bundle initialProps = new Bundle(); 49 | initialProps.putBundle("widgetInfo", getWidgetInfo()); 50 | return initialProps; 51 | } 52 | }; 53 | } 54 | 55 | private Bundle getWidgetInfo() { 56 | Bundle widgetInfo = new Bundle(); 57 | int widgetId = getWidgetId(); 58 | String widgetName = getWidgetName(widgetId); 59 | 60 | widgetInfo.putInt("widgetId", widgetId); 61 | widgetInfo.putString("widgetName", widgetName); 62 | widgetInfo.putInt("height", RNWidgetUtil.getWidgetHeight(getApplicationContext(), widgetId)); 63 | widgetInfo.putInt("width", RNWidgetUtil.getWidgetWidth(getApplicationContext(), widgetId)); 64 | widgetInfo.putBundle("screenInfo", Arguments.toBundle(RNWidgetUtil.getScreenInfo(getApplicationContext()))); 65 | return widgetInfo; 66 | } 67 | 68 | private int getWidgetId() { 69 | int widgetId = getIntent() 70 | .getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); 71 | 72 | if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { 73 | finish(); 74 | } 75 | 76 | return widgetId; 77 | } 78 | 79 | private String getWidgetName(int widgetId) { 80 | AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext()); 81 | AppWidgetProviderInfo providerInfo = appWidgetManager.getAppWidgetInfo(widgetId); 82 | String className = providerInfo.provider.getClassName(); 83 | String[] classNameParts = className.split("\\."); 84 | return classNameParts[classNameParts.length - 1]; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/RNWidgetJsCommunication.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.work.Data; 6 | import androidx.work.ExistingWorkPolicy; 7 | import androidx.work.OneTimeWorkRequest; 8 | import androidx.work.WorkManager; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | public class RNWidgetJsCommunication { 13 | public static void requestWidgetUpdate(Context context, String widgetName) { 14 | int[] widgetIds = RNWidgetUtil.getWidgetIds(context, widgetName); 15 | 16 | for (int widgetId : widgetIds) { 17 | Data data = buildData(context, widgetName, widgetId, "WIDGET_UPDATE"); 18 | startBackgroundTask(context, data); 19 | } 20 | } 21 | 22 | protected static void startBackgroundTask(Context context, Data data) { 23 | workManagerWorkaround(context); 24 | 25 | OneTimeWorkRequest headlessJsTaskWorkRequest = 26 | new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class) 27 | .setInputData(data) 28 | .build(); 29 | 30 | WorkManager 31 | .getInstance(context) 32 | .enqueue(headlessJsTaskWorkRequest); 33 | } 34 | 35 | // `APPWIDGET_UPDATE` (`onUpdate`) method is called when the WorkManager queue is empty. 36 | // Since we enqueue only on WorkRequest, the queue will be empty after every execution of the 37 | // widgetTaskHandler. 38 | // This is a bug in android (https://issuetracker.google.com/issues/115575872). 39 | // 40 | // The suggested workaround is to schedule another WorkRequest really far out into the future. 41 | // So, we are creating a OneTimeWorkRequest with an initial delay of 10 years, with a name 42 | // `app.package.WORK_MANAGER_HACK`. 43 | // 44 | // Every time when we schedule another WorkRequest, we REPLACE the WorkRequest with a new one. 45 | // That way there is always at least one outstanding request, and `onUpdate` is not called when 46 | // the WorkManager finishes with its work. 47 | private static void workManagerWorkaround(Context context) { 48 | OneTimeWorkRequest headlessJsTaskWorkRequest = 49 | new OneTimeWorkRequest.Builder(RNWidgetBackgroundTaskWorker.class) 50 | .setInputData(Data.EMPTY) 51 | .setInitialDelay(10 * 365, TimeUnit.DAYS) 52 | .build(); 53 | 54 | WorkManager.getInstance(context) 55 | .enqueueUniqueWork( 56 | context.getPackageName() + ".WORK_MANAGER_HACK", 57 | ExistingWorkPolicy.REPLACE, 58 | headlessJsTaskWorkRequest 59 | ); 60 | } 61 | 62 | protected static Data buildData(Context context, String widgetName, int widgetId, String widgetAction, Data additionalData) { 63 | return new Data.Builder() 64 | .putString("widgetName", widgetName) 65 | .putInt("widgetId", widgetId) 66 | .putInt("width", RNWidgetUtil.getWidgetWidth(context, widgetId)) 67 | .putInt("height", RNWidgetUtil.getWidgetHeight(context, widgetId)) 68 | .putString("widgetAction", widgetAction) 69 | .putAll(additionalData) 70 | .build(); 71 | } 72 | 73 | protected static Data buildData(Context context, String widgetName, int widgetId, String widgetAction) { 74 | return buildData(context, widgetName, widgetId, widgetAction, Data.EMPTY); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/ClickableView.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder; 2 | 3 | import android.view.View; 4 | 5 | import com.facebook.react.bridge.ReadableMap; 6 | 7 | public class ClickableView implements Comparable { 8 | private final String id; 9 | private final View view; 10 | private final String clickAction; 11 | private final ReadableMap clickActionData; 12 | 13 | public ClickableView(String id, View view, String clickAction, ReadableMap clickActionData) { 14 | this.id = id; 15 | this.view = view; 16 | this.clickAction = clickAction; 17 | this.clickActionData = clickActionData; 18 | } 19 | 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | public View getView() { 25 | return view; 26 | } 27 | 28 | public String getClickAction() { 29 | return clickAction; 30 | } 31 | 32 | public ReadableMap getClickActionData() { 33 | return clickActionData; 34 | } 35 | 36 | @Override 37 | public int compareTo(ClickableView o) { 38 | return this.id.compareTo(o.getId()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/CollectionViewItem.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder; 2 | 3 | import android.graphics.Bitmap; 4 | import android.view.ViewGroup; 5 | 6 | import com.facebook.react.bridge.Arguments; 7 | import com.facebook.react.bridge.ReadableMap; 8 | 9 | import java.util.List; 10 | 11 | public class CollectionViewItem { 12 | private final ViewGroup view; 13 | private final Bitmap bitmap; 14 | private final List clickableViews; 15 | private final String clickAction; 16 | private final ReadableMap clickActionData; 17 | 18 | public CollectionViewItem(ViewGroup view, Bitmap bitmap, List clickableViews, String clickAction, ReadableMap clickActionData) { 19 | this.view = view; 20 | this.bitmap = bitmap; 21 | this.clickableViews = clickableViews; 22 | this.clickAction = clickAction; 23 | if (clickActionData == null) { 24 | this.clickActionData = Arguments.createMap(); 25 | } else { 26 | this.clickActionData = clickActionData; 27 | } 28 | } 29 | 30 | public CollectionViewItem(ViewGroup view, Bitmap bitmap, List clickableViews) { 31 | this(view, bitmap, clickableViews, null, null); 32 | } 33 | 34 | public ViewGroup getView() { 35 | return view; 36 | } 37 | 38 | public Bitmap getBitmap() { 39 | return bitmap; 40 | } 41 | 42 | public List getClickableViews() { 43 | return clickableViews; 44 | } 45 | 46 | public String getClickAction() { 47 | return clickAction; 48 | } 49 | 50 | public ReadableMap getClickActionData() { 51 | return clickActionData; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/WidgetWithViews.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder; 2 | 3 | import android.view.View; 4 | 5 | import java.util.List; 6 | 7 | public class WidgetWithViews { 8 | private final View rootView; 9 | private final List clickableViews; 10 | private final List collectionViews; 11 | 12 | WidgetWithViews(View rootView, List clickableViews, List collectionViews) { 13 | this.rootView = rootView; 14 | this.clickableViews = clickableViews; 15 | this.collectionViews = collectionViews; 16 | } 17 | 18 | public View getRootView() { 19 | return rootView; 20 | } 21 | 22 | public List getClickableViews() { 23 | return clickableViews; 24 | } 25 | 26 | public List getCollectionViews() { 27 | return collectionViews; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/BaseLayoutWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.bridge.ReadableMap; 8 | 9 | import java.util.List; 10 | 11 | public abstract class BaseLayoutWidget extends BaseWidget { 12 | public BaseLayoutWidget(ReactApplicationContext context, ReadableMap props, List children) { 13 | super(context, props, children); 14 | } 15 | 16 | protected void addChildren() { 17 | for (int i = 0; i < children.size(); i++) { 18 | View childView = children.get(i); 19 | view.addView(childView); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/FrameLayoutWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.graphics.RectF; 7 | import android.view.View; 8 | import android.widget.FrameLayout; 9 | 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.bridge.ReadableMap; 12 | 13 | import java.util.List; 14 | 15 | public class FrameLayoutWidget extends BaseLayoutWidget { 16 | private class ClippedFrameLayout extends FrameLayout { 17 | private Path path; 18 | 19 | public ClippedFrameLayout(Context appContext) { 20 | super(appContext); 21 | } 22 | 23 | @Override 24 | protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 25 | super.onSizeChanged(width, height, oldWidth, oldHeight); 26 | 27 | if ("hidden".equals(props.getString("overflow")) && props.hasKey("borderRadius")) { 28 | ReadableMap borderRadius = props.getMap("borderRadius"); 29 | float[] radii = new float[]{ 30 | (float) dpToPx(borderRadius.getDouble("topLeft")), 31 | (float) dpToPx(borderRadius.getDouble("topLeft")), 32 | (float) dpToPx(borderRadius.getDouble("topRight")), 33 | (float) dpToPx(borderRadius.getDouble("topRight")), 34 | (float) dpToPx(borderRadius.getDouble("bottomRight")), 35 | (float) dpToPx(borderRadius.getDouble("bottomRight")), 36 | (float) dpToPx(borderRadius.getDouble("bottomLeft")), 37 | (float) dpToPx(borderRadius.getDouble("bottomLeft")) 38 | }; 39 | 40 | this.path = new Path(); 41 | this.path.addRoundRect(new RectF(0, 0, width, height), radii, Path.Direction.CW); 42 | } 43 | } 44 | 45 | @Override 46 | protected void dispatchDraw(Canvas canvas) { 47 | if (this.path != null) { 48 | canvas.clipPath(this.path); 49 | } 50 | super.dispatchDraw(canvas); 51 | } 52 | } 53 | 54 | public FrameLayoutWidget(ReactApplicationContext context, ReadableMap props, List children) { 55 | super(context, props, children); 56 | } 57 | 58 | @Override 59 | protected FrameLayout createView() { 60 | return new ClippedFrameLayout(appContext); 61 | } 62 | 63 | @Override 64 | public void applyProps() { 65 | addChildren(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/IconWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import static android.util.TypedValue.COMPLEX_UNIT_SP; 4 | 5 | import android.graphics.Color; 6 | import android.os.Build; 7 | import android.widget.TextView; 8 | 9 | import androidx.core.widget.TextViewCompat; 10 | 11 | import com.facebook.react.bridge.ReactApplicationContext; 12 | import com.facebook.react.bridge.ReadableMap; 13 | import com.reactnativeandroidwidget.builder.widget.utils.ResourceUtils; 14 | 15 | public class IconWidget extends BaseWidget { 16 | public IconWidget(ReactApplicationContext context, ReadableMap props) { 17 | super(context, props); 18 | } 19 | 20 | @Override 21 | protected TextView createView() { 22 | return new TextView(appContext); 23 | } 24 | 25 | @Override 26 | protected void applyProps() { 27 | view.setText(getString("icon", "")); 28 | view.setTextSize(COMPLEX_UNIT_SP, getTextSize()); 29 | view.setTextColor(Color.parseColor(getString("color", "#000000"))); 30 | 31 | if (props.hasKey("adjustsFontSizeToFit") && props.getBoolean("adjustsFontSizeToFit")) { 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 33 | view.setAutoSizeTextTypeUniformWithConfiguration(1, Math.round(getTextSize()), 1, COMPLEX_UNIT_SP); 34 | } else { 35 | TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(view, 1, Math.round(getTextSize()), 1, COMPLEX_UNIT_SP); 36 | } 37 | } 38 | 39 | if (props.hasKey("font")) { 40 | view.setTypeface(ResourceUtils.getTypeface(appContext, props.getString("font"))); 41 | } 42 | } 43 | 44 | private float getTextSize() { 45 | float textSize = 12; 46 | if (props.hasKey("size")) { 47 | textSize = (float) props.getDouble("size"); 48 | } 49 | return textSize; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/ImageWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.graphics.Bitmap; 4 | import android.widget.ImageView; 5 | 6 | import androidx.core.graphics.drawable.RoundedBitmapDrawable; 7 | import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; 8 | 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReadableMap; 11 | import com.reactnativeandroidwidget.builder.widget.utils.ResourceUtils; 12 | 13 | import java.io.IOException; 14 | 15 | public class ImageWidget extends BaseWidget { 16 | public ImageWidget(ReactApplicationContext context, ReadableMap props) { 17 | super(context, props); 18 | } 19 | 20 | @Override 21 | protected ImageView createView() { 22 | return new ImageView(appContext); 23 | } 24 | 25 | @Override 26 | protected void applyProps() { 27 | Bitmap bitmapFromURL = getBitmapFromURL(props.getMap("image").getString("uri"), 28 | dpToPx(props.getDouble("imageWidth")), 29 | dpToPx(props.getDouble("imageHeight")) 30 | ); 31 | 32 | RoundedBitmapDrawable bitmapDrawable = 33 | RoundedBitmapDrawableFactory.create(appContext.getResources(), bitmapFromURL); 34 | 35 | if (props.hasKey("radius")) { 36 | bitmapDrawable.setCornerRadius(dpToPx(props.getDouble("radius"))); 37 | } 38 | 39 | view.setImageDrawable(bitmapDrawable); 40 | } 41 | 42 | private Bitmap getBitmapFromURL(String src, int width, int height) { 43 | try { 44 | Bitmap bitmap = ResourceUtils.getBitmap(appContext, src); 45 | if (bitmap == null) { 46 | return null; 47 | } 48 | return Bitmap.createScaledBitmap(bitmap, width, height, true); 49 | } catch (IOException e) { 50 | e.printStackTrace(); 51 | return null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/LinearLayoutWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.Path; 7 | import android.graphics.RectF; 8 | import android.graphics.drawable.ShapeDrawable; 9 | import android.view.View; 10 | import android.widget.LinearLayout; 11 | 12 | import com.facebook.react.bridge.ReactApplicationContext; 13 | import com.facebook.react.bridge.ReadableMap; 14 | 15 | import java.util.List; 16 | 17 | public class LinearLayoutWidget extends BaseLayoutWidget { 18 | private class ClippedLinearLayout extends LinearLayout { 19 | private Path path; 20 | 21 | public ClippedLinearLayout(Context appContext) { 22 | super(appContext); 23 | } 24 | 25 | @Override 26 | protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 27 | super.onSizeChanged(width, height, oldWidth, oldHeight); 28 | 29 | if ("hidden".equals(props.getString("overflow")) && props.hasKey("borderRadius")) { 30 | ReadableMap borderRadius = props.getMap("borderRadius"); 31 | float[] radii = new float[]{ 32 | (float) dpToPx(borderRadius.getDouble("topLeft")), 33 | (float) dpToPx(borderRadius.getDouble("topLeft")), 34 | (float) dpToPx(borderRadius.getDouble("topRight")), 35 | (float) dpToPx(borderRadius.getDouble("topRight")), 36 | (float) dpToPx(borderRadius.getDouble("bottomRight")), 37 | (float) dpToPx(borderRadius.getDouble("bottomRight")), 38 | (float) dpToPx(borderRadius.getDouble("bottomLeft")), 39 | (float) dpToPx(borderRadius.getDouble("bottomLeft")) 40 | }; 41 | 42 | this.path = new Path(); 43 | this.path.addRoundRect(new RectF(0, 0, width, height), radii, Path.Direction.CW); 44 | } 45 | } 46 | 47 | @Override 48 | protected void dispatchDraw(Canvas canvas) { 49 | if (this.path != null) { 50 | canvas.clipPath(this.path); 51 | } 52 | super.dispatchDraw(canvas); 53 | } 54 | } 55 | 56 | public LinearLayoutWidget(ReactApplicationContext context, ReadableMap props, List children) { 57 | super(context, props, children); 58 | } 59 | 60 | @Override 61 | protected LinearLayout createView() { 62 | return new ClippedLinearLayout(appContext); 63 | } 64 | 65 | @Override 66 | public void applyProps() { 67 | view.setOrientation(("HORIZONTAL").equals(props.getString("orientation")) 68 | ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL); 69 | 70 | if (props.hasKey("gravity")) { 71 | view.setGravity(props.getInt("gravity")); 72 | } 73 | 74 | if (props.hasKey("separator")) { 75 | ReadableMap separator = props.getMap("separator"); 76 | 77 | ShapeDrawable divider = new ShapeDrawable(); 78 | divider.getPaint().setColor(Color.parseColor(separator.getString("color"))); 79 | if (("HORIZONTAL").equals(props.getString("orientation"))) { 80 | divider.setIntrinsicWidth(dpToPx(separator.getDouble("size"))); 81 | } else { 82 | divider.setIntrinsicHeight(dpToPx(separator.getDouble("size"))); 83 | } 84 | view.setDividerDrawable(divider); 85 | view.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); 86 | } 87 | 88 | addChildren(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/ListWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.widget.FrameLayout; 4 | 5 | import com.facebook.react.bridge.ReactApplicationContext; 6 | import com.facebook.react.bridge.ReadableMap; 7 | 8 | public class ListWidget extends BaseWidget { 9 | public ListWidget(ReactApplicationContext context, ReadableMap props) { 10 | super(context, props); 11 | } 12 | 13 | @Override 14 | protected FrameLayout createView() { 15 | return new FrameLayout(appContext); 16 | } 17 | 18 | @Override 19 | public void applyProps() { 20 | } 21 | 22 | @Override 23 | public boolean isCollection() { 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/RootWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.view.View; 4 | import android.widget.FrameLayout; 5 | 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.bridge.ReadableMap; 8 | 9 | import java.util.List; 10 | 11 | public class RootWidget extends BaseLayoutWidget { 12 | public RootWidget(ReactApplicationContext context, ReadableMap props, List children) { 13 | super(context, props, children); 14 | } 15 | 16 | @Override 17 | protected FrameLayout createView() { 18 | return new FrameLayout(appContext); 19 | } 20 | 21 | @Override 22 | protected void applyProps() { 23 | addChildren(); 24 | 25 | int specWidth = View.MeasureSpec.makeMeasureSpec(dpToPx(props.getInt("widgetWidth")), View.MeasureSpec.EXACTLY); 26 | int specHeight = View.MeasureSpec.makeMeasureSpec(dpToPx(props.getInt("widgetHeight")), View.MeasureSpec.EXACTLY); 27 | 28 | view.measure(specWidth, specHeight); 29 | view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /android/src/main/java/com/reactnativeandroidwidget/builder/widget/SvgWidget.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget.builder.widget; 2 | 3 | import android.graphics.drawable.PictureDrawable; 4 | import android.widget.ImageView; 5 | 6 | import com.caverock.androidsvg.SVG; 7 | import com.caverock.androidsvg.SVGParseException; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.bridge.ReadableMap; 10 | import com.reactnativeandroidwidget.builder.widget.utils.ResourceUtils; 11 | 12 | import java.io.IOException; 13 | 14 | public class SvgWidget extends BaseWidget { 15 | public SvgWidget(ReactApplicationContext context, ReadableMap props) { 16 | super(context, props); 17 | } 18 | 19 | @Override 20 | protected ImageView createView() { 21 | return new ImageView(appContext); 22 | } 23 | 24 | @Override 25 | protected void applyProps() { 26 | try { 27 | SVG svg = getSvg(); 28 | PictureDrawable pd = new PictureDrawable(svg.renderToPicture()); 29 | view.setImageDrawable(pd); 30 | } catch (SVGParseException | IOException e) { 31 | e.printStackTrace(); 32 | } 33 | } 34 | 35 | private SVG getSvg() throws SVGParseException, IOException { 36 | if (props.hasKey("svgString")) { 37 | return SVG.getFromString(props.getString("svgString")); 38 | } else { 39 | return ResourceUtils.getSvg(appContext, props.getString("svgUrl")); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/src/main/res/layout/rn_widget.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 17 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /android/src/main/res/layout/rn_widget_clickable.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /android/src/main/res/layout/rn_widget_list.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 17 | 18 | 19 | 23 | 24 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /android/src/main/res/layout/rn_widget_list_item.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 15 | 16 | -------------------------------------------------------------------------------- /android/src/newarch/AndroidWidgetSpec.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import com.facebook.react.bridge.ReactApplicationContext; 4 | 5 | abstract class AndroidWidgetSpec extends NativeAndroidWidgetSpec { 6 | AndroidWidgetSpec(ReactApplicationContext context) { 7 | super(context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /android/src/oldarch/AndroidWidgetSpec.java: -------------------------------------------------------------------------------- 1 | package com.reactnativeandroidwidget; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.bridge.Promise; 6 | import com.facebook.react.bridge.ReactApplicationContext; 7 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 8 | import com.facebook.react.bridge.ReadableMap; 9 | 10 | abstract class AndroidWidgetSpec extends ReactContextBaseJavaModule { 11 | AndroidWidgetSpec(ReactApplicationContext context) { 12 | super(context); 13 | } 14 | 15 | public abstract void drawWidget(ReadableMap config, String widgetName); 16 | 17 | public abstract void drawWidgetById(ReadableMap config, String widgetName, double widgetId); 18 | 19 | public abstract void createPreview(ReadableMap config, double width, double height, Promise promise); 20 | 21 | public abstract void getWidgetInfo(String widgetName, Promise promise); 22 | 23 | public abstract void finishWidgetConfiguration(double widgetId, String result); 24 | } 25 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/api/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "API", 3 | "position": 3 4 | } 5 | -------------------------------------------------------------------------------- /docs/docs/api/request-widget-update-by-id.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # requestWidgetUpdateById 6 | 7 | `react-native-android-widget` exports a `requestWidgetUpdateById` function that can be used to request a widget update while the application is open (or with some background task) for a single widget with known id. 8 | 9 | This is an alternative to [`requestWidgetUpdate`](./request-widget-update.md) and should be used in special cases when the widget id is known, and you don't want to update the other widgets with the same name. 10 | 11 | ## Usage 12 | 13 | Lets assume we have a `CounterWidget` widget that shows a single number, which it gets as a prop. 14 | 15 | If the user has added the `CounterWidget` multiple times, `requestWidgetUpdateById` will update only one widget which corresponds with the given `widgetId`. 16 | 17 | If a widget with the given `widgetId` does not exist, the optional callback `widgetNotFound` will be called. 18 | 19 | ### Example 20 | 21 | ```jsx title="CounterScreen.tsx" 22 | import * as React from 'react'; 23 | import { Button, StyleSheet, View, Text } from 'react-native'; 24 | import { requestWidgetUpdateById } from 'react-native-android-widget'; 25 | 26 | import { CounterWidget } from './CounterWidget'; 27 | 28 | export function CounterScreen() { 29 | const [count, setCount] = React.useState(0); 30 | 31 | React.useEffect(() => { 32 | requestWidgetUpdateById({ 33 | widgetName: 'Counter', 34 | widgetId: 1, 35 | renderWidget: () => , 36 | widgetNotFound: () => { 37 | // Called if no widget is present on the home screen 38 | }, 39 | }); 40 | }, [count]); 41 | 42 | return ( 43 | 44 | {count} 45 |