0%

How to use ReactNative to realize all the functions of Huobi K-line

Data transfer

We can’t directly pass Dictionary to Native, because yogo will parse these large amounts of data, which will cause performance problems. We’d better serialize them into JSON strings and pass large amounts of data between JS and Native. We calculate all index values in the background thread and then update the UI on the main thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// RNKLineView
@objc func reloadOptionList(_ reactTag: NSNumber, optionList: String) {
bridge.uiManager.addUIBlock { (uimanager, viewRegistry) in
guard let viewRegistry = viewRegistry, let containerView = viewRegistry[reactTag] as? HTKLineContainerView else {
return
}
type(of: self). queue. async {
do {
guard let optionList = try JSONSerialization.jsonObject(with: optionList.data(using: .utf8) ?? Data(), options: .allowFragments) as? [String: Any] else {
return
}
containerView.configManager.reloadOptionList(optionList)
DispatchQueue. main. async {
containerView. reloadConfigManager(containerView.configManager)
}
} catch(_) {

}
}
}
}

Chart wrapper

There are many kinds of charts and indicators that need to be drawn, so we encapsulate the same parts of them into protocols. Similarly, in swift, we can add default implementations to protocols, or add public methods

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//HTKLineDrawProtocolx
protocol HTKLineDrawProtocol: class {

func minMaxRange(_ visibleModelArray: [HTKLineModel], _ configManager: HTKLineConfigManager) -> Range<CGFloat>

func drawCandle(_ model: HTKLineModel, _ index: Int, _ maxValue: CGFloat, _ minValue: CGFloat, _ baseY: CGFloat, _ height: CGFloat, _ context: CGContext, _ configManager: HTKLineConfigManager)

func drawLine(_ model: HTKLineModel, _ lastModel: HTKLineModel, _ maxValue: CGFloat, _ minValue: CGFloat, _ baseY: CGFloat, _ height: CGFloat, _ index: Int, _ lastIndex: Int, _ context: CGContext, _ configManager : HTKLineConfigManager)

func drawText(_ model: HTKLineModel, _ baseX: CGFloat, _ baseY: CGFloat, _ context: CGContext, _ configManager: HTKLineConfigManager)

func drawValue(_ maxValue: CGFloat, _ minValue: CGFloat, _ baseX: CGFloat, _ baseY: CGFloat, _ height: CGFloat, _ context: CGContext, _ configManager: HTKLineConfigManager)

}

Chart drawing

Drawing processing

We need a common point conversion method. We first draw the candles of the main chart and the sub-chart, and then draw the text, ruler, maximum and minimum, time scale, current price horizontal line, long-pressed detail panel, etc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//HTKLineView
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(), configManager.modelArray.count > 0 else {
return
}

calculateBaseHeight()
contextTranslate(context, CGFloat(visibleRange.lowerBound) * configManager.itemWidth, { context in
drawCandle(context)
})

contextTranslate(context, contentOffset.x, { context in

drawText(context)
drawValue(context)



drawHighLow(context)
drawTime(context)
drawClosePrice(context)
drawSelectedLine(context)
drawSelectedBoard(context)
drawSelectedTime(context)

drawContext. draw(contentOffset. x)
})


}

Scroll Gesture Handling

When the ScrollView scrolls horizontally, in order to save performance, we only draw in the visible range of the screen, and then calculate the accurate offset for drawing

1
2
3
4
5
6
7
8
9
10
//HTKLineView
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetX = scrollView. contentOffset. x
var visibleStartIndex = Int(floor(contentOffsetX / configManager. itemWidth))
var visibleEndIndex = Int(ceil((contentOffsetX + scrollView. bounds. size. width) / configManager. itemWidth))
visibleStartIndex = min(max(0, visibleStartIndex), configManager.modelArray.count - 1)
visibleEndIndex = min(max(0, visibleEndIndex), configManager.modelArray.count - 1)
visibleRange = visibleStartIndex...visibleEndIndex
self. setNeedsDisplay()
}

Zoom gesture handling

When the ScrollView pinch zoom gesture, we need to recalculate the offset for drawing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//HTKLineView
@objc
func pinchSelector(_ gesture: UIPinchGestureRecognizer) {
switch gesture. state {
case.changed:
scale += (gesture. scale - 1) / 10
default:
break
}
scale = max(0.3, min(scale, 3))

let width = bounds. size. width
let halfWidth = width / 2
let offsetScale = (contentOffset.x + halfWidth) / (contentSize.width - configManager.paddingRight)

reloadContentSize()
let contentOffsetX = max(0, min((contentSize. width - configManager. paddingRight) * offsetScale - halfWidth, contentSize. width - width))
reloadContentOffset(contentOffsetX)
scrollViewDidScroll(self)
}

Long press gesture panel processing

When ScrollView receives a long press or click gesture, we need to show or hide the details panel

1
2
3
4
5
6
7
8
9
10
//HTKLineView
@objc
func longPressSelector(_ gesture: UILongPressGestureRecognizer) {
let index = Int(floor(gesture. location(in: self). x / configManager. itemWidth))
selectedIndex = index
if (selectedIndex >= configManager. modelArray. count) {
selectedIndex = configManager.modelArray.count - 1
}
self. setNeedsDisplay()
}

Chart magnifying glass

We integrate all the charts into the container HTKLineContainerView, where we handle the display of the magnifying glass according to gestures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//HTKLineContainerView
func touchesGesture(_ touched: Set<UITouch>, _ state: UIGestureRecognizerState) {
guard var location = touched.first?.location(in: self) else {
shotView. shotPoint = nil
return
}
var previousLocation = touched.first?.previousLocation(in: self) ?? location
location = convertLocation(location)
previousLocation = convertLocation(previousLocation)

let translation = CGPoint.init(x: location.x - previousLocation.x, y: location.y - previousLocation.y)

klineView.drawContext.touchesGesture(location, translation, state)
shotView.shotPoint = state != .ended ? touched.first?.location(in: self) : nil
}

Finger marker drawing

Our finger mark drawings are all drawn through HTDrawContext, we need to pay attention to including parallelograms, we need to convert all coordinates to the current price coordinates, because our zoom gesture needs to keep these finger mark drawings in the correct position

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
//HTKLineContainerView
func reloadConfigManager(_ configManager: HTKLineConfigManager) {

configManager.onDrawItemDidTouch = { [weak self] (drawItem, drawItemIndex) in
self?.configManager.shouldReloadDrawItemIndex = drawItemIndex
guard let drawItem = drawItem, let colorList = drawItem.drawColor.cgColor.components else {
self?.onDrawItemDidTouch?([
"shouldReloadDrawItemIndex": drawItemIndex,
])
return
}
self?.onDrawItemDidTouch?([
"shouldReloadDrawItemIndex": drawItemIndex,
"drawColor": colorList,
"drawLineHeight": drawItem. drawLineHeight,
"drawDashWidth": drawItem. drawDashWidth,
"drawDashSpace": drawItem. drawDashSpace,
"drawIsLock": drawItem. drawIsLock
])
}
configManager.onDrawItemComplete = { [weak self] (drawItem, drawItemIndex) in
self?.onDrawItemComplete?([AnyHashable: Any].init())
}
configManager.onDrawPointComplete = { [weak self] (drawItem, drawItemIndex) in
guard let drawItem = drawItem else {
return
}
self?.onDrawPointComplete?([
"pointCount": drawItem.pointList.count
])
}

let reloadIndex = configManager.shouldReloadDrawItemIndex
if reloadIndex >= 0, reloadIndex < klineView.drawContext.drawItemList.count {
let drawItem = klineView.drawContext.drawItemList[reloadIndex]
drawItem.drawColor = configManager.drawColor
drawItem.drawLineHeight = configManager.drawLineHeight
drawItem.drawDashWidth = configManager.drawDashWidth
drawItem.drawDashSpace = configManager.drawDashSpace
drawItem.drawIsLock = configManager.drawIsLock
if (configManager. drawShouldTrash) {
configManager.shouldReloadDrawItemIndex = HTDrawState.showPencil.rawValue
klineView.drawContext.drawItemList.remove(at: reloadIndex)
configManager.drawShouldTrash = false
}
klineView.drawContext.setNeedsDisplay()
}

if configManager. shouldFixDraw {
configManager.shouldFixDraw = false
klineView.drawContext.fixDrawItemList()
}
if (configManager. shouldClearDraw) {
configManager.drawType = .none
configManager.shouldClearDraw = false
klineView.drawContext.clearDrawItemList()
}
}

Affiliated

The full code is here https://github.com/hellohublot/react-native-kline-view
I’m hublot, sharing some of my thoughts, I’m too busy with work, it’s inevitable that there will be negligence, please forgive me