From b10d1920def2ed9c8f446d013c0384d4b802d977 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 11 Jul 2026 18:54:45 -0700 Subject: [PATCH] Fix viewport jumping sideways when axis offsets are recalculated The pan is stored in the touch matrix as a pixel translation, while the value-to-pixel scale is derived from the content rect. Resizing the content rect therefore makes the same pixel translation denote a different value, sliding the visible range. This is most visible after a zoom gesture: BarLineChartTouchListener defers chart.calculateOffsets() to ACTION_UP, so a y-zoom that widens the y-axis labels resizes the content rect only once the gesture ends, and the chart jumps sideways. The same happens when a fling settles, via computeScroll(). Scale the translation by the same ratio as the content rect so the visible range survives the resize. The visible extent is unaffected: it is (axis range / touch scale), which does not depend on the content width. --- .../appdev/charting/utils/ViewPortHandler.kt | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/chartLib/src/main/kotlin/info/appdev/charting/utils/ViewPortHandler.kt b/chartLib/src/main/kotlin/info/appdev/charting/utils/ViewPortHandler.kt index bc6c2b60c..c6ec5006a 100644 --- a/chartLib/src/main/kotlin/info/appdev/charting/utils/ViewPortHandler.kt +++ b/chartLib/src/main/kotlin/info/appdev/charting/utils/ViewPortHandler.kt @@ -105,11 +105,52 @@ open class ViewPortHandler { fun hasChartDimens(): Boolean = chartHeight > 0 && chartWidth > 0 fun restrainViewPort(offsetLeft: Float, offsetTop: Float, offsetRight: Float, offsetBottom: Float, logging: Boolean = false) { + val previousWidth = contentRect.width() + val previousHeight = contentRect.height() + contentRect[offsetLeft, offsetTop, chartWidth - offsetRight] = (chartHeight - offsetBottom) + + preserveViewPortOnResize(previousWidth, previousHeight) + if (logging) Timber.i(contentRect.toString()) } + /** + * Keeps the visible data range fixed when the content rect is resized. + * + * The pan is held in [matrixTouch] as a *pixel* translation, while the value-to-pixel + * scale is derived from the content rect. Resizing the rect therefore makes the same + * pixel translation denote a different value, sliding the visible range. + * + * This bites hardest after a zoom gesture: BarLineChartTouchListener defers + * chart.calculateOffsets() to ACTION_UP, so a y-zoom that widens the y-axis labels + * resizes the content rect only once the gesture ends. The chart then jumps sideways by + * roughly (x-range * change-in-width / content width) - on a chart whose x-axis spans a + * long range, a few pixels of label growth is a very visible jump. + * + * Scaling the translation by the same ratio as the rect keeps the visible range where the + * user left it. The visible extent itself is unaffected: it is (axis range / touch scale), + * which does not depend on the content width. + */ + private fun preserveViewPortOnResize(previousWidth: Float, previousHeight: Float) { + // Nothing to preserve before the first layout, or when the rect did not change size. + if (previousWidth <= 0f || previousHeight <= 0f) return + + val width = contentRect.width() + val height = contentRect.height() + + if (width == previousWidth && height == previousHeight) return + if (width <= 0f || height <= 0f) return + + matrixTouch.getValues(matrixBuffer) + matrixBuffer[Matrix.MTRANS_X] *= width / previousWidth + matrixBuffer[Matrix.MTRANS_Y] *= height / previousHeight + matrixTouch.setValues(matrixBuffer) + + limitTransAndScale(matrixTouch, contentRect) + } + fun offsetLeft(): Float = contentRect.left fun offsetRight(): Float = chartWidth - contentRect.right