clox-view-switcher 0.1.2 → 0.1.4
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/android/build.gradle +6 -0
- package/android/src/main/java/expo/modules/cloxviewswitcher/AppCardView.kt +197 -0
- package/android/src/main/java/expo/modules/cloxviewswitcher/CloxViewSwitcherModule.kt +48 -3
- package/android/src/main/java/expo/modules/cloxviewswitcher/CloxViewSwitcherView.kt +657 -16
- package/build/CloxViewSwitcher.types.d.ts +2 -2
- package/build/CloxViewSwitcher.types.d.ts.map +1 -1
- package/build/CloxViewSwitcher.types.js.map +1 -1
- package/build/CloxViewSwitcherView.js +1 -1
- package/build/CloxViewSwitcherView.js.map +1 -1
- package/example/App.tsx +4 -4
- package/example/ios/Podfile.lock +2 -2
- package/example/ios/cloxviewswitcherexample.xcodeproj/project.pbxproj +72 -72
- package/ios/AppView.swift +14 -12
- package/package.json +1 -1
- package/src/CloxViewSwitcher.types.ts +2 -2
- package/src/CloxViewSwitcherView.tsx +1 -1
package/android/build.gradle
CHANGED
|
@@ -39,4 +39,10 @@ android {
|
|
|
39
39
|
dependencies {
|
|
40
40
|
implementation project(':expo-modules-core')
|
|
41
41
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.23"
|
|
42
|
+
|
|
43
|
+
// Image loading
|
|
44
|
+
implementation "io.coil-kt:coil:2.5.0"
|
|
45
|
+
|
|
46
|
+
// For rounded corners and shadows
|
|
47
|
+
implementation "androidx.cardview:cardview:1.0.0"
|
|
42
48
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
package expo.modules.cloxviewswitcher
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.Typeface
|
|
6
|
+
import android.graphics.drawable.GradientDrawable
|
|
7
|
+
import android.util.TypedValue
|
|
8
|
+
import android.view.Gravity
|
|
9
|
+
import android.view.View
|
|
10
|
+
import android.widget.FrameLayout
|
|
11
|
+
import android.widget.ImageView
|
|
12
|
+
import android.widget.LinearLayout
|
|
13
|
+
import android.widget.TextView
|
|
14
|
+
import coil.load
|
|
15
|
+
import coil.transform.RoundedCornersTransformation
|
|
16
|
+
|
|
17
|
+
data class AppItem(
|
|
18
|
+
val id: String,
|
|
19
|
+
val title: String,
|
|
20
|
+
val image: String,
|
|
21
|
+
val icon: String
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* AppCardView - Matches iOS AppCardView exactly
|
|
26
|
+
*
|
|
27
|
+
* Layout (from top to bottom):
|
|
28
|
+
* - Header HStack (icon + title) - 30pt height area
|
|
29
|
+
* - Card screenshot - 260x540
|
|
30
|
+
* Total frame: 260x580
|
|
31
|
+
*
|
|
32
|
+
* Header has x offset of 15
|
|
33
|
+
*/
|
|
34
|
+
class AppCardView(
|
|
35
|
+
context: Context,
|
|
36
|
+
val item: AppItem,
|
|
37
|
+
private var backgroundColor: Int = Color.WHITE,
|
|
38
|
+
private var borderRadius: Float = 20f,
|
|
39
|
+
private var titleColor: Int = Color.WHITE,
|
|
40
|
+
private var titleSize: Float = 16f,
|
|
41
|
+
private var titleWeight: Int = Typeface.BOLD
|
|
42
|
+
) : LinearLayout(context) {
|
|
43
|
+
|
|
44
|
+
// iOS dimensions (in points, need to convert to pixels)
|
|
45
|
+
companion object {
|
|
46
|
+
const val CARD_WIDTH_DP = 260f
|
|
47
|
+
const val CARD_HEIGHT_DP = 540f
|
|
48
|
+
const val TOTAL_HEIGHT_DP = 580f
|
|
49
|
+
const val HEADER_HEIGHT_DP = 40f // Space for header (580 - 540 = 40)
|
|
50
|
+
const val ICON_SIZE_DP = 30f
|
|
51
|
+
const val ICON_CORNER_DP = 6f
|
|
52
|
+
const val HEADER_OFFSET_X_DP = 15f
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private val density = context.resources.displayMetrics.density
|
|
56
|
+
|
|
57
|
+
// Card dimensions in pixels
|
|
58
|
+
val cardWidthPx = (CARD_WIDTH_DP * density).toInt()
|
|
59
|
+
val cardHeightPx = (CARD_HEIGHT_DP * density).toInt()
|
|
60
|
+
val totalHeightPx = (TOTAL_HEIGHT_DP * density).toInt()
|
|
61
|
+
|
|
62
|
+
private val headerContainer: LinearLayout
|
|
63
|
+
private val iconImageView: ImageView
|
|
64
|
+
private val titleTextView: TextView
|
|
65
|
+
private val cardContainer: FrameLayout
|
|
66
|
+
private val cardImageView: ImageView
|
|
67
|
+
|
|
68
|
+
var onCardPress: ((String) -> Unit)? = null
|
|
69
|
+
|
|
70
|
+
init {
|
|
71
|
+
orientation = VERTICAL
|
|
72
|
+
layoutParams = LayoutParams(cardWidthPx, totalHeightPx)
|
|
73
|
+
setBackgroundColor(Color.TRANSPARENT)
|
|
74
|
+
|
|
75
|
+
// ==========================================
|
|
76
|
+
// HEADER (icon + title) - comes FIRST at TOP
|
|
77
|
+
// ==========================================
|
|
78
|
+
headerContainer = LinearLayout(context).apply {
|
|
79
|
+
orientation = HORIZONTAL
|
|
80
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
81
|
+
layoutParams = LayoutParams(
|
|
82
|
+
LayoutParams.MATCH_PARENT,
|
|
83
|
+
(HEADER_HEIGHT_DP * density).toInt()
|
|
84
|
+
)
|
|
85
|
+
// Header has x offset of 15 (like iOS .offset(x: 15))
|
|
86
|
+
setPadding((HEADER_OFFSET_X_DP * density).toInt(), 0, 0, 0)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// App icon - 30x30 with corner radius 6 (only show if icon is provided)
|
|
90
|
+
val iconSizePx = (ICON_SIZE_DP * density).toInt()
|
|
91
|
+
iconImageView = ImageView(context).apply {
|
|
92
|
+
layoutParams = LayoutParams(iconSizePx, iconSizePx).apply {
|
|
93
|
+
marginEnd = (8 * density).toInt()
|
|
94
|
+
}
|
|
95
|
+
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
96
|
+
clipToOutline = true
|
|
97
|
+
// No background at all
|
|
98
|
+
background = null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Only add icon to header if icon URL is provided
|
|
102
|
+
if (item.icon.isNotEmpty()) {
|
|
103
|
+
iconImageView.load(item.icon) {
|
|
104
|
+
crossfade(true)
|
|
105
|
+
transformations(RoundedCornersTransformation(ICON_CORNER_DP * density))
|
|
106
|
+
}
|
|
107
|
+
headerContainer.addView(iconImageView)
|
|
108
|
+
}
|
|
109
|
+
// If no icon, don't add iconImageView to header at all
|
|
110
|
+
|
|
111
|
+
// App title
|
|
112
|
+
titleTextView = TextView(context).apply {
|
|
113
|
+
text = item.title
|
|
114
|
+
setTextColor(titleColor)
|
|
115
|
+
setTextSize(TypedValue.COMPLEX_UNIT_SP, titleSize)
|
|
116
|
+
setTypeface(typeface, titleWeight)
|
|
117
|
+
}
|
|
118
|
+
headerContainer.addView(titleTextView)
|
|
119
|
+
|
|
120
|
+
addView(headerContainer)
|
|
121
|
+
|
|
122
|
+
// ==========================================
|
|
123
|
+
// CARD SCREENSHOT - comes AFTER header
|
|
124
|
+
// ==========================================
|
|
125
|
+
cardContainer = FrameLayout(context).apply {
|
|
126
|
+
layoutParams = LayoutParams(cardWidthPx, cardHeightPx)
|
|
127
|
+
|
|
128
|
+
// Background with rounded corners
|
|
129
|
+
val bgDrawable = GradientDrawable().apply {
|
|
130
|
+
setColor(backgroundColor)
|
|
131
|
+
cornerRadius = borderRadius * density
|
|
132
|
+
}
|
|
133
|
+
background = bgDrawable
|
|
134
|
+
clipToOutline = true
|
|
135
|
+
elevation = 6 * density // Shadow like iOS shadow(radius: 3)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
cardImageView = ImageView(context).apply {
|
|
139
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
140
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
141
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
142
|
+
)
|
|
143
|
+
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
144
|
+
|
|
145
|
+
if (item.image.isNotEmpty()) {
|
|
146
|
+
load(item.image) {
|
|
147
|
+
crossfade(true)
|
|
148
|
+
transformations(RoundedCornersTransformation(borderRadius * density))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
cardContainer.addView(cardImageView)
|
|
153
|
+
|
|
154
|
+
addView(cardContainer)
|
|
155
|
+
|
|
156
|
+
// Click listener on entire card
|
|
157
|
+
setOnClickListener {
|
|
158
|
+
onCardPress?.invoke(item.id)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fun setTitleOpacity(opacity: Float) {
|
|
163
|
+
titleTextView.alpha = opacity
|
|
164
|
+
// Icon stays visible, only title opacity changes (matching iOS)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fun updateStyles(
|
|
168
|
+
newBackgroundColor: Int,
|
|
169
|
+
newBorderRadius: Float,
|
|
170
|
+
newTitleColor: Int,
|
|
171
|
+
newTitleSize: Float,
|
|
172
|
+
newTitleWeight: Int
|
|
173
|
+
) {
|
|
174
|
+
backgroundColor = newBackgroundColor
|
|
175
|
+
borderRadius = newBorderRadius
|
|
176
|
+
titleColor = newTitleColor
|
|
177
|
+
titleSize = newTitleSize
|
|
178
|
+
titleWeight = newTitleWeight
|
|
179
|
+
|
|
180
|
+
// Update card background
|
|
181
|
+
val bgDrawable = GradientDrawable().apply {
|
|
182
|
+
setColor(backgroundColor)
|
|
183
|
+
cornerRadius = borderRadius * density
|
|
184
|
+
}
|
|
185
|
+
cardContainer.background = bgDrawable
|
|
186
|
+
|
|
187
|
+
// Icon has transparent background, no update needed
|
|
188
|
+
|
|
189
|
+
// Update title
|
|
190
|
+
titleTextView.setTextColor(titleColor)
|
|
191
|
+
titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, titleSize)
|
|
192
|
+
titleTextView.setTypeface(titleTextView.typeface, titleWeight)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fun getCardWidthDp(): Float = CARD_WIDTH_DP
|
|
196
|
+
fun getTotalHeightDp(): Float = TOTAL_HEIGHT_DP
|
|
197
|
+
}
|
|
@@ -7,15 +7,60 @@ class CloxViewSwitcherModule : Module() {
|
|
|
7
7
|
override fun definition() = ModuleDefinition {
|
|
8
8
|
Name("CloxViewSwitcher")
|
|
9
9
|
|
|
10
|
-
Events("onChange")
|
|
10
|
+
Events("onChange", "onItemPress", "onCardChange", "onItemRemove")
|
|
11
11
|
|
|
12
12
|
Function("hello") {
|
|
13
13
|
"Hello World! 👋"
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
// Enables the module to be used as a native view.
|
|
17
16
|
View(CloxViewSwitcherView::class) {
|
|
18
|
-
//
|
|
17
|
+
// Items prop - array of app items
|
|
18
|
+
Prop("items") { view: CloxViewSwitcherView, items: List<Map<String, String>> ->
|
|
19
|
+
view.setItems(items)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Spring animation response (duration)
|
|
23
|
+
Prop("springResponse") { view: CloxViewSwitcherView, value: Double ->
|
|
24
|
+
view.setSpringResponse(value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Spring animation damping fraction
|
|
28
|
+
Prop("springDamping") { view: CloxViewSwitcherView, value: Double ->
|
|
29
|
+
view.setSpringDamping(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Scroll speed multiplier
|
|
33
|
+
Prop("scrollSpeed") { view: CloxViewSwitcherView, value: Double ->
|
|
34
|
+
view.setScrollSpeed(value)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Card background color (placeholder before images load)
|
|
38
|
+
Prop("cardBackgroundColor") { view: CloxViewSwitcherView, value: String ->
|
|
39
|
+
view.setCardBackgroundColor(value)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Card border radius
|
|
43
|
+
Prop("cardBorderRadius") { view: CloxViewSwitcherView, value: Double ->
|
|
44
|
+
view.setCardBorderRadius(value)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Title font color
|
|
48
|
+
Prop("titleFontColor") { view: CloxViewSwitcherView, value: String ->
|
|
49
|
+
view.setTitleFontColor(value)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Title font size
|
|
53
|
+
Prop("titleFontSize") { view: CloxViewSwitcherView, value: Double ->
|
|
54
|
+
view.setTitleFontSize(value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Title font weight
|
|
58
|
+
Prop("titleFontWeight") { view: CloxViewSwitcherView, value: String ->
|
|
59
|
+
view.setTitleFontWeight(value)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Events
|
|
63
|
+
Events("onItemPress", "onCardChange", "onItemRemove")
|
|
19
64
|
}
|
|
20
65
|
}
|
|
21
66
|
}
|