diff --git a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java index 7ebe8fe..1cee40c 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1428,11 +1428,28 @@ public int getMinScrollX() { return 0; } + public int getExtraScrollX() { + float minVisibleCandles = getMinVisibleCandles(); + int extraScrollX = (int) (mWidth / mScaleX - Math.min(minVisibleCandles * mPointWidth / mScaleX, mWidth / mScaleX * 0.8f)); + // android.util.Log.d("BaseKLineChartView", "getExtraScrollX: " + extraScrollX + ", mScaleX: " + mScaleX + ", minVisibleCandles: " + minVisibleCandles + ", mWidth: " + mWidth); + return extraScrollX; + } + public int getMaxScrollX() { - int contentWidth = (int) Math.max((mDataLen - (mWidth - configManager.paddingRight) / mScaleX), 0); + int contentWidth = (int) Math.max((mDataLen - (mWidth - configManager.paddingRight) / mScaleX + getExtraScrollX()), 0); return contentWidth; } + @Override + protected float getMinVisibleCandles() { + return configManager.minVisibleCandles; + } + + @Override + public float getDataLength() { + return mDataLen; + } + /** * 在主区域画线 * @@ -1989,19 +2006,20 @@ public boolean onSingleTapUp(MotionEvent e) { } public void smoothScrollToEnd() { - int endScrollX = getMaxScrollX(); - int currentScrollX = getScrollOffset(); - int distance = endScrollX - currentScrollX; - - // android.util.Log.d("BaseKLineChartView", "smoothScrollToEnd DEBUG:"); - // android.util.Log.d("BaseKLineChartView", " mDataLen=" + mDataLen + ", mItemCount=" + mItemCount + ", mPointWidth=" + mPointWidth); - // android.util.Log.d("BaseKLineChartView", " mWidth=" + mWidth + ", mScaleX=" + mScaleX + ", paddingRight=" + configManager.paddingRight); - // android.util.Log.d("BaseKLineChartView", " current=" + currentScrollX + ", end=" + endScrollX + ", distance=" + distance); - - // Always scroll to end position, regardless of current position - // This ensures we go to the rightmost position to show the latest data - setScrollX(endScrollX); - // android.util.Log.d("BaseKLineChartView", "Set scroll position to end: " + endScrollX); + int screenWidthInLogicalUnits = getExtraScrollX(); + int endScrollX = (int)(mDataLen + configManager.paddingRight - screenWidthInLogicalUnits); + + setScrollXWithoutMinCandlesLimit(Math.max(0, endScrollX)); + } + + /** + * Set scroll position without applying minVisibleCandles limit + */ + private void setScrollXWithoutMinCandlesLimit(int scrollX) { + int oldX = this.mScrollX; + this.mScrollX = Math.max(0, Math.min(scrollX, (int)mDataLen)); + onScrollChanged(this.mScrollX, 0, oldX, 0); + invalidate(); } // Public getter methods for accessing protected fields diff --git a/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java b/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java index 29895f6..4574834 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/HTKLineConfigManager.java @@ -103,6 +103,8 @@ public class HTKLineConfigManager { public float candleCornerRadius = 0; + public float minVisibleCandles = 5; + public int minuteVolumeCandleColor = Color.RED; public float minuteVolumeCandleWidth = 1.5f; @@ -455,6 +457,11 @@ public void reloadOptionList(Map optionList) { this.candleCornerRadius = candleCornerRadiusValue.floatValue(); } + Number minVisibleCandlesValue = (Number)configList.get("minVisibleCandles"); + if (minVisibleCandlesValue != null) { + this.minVisibleCandles = minVisibleCandlesValue.floatValue(); + } + this.fontFamily = (configList.get("fontFamily")).toString(); this.textColor = ((Number) configList.get("textColor")).intValue(); this.headerTextFontSize = ((Number)configList.get("headerTextFontSize")).floatValue(); diff --git a/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java b/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java index 8ab1668..8ca9f34 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/ScrollAndScaleView.java @@ -17,6 +17,12 @@ public abstract class ScrollAndScaleView extends RelativeLayout implements GestureDetector.OnGestureListener, ScaleGestureDetector.OnScaleGestureListener { protected int mScrollX = 0; + + /** + * Get minimum visible candles + * @return minimum number of candles that should be visible + */ + protected abstract float getMinVisibleCandles(); protected GestureDetectorCompat mDetector; protected ScaleGestureDetector mScaleDetector; @@ -265,6 +271,20 @@ public boolean isTouch() { */ public abstract int getMaxScrollX(); + /** + * Get the point width + * + * @return + */ + public abstract float getPointWidth(); + + /** + * Get the total data length (itemCount * pointWidth) + * + * @return + */ + public abstract float getDataLength(); + /** * Set ScrollX * @@ -286,7 +306,6 @@ public boolean isMultipleTouch() { protected void checkAndFixScrollX() { int contentSizeWidth = (getMaxScrollX()); - if (mScrollX < getMinScrollX()) { mScrollX = getMinScrollX(); mScroller.forceFinished(true); diff --git a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java index 3ce63bb..f7028ae 100644 --- a/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/container/HTKLineContainerView.java @@ -79,7 +79,6 @@ public void reloadConfigManager() { klineView.setMTextSize(klineView.configManager.candleTextFontSize); klineView.setMTextColor(klineView.configManager.candleTextColor); klineView.reloadColor(); - Boolean isEnd = klineView.getScrollOffset() >= klineView.getMaxScrollX(); int previousScrollX = klineView.getScrollOffset(); klineView.notifyChanged(); @@ -87,8 +86,9 @@ public void reloadConfigManager() { // 调整滚动位置以补偿新增的数据 int newScrollX = previousScrollX + klineView.configManager.scrollPositionAdjustment; klineView.setScrollX(newScrollX); - } else if (isEnd || klineView.configManager.shouldScrollToEnd) { - klineView.setScrollX(klineView.getMaxScrollX()); + } else if (klineView.configManager.shouldScrollToEnd) { + int scrollToEnd = klineView.getMaxScrollX() - klineView.getExtraScrollX(); + klineView.setScrollX(scrollToEnd); } @@ -409,8 +409,9 @@ public void addCandlesticksAtTheEnd(ReadableArray candlesticksArray) { } try { + float endPosition = klineView.getMaxScrollX() - klineView.getExtraScrollX(); // Check if user is currently at the end of the chart - boolean wasAtEnd = klineView.getScrollOffset() >= klineView.getMaxScrollX() - 10; + boolean wasAtEnd = endPosition - 10 <= klineView.getScrollOffset() && klineView.getScrollOffset() <= endPosition + 10; // Get existing model for preserving indicator lists structure KLineEntity templateEntity = null; @@ -469,7 +470,7 @@ public void run() { @Override public void run() { android.util.Log.d("HTKLineContainerView", "Scrolling to end after adding new data"); - klineView.setScrollX(klineView.getMaxScrollX()); + klineView.setScrollX(klineView.getMaxScrollX() - klineView.getExtraScrollX()); } }, 100); // Additional delay for scroll } diff --git a/example/App.js b/example/App.js index c5b8c2f..388f2d3 100644 --- a/example/App.js +++ b/example/App.js @@ -42,6 +42,7 @@ import { const App = () => { + const MIN_VISIBLE_CANDLES = 10 const [isDarkTheme, setIsDarkTheme] = useState(false) const [selectedTimeType, setSelectedTimeType] = useState(2) // Corresponds to 1 minute const [selectedMainIndicator, setSelectedMainIndicator] = useState(1) // Corresponds to MA (1=MA, 2=BOLL) @@ -167,7 +168,8 @@ const App = () => { lastDataLength, currentScrollPosition, showVolumeChart, - candleCornerRadius + candleCornerRadius, + minVisibleCandles: MIN_VISIBLE_CANDLES }, shouldScrollToEnd, kLineViewRef.current ? true : false) setOptionListValue(newOptionList) }, [klineData, selectedMainIndicator, selectedSubIndicator, showVolumeChart, isDarkTheme, selectedTimeType, selectedDrawTool, showIndicatorSelector, showTimeSelector, showDrawToolSelector, drawShouldContinue, optionList, lastDataLength, currentScrollPosition, candleCornerRadius]) @@ -216,7 +218,8 @@ const App = () => { lastDataLength, currentScrollPosition, showVolumeChart, - candleCornerRadius + candleCornerRadius, + minVisibleCandles: MIN_VISIBLE_CANDLES }, false) // Calculate scroll distance adjustment needed (based on item width) diff --git a/example/utils/businessLogic.js b/example/utils/businessLogic.js index 1f66904..acbb500 100644 --- a/example/utils/businessLogic.js +++ b/example/utils/businessLogic.js @@ -282,7 +282,8 @@ export const packOptionList = (modelArray, appState, shouldScrollToEnd = true, u selectedDrawTool, showVolumeChart, candleCornerRadius, - drawShouldContinue + drawShouldContinue, + minVisibleCandles } = appState const theme = ThemeManager.getCurrentTheme(isDarkTheme) @@ -355,6 +356,7 @@ export const packOptionList = (modelArray, appState, shouldScrollToEnd = true, u itemWidth: 8 * pixelRatio, candleWidth: 6 * pixelRatio, candleCornerRadius: candleCornerRadius * pixelRatio, + minVisibleCandles: minVisibleCandles || 5, minuteVolumeCandleColor: processColor(showVolumeChart ? COLOR(0.0941176, 0.509804, 0.831373, 0.501961) : 'transparent'), minuteVolumeCandleWidth: showVolumeChart ? 2 * pixelRatio : 0, macdCandleWidth: 1 * pixelRatio, diff --git a/ios/Classes/HTKLineConfigManager.swift b/ios/Classes/HTKLineConfigManager.swift index 32612eb..26360b9 100644 --- a/ios/Classes/HTKLineConfigManager.swift +++ b/ios/Classes/HTKLineConfigManager.swift @@ -134,6 +134,8 @@ class HTKLineConfigManager: NSObject { var candleCornerRadius: CGFloat = 0 + var minVisibleCandles: CGFloat = 5 + var minuteVolumeCandleWidth: CGFloat = 0 var _minuteVolumeCandleWidth: CGFloat = 0 @@ -448,6 +450,7 @@ class HTKLineConfigManager: NSObject { _minuteVolumeCandleWidth = configList["minuteVolumeCandleWidth"] as? CGFloat ?? 0 _macdCandleWidth = configList["macdCandleWidth"] as? CGFloat ?? 0 candleCornerRadius = configList["candleCornerRadius"] as? CGFloat ?? 0 + minVisibleCandles = configList["minVisibleCandles"] as? CGFloat ?? 5 reloadScrollViewScale(1) paddingTop = configList["paddingTop"] as? CGFloat ?? 0 paddingRight = configList["paddingRight"] as? CGFloat ?? 0 diff --git a/ios/Classes/HTKLineContainerView.swift b/ios/Classes/HTKLineContainerView.swift index 885ed43..0c3d19b 100644 --- a/ios/Classes/HTKLineContainerView.swift +++ b/ios/Classes/HTKLineContainerView.swift @@ -21,7 +21,7 @@ class HTKLineContainerView: UIView { // Buy/sell mark management - indexed by timestamp for O(1) lookup private var buyMarks: [Int64: [String: Any]] = [:] private var sellMarks: [Int64: [String: Any]] = [:] - private var buySellMarks: [String: [String: Any]] = [:] // Keep for compatibility + private var buySellMarks: [String: [String: Any]] = [:] // Keep for compatibility func getAllBuySellMarks() -> [String: [String: Any]] { return buySellMarks @@ -44,17 +44,19 @@ class HTKLineContainerView: UIView { @objc var onDrawItemComplete: RCTBubblingEventBlock? @objc var onDrawPointComplete: RCTBubblingEventBlock? - + @objc var optionList: String? { didSet { guard let optionList = optionList else { return } - + RNKLineView.queue.async { [weak self] in do { guard let optionListData = optionList.data(using: .utf8), - let optionListDict = try JSONSerialization.jsonObject(with: optionListData, options: .allowFragments) as? [String: Any] else { + let optionListDict = try JSONSerialization.jsonObject( + with: optionListData, options: .allowFragments) as? [String: Any] + else { return } self?.configManager.reloadOptionList(optionListDict) @@ -73,7 +75,7 @@ class HTKLineContainerView: UIView { let klineView = HTKLineView.init(CGRect.zero, configManager) return klineView }() - + lazy var shotView: HTShotView = { let shotView = HTShotView.init(frame: CGRect.zero) shotView.dimension = 100 @@ -85,36 +87,38 @@ class HTKLineContainerView: UIView { let superShotView = reactSuperview()?.reactSuperview()?.reactSuperview() superShotView?.reactSuperview()?.addSubview(shotView) shotView.shotView = superShotView - shotView.reactSetFrame(CGRect.init(x: 50, y: 50, width: shotView.dimension, height: shotView.dimension)) + shotView.reactSetFrame( + CGRect.init(x: 50, y: 50, width: shotView.dimension, height: shotView.dimension)) } override var frame: CGRect { didSet { - setupChildViews() + setupChildViews() } } - + override func reactSetFrame(_ frame: CGRect) { super.reactSetFrame(frame) setupChildViews() } - + override init(frame: CGRect) { super.init(frame: frame) addSubview(klineView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + 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 { + guard let drawItem = drawItem, let colorList = drawItem.drawColor.cgColor.components + else { self?.onDrawItemDidTouch?([ - "shouldReloadDrawItemIndex": drawItemIndex, + "shouldReloadDrawItemIndex": drawItemIndex ]) return } @@ -124,12 +128,12 @@ class HTKLineContainerView: UIView { "drawLineHeight": drawItem.drawLineHeight, "drawDashWidth": drawItem.drawDashWidth, "drawDashSpace": drawItem.drawDashSpace, - "drawIsLock": drawItem.drawIsLock + "drawIsLock": drawItem.drawIsLock, ]) } configManager.onScrollLeft = { [weak self] (timestamp) in self?.onScrollLeft?([ - "timestamp": timestamp, + "timestamp": timestamp ]) } configManager.onChartTouch = { [weak self] (location, isOnClosePriceLabel) in @@ -163,7 +167,7 @@ class HTKLineContainerView: UIView { "pointCount": drawItem.pointList.count ]) } - + let reloadIndex = configManager.shouldReloadDrawItemIndex if reloadIndex >= 0, reloadIndex < klineView.drawContext.drawItemList.count { let drawItem = klineView.drawContext.drawItemList[reloadIndex] @@ -172,36 +176,36 @@ class HTKLineContainerView: UIView { drawItem.drawDashWidth = configManager.drawDashWidth drawItem.drawDashSpace = configManager.drawDashSpace drawItem.drawIsLock = configManager.drawIsLock - if (configManager.drawShouldTrash) { + if configManager.drawShouldTrash { configManager.shouldReloadDrawItemIndex = HTDrawState.showPencil.rawValue klineView.drawContext.drawItemList.remove(at: reloadIndex) configManager.drawShouldTrash = false } klineView.drawContext.setNeedsDisplay() } - + klineView.reloadConfigManager(configManager) shotView.shotColor = configManager.shotBackgroundColor if configManager.shouldFixDraw { configManager.shouldFixDraw = false klineView.drawContext.fixDrawItemList() } - if (configManager.shouldClearDraw) { + if configManager.shouldClearDraw { configManager.drawType = .none configManager.shouldClearDraw = false klineView.drawContext.clearDrawItemList() } } - + private func convertLocation(_ location: CGPoint) -> CGPoint { var reloadLocation = location reloadLocation.x = max(min(reloadLocation.x, bounds.size.width), 0) reloadLocation.y = max(min(reloadLocation.y, bounds.size.height), 0) -// reloadLocation.x += klineView.contentOffset.x + // reloadLocation.x += klineView.contentOffset.x reloadLocation = klineView.valuePointFromViewPoint(reloadLocation) return reloadLocation } - + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let view = super.hitTest(point, with: event) if view == klineView { @@ -210,7 +214,10 @@ class HTKLineContainerView: UIView { return view case HTDrawState.showPencil.rawValue: if configManager.drawType == .none { - if HTDrawItem.canResponseLocation(klineView.drawContext.drawItemList, convertLocation(point), klineView) != nil { + if HTDrawItem.canResponseLocation( + klineView.drawContext.drawItemList, convertLocation(point), klineView) + != nil + { return self } else { return view @@ -225,28 +232,28 @@ class HTKLineContainerView: UIView { } } return view -// if view == drawView, configManager.enabledDraw == false { -// return klineView -// } -// return view + // if view == drawView, configManager.enabledDraw == false { + // return klineView + // } + // return view } - + override func touchesBegan(_ touches: Set, with event: UIEvent?) { touchesGesture(touches, .began) } - + override func touchesMoved(_ touches: Set, with event: UIEvent?) { touchesGesture(touches, .changed) } - + override func touchesEnded(_ touches: Set, with event: UIEvent?) { touchesGesture(touches, .ended) } - + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { touchesEnded(touches, with: event) } - + func touchesGesture(_ touched: Set, _ state: UIGestureRecognizerState) { guard var location = touched.first?.location(in: self) else { shotView.shotPoint = nil @@ -256,7 +263,8 @@ class HTKLineContainerView: UIView { location = convertLocation(location) previousLocation = convertLocation(previousLocation) - let translation = CGPoint.init(x: location.x - previousLocation.x, y: location.y - previousLocation.y) + 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 @@ -266,8 +274,11 @@ class HTKLineContainerView: UIView { print("HTKLineContainerView: updateLastCandlestick called with data: \(candlestick)") guard let candlestickDict = candlestick as? [String: Any], - configManager.modelArray.count > 0 else { - print("HTKLineContainerView: updateLastCandlestick - Null check failed or empty model array") + configManager.modelArray.count > 0 + else { + print( + "HTKLineContainerView: updateLastCandlestick - Null check failed or empty model array" + ) return } @@ -300,16 +311,23 @@ class HTKLineContainerView: UIView { updatedModel.selectedItemList = existingModel.selectedItemList } - print("HTKLineContainerView: New maVolumeList count: \(updatedModel.maVolumeList.count)") + print( + "HTKLineContainerView: New maVolumeList count: \(updatedModel.maVolumeList.count)") if !updatedModel.maVolumeList.isEmpty { - print("HTKLineContainerView: Volume MA5: \(updatedModel.maVolumeList[0].value), MA10: \(updatedModel.maVolumeList[1].value)") + print( + "HTKLineContainerView: Volume MA5: \(updatedModel.maVolumeList[0].value), MA10: \(updatedModel.maVolumeList[1].value)" + ) } // Update the model array configManager.modelArray[lastIndex] = updatedModel - print("HTKLineContainerView: Updated last candlestick at index \(lastIndex) with close: \(updatedModel.close)") - print("HTKLineContainerView: Preserved maVolumeList count: \(updatedModel.maVolumeList.count)") + print( + "HTKLineContainerView: Updated last candlestick at index \(lastIndex) with close: \(updatedModel.close)" + ) + print( + "HTKLineContainerView: Preserved maVolumeList count: \(updatedModel.maVolumeList.count)" + ) // Force redraw without reloading the entire configuration DispatchQueue.main.async { [weak self] in @@ -322,11 +340,16 @@ class HTKLineContainerView: UIView { } @objc func addCandlesticksAtTheEnd(_ candlesticks: NSArray) { - print("HTKLineContainerView: addCandlesticksAtTheEnd called with \(candlesticks.count) candlesticks") + print( + "HTKLineContainerView: addCandlesticksAtTheEnd called with \(candlesticks.count) candlesticks" + ) guard let candlesticksArray = candlesticks as? [[String: Any]], - !candlesticksArray.isEmpty else { - print("HTKLineContainerView: addCandlesticksAtTheEnd - Invalid or empty candlesticks array") + !candlesticksArray.isEmpty + else { + print( + "HTKLineContainerView: addCandlesticksAtTheEnd - Invalid or empty candlesticks array" + ) return } @@ -348,11 +371,17 @@ class HTKLineContainerView: UIView { // The indicator lists are now properly populated by packModelArray() from React Native data // No need for manual calculation since the data already includes calculated indicators for newModel in newModels { - print("HTKLineContainerView: Using indicator data from React Native - maList.count=\(newModel.maList.count), maVolumeList.count=\(newModel.maVolumeList.count)") + print( + "HTKLineContainerView: Using indicator data from React Native - maList.count=\(newModel.maList.count), maVolumeList.count=\(newModel.maVolumeList.count)" + ) } // Get the scroll position before adding data - let wasAtEnd = klineView.contentOffset.x >= (klineView.contentSize.width - klineView.frame.width - 10) + let wasAtEnd = + (klineView.contentSize.width - klineView.bounds.width - 10) + <= klineView.contentOffset.x + && klineView.contentOffset.x + <= (klineView.contentSize.width - klineView.bounds.width + 10) // Add new models to the end of the array configManager.modelArray.append(contentsOf: newModels) @@ -375,7 +404,8 @@ class HTKLineContainerView: UIView { if wasAtEnd { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { print("HTKLineContainerView: Scrolling to end after adding new data") - let maxContentOffsetX = max(0, self.klineView.contentSize.width - self.klineView.bounds.size.width) + let maxContentOffsetX = max( + 0, self.klineView.contentSize.width - self.klineView.bounds.size.width) self.klineView.reloadContentOffset(maxContentOffsetX, true) } } @@ -386,11 +416,16 @@ class HTKLineContainerView: UIView { } @objc func addCandlesticksAtTheStart(_ candlesticks: NSArray) { - print("HTKLineContainerView: addCandlesticksAtTheStart called with \(candlesticks.count) candlesticks") + print( + "HTKLineContainerView: addCandlesticksAtTheStart called with \(candlesticks.count) candlesticks" + ) guard let candlesticksArray = candlesticks as? [[String: Any]], - !candlesticksArray.isEmpty else { - print("HTKLineContainerView: addCandlesticksAtTheStart - Invalid or empty candlesticks array") + !candlesticksArray.isEmpty + else { + print( + "HTKLineContainerView: addCandlesticksAtTheStart - Invalid or empty candlesticks array" + ) return } @@ -405,7 +440,9 @@ class HTKLineContainerView: UIView { // The indicator lists are now properly populated by packModelArray() from React Native data for newModel in newModels { - print("HTKLineContainerView: Using indicator data from React Native - maList.count=\(newModel.maList.count), maVolumeList.count=\(newModel.maVolumeList.count)") + print( + "HTKLineContainerView: Using indicator data from React Native - maList.count=\(newModel.maList.count), maVolumeList.count=\(newModel.maVolumeList.count)" + ) } // Get the current scroll position to maintain it after adding data at start @@ -428,7 +465,9 @@ class HTKLineContainerView: UIView { let addedWidth = CGFloat(newModels.count) * self.configManager.itemWidth let newContentOffsetX = currentContentOffsetX + addedWidth - print("HTKLineContainerView: Reloading content size and adjusting scroll position by \(addedWidth) pixels") + print( + "HTKLineContainerView: Reloading content size and adjusting scroll position by \(addedWidth) pixels" + ) // Reload content size first self.klineView.reloadContentSize() @@ -449,7 +488,8 @@ class HTKLineContainerView: UIView { print("HTKLineContainerView: addOrderLine called with data: \(orderLine)") guard let orderLineDict = orderLine as? [String: Any], - let id = orderLineDict["id"] as? String else { + let id = orderLineDict["id"] as? String + else { print("HTKLineContainerView: addOrderLine - Invalid order line data") return } @@ -481,7 +521,8 @@ class HTKLineContainerView: UIView { print("HTKLineContainerView: updateOrderLine called with data: \(orderLine)") guard let orderLineDict = orderLine as? [String: Any], - let id = orderLineDict["id"] as? String else { + let id = orderLineDict["id"] as? String + else { print("HTKLineContainerView: updateOrderLine - Invalid order line data") return } @@ -508,9 +549,10 @@ class HTKLineContainerView: UIView { @objc func addBuySellMark(_ buySellMark: NSDictionary) { guard let buySellMarkDict = buySellMark as? [String: Any], - let id = buySellMarkDict["id"] as? String, - let time = buySellMarkDict["time"] as? Int64, - let type = buySellMarkDict["type"] as? String else { + let id = buySellMarkDict["id"] as? String, + let time = buySellMarkDict["time"] as? Int64, + let type = buySellMarkDict["type"] as? String + else { return } @@ -536,8 +578,9 @@ class HTKLineContainerView: UIView { // Find and remove from efficient lookup maps if let markData = buySellMarks[buySellMarkId], - let time = markData["time"] as? Int64, - let type = markData["type"] as? String { + let time = markData["time"] as? Int64, + let type = markData["type"] as? String + { if type == "buy" { buyMarks.removeValue(forKey: time) @@ -559,16 +602,18 @@ class HTKLineContainerView: UIView { @objc func updateBuySellMark(_ buySellMark: NSDictionary) { guard let buySellMarkDict = buySellMark as? [String: Any], - let id = buySellMarkDict["id"] as? String, - let time = buySellMarkDict["time"] as? Int64, - let type = buySellMarkDict["type"] as? String else { + let id = buySellMarkDict["id"] as? String, + let time = buySellMarkDict["time"] as? Int64, + let type = buySellMarkDict["type"] as? String + else { return } // Remove old entry from efficient lookup maps if it exists if let oldMarkData = buySellMarks[id], - let oldTime = oldMarkData["time"] as? Int64, - let oldType = oldMarkData["type"] as? String { + let oldTime = oldMarkData["time"] as? Int64, + let oldType = oldMarkData["type"] as? String + { if oldType == "buy" { buyMarks.removeValue(forKey: oldTime) @@ -604,4 +649,3 @@ class HTKLineContainerView: UIView { } } - diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift index 8ee26d6..1e57f81 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -131,16 +131,25 @@ class HTKLineView: UIScrollView { childDraw = wrDraw } - let isEnd = contentOffset.x + 1 + bounds.size.width >= contentSize.width let previousContentOffset = contentOffset.x reloadContentSize() + + let rightScreenOffset = contentOffset.x + bounds.size.width + 1 + let lastCandlestickOffset = contentSize.width - configManager.paddingRight - configManager.itemWidth / 2 + // how many candlesticks +- should it consider to auto scroll to end when new data is added + // if the user is over-scrolled, then the candlesticks have space to appear on screen without scrolling + // and at some point it will enter this range and become auto-scrolling to end + let candlesticksCountOffset = 1.5 * configManager.itemWidth + // Extra spacing at the end is bounds.size.width + let isEnd = lastCandlestickOffset - candlesticksCountOffset <= rightScreenOffset && rightScreenOffset <= lastCandlestickOffset + candlesticksCountOffset + if configManager.shouldAdjustScrollPosition { // Adjust scroll position to compensate for newly added data let newContentOffset = previousContentOffset + configManager.scrollPositionAdjustment reloadContentOffset(newContentOffset, false) } else if configManager.shouldScrollToEnd || isEnd { - let toEndContentOffset = contentSize.width - bounds.size.width + let toEndContentOffset = contentSize.width - 2 * bounds.size.width let distance = abs(contentOffset.x - toEndContentOffset) let animated = distance <= configManager.itemWidth reloadContentOffset(toEndContentOffset, animated) @@ -183,16 +192,19 @@ class HTKLineView: UIScrollView { let contentWidth = configManager.itemWidth * CGFloat(configManager.modelArray.count) + configManager.paddingRight + + bounds.size.width contentSize = CGSize.init(width: contentWidth, height: frame.size.height) } func reloadContentOffset(_ contentOffsetX: CGFloat, _ animated: Bool = false) { - let offsetX = max(0, min(contentOffsetX, contentSize.width - bounds.size.width)) + let allCandlesWidth = configManager.itemWidth * CGFloat(configManager.modelArray.count) + let maxAllowedOffset = max(0, allCandlesWidth - configManager.minVisibleCandles * configManager.itemWidth) + let offsetX = max(0, min(contentOffsetX, maxAllowedOffset)) setContentOffset(CGPoint.init(x: offsetX, y: 0), animated: animated) } func smoothScrollToEnd() { - let endOffsetX = contentSize.width - bounds.size.width + let endOffsetX = contentSize.width - 2 * bounds.size.width reloadContentOffset(endOffsetX, true) } @@ -232,6 +244,9 @@ class HTKLineView: UIScrollView { drawSelectedBoard(context) drawSelectedTime(context) + // Debug lines for offset visualization + drawOffsetDebugLines(context) + drawContext.draw(contentOffset.x) }) @@ -1021,6 +1036,37 @@ class HTKLineView: UIScrollView { markText.draw(in: textRect, withAttributes: textAttributes) } + func drawOffsetDebugLines(_ context: CGContext) { + let lastCandlestickOffset = contentSize.width - configManager.paddingRight - configManager.itemWidth / 2 + let candlesticksCountOffset = 1.5 * configManager.itemWidth + + // Convert to screen coordinates + let lastCandlestickX = lastCandlestickOffset - contentOffset.x + let leftRangeX = lastCandlestickOffset - candlesticksCountOffset - contentOffset.x + let rightRangeX = lastCandlestickOffset + candlesticksCountOffset - contentOffset.x + + context.setLineWidth(2.0) + context.setLineDash(phase: 0, lengths: []) + + // Draw lastCandlestickOffset line (red) + context.setStrokeColor(UIColor.red.cgColor) + context.move(to: CGPoint(x: lastCandlestickX, y: mainBaseY)) + context.addLine(to: CGPoint(x: lastCandlestickX, y: mainBaseY + mainHeight)) + context.strokePath() + + // Draw left range line (blue) - lastCandlestickOffset - candlesticksCountOffset + context.setStrokeColor(UIColor.blue.cgColor) + context.move(to: CGPoint(x: leftRangeX, y: mainBaseY)) + context.addLine(to: CGPoint(x: leftRangeX, y: mainBaseY + mainHeight)) + context.strokePath() + + // Draw right range line (green) - lastCandlestickOffset + candlesticksCountOffset + context.setStrokeColor(UIColor.green.cgColor) + context.move(to: CGPoint(x: rightRangeX, y: mainBaseY)) + context.addLine(to: CGPoint(x: rightRangeX, y: mainBaseY + mainHeight)) + context.strokePath() + } + func valuePointFromViewPoint(_ point: CGPoint) -> CGPoint { return CGPoint.init(x: valueFromX(point.x), y: valueFromY(point.y)) } @@ -1034,7 +1080,16 @@ class HTKLineView: UIScrollView { extension HTKLineView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { + let allCandlesWidth = configManager.itemWidth * CGFloat(configManager.modelArray.count) + let maxAllowedOffset = max(0, allCandlesWidth - configManager.minVisibleCandles * configManager.itemWidth) + let contentOffsetX = scrollView.contentOffset.x + + if contentOffsetX > maxAllowedOffset { + scrollView.contentOffset.x = maxAllowedOffset + return + } + var visibleStartIndex = Int(floor(contentOffsetX / configManager.itemWidth)) var visibleEndIndex = Int( ceil((contentOffsetX + scrollView.bounds.size.width) / configManager.itemWidth))