@synonymdev/react-native-toast 0.1.0
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/API.md +210 -0
- package/DEVELOPMENT.md +35 -0
- package/README.md +62 -0
- package/SynonymReactNativeToast.podspec +17 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/synonymdev/reactnativetoast/NativeToastModule.kt +332 -0
- package/android/src/main/java/com/synonymdev/reactnativetoast/NativeToastPackage.kt +15 -0
- package/ios/NativeToast.m +530 -0
- package/lib/commonjs/defaults.js +105 -0
- package/lib/commonjs/defaults.js.map +1 -0
- package/lib/commonjs/index.js +25 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/nativeModule.js +15 -0
- package/lib/commonjs/nativeModule.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/defaults.js +97 -0
- package/lib/module/defaults.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/nativeModule.js +10 -0
- package/lib/module/nativeModule.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/commonjs/defaults.d.ts +9 -0
- package/lib/typescript/commonjs/defaults.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +6 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/nativeModule.d.ts +3 -0
- package/lib/typescript/commonjs/nativeModule.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/types.d.ts +57 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/module/defaults.d.ts +9 -0
- package/lib/typescript/module/defaults.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +6 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/nativeModule.d.ts +3 -0
- package/lib/typescript/module/nativeModule.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/types.d.ts +57 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/package.json +68 -0
- package/src/defaults.ts +110 -0
- package/src/index.ts +25 -0
- package/src/nativeModule.ts +12 -0
- package/src/types.ts +87 -0
package/API.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# API
|
|
2
|
+
|
|
3
|
+
## `showToast(options)`
|
|
4
|
+
|
|
5
|
+
Displays a native toast notification.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { showToast } from '@synonymdev/react-native-toast';
|
|
9
|
+
|
|
10
|
+
showToast({
|
|
11
|
+
type: 'success',
|
|
12
|
+
title: 'Saved',
|
|
13
|
+
description: 'Your changes are ready.',
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Toast Options
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
type ToastOptions = {
|
|
21
|
+
type: 'success' | 'info' | 'warning' | 'error';
|
|
22
|
+
title: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
autoHide?: boolean;
|
|
25
|
+
dismissible?: boolean;
|
|
26
|
+
haptics?: boolean;
|
|
27
|
+
durationMs?: number;
|
|
28
|
+
style?: NativeToastStyle;
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Option | Default | Description |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `type` | Required | Visual variant for the toast. |
|
|
35
|
+
| `title` | Required | Primary toast text. |
|
|
36
|
+
| `description` | `undefined` | Secondary toast text. |
|
|
37
|
+
| `autoHide` | `true` | Whether the toast dismisses itself after `durationMs`. |
|
|
38
|
+
| `dismissible` | `true` | Whether the toast can be dismissed with an upward swipe. |
|
|
39
|
+
| `haptics` | `false` | Whether to trigger native haptic feedback when the toast is shown. |
|
|
40
|
+
| `durationMs` | `4000` | Auto-hide delay in milliseconds. Native implementations enforce a minimum visible duration. |
|
|
41
|
+
| `style` | `undefined` | Native style overrides. |
|
|
42
|
+
|
|
43
|
+
Set `autoHide: false` to keep a toast visible until another toast replaces it or the user dismisses it.
|
|
44
|
+
|
|
45
|
+
## Global Configuration
|
|
46
|
+
|
|
47
|
+
Use `configureToast` once during app startup to customize the base style or built-in variant styles for every toast.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { configureToast, resetToastConfig } from '@synonymdev/react-native-toast';
|
|
51
|
+
|
|
52
|
+
configureToast({
|
|
53
|
+
options: {
|
|
54
|
+
haptics: true,
|
|
55
|
+
dismissible: true,
|
|
56
|
+
durationMs: 4000,
|
|
57
|
+
},
|
|
58
|
+
defaults: {
|
|
59
|
+
borderRadius: 16,
|
|
60
|
+
paddingHorizontal: 24,
|
|
61
|
+
},
|
|
62
|
+
variants: {
|
|
63
|
+
success: {
|
|
64
|
+
backgroundColor: '#047857',
|
|
65
|
+
borderColor: '#34D399',
|
|
66
|
+
},
|
|
67
|
+
error: {
|
|
68
|
+
backgroundColor: '#991B1B',
|
|
69
|
+
borderColor: '#FCA5A5',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
resetToastConfig();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
type NativeToastVariantStyles = Partial<
|
|
79
|
+
Record<NativeToastVariant, NativeToastStyle>
|
|
80
|
+
>;
|
|
81
|
+
|
|
82
|
+
type NativeToastDefaultOptions = Pick<
|
|
83
|
+
ToastOptions,
|
|
84
|
+
'autoHide' | 'dismissible' | 'haptics' | 'durationMs'
|
|
85
|
+
>;
|
|
86
|
+
|
|
87
|
+
type NativeToastConfig = {
|
|
88
|
+
defaults?: NativeToastStyle;
|
|
89
|
+
variants?: NativeToastVariantStyles;
|
|
90
|
+
options?: NativeToastDefaultOptions;
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Configured behavior options are merged in this order:
|
|
95
|
+
|
|
96
|
+
1. Configured `options`.
|
|
97
|
+
2. Per-toast options.
|
|
98
|
+
|
|
99
|
+
Per-toast options always win.
|
|
100
|
+
|
|
101
|
+
Configured styles are merged in this order:
|
|
102
|
+
|
|
103
|
+
1. Built-in default style.
|
|
104
|
+
2. Configured `defaults`.
|
|
105
|
+
3. Built-in variant style.
|
|
106
|
+
4. Configured variant style.
|
|
107
|
+
5. Per-toast `style`.
|
|
108
|
+
|
|
109
|
+
## Styling
|
|
110
|
+
|
|
111
|
+
Pass `style` to override the native toast appearance.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
showToast({
|
|
115
|
+
type: 'info',
|
|
116
|
+
title: 'New message',
|
|
117
|
+
description: 'You have a new notification.',
|
|
118
|
+
style: {
|
|
119
|
+
backgroundColor: '#111111',
|
|
120
|
+
iosBlurEffect: 'systemThinMaterialDark',
|
|
121
|
+
iosBlurAmount: 20,
|
|
122
|
+
iosBlurTintOpacity: 0.72,
|
|
123
|
+
borderColor: '#333333',
|
|
124
|
+
borderRadius: 12,
|
|
125
|
+
borderWidth: 1,
|
|
126
|
+
paddingHorizontal: 20,
|
|
127
|
+
paddingVertical: 16,
|
|
128
|
+
width: 320,
|
|
129
|
+
maxWidth: 420,
|
|
130
|
+
titleColor: '#FFFFFF',
|
|
131
|
+
titleFontSize: 15,
|
|
132
|
+
descriptionColor: '#D4D4D4',
|
|
133
|
+
descriptionFontSize: 13,
|
|
134
|
+
shadowColor: '#000000',
|
|
135
|
+
shadowOpacity: 0.35,
|
|
136
|
+
shadowRadius: 16,
|
|
137
|
+
shadowOffsetY: 8,
|
|
138
|
+
shadowElevation: 8,
|
|
139
|
+
animation: 'slide-fade',
|
|
140
|
+
animationDurationMs: 200,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Style Options
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
type NativeToastBlurEffect =
|
|
149
|
+
| 'none'
|
|
150
|
+
| 'extraLight'
|
|
151
|
+
| 'light'
|
|
152
|
+
| 'dark'
|
|
153
|
+
| 'regular'
|
|
154
|
+
| 'prominent'
|
|
155
|
+
| 'systemUltraThinMaterial'
|
|
156
|
+
| 'systemThinMaterial'
|
|
157
|
+
| 'systemMaterial'
|
|
158
|
+
| 'systemThickMaterial'
|
|
159
|
+
| 'systemChromeMaterial'
|
|
160
|
+
| 'systemUltraThinMaterialLight'
|
|
161
|
+
| 'systemThinMaterialLight'
|
|
162
|
+
| 'systemMaterialLight'
|
|
163
|
+
| 'systemThickMaterialLight'
|
|
164
|
+
| 'systemChromeMaterialLight'
|
|
165
|
+
| 'systemUltraThinMaterialDark'
|
|
166
|
+
| 'systemThinMaterialDark'
|
|
167
|
+
| 'systemMaterialDark'
|
|
168
|
+
| 'systemThickMaterialDark'
|
|
169
|
+
| 'systemChromeMaterialDark';
|
|
170
|
+
|
|
171
|
+
type NativeToastStyle = {
|
|
172
|
+
backgroundColor?: string;
|
|
173
|
+
iosBlurEffect?: NativeToastBlurEffect;
|
|
174
|
+
iosBlurAmount?: number;
|
|
175
|
+
iosBlurTintOpacity?: number;
|
|
176
|
+
borderRadius?: number;
|
|
177
|
+
borderColor?: string;
|
|
178
|
+
borderWidth?: number;
|
|
179
|
+
shadowColor?: string;
|
|
180
|
+
shadowOpacity?: number;
|
|
181
|
+
shadowRadius?: number;
|
|
182
|
+
shadowOffsetX?: number;
|
|
183
|
+
shadowOffsetY?: number;
|
|
184
|
+
shadowElevation?: number;
|
|
185
|
+
animation?: 'none' | 'fade' | 'slide' | 'slide-fade';
|
|
186
|
+
animationDurationMs?: number;
|
|
187
|
+
width?: number;
|
|
188
|
+
maxWidth?: number;
|
|
189
|
+
marginHorizontal?: number;
|
|
190
|
+
padding?: number;
|
|
191
|
+
paddingHorizontal?: number;
|
|
192
|
+
paddingVertical?: number;
|
|
193
|
+
paddingTop?: number;
|
|
194
|
+
paddingRight?: number;
|
|
195
|
+
paddingBottom?: number;
|
|
196
|
+
paddingLeft?: number;
|
|
197
|
+
titleColor?: string;
|
|
198
|
+
titleFontFamily?: string;
|
|
199
|
+
titleFontSize?: number;
|
|
200
|
+
titleFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
|
201
|
+
descriptionColor?: string;
|
|
202
|
+
descriptionFontFamily?: string;
|
|
203
|
+
descriptionFontSize?: number;
|
|
204
|
+
descriptionFontWeight?: 'normal' | 'medium' | 'semibold' | 'bold';
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`iosBlurEffect` uses native `UIBlurEffect` on iOS and is ignored by Android. `iosBlurAmount` controls the custom blur radius and defaults to `20`. `iosBlurTintOpacity` controls the opacity of the `backgroundColor` tint layered over the blur and defaults to `0.72`.
|
|
209
|
+
|
|
210
|
+
Supported animations are `none`, `fade`, `slide`, and `slide-fade`.
|
package/DEVELOPMENT.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
## Example App
|
|
4
|
+
|
|
5
|
+
The `example` directory contains a small React Native app that links this package locally and demonstrates the main toast flows.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
cd example
|
|
9
|
+
yarn install
|
|
10
|
+
yarn pods
|
|
11
|
+
yarn start
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Run it from another terminal:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
yarn ios
|
|
18
|
+
yarn android
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Package Commands
|
|
22
|
+
|
|
23
|
+
Run tests:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
yarn test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Build the package:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
yarn build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The React Native entry points at `src/index.ts` for local app development. Published JavaScript and TypeScript declarations are emitted to `lib`.
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @synonymdev/react-native-toast
|
|
2
|
+
|
|
3
|
+
Native toast notifications for React Native.
|
|
4
|
+
|
|
5
|
+
Renders toasts from native iOS and Android code so notifications can appear above native sheet presentation surfaces.
|
|
6
|
+
|
|
7
|
+
Designed for use with native sheet implementations like React Navigation `formSheet`, [software-mansion-labs/react-native-bottom-sheet](https://github.com/software-mansion-labs/react-native-bottom-sheet) and [lodev09/react-native-true-sheet](https://github.com/lodev09/react-native-true-sheet).
|
|
8
|
+
|
|
9
|
+
## Demo
|
|
10
|
+
|
|
11
|
+
<video src="https://github.com/synonymdev/react-native-toast/raw/main/media/demo.mov" controls width="300">
|
|
12
|
+
<a href="./media/demo.mov">Watch the demo</a>
|
|
13
|
+
</video>
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Native iOS and Android rendering.
|
|
18
|
+
- Appears above native sheet presentation surfaces.
|
|
19
|
+
- Simple `showToast(options)` JavaScript API.
|
|
20
|
+
- Built-in `success`, `info`, `warning`, and `error` variants.
|
|
21
|
+
- Typed options for behavior, haptics, blur, and styling.
|
|
22
|
+
- Global configuration for default behavior and variant styling.
|
|
23
|
+
|
|
24
|
+
## Why?
|
|
25
|
+
|
|
26
|
+
React Native toast components usually render inside the JavaScript view hierarchy. That works for normal screens, but it can fall behind native sheet presentation layers.
|
|
27
|
+
|
|
28
|
+
This package sends a fully resolved toast payload to a small native module, then renders the toast from native iOS and Android code. That makes it useful for app-level feedback that should remain visible across native sheet presentations.
|
|
29
|
+
|
|
30
|
+
On Android, React Native `Modal` and modal-backed sheet libraries create their own native window. This toast currently renders in the Activity window, so those modal surfaces can appear above it.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
yarn add @synonymdev/react-native-toast
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { showToast } from '@synonymdev/react-native-toast';
|
|
42
|
+
|
|
43
|
+
showToast({
|
|
44
|
+
type: 'success',
|
|
45
|
+
title: 'Saved',
|
|
46
|
+
description: 'Your changes are ready.',
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Configure shared defaults once during app startup:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { configureToast } from '@synonymdev/react-native-toast';
|
|
54
|
+
|
|
55
|
+
configureToast({
|
|
56
|
+
options: {
|
|
57
|
+
haptics: true,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See [API.md](./API.md) for configuration, styling, and the full type reference. Local development and example app instructions live in [DEVELOPMENT.md](./DEVELOPMENT.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'SynonymReactNativeToast'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license']
|
|
10
|
+
s.authors = { 'Synonym' => 'engineering@synonym.to' }
|
|
11
|
+
s.homepage = 'https://github.com/synonymdev'
|
|
12
|
+
s.platforms = { :ios => '13.0' }
|
|
13
|
+
s.source = { :git => 'https://github.com/synonymdev/react-native-toast.git', :tag => s.version.to_s }
|
|
14
|
+
s.source_files = 'ios/**/*.{h,m,mm,swift}'
|
|
15
|
+
|
|
16
|
+
s.dependency 'React-Core'
|
|
17
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.safeExtGet = { prop, fallback ->
|
|
3
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
plugins {
|
|
8
|
+
id "com.android.library"
|
|
9
|
+
id "org.jetbrains.kotlin.android"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
android {
|
|
13
|
+
namespace "com.synonymdev.reactnativetoast"
|
|
14
|
+
compileSdk safeExtGet("compileSdkVersion", 36)
|
|
15
|
+
|
|
16
|
+
defaultConfig {
|
|
17
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
18
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
implementation "com.facebook.react:react-android"
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
package com.synonymdev.reactnativetoast
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.graphics.drawable.GradientDrawable
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.util.TypedValue
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.HapticFeedbackConstants
|
|
10
|
+
import android.view.MotionEvent
|
|
11
|
+
import android.view.View
|
|
12
|
+
import android.view.ViewGroup
|
|
13
|
+
import android.view.WindowManager
|
|
14
|
+
import android.widget.FrameLayout
|
|
15
|
+
import android.widget.LinearLayout
|
|
16
|
+
import android.widget.TextView
|
|
17
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
18
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
19
|
+
import com.facebook.react.bridge.ReactMethod
|
|
20
|
+
import com.facebook.react.bridge.ReadableMap
|
|
21
|
+
import kotlin.math.roundToInt
|
|
22
|
+
|
|
23
|
+
class NativeToastModule(private val reactContext: ReactApplicationContext) :
|
|
24
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
25
|
+
override fun getName(): String = "NativeToast"
|
|
26
|
+
|
|
27
|
+
@ReactMethod
|
|
28
|
+
fun show(options: ReadableMap) {
|
|
29
|
+
val title = options.getString("title").orEmpty()
|
|
30
|
+
val description = options.getString("description").orEmpty()
|
|
31
|
+
val variant = options.getString("type") ?: "info"
|
|
32
|
+
val autoHide = if (options.hasKey("autoHide") && !options.isNull("autoHide")) options.getBoolean("autoHide") else true
|
|
33
|
+
val dismissible = if (options.hasKey("dismissible") && !options.isNull("dismissible")) options.getBoolean("dismissible") else true
|
|
34
|
+
val haptics = if (options.hasKey("haptics") && !options.isNull("haptics")) options.getBoolean("haptics") else false
|
|
35
|
+
val durationMs = if (options.hasKey("durationMs")) options.getDouble("durationMs") else 4000.0
|
|
36
|
+
val style = if (options.hasKey("style") && !options.isNull("style")) options.getMap("style") else null
|
|
37
|
+
val animation = string(style, "animation", "slide-fade")
|
|
38
|
+
val animationDurationMs = number(style, "animationDurationMs", 200.0).coerceAtLeast(0.0).toLong()
|
|
39
|
+
|
|
40
|
+
if (title.isBlank() && description.isBlank()) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reactContext.runOnUiQueueThread {
|
|
45
|
+
val activity = reactContext.currentActivity ?: return@runOnUiQueueThread
|
|
46
|
+
val root = activity.window.decorView as? ViewGroup ?: return@runOnUiQueueThread
|
|
47
|
+
|
|
48
|
+
root.findViewWithTag<View>(TOAST_TAG)?.let {
|
|
49
|
+
it.animate().cancel()
|
|
50
|
+
root.removeView(it)
|
|
51
|
+
}
|
|
52
|
+
root.clipChildren = false
|
|
53
|
+
root.clipToPadding = false
|
|
54
|
+
|
|
55
|
+
val paddingTop = padding(style, "paddingTop", "paddingVertical", 12.0)
|
|
56
|
+
val paddingRight = padding(style, "paddingRight", "paddingHorizontal", 16.0)
|
|
57
|
+
val paddingBottom = padding(style, "paddingBottom", "paddingVertical", 12.0)
|
|
58
|
+
val paddingLeft = padding(style, "paddingLeft", "paddingHorizontal", 16.0)
|
|
59
|
+
val marginHorizontal = dp(number(style, "marginHorizontal", 16.0))
|
|
60
|
+
val availableWidth = (screenWidth() - (marginHorizontal * 2)).coerceAtLeast(0)
|
|
61
|
+
val toastWidth = toastWidth(style, availableWidth)
|
|
62
|
+
|
|
63
|
+
val toast = LinearLayout(activity).apply {
|
|
64
|
+
tag = TOAST_TAG
|
|
65
|
+
alpha = if (animationUsesFade(animation)) 0f else 1f
|
|
66
|
+
clipToOutline = true
|
|
67
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
68
|
+
orientation = LinearLayout.VERTICAL
|
|
69
|
+
visibility = View.INVISIBLE
|
|
70
|
+
setPadding(dp(paddingLeft), dp(paddingTop), dp(paddingRight), dp(paddingBottom))
|
|
71
|
+
isClickable = dismissible
|
|
72
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
73
|
+
background =
|
|
74
|
+
GradientDrawable().apply {
|
|
75
|
+
setColor(color(style, "backgroundColor", Color.rgb(51, 51, 51)))
|
|
76
|
+
cornerRadius = dp(number(style, "borderRadius", 12.0)).toFloat()
|
|
77
|
+
setStroke(dp(number(style, "borderWidth", 1.0)), color(style, "borderColor", Color.rgb(68, 68, 68)))
|
|
78
|
+
}
|
|
79
|
+
elevation = dp(number(style, "shadowElevation", 8.0)).toFloat()
|
|
80
|
+
translationZ = elevation
|
|
81
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
82
|
+
val shadowColor = color(style, "shadowColor", Color.BLACK)
|
|
83
|
+
outlineAmbientShadowColor = shadowColor
|
|
84
|
+
outlineSpotShadowColor = shadowColor
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (title.isNotBlank()) {
|
|
89
|
+
toast.addView(
|
|
90
|
+
TextView(activity).apply {
|
|
91
|
+
text = title
|
|
92
|
+
maxLines = 2
|
|
93
|
+
setTextColor(color(style, "titleColor", Color.WHITE))
|
|
94
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, number(style, "titleFontSize", 15.0).toFloat())
|
|
95
|
+
typeface = typeface(style, "titleFontFamily", "titleFontWeight", Typeface.BOLD)
|
|
96
|
+
includeFontPadding = false
|
|
97
|
+
},
|
|
98
|
+
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (description.isNotBlank()) {
|
|
103
|
+
toast.addView(
|
|
104
|
+
TextView(activity).apply {
|
|
105
|
+
text = description
|
|
106
|
+
maxLines = 3
|
|
107
|
+
setTextColor(color(style, "descriptionColor", Color.rgb(204, 204, 204)))
|
|
108
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, number(style, "descriptionFontSize", 13.0).toFloat())
|
|
109
|
+
typeface = typeface(style, "descriptionFontFamily", "descriptionFontWeight", Typeface.NORMAL)
|
|
110
|
+
includeFontPadding = false
|
|
111
|
+
},
|
|
112
|
+
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
|
|
113
|
+
topMargin = if (title.isNotBlank()) dp(2.0) else 0
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
val layoutParams =
|
|
119
|
+
FrameLayout.LayoutParams(toastWidth, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP or Gravity.CENTER_HORIZONTAL)
|
|
120
|
+
.apply {
|
|
121
|
+
topMargin = statusBarHeight() + dp(12.0)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
root.addView(toast, layoutParams)
|
|
125
|
+
if (haptics) {
|
|
126
|
+
triggerHaptic(toast, variant)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
toast.post {
|
|
130
|
+
val dismissedTranslation = if (animationUsesSlide(animation)) -(toast.height + layoutParams.topMargin + dp(8.0)).toFloat() else 0f
|
|
131
|
+
toast.translationY = dismissedTranslation
|
|
132
|
+
toast.visibility = View.VISIBLE
|
|
133
|
+
if (dismissible) {
|
|
134
|
+
configureDragDismiss(toast, root, dismissedTranslation, animationDurationMs)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
toast
|
|
138
|
+
.animate()
|
|
139
|
+
.alpha(1f)
|
|
140
|
+
.translationY(0f)
|
|
141
|
+
.setDuration(animationDurationMs)
|
|
142
|
+
.withEndAction {
|
|
143
|
+
if (autoHide) {
|
|
144
|
+
toast.postDelayed(
|
|
145
|
+
{
|
|
146
|
+
toast
|
|
147
|
+
.animate()
|
|
148
|
+
.alpha(if (animationUsesFade(animation)) 0f else 1f)
|
|
149
|
+
.translationY(dismissedTranslation)
|
|
150
|
+
.setDuration(animationDurationMs)
|
|
151
|
+
.withEndAction {
|
|
152
|
+
if (toast.parent === root) {
|
|
153
|
+
root.removeView(toast)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
.start()
|
|
157
|
+
},
|
|
158
|
+
durationMs.coerceAtLeast(1500.0).toLong(),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.start()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun triggerHaptic(view: View, variant: String) {
|
|
168
|
+
val feedbackConstant =
|
|
169
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
170
|
+
when (variant) {
|
|
171
|
+
"success" -> HapticFeedbackConstants.CONFIRM
|
|
172
|
+
"warning" -> HapticFeedbackConstants.LONG_PRESS
|
|
173
|
+
"error" -> HapticFeedbackConstants.REJECT
|
|
174
|
+
else -> HapticFeedbackConstants.CONTEXT_CLICK
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
when (variant) {
|
|
178
|
+
"warning", "error" -> HapticFeedbackConstants.LONG_PRESS
|
|
179
|
+
else -> HapticFeedbackConstants.KEYBOARD_TAP
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
view.isHapticFeedbackEnabled = true
|
|
184
|
+
view.performHapticFeedback(feedbackConstant)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun color(style: ReadableMap?, key: String, fallback: Int): Int {
|
|
188
|
+
val value = if (style?.hasKey(key) == true && !style.isNull(key)) style.getString(key) else null
|
|
189
|
+
return try {
|
|
190
|
+
if (value.isNullOrBlank()) fallback else Color.parseColor(value)
|
|
191
|
+
} catch (_: IllegalArgumentException) {
|
|
192
|
+
fallback
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fun number(style: ReadableMap?, key: String, fallback: Double): Double {
|
|
197
|
+
return if (style?.hasKey(key) == true && !style.isNull(key)) style.getDouble(key) else fallback
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun string(style: ReadableMap?, key: String, fallback: String): String {
|
|
201
|
+
return if (style?.hasKey(key) == true && !style.isNull(key)) style.getString(key) ?: fallback else fallback
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private fun animationUsesFade(animation: String): Boolean {
|
|
205
|
+
return animation != "none" && animation != "slide"
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun animationUsesSlide(animation: String): Boolean {
|
|
209
|
+
return animation == "slide" || animation == "slide-fade"
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun optionalNumber(style: ReadableMap?, key: String): Double? {
|
|
213
|
+
return if (style?.hasKey(key) == true && !style.isNull(key)) style.getDouble(key) else null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private fun toastWidth(style: ReadableMap?, availableWidth: Int): Int {
|
|
217
|
+
val width = optionalNumber(style, "width")?.let { dp(it).coerceIn(0, availableWidth) }
|
|
218
|
+
if (width != null) {
|
|
219
|
+
return width
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
val maxWidth = optionalNumber(style, "maxWidth")?.let { dp(it).coerceIn(0, availableWidth) }
|
|
223
|
+
return maxWidth ?: availableWidth
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private fun padding(style: ReadableMap?, edgeKey: String, axisKey: String, fallback: Double): Double {
|
|
227
|
+
return when {
|
|
228
|
+
style?.hasKey(edgeKey) == true && !style.isNull(edgeKey) -> style.getDouble(edgeKey)
|
|
229
|
+
style?.hasKey(axisKey) == true && !style.isNull(axisKey) -> style.getDouble(axisKey)
|
|
230
|
+
style?.hasKey("padding") == true && !style.isNull("padding") -> style.getDouble("padding")
|
|
231
|
+
else -> fallback
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private fun typeface(style: ReadableMap?, familyKey: String, weightKey: String, fallbackStyle: Int): Typeface {
|
|
236
|
+
val family = if (style?.hasKey(familyKey) == true && !style.isNull(familyKey)) style.getString(familyKey) else null
|
|
237
|
+
val fontStyle =
|
|
238
|
+
when (if (style?.hasKey(weightKey) == true && !style.isNull(weightKey)) style.getString(weightKey) else null) {
|
|
239
|
+
"bold", "semibold" -> Typeface.BOLD
|
|
240
|
+
"medium" -> Typeface.NORMAL
|
|
241
|
+
"normal" -> Typeface.NORMAL
|
|
242
|
+
else -> fallbackStyle
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return if (family.isNullOrBlank()) {
|
|
246
|
+
Typeface.defaultFromStyle(fontStyle)
|
|
247
|
+
} else {
|
|
248
|
+
Typeface.create(family, fontStyle)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun configureDragDismiss(
|
|
253
|
+
toast: View,
|
|
254
|
+
root: ViewGroup,
|
|
255
|
+
dismissedTranslation: Float,
|
|
256
|
+
animationDurationMs: Long,
|
|
257
|
+
) {
|
|
258
|
+
var downRawY = 0f
|
|
259
|
+
|
|
260
|
+
toast.setOnTouchListener { view, event ->
|
|
261
|
+
when (event.actionMasked) {
|
|
262
|
+
MotionEvent.ACTION_DOWN -> {
|
|
263
|
+
view.animate().cancel()
|
|
264
|
+
downRawY = event.rawY
|
|
265
|
+
view.parent?.requestDisallowInterceptTouchEvent(true)
|
|
266
|
+
true
|
|
267
|
+
}
|
|
268
|
+
MotionEvent.ACTION_MOVE -> {
|
|
269
|
+
val offsetY = (event.rawY - downRawY).coerceAtMost(0f)
|
|
270
|
+
view.translationY = offsetY
|
|
271
|
+
view.alpha = (1f + (offsetY / view.height.coerceAtLeast(1))).coerceAtLeast(0.35f)
|
|
272
|
+
true
|
|
273
|
+
}
|
|
274
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
275
|
+
val offsetY = (event.rawY - downRawY).coerceAtMost(0f)
|
|
276
|
+
val shouldDismiss = offsetY < -dp(40.0)
|
|
277
|
+
view.parent?.requestDisallowInterceptTouchEvent(false)
|
|
278
|
+
|
|
279
|
+
if (shouldDismiss) {
|
|
280
|
+
view
|
|
281
|
+
.animate()
|
|
282
|
+
.alpha(0f)
|
|
283
|
+
.translationY(dismissedTranslation)
|
|
284
|
+
.setDuration(160L)
|
|
285
|
+
.withEndAction {
|
|
286
|
+
if (view.parent === root) {
|
|
287
|
+
root.removeView(view)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
.start()
|
|
291
|
+
} else {
|
|
292
|
+
view
|
|
293
|
+
.animate()
|
|
294
|
+
.alpha(1f)
|
|
295
|
+
.translationY(0f)
|
|
296
|
+
.setDuration(animationDurationMs.coerceAtMost(160L))
|
|
297
|
+
.start()
|
|
298
|
+
}
|
|
299
|
+
true
|
|
300
|
+
}
|
|
301
|
+
else -> false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private fun dp(value: Double): Int {
|
|
307
|
+
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value.toFloat(), reactContext.resources.displayMetrics)
|
|
308
|
+
.roundToInt()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private fun statusBarHeight(): Int {
|
|
312
|
+
val resourceId = reactContext.resources.getIdentifier("status_bar_height", "dimen", "android")
|
|
313
|
+
return if (resourceId > 0) reactContext.resources.getDimensionPixelSize(resourceId) else 0
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private fun screenWidth(): Int {
|
|
317
|
+
val activity = reactContext.currentActivity ?: return reactContext.resources.displayMetrics.widthPixels
|
|
318
|
+
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
319
|
+
activity.windowManager.currentWindowMetrics.bounds.width()
|
|
320
|
+
} else {
|
|
321
|
+
@Suppress("DEPRECATION")
|
|
322
|
+
val display = (activity.getSystemService(android.content.Context.WINDOW_SERVICE) as WindowManager).defaultDisplay
|
|
323
|
+
@Suppress("DEPRECATION")
|
|
324
|
+
val metrics = android.util.DisplayMetrics().also { display.getMetrics(it) }
|
|
325
|
+
metrics.widthPixels
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private companion object {
|
|
330
|
+
const val TOAST_TAG = 900413
|
|
331
|
+
}
|
|
332
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package com.synonymdev.reactnativetoast
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class NativeToastPackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
listOf(NativeToastModule(reactContext))
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(
|
|
13
|
+
reactContext: ReactApplicationContext
|
|
14
|
+
): List<ViewManager<*, *>> = emptyList()
|
|
15
|
+
}
|