Skip to content

Commit 198a73c

Browse files
authored
Merge pull request #20 from 4cc3ssX/main
feat: custom color support + android custom progress color
2 parents ba2baaa + cf39de5 commit 198a73c

File tree

6 files changed

+144
-37
lines changed

6 files changed

+144
-37
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ alert({
154154
| `preset` | string | `done` | ![Both] ||| `done, error, none, spinner` |
155155
| `duration` | number | `3` | ![Both] ||| The lifetime of the toast (seconds) |
156156
| `titleColor` | string | null | ![Both] ||| |
157-
| `shouldDismissByDrag` | boolean | true | ![Both] || | The behavior on `Android` is click |
157+
| `shouldDismissByDrag` | boolean | true | ![Both] ||
158158
| `position` | string | `top` | ![Both] || | **Toast** is displayed from `top` or `bottom` |
159159
| `haptic` | string | null | ![iOS] || | `success, warning, error, none` |
160160
| `shouldDismissByTap` | boolean | true | ![Both] | || |
@@ -177,8 +177,8 @@ alert({
177177
- [x] `tintColor` for icon (v.1.0.5)
178178
- [x] `backgroundColor` (v.1.0.3)
179179
- [x] `spinner preset for toast` (v.1.1.0)
180-
- [ ] custom color like StyleSheet (e.g. `#fff`, `red`, `rgba(0, 0, 0, .8)`)
181-
- [ ] `shouldDismissByDrag` on **Android**
180+
- [x] custom color like StyleSheet (e.g. `#fff`, `red`, `rgba(0, 0, 0, .8)`)
181+
- [x] `shouldDismissByDrag` on **Android**
182182
- [ ] `callback`
183183

184184

android/src/main/java/com/ting/TingModule.kt

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import android.graphics.Bitmap
66
import android.graphics.BitmapFactory
77
import android.graphics.Color
88
import android.graphics.PorterDuff
9+
import android.graphics.PorterDuffColorFilter
910
import android.graphics.drawable.AnimatedVectorDrawable
1011
import android.graphics.drawable.GradientDrawable
1112
import android.os.Handler
1213
import android.os.Looper
14+
import android.view.GestureDetector
1315
import android.view.Gravity
1416
import android.view.LayoutInflater
17+
import android.view.MotionEvent
18+
import android.view.View
1519
import android.widget.ImageView
1620
import android.widget.LinearLayout
1721
import android.widget.ProgressBar
@@ -25,6 +29,7 @@ import com.facebook.react.bridge.WritableMap
2529
import com.hjq.window.EasyWindow
2630
import java.io.IOException
2731
import java.net.URL
32+
import kotlin.math.abs
2833
import kotlin.math.roundToInt
2934

3035

@@ -37,9 +42,9 @@ class TingModule internal constructor(context: ReactApplicationContext) : TingSp
3742
private var alertOptionInit: ReadableMap? = null
3843

3944
@ReactMethod
40-
override fun toast(RNOptions: ReadableMap) {
45+
override fun toast(rnOptions: ReadableMap) {
4146
// get container View
42-
val options = toastOptionInit?.let { mergeMaps(it, RNOptions) } ?: RNOptions
47+
val options = toastOptionInit?.let { mergeMaps(it, rnOptions) } ?: rnOptions
4348

4449
val container = getContainerView(R.layout.toast, options, "toast")
4550
val duration: Int = getDuration(options)
@@ -65,19 +70,46 @@ class TingModule internal constructor(context: ReactApplicationContext) : TingSp
6570
setYOffset(48)
6671
setAnimStyle(toastAnim)
6772
setOutsideTouchable(true)
68-
setOnClickListener(R.id.toast,
69-
EasyWindow.OnClickListener { toast: EasyWindow<*>, _: LinearLayout? ->
70-
val shouldDismissByTap =
71-
if (options.hasKey("shouldDismissByDrag")) options.getBoolean("shouldDismissByDrag") else true
72-
if (shouldDismissByTap) toast.cancel()
73-
})
73+
74+
if (options.hasKey("shouldDismissByDrag") && options.getBoolean("shouldDismissByDrag")) {
75+
// Define dragThreshold in density-independent pixels (dp)
76+
val dragThresholdDP = 12
77+
val scale = context.resources.displayMetrics.density
78+
val dragThreshold = (dragThresholdDP * scale + 0.5f).toInt()
79+
80+
// Add drag gesture recognizer
81+
contentView?.let { contentView ->
82+
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
83+
override fun onScroll(e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean {
84+
// Check if the user scrolls vertically and dismiss the toast window if needed
85+
if (abs(distanceY) > abs(distanceX)) {
86+
if (position == Gravity.TOP && distanceY > dragThreshold) { // Dismiss upward if toast is at the top
87+
toastWindow.cancel()
88+
return true
89+
} else if (position == Gravity.BOTTOM && distanceY < -dragThreshold) { // Dismiss downward if toast is at the bottom
90+
toastWindow.cancel()
91+
return true
92+
}
93+
}
94+
95+
return super.onScroll(e1, e2, distanceX, distanceY)
96+
}
97+
})
98+
99+
contentView.setOnTouchListener(fun(_: View, event: MotionEvent): Boolean {
100+
gestureDetector.onTouchEvent(event)
101+
return true // Consume the touch event
102+
})
103+
}
104+
}
105+
74106
}.show()
75107
}
76108
}
77109

78110
@ReactMethod
79-
override fun alert(RNOptions: ReadableMap) {
80-
val options = alertOptionInit?.let { mergeMaps(it, RNOptions) } ?: RNOptions
111+
override fun alert(rnOptions: ReadableMap) {
112+
val options = alertOptionInit?.let { mergeMaps(it, rnOptions) } ?: rnOptions
81113

82114
val container = getContainerView(R.layout.alert, options, "alert")
83115
val duration: Int = getDuration(options)
@@ -147,6 +179,7 @@ class TingModule internal constructor(context: ReactApplicationContext) : TingSp
147179
val titleColor = options?.getString("titleColor")
148180
val message = options?.getString("message")
149181
val messageColor = options?.getString("messageColor")
182+
val progressColor = options?.getString("progressColor")
150183
val preset = options?.getString("preset")
151184
val backgroundColor = options?.getString("backgroundColor")
152185
val borderRadius = if (options.hasKey("borderRadius")) options?.getInt("borderRadius") else null
@@ -213,6 +246,13 @@ class TingModule internal constructor(context: ReactApplicationContext) : TingSp
213246
iconView.visibility = ImageView.GONE
214247
progressBar.id = R.id.loading_spinner
215248
progressBar.layoutParams = progressSize
249+
250+
if (progressColor != null) {
251+
val progressDrawable = progressBar.indeterminateDrawable.mutate()
252+
progressDrawable.colorFilter = PorterDuffColorFilter(parseColor(progressColor), PorterDuff.Mode.SRC_IN)
253+
progressBar.indeterminateDrawable = progressDrawable
254+
}
255+
216256
container.addView(progressBar, 0)
217257
}
218258

@@ -271,14 +311,36 @@ fun isNumber(value: Any?): Boolean {
271311
return value != null && (value is Int || value is Long || value is Float || value is Double)
272312
}
273313

274-
fun parseColor(hexColor: String): Int {
314+
fun parseColor(colorString: String): Int {
275315
val fallbackColor = Color.BLACK
276-
val color = try {
277-
Color.parseColor(hexColor)
278-
} catch (e: IllegalArgumentException) {
279-
fallbackColor
316+
// Try parsing color as hex
317+
if (colorString.startsWith("#")) {
318+
return Color.parseColor(colorString)
280319
}
281-
return color
320+
321+
// Try parsing color as named color
322+
val namedColor = try {
323+
Color::class.java.getField(colorString.uppercase()).get(null) as Int
324+
} catch (e: Exception) {
325+
null
326+
}
327+
if (namedColor != null) {
328+
return namedColor
329+
}
330+
331+
// Try parsing color as RGB or RGBA
332+
val rgbRegex = Regex("""rgba?\((\d{1,3}), (\d{1,3}), (\d{1,3})(, (\d(\.\d)?))?\)""")
333+
val rgbMatchResult = rgbRegex.matchEntire(colorString)
334+
if (rgbMatchResult != null) {
335+
val red = rgbMatchResult.groups[1]?.value?.toIntOrNull() ?: return fallbackColor
336+
val green = rgbMatchResult.groups[2]?.value?.toIntOrNull() ?: return fallbackColor
337+
val blue = rgbMatchResult.groups[3]?.value?.toIntOrNull() ?: return fallbackColor
338+
val alpha = if (colorString.startsWith("rgb(")) 1.0f else rgbMatchResult.groups[5]?.value?.toFloatOrNull() ?: 1.0f
339+
return Color.argb((alpha * 255).toInt(), red, green, blue)
340+
}
341+
342+
// Return fallback color if parsing fails
343+
return fallbackColor
282344
}
283345

284346
fun convertInt2Size(number: Int?): Int {

example/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ const DATA: ItemType[] = [
160160
onPress: () =>
161161
toast({
162162
duration: 10,
163-
164163
title: 'Việt Nam',
165164
titleColor: '#ffffff',
166165
message: 'Vietnamese Gangz 🇻🇳',
@@ -194,6 +193,8 @@ const DATA: ItemType[] = [
194193
title: 'Đáy xã hội',
195194
message: 'Chờ xíu',
196195
preset: 'spinner',
196+
progressColor: '#FD966A',
197+
shouldDismissByDrag: true,
197198
}),
198199
},
199200
{
@@ -254,6 +255,7 @@ const DATA: ItemType[] = [
254255
blurBackdrop: 20,
255256
backdropOpacity: 0.1,
256257
preset: 'spinner',
258+
progressColor: '#FD966A',
257259
}),
258260
},
259261
];

ios/Type.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ class Options {
2727
self.title = option["title"] as? String ?? "Title"
2828
self.message = option["message"] as? String
2929
self.duration = option["duration"] as? TimeInterval ?? 3
30-
self.backgroundColor = UIColor.hexStringToColor(option["backgroundColor"] as? String)
30+
self.backgroundColor = UIColor.parseColor(option["backgroundColor"] as? String)
3131

3232
if let messageColor = option["messageColor"] as? String {
33-
self.messageColor = UIColor.hexStringToColor(messageColor)
33+
self.messageColor = UIColor.parseColor(messageColor)
3434
}
3535

3636
if let titleColor = option["titleColor"] as? String {
37-
self.titleColor = UIColor.hexStringToColor(titleColor)
37+
self.titleColor = UIColor.parseColor(titleColor)
3838
}
3939

4040
// custom icon
@@ -53,7 +53,7 @@ class Options {
5353
func getCustomIcon(icon: NSDictionary) -> UIImage? {
5454
if let iconURI = icon["uri"] as? String {
5555
if let iconImage = getImage(icon: iconURI) {
56-
let color = UIColor.hexStringToColor(icon["tintColor"] as? String)
56+
let color = UIColor.parseColor(icon["tintColor"] as? String)
5757
if(color != nil){
5858
return iconImage.getTintColor(color!)
5959
}

ios/Utils.swift

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,57 @@ extension UIImage {
3030
}
3131

3232
extension UIColor {
33-
static func hexStringToColor(_ stringToConvert: String?) -> UIColor? {
34-
if stringToConvert == nil {
33+
static func parseColor(_ colorString: String?) -> UIColor? {
34+
guard let colorString = colorString else {
3535
return nil
3636
}
3737

38-
let noHashString = stringToConvert!.replacingOccurrences(of: "#", with: "")
39-
40-
// Đảm bảo độ dài chuỗi hợp lệ
41-
guard noHashString.count == 6 else {
42-
return nil
38+
// Try parsing color as hex
39+
if colorString.hasPrefix("#") {
40+
let hexString = String(colorString.dropFirst()) // Remove '#' character
41+
42+
// Check if the hex string has valid length
43+
guard hexString.count == 6 || hexString.count == 8 else {
44+
return .gray // Return gray color for invalid hex string
45+
}
46+
47+
// Parse hex values
48+
var rgbValue: UInt64 = 0
49+
Scanner(string: hexString).scanHexInt64(&rgbValue)
50+
51+
// Extract individual color components
52+
let red = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
53+
let green = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
54+
let blue = CGFloat(rgbValue & 0x0000FF) / 255.0
55+
let alpha = hexString.count == 8 ? CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0 : 1.0
56+
57+
// Create and return UIColor
58+
return UIColor(red: red, green: green, blue: blue, alpha: alpha)
4359
}
4460

45-
var rgbValue: UInt64 = 0
46-
Scanner(string: noHashString).scanHexInt64(&rgbValue)
61+
// Try parsing color as named color
62+
if let namedColor = UIColor(named: colorString) {
63+
return namedColor
64+
}
4765

48-
let r = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
49-
let g = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
50-
let b = CGFloat(rgbValue & 0x0000FF) / 255.0
66+
// Try parsing color as RGB or RGBA
67+
let rgbRegex = try! NSRegularExpression(pattern: #"rgba?\((\d{1,3}), (\d{1,3}), (\d{1,3})(, (\d(\.\d)?))?\)"#)
68+
if let rgbMatch = rgbRegex.firstMatch(in: colorString, range: NSRange(colorString.startIndex..., in: colorString)) {
69+
let red = Int(colorString[Range(rgbMatch.range(at: 1), in: colorString)!])!
70+
let green = Int(colorString[Range(rgbMatch.range(at: 2), in: colorString)!])!
71+
let blue = Int(colorString[Range(rgbMatch.range(at: 3), in: colorString)!])!
72+
let alpha = rgbMatch.numberOfRanges > 5 ? Float(colorString[Range(rgbMatch.range(at: 5), in: colorString)!])! : 1.0
73+
74+
// If the color string starts with "rgb(", ignore the alpha component
75+
if colorString.hasPrefix("rgb(") {
76+
return UIColor(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0)
77+
} else {
78+
return UIColor(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: CGFloat(alpha))
79+
}
80+
}
5181

52-
return UIColor(red: r, green: g, blue: b, alpha: 1.0)
82+
// Fallback to black color
83+
return .black
5384
}
5485
}
5586

src/Type.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ export interface ToastOptions {
3838
* Defaults to `null`.
3939
*/
4040
icon?: Icon;
41+
42+
/**
43+
* progress color for spinner preset
44+
* @platform android
45+
*/
46+
progressColor?: string;
4147
}
4248

4349
export interface AlertOptions {
@@ -87,4 +93,10 @@ export interface AlertOptions {
8793
* custom icon
8894
*/
8995
icon?: Icon;
96+
97+
/**
98+
* progress color for spinner preset
99+
* @platform android
100+
*/
101+
progressColor?: string;
90102
}

0 commit comments

Comments
 (0)