Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1428,11 +1428,25 @@ public int getMinScrollX() {
return 0;
}

public int getExtraScrollX() {
return (int) (mWidth / mScaleX - getMinVisibleCandles());
}

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 +2003,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 @@ -410,7 +410,7 @@ public void addCandlesticksAtTheEnd(ReadableArray candlesticksArray) {

try {
// Check if user is currently at the end of the chart
boolean wasAtEnd = klineView.getScrollOffset() >= klineView.getMaxScrollX() - 10;
boolean wasAtEnd = klineView.getScrollOffset() >= klineView.getMaxScrollX() - 10 - klineView.getExtraScrollX();

// Get existing model for preserving indicator lists structure
KLineEntity templateEntity = null;
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
29 changes: 25 additions & 4 deletions ios/Classes/HTKLineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 - bounds.size.width - 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)
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -1034,7 +1046,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