Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

<div align="center">
<img src="./example/logo.png" alt="React Native KLine View" width="120" height="120" style="border-radius: 60px;" />
</div>
Expand Down Expand Up @@ -74,6 +96,7 @@ yarn add react-native-kline-view@https://github.com/hellohublot/react-native-kli
```

### iOS Setup

```bash
cd ios && pod install
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
* 在主区域画线
*
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
*
Expand All @@ -286,7 +306,6 @@ public boolean isMultipleTouch() {

protected void checkAndFixScrollX() {
int contentSizeWidth = (getMaxScrollX());

if (mScrollX < getMinScrollX()) {
mScrollX = getMinScrollX();
mScroller.forceFinished(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,16 @@ 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();

if (klineView.configManager.shouldAdjustScrollPosition) {
// 调整滚动位置以补偿新增的数据
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);
}


Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down
7 changes: 5 additions & 2 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion example/utils/businessLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ export const packOptionList = (modelArray, appState, shouldScrollToEnd = true, u
selectedDrawTool,
showVolumeChart,
candleCornerRadius,
drawShouldContinue
drawShouldContinue,
minVisibleCandles
} = appState

const theme = ThemeManager.getCurrentTheme(isDarkTheme)
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions ios/Classes/HTKLineConfigManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class HTKLineConfigManager: NSObject {

var candleCornerRadius: CGFloat = 0

var minVisibleCandles: CGFloat = 5

var minuteVolumeCandleWidth: CGFloat = 0

var _minuteVolumeCandleWidth: CGFloat = 0
Expand Down Expand Up @@ -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
Expand Down
33 changes: 24 additions & 9 deletions ios/Classes/HTKLineContainerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
35 changes: 31 additions & 4 deletions ios/Classes/HTKLineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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))
Expand Down
Loading