diff --git a/README.md b/README.md
index f4fe5ee..4196441 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,27 @@
# React Native KLine View
+
+## Development
+
+To start the project for:
+
+ - Android:
+ - in `example` dir
+ - `yarn install --force`
+ - `yarn android`
+ - to see logs: `adb logcat`, needs grep as this prints everything
+
+ - ios:
+ - in `example` dir: `yarn install --force`
+ - in `example/ios` dir: `pod install`
+ - in `example` dir: `yarn ios` (or start from xcode)
+ - sometimes it throws code developer signature. if it shows the application started successfully means the simulator works fine
+ - to see logs start in xcode
+
+
+
+## Intro
+
@@ -74,6 +96,7 @@ yarn add react-native-kline-view@https://github.com/hellohublot/react-native-kli
```
### iOS Setup
+
```bash
cd ios && pod install
```
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..3b24075 100644
--- a/ios/Classes/HTKLineContainerView.swift
+++ b/ios/Classes/HTKLineContainerView.swift
@@ -351,33 +351,48 @@ class HTKLineContainerView: UIView {
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 rightScreenOffset = klineView.contentOffset.x + bounds.size.width + 1 - configManager.paddingRight
+ let lastCandlestickOffset = configManager.itemWidth * CGFloat(configManager.modelArray.count) - 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 = 3 * configManager.itemWidth
+ // Extra spacing at the end is bounds.size.width
+ let wasAtEnd = lastCandlestickOffset - candlesticksCountOffset <= rightScreenOffset && rightScreenOffset <= lastCandlestickOffset + candlesticksCountOffset + configManager.paddingRight
+
+ let isOverscrolled = rightScreenOffset > lastCandlestickOffset + candlesticksCountOffset
// Add new models to the end of the array
configManager.modelArray.append(contentsOf: newModels)
- print("HTKLineContainerView: Added \(newModels.count) new candlesticks to the end")
- print("HTKLineContainerView: Total candlesticks now: \(configManager.modelArray.count)")
- print("HTKLineContainerView: Was at end before adding: \(wasAtEnd)")
+ // print("HTKLineContainerView: Added \(newModels.count) new candlesticks to the end")
+ // print("HTKLineContainerView: Total candlesticks now: \(configManager.modelArray.count)")
+ // print("HTKLineContainerView: Was at end before adding: \(wasAtEnd)")
// Force redraw and optionally scroll to end
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
- print("HTKLineContainerView: Reloading content size after adding candlesticks")
+ // print("HTKLineContainerView: Reloading content size after adding candlesticks")
self.klineView.reloadContentSize()
- print("HTKLineContainerView: Triggering redraw after adding candlesticks")
+ // print("HTKLineContainerView: Triggering redraw after adding candlesticks")
self.klineView.setNeedsDisplay()
// If user was at the end, keep them at the end
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)
+ // print("HTKLineContainerView: Scrolling to end after adding new data")
+ let maxContentOffsetX = max(0, self.klineView.contentSize.width - 2 * self.klineView.bounds.size.width)
self.klineView.reloadContentOffset(maxContentOffsetX, true)
}
+ } else if isOverscrolled {
+ // If user is overscrolled, force redraw to ensure new candlesticks appear
+ // print("HTKLineContainerView: User is overscrolled, forcing redraw for new candlesticks")
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ // print("HTKLineContainerView: Scrolling to end after adding new data")
+ self.klineView.reloadContentOffset(self.klineView.contentOffset.x + self.configManager.itemWidth / 2, true)
+ }
}
}
} catch {
diff --git a/ios/Classes/HTKLineView.swift b/ios/Classes/HTKLineView.swift
index 8ee26d6..ab66ac1 100644
--- a/ios/Classes/HTKLineView.swift
+++ b/ios/Classes/HTKLineView.swift
@@ -131,16 +131,31 @@ 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 - configManager.paddingRight
+ let lastCandlestickOffset = configManager.itemWidth * CGFloat(configManager.modelArray.count) - 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 = 3 * configManager.itemWidth
+ // Extra spacing at the end is bounds.size.width
+ let isEnd = lastCandlestickOffset - candlesticksCountOffset <= rightScreenOffset && rightScreenOffset <= lastCandlestickOffset + candlesticksCountOffset
+
+ // Additional context for debugging over-scroll
+ let contentWidth = contentSize.width
+ let maxScrollOffset = max(0, contentWidth - bounds.size.width)
+ let isOverScrolled = contentOffset.x >= maxScrollOffset
+ let distanceFromEnd = maxScrollOffset - contentOffset.x
+
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 +198,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)
}
@@ -1034,7 +1052,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))