react-native-nitro-player 1.5.0-alpha.0 → 1.5.0-alpha.3
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/README.md +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCast.kt +75 -37
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +3 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +106 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +15 -3
- package/ios/core/CastSessionManager.swift +197 -56
- package/ios/core/TrackPlayerCast.swift +231 -10
- package/ios/core/TrackPlayerPlayback.swift +3 -1
- package/ios/core/TrackPlayerQueue.swift +27 -11
- package/ios/core/TrackPlayerQueueBuild.swift +6 -0
- package/ios/core/TrackPlayerUrlLoader.swift +26 -2
- package/lib/components/CastButton.js +68 -43
- package/package.json +1 -1
- package/src/components/CastButton.tsx +84 -47
|
@@ -20,11 +20,7 @@ extension TrackPlayerCore {
|
|
|
20
20
|
|
|
21
21
|
func skipToIndex(index: Int) async -> Bool {
|
|
22
22
|
if isCasting {
|
|
23
|
-
return await withPlayerQueueNoThrow {
|
|
24
|
-
guard index >= 0 && index < self.getActualQueueInternal().count else { return false }
|
|
25
|
-
self.castManager?.jump(toQueueIndex: index)
|
|
26
|
-
return true
|
|
27
|
-
}
|
|
23
|
+
return await withPlayerQueueNoThrow { self.skipToIndexCastInternal(index: index) }
|
|
28
24
|
}
|
|
29
25
|
return await withPlayerQueueNoThrow { self.skipToIndexInternal(index: index) }
|
|
30
26
|
}
|
|
@@ -40,6 +36,29 @@ extension TrackPlayerCore {
|
|
|
40
36
|
// MARK: - Internal (run on playerQueue)
|
|
41
37
|
|
|
42
38
|
func getStateInternal() -> PlayerState {
|
|
39
|
+
// While casting, report the receiver's cached state — the local AVQueuePlayer is paused/stale.
|
|
40
|
+
if isCasting, let cast = castManager {
|
|
41
|
+
let currentTrack = getCurrentTrack()
|
|
42
|
+
let playingType: CurrentPlayingType
|
|
43
|
+
if currentTrack == nil { playingType = .notPlaying }
|
|
44
|
+
else {
|
|
45
|
+
switch currentTemporaryType {
|
|
46
|
+
case .none: playingType = .playlist
|
|
47
|
+
case .playNext: playingType = .playNext
|
|
48
|
+
case .upNext: playingType = .upNext
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return PlayerState(
|
|
52
|
+
currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
|
|
53
|
+
currentPosition: cast.lastKnownRemotePosition,
|
|
54
|
+
totalDuration: cast.lastKnownRemoteDuration,
|
|
55
|
+
currentState: cast.cachedPlaybackState,
|
|
56
|
+
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
57
|
+
currentIndex: currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0,
|
|
58
|
+
currentPlayingType: playingType
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
guard let player else {
|
|
44
63
|
return PlayerState(
|
|
45
64
|
currentTrack: nil, currentPosition: 0.0, totalDuration: 0.0,
|
|
@@ -96,7 +115,7 @@ extension TrackPlayerCore {
|
|
|
96
115
|
if let current = getCurrentTrack() { queue.append(current) }
|
|
97
116
|
|
|
98
117
|
// Add playNext stack — skip the currently playing track by ID (already added as current)
|
|
99
|
-
let currentId =
|
|
118
|
+
let currentId = activeCurrentTrackId
|
|
100
119
|
if currentTemporaryType == .playNext, let currentId = currentId {
|
|
101
120
|
var skipped = false
|
|
102
121
|
for track in playNextStack {
|
|
@@ -126,10 +145,7 @@ extension TrackPlayerCore {
|
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
func getCurrentTrack() -> TrackItem? {
|
|
129
|
-
if currentTemporaryType != .none,
|
|
130
|
-
let currentItem = player?.currentItem,
|
|
131
|
-
let trackId = currentItem.trackId
|
|
132
|
-
{
|
|
148
|
+
if currentTemporaryType != .none, let trackId = activeCurrentTrackId {
|
|
133
149
|
if currentTemporaryType == .playNext { return playNextStack.first(where: { $0.id == trackId }) }
|
|
134
150
|
if currentTemporaryType == .upNext { return upNextQueue.first(where: { $0.id == trackId }) }
|
|
135
151
|
}
|
|
@@ -222,7 +238,7 @@ extension TrackPlayerCore {
|
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
func determineCurrentTemporaryType() -> TemporaryType {
|
|
225
|
-
guard let trackId =
|
|
241
|
+
guard let trackId = activeCurrentTrackId else { return .none }
|
|
226
242
|
if playNextStack.contains(where: { $0.id == trackId }) { return .playNext }
|
|
227
243
|
if upNextQueue.contains(where: { $0.id == trackId }) { return .upNext }
|
|
228
244
|
return .none
|
|
@@ -240,6 +240,12 @@ extension TrackPlayerCore {
|
|
|
240
240
|
/// - Parameter changedTrackIds: When non-nil, performs a surgical update:
|
|
241
241
|
/// only AVPlayerItems whose track ID is in this set are removed and re-created.
|
|
242
242
|
func rebuildAVQueueFromCurrentPosition(changedTrackIds: Set<String>? = nil) {
|
|
243
|
+
// While casting, queue changes are applied to the receiver — the local AVQueuePlayer is dormant.
|
|
244
|
+
if isCasting {
|
|
245
|
+
syncCastQueueAfterCurrent()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
243
249
|
guard let player = self.player else { return }
|
|
244
250
|
|
|
245
251
|
let currentItem = player.currentItem
|
|
@@ -78,8 +78,8 @@ extension TrackPlayerCore {
|
|
|
78
78
|
// Update in PlaylistManager
|
|
79
79
|
let affectedPlaylists = self.playlistManager.updateTracks(tracks: safeTracks)
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
if let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
|
|
81
|
+
// Replace the current AVPlayerItem when its URL just resolved (local only — cast reloads below instead).
|
|
82
|
+
if !self.isCasting, let update = currentTrack, currentTrackIsEmpty, !update.url.isEmpty {
|
|
83
83
|
NitroPlayerLogger.log("TrackPlayerCore",
|
|
84
84
|
"🔄 Replacing current AVPlayerItem for track with resolved URL: \(update.id)")
|
|
85
85
|
if let newItem = self.createGaplessPlayerItem(for: update, isPreload: false) {
|
|
@@ -101,6 +101,30 @@ extension TrackPlayerCore {
|
|
|
101
101
|
"📥 Synced currentTracks from PlaylistManager (\(self.currentTracks.count) tracks)")
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
if self.isCasting {
|
|
105
|
+
// Resolved URLs are applied by (re)loading/extending the remote queue — never by touching the silent local player.
|
|
106
|
+
let currentResolvedNow = currentTrackIsEmpty
|
|
107
|
+
&& currentTrackId.map { id in updatedTrackIds.contains(id) } ?? false
|
|
108
|
+
let staleOnReceiver = !updatedTrackIds
|
|
109
|
+
.isDisjoint(with: Set(self.castManager?.loadedTrackIds ?? []))
|
|
110
|
+
|
|
111
|
+
if currentResolvedNow || !(self.castManager?.hasLoadedMedia ?? false) {
|
|
112
|
+
// Parked track just became castable (or nothing loaded) — atomic load from current.
|
|
113
|
+
self.loadCastQueue(autoplay: self.intendedToPlay, position: 0)
|
|
114
|
+
} else if staleOnReceiver {
|
|
115
|
+
// URLs changed for items already on the receiver — reload at position before stale ones fail.
|
|
116
|
+
self.loadCastQueue(
|
|
117
|
+
autoplay: self.intendedToPlay,
|
|
118
|
+
position: self.castManager?.lastKnownRemotePosition ?? 0
|
|
119
|
+
)
|
|
120
|
+
} else {
|
|
121
|
+
// Newly castable tracks extend the receiver queue without interrupting playback.
|
|
122
|
+
self.syncCastQueueAfterCurrent()
|
|
123
|
+
}
|
|
124
|
+
NitroPlayerLogger.log("TrackPlayerCore", "✅ Cast queue synced after track updates")
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
104
128
|
if self.player?.currentItem == nil, let player = self.player {
|
|
105
129
|
// No AVPlayerItem exists yet — lazy-load mode: URLs were empty when the queue first loaded.
|
|
106
130
|
NitroPlayerLogger.log("TrackPlayerCore",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Pressable,
|
|
2
|
+
import { Pressable, View, } from 'react-native';
|
|
3
3
|
import { Cast } from '../index';
|
|
4
4
|
import { useCastState } from '../hooks/useCastState';
|
|
5
5
|
/**
|
|
@@ -40,61 +40,86 @@ export function CastButton({ size = 24, color = '#000000', activeColor = '#1E88E
|
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
42
|
* Dependency-free rendition of the standard Cast glyph: a rounded "screen"
|
|
43
|
-
* rectangle with
|
|
44
|
-
*
|
|
43
|
+
* rectangle (with its bottom-left corner open) and a broadcast signal — a dot
|
|
44
|
+
* and two concentric arcs — radiating from the bottom-left. Drawn purely with
|
|
45
|
+
* `View`s so the library stays dependency-free.
|
|
45
46
|
*/
|
|
46
47
|
function CastGlyph({ size, color, }) {
|
|
47
|
-
const stroke = Math.max(1.5, Math.round(size
|
|
48
|
-
const inset =
|
|
49
|
-
const dotSize = stroke * 1.
|
|
50
|
-
|
|
51
|
-
const
|
|
48
|
+
const stroke = Math.max(1.5, Math.round(size / 13));
|
|
49
|
+
const inset = Math.round(size * 0.14);
|
|
50
|
+
const dotSize = stroke * 1.5;
|
|
51
|
+
// Origin of the broadcast signal — the screen's bottom-left corner.
|
|
52
|
+
const cx = inset + stroke / 2;
|
|
53
|
+
const cy = size - inset - stroke / 2;
|
|
54
|
+
// Leave the screen's bottom-left corner open so the signal reads clearly.
|
|
55
|
+
const gap = size * 0.46;
|
|
52
56
|
return (React.createElement(View, { style: { width: size, height: size } },
|
|
53
|
-
React.createElement(View, { style: [
|
|
54
|
-
StyleSheet.absoluteFill,
|
|
55
|
-
{
|
|
56
|
-
borderWidth: stroke,
|
|
57
|
-
borderColor: color,
|
|
58
|
-
borderRadius: Math.max(2, size * 0.12),
|
|
59
|
-
},
|
|
60
|
-
] }),
|
|
61
57
|
React.createElement(View, { style: {
|
|
62
58
|
position: 'absolute',
|
|
63
59
|
left: inset,
|
|
60
|
+
top: inset,
|
|
61
|
+
right: inset,
|
|
62
|
+
height: stroke,
|
|
63
|
+
backgroundColor: color,
|
|
64
|
+
borderRadius: stroke,
|
|
65
|
+
} }),
|
|
66
|
+
React.createElement(View, { style: {
|
|
67
|
+
position: 'absolute',
|
|
68
|
+
top: inset,
|
|
69
|
+
bottom: inset,
|
|
70
|
+
right: inset,
|
|
71
|
+
width: stroke,
|
|
72
|
+
backgroundColor: color,
|
|
73
|
+
borderRadius: stroke,
|
|
74
|
+
} }),
|
|
75
|
+
React.createElement(View, { style: {
|
|
76
|
+
position: 'absolute',
|
|
77
|
+
left: inset + gap,
|
|
64
78
|
bottom: inset,
|
|
79
|
+
right: inset,
|
|
80
|
+
height: stroke,
|
|
81
|
+
backgroundColor: color,
|
|
82
|
+
borderRadius: stroke,
|
|
83
|
+
} }),
|
|
84
|
+
React.createElement(View, { style: {
|
|
85
|
+
position: 'absolute',
|
|
86
|
+
left: inset,
|
|
87
|
+
top: inset,
|
|
88
|
+
height: size - 2 * inset - gap,
|
|
89
|
+
width: stroke,
|
|
90
|
+
backgroundColor: color,
|
|
91
|
+
borderRadius: stroke,
|
|
92
|
+
} }),
|
|
93
|
+
React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.2, stroke: stroke, color: color }),
|
|
94
|
+
React.createElement(RippleArc, { cx: cx, cy: cy, radius: size * 0.34, stroke: stroke, color: color }),
|
|
95
|
+
React.createElement(View, { style: {
|
|
96
|
+
position: 'absolute',
|
|
97
|
+
left: cx - dotSize / 2,
|
|
98
|
+
top: cy - dotSize / 2,
|
|
65
99
|
width: dotSize,
|
|
66
100
|
height: dotSize,
|
|
67
101
|
borderRadius: dotSize / 2,
|
|
68
102
|
backgroundColor: color,
|
|
69
|
-
} })
|
|
70
|
-
React.createElement(Arc, { origin: inset + dotSize / 2, radius: r1, stroke: stroke, color: color }),
|
|
71
|
-
React.createElement(Arc, { origin: inset + dotSize / 2, radius: r2, stroke: stroke, color: color })));
|
|
103
|
+
} })));
|
|
72
104
|
}
|
|
73
105
|
/**
|
|
74
|
-
* A quarter-circle arc (the
|
|
75
|
-
*
|
|
106
|
+
* A quarter-circle arc (the up-right quadrant) centered at (cx, cy). Rendered as
|
|
107
|
+
* a circle with only its top border colored, rotated 45° so the visible cap
|
|
108
|
+
* faces up-and-to-the-right — i.e. radiating from the bottom-left dot.
|
|
76
109
|
*/
|
|
77
|
-
function
|
|
78
|
-
return (React.createElement(View
|
|
79
|
-
// Clip box reveals only the quadrant facing up-and-right from the origin.
|
|
80
|
-
, {
|
|
81
|
-
// Clip box reveals only the quadrant facing up-and-right from the origin.
|
|
82
|
-
style: {
|
|
110
|
+
function RippleArc({ cx, cy, radius, stroke, color, }) {
|
|
111
|
+
return (React.createElement(View, { style: {
|
|
83
112
|
position: 'absolute',
|
|
84
|
-
left:
|
|
85
|
-
|
|
86
|
-
width: radius,
|
|
87
|
-
height: radius,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
borderRadius: radius,
|
|
97
|
-
borderWidth: stroke,
|
|
98
|
-
borderColor: color,
|
|
99
|
-
} })));
|
|
113
|
+
left: cx - radius,
|
|
114
|
+
top: cy - radius,
|
|
115
|
+
width: radius * 2,
|
|
116
|
+
height: radius * 2,
|
|
117
|
+
borderRadius: radius,
|
|
118
|
+
borderWidth: stroke,
|
|
119
|
+
borderTopColor: color,
|
|
120
|
+
borderRightColor: 'transparent',
|
|
121
|
+
borderBottomColor: 'transparent',
|
|
122
|
+
borderLeftColor: 'transparent',
|
|
123
|
+
transform: [{ rotate: '45deg' }],
|
|
124
|
+
} }));
|
|
100
125
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-player",
|
|
3
|
-
"version": "1.5.0-alpha.
|
|
3
|
+
"version": "1.5.0-alpha.3",
|
|
4
4
|
"description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"module": "lib/index",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {
|
|
3
3
|
Pressable,
|
|
4
|
-
StyleSheet,
|
|
5
4
|
View,
|
|
6
5
|
type PressableProps,
|
|
7
6
|
type StyleProp,
|
|
@@ -104,8 +103,9 @@ export function CastButton({
|
|
|
104
103
|
|
|
105
104
|
/**
|
|
106
105
|
* Dependency-free rendition of the standard Cast glyph: a rounded "screen"
|
|
107
|
-
* rectangle with
|
|
108
|
-
*
|
|
106
|
+
* rectangle (with its bottom-left corner open) and a broadcast signal — a dot
|
|
107
|
+
* and two concentric arcs — radiating from the bottom-left. Drawn purely with
|
|
108
|
+
* `View`s so the library stays dependency-free.
|
|
109
109
|
*/
|
|
110
110
|
function CastGlyph({
|
|
111
111
|
size,
|
|
@@ -114,83 +114,120 @@ function CastGlyph({
|
|
|
114
114
|
size: number
|
|
115
115
|
color: string
|
|
116
116
|
}): React.ReactElement {
|
|
117
|
-
const stroke = Math.max(1.5, Math.round(size
|
|
118
|
-
const inset =
|
|
119
|
-
const dotSize = stroke * 1.
|
|
120
|
-
|
|
121
|
-
const
|
|
117
|
+
const stroke = Math.max(1.5, Math.round(size / 13))
|
|
118
|
+
const inset = Math.round(size * 0.14)
|
|
119
|
+
const dotSize = stroke * 1.5
|
|
120
|
+
// Origin of the broadcast signal — the screen's bottom-left corner.
|
|
121
|
+
const cx = inset + stroke / 2
|
|
122
|
+
const cy = size - inset - stroke / 2
|
|
123
|
+
// Leave the screen's bottom-left corner open so the signal reads clearly.
|
|
124
|
+
const gap = size * 0.46
|
|
122
125
|
|
|
123
126
|
return (
|
|
124
127
|
<View style={{ width: size, height: size }}>
|
|
125
|
-
{/* Screen
|
|
128
|
+
{/* Screen: top + right + most of bottom + most of left, leaving the
|
|
129
|
+
bottom-left corner open (composed from edges for a clean cast look). */}
|
|
130
|
+
{/* Top edge */}
|
|
126
131
|
<View
|
|
127
|
-
style={
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
style={{
|
|
133
|
+
position: 'absolute',
|
|
134
|
+
left: inset,
|
|
135
|
+
top: inset,
|
|
136
|
+
right: inset,
|
|
137
|
+
height: stroke,
|
|
138
|
+
backgroundColor: color,
|
|
139
|
+
borderRadius: stroke,
|
|
140
|
+
}}
|
|
135
141
|
/>
|
|
136
|
-
{/*
|
|
142
|
+
{/* Right edge */}
|
|
137
143
|
<View
|
|
138
144
|
style={{
|
|
139
145
|
position: 'absolute',
|
|
140
|
-
|
|
146
|
+
top: inset,
|
|
147
|
+
bottom: inset,
|
|
148
|
+
right: inset,
|
|
149
|
+
width: stroke,
|
|
150
|
+
backgroundColor: color,
|
|
151
|
+
borderRadius: stroke,
|
|
152
|
+
}}
|
|
153
|
+
/>
|
|
154
|
+
{/* Bottom edge (stops short of the bottom-left corner) */}
|
|
155
|
+
<View
|
|
156
|
+
style={{
|
|
157
|
+
position: 'absolute',
|
|
158
|
+
left: inset + gap,
|
|
141
159
|
bottom: inset,
|
|
160
|
+
right: inset,
|
|
161
|
+
height: stroke,
|
|
162
|
+
backgroundColor: color,
|
|
163
|
+
borderRadius: stroke,
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
{/* Left edge (stops short of the bottom-left corner) */}
|
|
167
|
+
<View
|
|
168
|
+
style={{
|
|
169
|
+
position: 'absolute',
|
|
170
|
+
left: inset,
|
|
171
|
+
top: inset,
|
|
172
|
+
height: size - 2 * inset - gap,
|
|
173
|
+
width: stroke,
|
|
174
|
+
backgroundColor: color,
|
|
175
|
+
borderRadius: stroke,
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
{/* Two concentric broadcast arcs radiating from the corner dot */}
|
|
179
|
+
<RippleArc cx={cx} cy={cy} radius={size * 0.2} stroke={stroke} color={color} />
|
|
180
|
+
<RippleArc cx={cx} cy={cy} radius={size * 0.34} stroke={stroke} color={color} />
|
|
181
|
+
{/* Broadcast dot */}
|
|
182
|
+
<View
|
|
183
|
+
style={{
|
|
184
|
+
position: 'absolute',
|
|
185
|
+
left: cx - dotSize / 2,
|
|
186
|
+
top: cy - dotSize / 2,
|
|
142
187
|
width: dotSize,
|
|
143
188
|
height: dotSize,
|
|
144
189
|
borderRadius: dotSize / 2,
|
|
145
190
|
backgroundColor: color,
|
|
146
191
|
}}
|
|
147
192
|
/>
|
|
148
|
-
{/* Two concentric quarter arcs radiating from the dot */}
|
|
149
|
-
<Arc origin={inset + dotSize / 2} radius={r1} stroke={stroke} color={color} />
|
|
150
|
-
<Arc origin={inset + dotSize / 2} radius={r2} stroke={stroke} color={color} />
|
|
151
193
|
</View>
|
|
152
194
|
)
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
/**
|
|
156
|
-
* A quarter-circle arc (the
|
|
157
|
-
*
|
|
198
|
+
* A quarter-circle arc (the up-right quadrant) centered at (cx, cy). Rendered as
|
|
199
|
+
* a circle with only its top border colored, rotated 45° so the visible cap
|
|
200
|
+
* faces up-and-to-the-right — i.e. radiating from the bottom-left dot.
|
|
158
201
|
*/
|
|
159
|
-
function
|
|
160
|
-
|
|
202
|
+
function RippleArc({
|
|
203
|
+
cx,
|
|
204
|
+
cy,
|
|
161
205
|
radius,
|
|
162
206
|
stroke,
|
|
163
207
|
color,
|
|
164
208
|
}: {
|
|
165
|
-
|
|
209
|
+
cx: number
|
|
210
|
+
cy: number
|
|
166
211
|
radius: number
|
|
167
212
|
stroke: number
|
|
168
213
|
color: string
|
|
169
214
|
}): React.ReactElement {
|
|
170
215
|
return (
|
|
171
216
|
<View
|
|
172
|
-
// Clip box reveals only the quadrant facing up-and-right from the origin.
|
|
173
217
|
style={{
|
|
174
218
|
position: 'absolute',
|
|
175
|
-
left:
|
|
176
|
-
|
|
177
|
-
width: radius,
|
|
178
|
-
height: radius,
|
|
179
|
-
|
|
219
|
+
left: cx - radius,
|
|
220
|
+
top: cy - radius,
|
|
221
|
+
width: radius * 2,
|
|
222
|
+
height: radius * 2,
|
|
223
|
+
borderRadius: radius,
|
|
224
|
+
borderWidth: stroke,
|
|
225
|
+
borderTopColor: color,
|
|
226
|
+
borderRightColor: 'transparent',
|
|
227
|
+
borderBottomColor: 'transparent',
|
|
228
|
+
borderLeftColor: 'transparent',
|
|
229
|
+
transform: [{ rotate: '45deg' }],
|
|
180
230
|
}}
|
|
181
|
-
|
|
182
|
-
<View
|
|
183
|
-
style={{
|
|
184
|
-
position: 'absolute',
|
|
185
|
-
left: -radius,
|
|
186
|
-
top: 0,
|
|
187
|
-
width: radius * 2,
|
|
188
|
-
height: radius * 2,
|
|
189
|
-
borderRadius: radius,
|
|
190
|
-
borderWidth: stroke,
|
|
191
|
-
borderColor: color,
|
|
192
|
-
}}
|
|
193
|
-
/>
|
|
194
|
-
</View>
|
|
231
|
+
/>
|
|
195
232
|
)
|
|
196
233
|
}
|