diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..cc37007 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @furkando @NGabuaeva @EdiOanceaV2 @sinantalhakosar @rturtu @hikmet-demir @Matteoverzotti @robertbarbu27 @snowtema \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2d04a37 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## Proposed Change + +_Describe what this pull request does?_ + +## To-Do + +_Add to-do items here_ + +- [ ] Does it need backend deployment? +- [ ] Does it need confirmation on calculations? + +## Screenshot + +_Add screenshots or short video_ 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 2f935a6..547357d 100755 --- a/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java +++ b/android/src/main/java/com/github/fujianlian/klinechart/BaseKLineChartView.java @@ -1110,10 +1110,20 @@ public int getMinScrollX() { } public int getMaxScrollX() { - int contentWidth = (int) Math.max((mDataLen - (mWidth - configManager.paddingRight) / mScaleX), 0); + int contentWidth = (int) Math.max((mDataLen - (mWidth - configManager.paddingRight) / mScaleX + mWidth / mScaleX), 0); return contentWidth; } + @Override + protected float getMinVisibleCandles() { + return configManager.minVisibleCandles; + } + + @Override + public float getDataLength() { + return mDataLen; + } + /** * 在主区域画线 * @@ -1670,19 +1680,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 = (int)(mWidth / mScaleX); + 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 4148528..e4f578e 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; @@ -453,6 +455,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..620ca49 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,14 +271,30 @@ 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 * * @param scrollX */ public void setScrollX(int scrollX) { - this.mScrollX = scrollX; - scrollTo(scrollX, 0); + int maxAllowedScroll = (int)(getDataLength() - getMinVisibleCandles() * getPointWidth()); + int normalizedMaxAllowedScroll = Math.max(0, maxAllowedScroll); + this.mScrollX = Math.max(0, Math.min(scrollX, normalizedMaxAllowedScroll)); + scrollTo(this.mScrollX, 0); } /** @@ -285,13 +307,17 @@ public boolean isMultipleTouch() { } protected void checkAndFixScrollX() { - int contentSizeWidth = (getMaxScrollX()); + float dataLength = getDataLength(); + float minVisibleCandles = getMinVisibleCandles(); + float pointWidth = getPointWidth(); + int maxAllowedScroll = (int)(dataLength - minVisibleCandles * pointWidth); + int normalizedMaxAllowedScroll = Math.max(0, maxAllowedScroll); if (mScrollX < getMinScrollX()) { mScrollX = getMinScrollX(); mScroller.forceFinished(true); - } else if (mScrollX > contentSizeWidth) { - mScrollX = contentSizeWidth; + } else if (mScrollX > normalizedMaxAllowedScroll) { + mScrollX = normalizedMaxAllowedScroll; mScroller.forceFinished(true); if (!mHasTriggeredRightSide) { mHasTriggeredRightSide = 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 c746653..a522597 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 @@ -71,7 +71,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(); @@ -79,8 +78,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.getWidth(); + klineView.setScrollX(scrollToEnd); } diff --git a/example/App.js b/example/App.js index 572f63b..529babd 100644 --- a/example/App.js +++ b/example/App.js @@ -39,6 +39,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) @@ -164,7 +165,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]) @@ -213,7 +215,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/HTKLineView.swift b/ios/Classes/HTKLineView.swift index 7cac2a3..ea32988 100644 --- a/ios/Classes/HTKLineView.swift +++ b/ios/Classes/HTKLineView.swift @@ -98,7 +98,6 @@ class HTKLineView: UIScrollView { childDraw = wrDraw } - let isEnd = contentOffset.x + 1 + bounds.size.width >= contentSize.width let previousContentOffset = contentOffset.x reloadContentSize() @@ -106,8 +105,8 @@ class HTKLineView: UIScrollView { // 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 + } else if configManager.shouldScrollToEnd { + let toEndContentOffset = contentSize.width - 2 * bounds.size.width let distance = abs(contentOffset.x - toEndContentOffset) let animated = distance <= configManager.itemWidth reloadContentOffset(toEndContentOffset, animated) @@ -150,16 +149,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 = 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) } @@ -721,7 +723,16 @@ class HTKLineView: UIScrollView { extension HTKLineView: UIScrollViewDelegate { func scrollViewDidScroll(_ scrollView: UIScrollView) { + let allCandlesWidth = configManager.itemWidth * CGFloat(configManager.modelArray.count) + let maxAllowedOffset = 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))