Skip to content

Commit

Permalink
Implementation of scrolling fix for Android 33/34
Browse files Browse the repository at this point in the history
  • Loading branch information
gosha212 committed Jan 2, 2024
1 parent 44a212d commit 18cdc36
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object DeviceDisplay {
}

@JvmStatic
fun getScreenSizeInPX(): FloatArray? {
fun getScreenSizeInPX(): FloatArray {
val metrics = getDisplayMetrics()
return floatArrayOf(metrics.widthPixels.toFloat(), metrics.heightPixels.toFloat())
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.wix.detox.espresso.scroll;

import android.content.Context;
import android.graphics.Insets;
import android.graphics.Point;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowInsets;

import com.wix.detox.action.common.MotionDir;
import com.wix.detox.espresso.DeviceDisplay;
Expand Down Expand Up @@ -169,6 +172,50 @@ private static Point getScrollStartPoint(View view, @MotionDir int direction, Fl
int offsetX = ((int) (view.getWidth() * offsetFactorX) + safetyOffsetX);
int offsetY = ((int) (view.getHeight() * offsetFactorY) + safetyOffsetY);

final float[] displaySize = DeviceDisplay.getScreenSizeInPX();

// Calculate the min/max scrollable area, taking into account the system gesture insets.
// By default we assume the scrollable area is the entire view.
int gestureSafeOffset = DeviceDisplay.convertDpiToPx(1) * 3;
int minX = gestureSafeOffset;
int minY = gestureSafeOffset;
float maxX = displaySize[0] - gestureSafeOffset;
float maxY = displaySize[1] - gestureSafeOffset;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// System gesture insets are only available on Android Q (29) and above.
WindowInsets rootWindowInsets = view.getRootWindowInsets();
if (rootWindowInsets == null) {
Log.w(LOG_TAG, "Could not get root window insets. Using default calculation.");
} else {
Insets gestureInsets = rootWindowInsets.getSystemGestureInsets();
minX = gestureInsets.left;
minY = gestureInsets.top;
maxX -= gestureInsets.right;
maxY -= gestureInsets.bottom;

Log.d(LOG_TAG,
"System gesture insets: " +
gestureInsets + " minX=" +
minX + " minY=" + minY + " maxX=" + maxX + " maxY=" + maxY + " offsetX=" + offsetX + " offsetY=" + offsetY);
}
}

switch (direction) {
case MOTION_DIR_UP:
offsetY = (int) Math.max(offsetY, minY);
break;
case MOTION_DIR_DOWN:
offsetY = (int) Math.min(offsetY, maxY);
break;
case MOTION_DIR_LEFT:
offsetX = (int) Math.max(offsetX, minX);
break;
case MOTION_DIR_RIGHT:
offsetX = (int) Math.min(offsetX, maxX);
break;
}

point.offset(offsetX, offsetY);
return point;
}
Expand Down Expand Up @@ -215,7 +262,7 @@ private static Point getGlobalViewLocation(View view) {
return new Point(pos[0], pos[1]);
}

private static ViewConfiguration getViewConfiguration() {
public static ViewConfiguration getViewConfiguration() {
if (viewConfiguration == null) {
final Context applicationContext = InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
viewConfiguration = ViewConfiguration.get(applicationContext);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.wix.detox.espresso.scroll

import android.graphics.Insets
import android.view.MotionEvent
import android.view.View
import android.view.WindowInsets
import androidx.test.espresso.UiController
import androidx.test.platform.app.InstrumentationRegistry
import com.wix.detox.action.common.MOTION_DIR_DOWN
import com.wix.detox.espresso.DeviceDisplay
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals

@Config(qualifiers = "xxxhdpi", sdk = [33])
@RunWith(RobolectricTestRunner::class)
class ScrollHelperTest {

private val display = DeviceDisplay.getScreenSizeInPX()
private val displayWidth = display[0].toInt()
private val displayHeight = display[1].toInt()

private val uiController = mock<UiController>()
private val view = mockView(displayWidth, displayHeight)

@Test
fun `perform scroll down for 200 dp on full screen view`() {
val amountInDp = 200.0
val amountInPx = amountInDp * DeviceDisplay.getDensity()
val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop

ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, null)
val capture = argumentCaptor<Iterable<MotionEvent>>()
verify(uiController).injectMotionEventSequence(capture.capture())

val listOfCapturedEvents = capture.firstValue.toList()
val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates
// the joinery of the swipe is not interesting
val lastEventX = lastEvent.x
val lastEventY = lastEvent.y
assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0)
assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0)
}

@Test
fun `perform scroll down for 200 dp on full screen view with offset y`() {
val amountInDp = 200.0
val amountInPx = amountInDp * DeviceDisplay.getDensity()
val touchSlopPx = ScrollHelper.getViewConfiguration().scaledTouchSlop
val offsetPercent = 0.9f

ScrollHelper.perform(uiController, view, MOTION_DIR_DOWN, amountInDp, null, offsetPercent)
val capture = argumentCaptor<Iterable<MotionEvent>>()
verify(uiController).injectMotionEventSequence(capture.capture())

val listOfCapturedEvents = capture.firstValue.toList()
val lastEvent = listOfCapturedEvents.last() // The last event is the UP event with the target coordinates
// the joinery of the swipe is not interesting
val lastEventX = lastEvent.x
val lastEventY = lastEvent.y
assertEquals(displayWidth / 2.0, lastEventX.toDouble(), 0.0)
assertEquals(displayHeight - amountInPx - touchSlopPx - DeviceDisplay.convertDpiToPx(1.0), lastEventY.toDouble(), 0.0)

}

private fun mockView(displayWidth: Int, displayHeight: Int): View {
val windowInsets = mock<WindowInsets>() {
whenever(it.systemGestureInsets).thenReturn(
Insets.of(88, 88, 88, 100)
)
}

val view = mock<View>() {
whenever(it.width).thenReturn(displayWidth)
whenever(it.height).thenReturn(displayHeight)
whenever(it.canScrollVertically(any())).thenReturn(true) // We allow endless scroll
whenever(it.context).thenReturn(InstrumentationRegistry.getInstrumentation().targetContext)
whenever(it.rootWindowInsets).thenReturn(windowInsets)
}
return view
}
}

0 comments on commit 18cdc36

Please sign in to comment.