Skip to content

Commit 16cc6d7

Browse files
authored
Merge pull request #8 from nos-digital/task/7-double-tap
Improved double tap zooming
2 parents 1096c57 + 3468efa commit 16cc6d7

File tree

5 files changed

+267
-89
lines changed

5 files changed

+267
-89
lines changed

library/src/main/java/nl/nos/imagin/Calculator.kt

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package nl.nos.imagin
22

33
import android.widget.ImageView
44
import kotlin.math.max
5+
import kotlin.math.min
6+
7+
object Calculator {
58

6-
class Calculator {
79
fun calculateMaxTranslation(
810
scale: Float,
911
imageSize: Int,
@@ -22,4 +24,86 @@ class Calculator {
2224
Pair((imageView.drawable.intrinsicWidth / heightScale).toInt(), imageView.height)
2325
}
2426
}
27+
28+
/**
29+
* Calculate the translation (x or y) we want to animate to after a double tap.
30+
*
31+
* @param futureScale The scale we want to animate to
32+
* @param touchPoint The x or y value of the double tap.
33+
* @param currentTranslation The current translation (x or y) value of the image view.
34+
* @param currentViewSize The current image view size (width or height).
35+
* @param currentViewScale The current image view scale (x or y).
36+
*/
37+
fun calculateFutureTranslation(
38+
imageSize: Int,
39+
futureScale: Float,
40+
touchPoint: Float,
41+
currentTranslation: Float,
42+
currentViewSize: Int,
43+
currentViewScale: Float
44+
): Float {
45+
/**
46+
* The maximum and minimum translation the ImageView should have (with the futureScale),
47+
* so the View is still fully visible.
48+
*/
49+
val translationRange = calculateMaxTranslation(futureScale, imageSize, currentViewSize).let {
50+
-it..it
51+
}
52+
53+
/**
54+
* The maximum and minimum translation the ImageView should have (with the futureScale)
55+
* when the view should not be fully visible, but only half of it is visible.
56+
*/
57+
val overEdgeTranslationRange = (translationRange.endInclusive + currentViewSize * .5f).let {
58+
-it..it
59+
}
60+
61+
// The maximum translation at this moment.
62+
val currentMaxTranslation = (currentViewScale - 1) / 2 * currentViewSize
63+
64+
/**
65+
* Calculate the [touchPoint] back to how it would be if the scale was 1.0,
66+
* so it is relative to the image view size.
67+
*/
68+
val touchPointRelativeToImageView
69+
= ((-currentTranslation) + currentMaxTranslation + touchPoint) /
70+
currentViewScale
71+
72+
/**
73+
* When the imageSize is smaller then the currentViewSize, we need to correct our
74+
* calculations so that the pixels outsize the image are not used to calculate the
75+
* relative position.
76+
*/
77+
val fixedTouchPointRelativeToImageView = if (imageSize < currentViewSize) {
78+
val correction = (currentViewSize - imageSize) / 2f
79+
if (touchPointRelativeToImageView > 0f) {
80+
max(0f, touchPointRelativeToImageView - correction)
81+
} else {
82+
min(0f, touchPointRelativeToImageView + correction)
83+
}
84+
} else {
85+
touchPointRelativeToImageView
86+
}
87+
88+
/**
89+
* Now that we have the touchPointRelativeToImageView, we can calculate what this
90+
* would be with within the new translation values.
91+
* with "touchPointRelativeToImageView / currentViewSize" we calculate the x in a
92+
* number between 0.0 and 1.0.
93+
* with "maxTranslation - minTranslation" we calculate the difference between the
94+
* lowest possible translation value and the highest possible value, the answer is
95+
* somewhere in between. We multiply this with the previous number.
96+
* then with "- maxTranslation" we correct this, because the minimum translation is
97+
* not 0, but somewhere lower then that.
98+
*/
99+
val uncorrectedResult = -(
100+
(fixedTouchPointRelativeToImageView / imageSize) * (overEdgeTranslationRange.endInclusive - overEdgeTranslationRange.start)
101+
- overEdgeTranslationRange.endInclusive
102+
)
103+
104+
return uncorrectedResult.limitByRange(translationRange)
105+
}
106+
107+
private fun Float.limitByRange(range: ClosedFloatingPointRange<Float>) =
108+
max(range.start, min(range.endInclusive, this))
25109
}

library/src/main/java/nl/nos/imagin/DoubleTapToZoomTouchHandler.kt

Lines changed: 21 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ class DoubleTapToZoomTouchHandler(
1616
val maxZoom: Float
1717
) : View.OnTouchListener {
1818

19-
private val calculator = Calculator()
20-
2119
private val gestureDetector = GestureDetector(imageView.context,
2220
object : GestureDetector.SimpleOnGestureListener() {
2321
override fun onLongPress(e: MotionEvent) {}
@@ -31,91 +29,37 @@ class DoubleTapToZoomTouchHandler(
3129
// maxZoom value.
3230
val futureScale = if (imageView.scaleX >= maxZoom) minZoom else maxZoom
3331

34-
val imageSize = calculator.calculateImageSize(imageView) ?: return false
32+
val imageSize = Calculator.calculateImageSize(imageView) ?: return false
3533

36-
val translationX = calculateFutureTranslation(
37-
imageSize.first,
38-
futureScale,
39-
e.rawX,
40-
imageView.translationX,
41-
imageView.width,
42-
imageView.scaleX
34+
val translationX = Calculator.calculateFutureTranslation(
35+
imageSize.first,
36+
futureScale,
37+
e.rawX,
38+
imageView.translationX,
39+
imageView.width,
40+
imageView.scaleX
4341
)
44-
val translationY = calculateFutureTranslation(
45-
imageSize.second,
46-
futureScale,
47-
e.rawY,
48-
imageView.translationY,
49-
imageView.height,
50-
imageView.scaleY
42+
val translationY = Calculator.calculateFutureTranslation(
43+
imageSize.second,
44+
futureScale,
45+
e.rawY,
46+
imageView.translationY,
47+
imageView.height,
48+
imageView.scaleY
5149
)
5250

5351
// Zoom in or out after a double tap
5452
isAnimating = true
5553
imageView.animate()
56-
.scaleX(futureScale)
57-
.scaleY(futureScale)
58-
.translationX(translationX)
59-
.translationY(translationY)
60-
.withEndAction { isAnimating = false }
61-
.start()
54+
.scaleX(futureScale)
55+
.scaleY(futureScale)
56+
.translationX(translationX)
57+
.translationY(translationY)
58+
.withEndAction { isAnimating = false }
59+
.start()
6260
return true
6361
}
6462

65-
/**
66-
* Calculate the translation (x or y) we want to animate to after a double tap.
67-
*
68-
* @param futureScale The scale we want to animate to
69-
* @param relativeTouchPoint The x or y value of the double tap, relative to the current
70-
* visible viewport.
71-
* @param currentTranslation The current translation (x or y) value of the image view.
72-
* @param currentViewSize The current image view size (width or height).
73-
* @param currentViewScale The current image view scale (x or y).
74-
*/
75-
fun calculateFutureTranslation(
76-
imageSize: Int,
77-
futureScale: Float,
78-
relativeTouchPoint: Float,
79-
currentTranslation: Float,
80-
currentViewSize: Int,
81-
currentViewScale: Float
82-
): Float {
83-
/**
84-
* The maximum and minimum translation the ImageView should have (with the futureScale),
85-
* so the View is still visible.
86-
*/
87-
val maxTranslation = calculator
88-
.calculateMaxTranslation(futureScale, imageSize, currentViewSize)
89-
val minTranslation = -maxTranslation
90-
91-
// The maximum translation at this moment.
92-
val currentMaxTranslation = (currentViewScale - 1) / 2 * currentViewSize
93-
94-
/**
95-
* Calculate the [relativeTouchPoint] back to how it would be if the scale was 1.0,
96-
* so it is relative to the image view size.
97-
*/
98-
val touchPointRelativeToImageView
99-
= ((-currentTranslation) + currentMaxTranslation + relativeTouchPoint) /
100-
currentViewScale
101-
102-
/**
103-
* Now that we have the touchPointRelativeToImageView, we can calculate what this
104-
* would be with within the new translation values.
105-
* with "touchPointRelativeToImageView / currentViewSize" we calculate the x in a
106-
* number between 0.0 and 1.0.
107-
* with "maxTranslation - minTranslation" we calculate the difference between the
108-
* lowest possible translation value and the highest possible value, the answer is
109-
* somewhere in between. We multiply this with the previous number.
110-
* then with "- maxTranslation" we correct this, because the minimum translation is
111-
* not 0, but somewhere lower then that.
112-
*/
113-
return -(
114-
(touchPointRelativeToImageView / currentViewSize) * (maxTranslation - minTranslation)
115-
- maxTranslation
116-
)
117-
}
118-
11963
override fun onDoubleTapEvent(e: MotionEvent): Boolean {
12064
return false
12165
}

library/src/main/java/nl/nos/imagin/PinchToZoomScaleGestureDetectorHandler.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ class PinchToZoomScaleGestureDetectorHandler(
1212
private val maxZoom: Float
1313
) : ScaleGestureDetector.SimpleOnScaleGestureListener() {
1414

15-
private val calculator = Calculator()
16-
1715
override fun onScale(detector: ScaleGestureDetector): Boolean {
1816
if (isMaxZoomedIn() && detector.scaleFactor >= 1f) {
1917
return true
@@ -24,7 +22,7 @@ class PinchToZoomScaleGestureDetectorHandler(
2422
// Don't let the object get too small or too large.
2523
scaleFactor = Math.max(minZoom, Math.min(scaleFactor, maxZoom))
2624

27-
val imageSize = calculator.calculateImageSize(imageView) ?: return false
25+
val imageSize = Calculator.calculateImageSize(imageView) ?: return false
2826

2927
imageView.translationX = calculateNewTranslationMinMaxed(
3028
imageSize.first,
@@ -86,7 +84,7 @@ class PinchToZoomScaleGestureDetectorHandler(
8684
imageViewSize: Int
8785
): Float {
8886
val maxTranslation =
89-
calculator.calculateMaxTranslation(newImageScale, imageSize, imageViewSize)
87+
Calculator.calculateMaxTranslation(newImageScale, imageSize, imageViewSize)
9088

9189
// Never pinch out of the bounds
9290
return Math.max(-maxTranslation, Math.min(translation, maxTranslation))
@@ -100,8 +98,8 @@ class PinchToZoomScaleGestureDetectorHandler(
10098
): Float {
10199
if (currentScale <= 1.0f) return 0f
102100

103-
return calculator.calculateMaxTranslation(newScale, imageViewSize, imageSize) /
104-
calculator.calculateMaxTranslation(currentScale, imageViewSize, imageSize)
101+
return Calculator.calculateMaxTranslation(newScale, imageViewSize, imageSize) /
102+
Calculator.calculateMaxTranslation(currentScale, imageViewSize, imageSize)
105103
}
106104

107105
/**

library/src/main/java/nl/nos/imagin/ScrollHandler.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ class ScrollHandler(
2727
private val outOfBoundScrolledListener: (() -> Unit)?
2828
) : View.OnTouchListener {
2929

30-
private val calculator = Calculator()
31-
3230
private val gestureDetector = GestureDetector(imageView.context,
3331
object : GestureDetector.SimpleOnGestureListener() {
3432

@@ -113,7 +111,7 @@ class ScrollHandler(
113111
val consumed = gestureDetector.onTouchEvent(event)
114112

115113
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
116-
val imageSize = calculator.calculateImageSize(imageView) ?: return consumed
114+
val imageSize = Calculator.calculateImageSize(imageView) ?: return consumed
117115

118116
if (allowScrollOutOfBoundsHorizontally && shouldTriggerOutOfBoundListener(
119117
scrollDistanceToCloseInPx,
@@ -174,7 +172,7 @@ class ScrollHandler(
174172
currentTranslation: Float
175173
): Float? {
176174
val maxTranslation =
177-
calculator.calculateMaxTranslation(
175+
Calculator.calculateMaxTranslation(
178176
scale,
179177
imageSize,
180178
imageViewSize
@@ -204,7 +202,7 @@ class ScrollHandler(
204202
if (imageViewScale > 1f) return false
205203

206204
val maxTranslation =
207-
calculator.calculateMaxTranslation(imageViewScale, imageSize, imageViewSize)
205+
Calculator.calculateMaxTranslation(imageViewScale, imageSize, imageViewSize)
208206
return imageViewTranslation < -(maxTranslation + distanceToClose) ||
209207
imageViewTranslation > maxTranslation + distanceToClose
210208
}

0 commit comments

Comments
 (0)