Skip to content

Commit 1e9d195

Browse files
authored
1 parent 5c32f67 commit 1e9d195

6 files changed

Lines changed: 182 additions & 10 deletions

File tree

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/interaction/BasicInteractionTest.kt

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import androidx.compose.foundation.ComposeFoundationFlags
99
import androidx.compose.foundation.ExperimentalFoundationApi
1010
import androidx.compose.foundation.ScrollState
1111
import androidx.compose.foundation.background
12+
import androidx.compose.foundation.gestures.awaitEachGesture
1213
import androidx.compose.foundation.layout.Box
1314
import androidx.compose.foundation.layout.Column
1415
import androidx.compose.foundation.layout.fillMaxSize
1516
import androidx.compose.foundation.layout.fillMaxWidth
1617
import androidx.compose.foundation.layout.height
1718
import androidx.compose.foundation.layout.safeDrawingPadding
19+
import androidx.compose.foundation.layout.size
1820
import androidx.compose.foundation.text.BasicTextField
1921
import androidx.compose.foundation.text.input.TextFieldState
2022
import androidx.compose.foundation.verticalScroll
@@ -26,6 +28,10 @@ import androidx.compose.runtime.mutableStateOf
2628
import androidx.compose.ui.Alignment
2729
import androidx.compose.ui.Modifier
2830
import androidx.compose.ui.graphics.Color
31+
import androidx.compose.ui.input.pointer.PointerEventPass
32+
import androidx.compose.ui.input.pointer.changedToDown
33+
import androidx.compose.ui.input.pointer.changedToUp
34+
import androidx.compose.ui.input.pointer.pointerInput
2935
import androidx.compose.ui.layout.boundsInWindow
3036
import androidx.compose.ui.layout.onGloballyPositioned
3137
import androidx.compose.ui.platform.testTag
@@ -48,6 +54,7 @@ import kotlin.test.Test
4854
import kotlin.test.assertEquals
4955
import kotlin.test.assertFalse
5056
import kotlin.test.assertTrue
57+
import kotlin.time.Duration.Companion.seconds
5158
import kotlinx.cinterop.ExperimentalForeignApi
5259
import org.jetbrains.skiko.OS
5360
import org.jetbrains.skiko.OSVersion
@@ -236,10 +243,70 @@ class BasicInteractionTest {
236243
}
237244
}
238245

246+
@Test
247+
fun testTapsCountingWithMultiTouch() = repeat(10) {
248+
runUIKitInstrumentedTest {
249+
var touchesDown = 0
250+
var touchesUp = 0
251+
252+
setContent {
253+
Column {
254+
Box(
255+
modifier = Modifier
256+
.size(250.dp)
257+
.testTag("Box 1")
258+
.pointerInput(Unit) {
259+
awaitEachGesture {
260+
while (true) {
261+
val event = awaitPointerEvent(pass = PointerEventPass.Initial)
262+
event.changes.forEach { change ->
263+
if (change.changedToDown()) {
264+
touchesDown++
265+
} else if (change.changedToUp()) {
266+
touchesUp++
267+
}
268+
}
269+
}
270+
}
271+
}
272+
)
273+
Box(
274+
modifier = Modifier
275+
.size(250.dp)
276+
.testTag("Box 2")
277+
.pointerInput(Unit) {
278+
awaitEachGesture {
279+
while (true) {
280+
awaitPointerEvent(pass = PointerEventPass.Initial)
281+
}
282+
}
283+
}
284+
)
285+
}
286+
}
287+
288+
val tap1 = findNodeWithTag("Box 1").touchDown()
289+
val tap2 = findNodeWithTag("Box 2").touchDown()
290+
291+
assertEquals(1, touchesDown)
292+
assertEquals(0, touchesUp)
293+
294+
tap1.dragBy(dx = 20.dp, duration = 0.1.seconds)
295+
tap2.dragBy(dx = 20.dp, duration = 0.1.seconds)
296+
297+
tap1.up()
298+
tap2.up()
299+
waitForIdle()
300+
301+
assertEquals(1, touchesDown)
302+
assertEquals(1, touchesUp)
303+
}
304+
}
305+
239306
private fun UIKitInstrumentedTest.openToolbar(textFieldTag: String) {
240-
findNodeWithTag("TextField").tap()
307+
findNodeWithTag(textFieldTag).tap()
241308
delay(500)
242-
findNodeWithTag("TextField").doubleTap()
309+
findNodeWithTag(textFieldTag).doubleTap()
243310
waitForContextMenu()
244311
}
245312

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/scroll/ScrollTest.kt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package androidx.compose.ui.scroll
1818

1919
import androidx.compose.foundation.ScrollState
2020
import androidx.compose.foundation.background
21+
import androidx.compose.foundation.horizontalScroll
2122
import androidx.compose.foundation.layout.Box
2223
import androidx.compose.foundation.layout.Column
24+
import androidx.compose.foundation.layout.Row
2325
import androidx.compose.foundation.layout.fillMaxSize
2426
import androidx.compose.foundation.layout.fillMaxWidth
2527
import androidx.compose.foundation.layout.height
@@ -972,6 +974,47 @@ internal class ScrollTest {
972974
assertEquals(DpRect(DpOffset(x = 0.dp, y = 100.dp), DpSize(screenSize.width, 200.dp)), uiKitViewRect())
973975
assertEquals(DpOffset.Zero, contentOffset())
974976
}
977+
978+
@Test
979+
fun testMultiTouchScroll() = repeat(10) {
980+
runUIKitInstrumentedTest {
981+
val state1 = ScrollState(0)
982+
val state2 = ScrollState(0)
983+
setContent {
984+
Column(modifier = Modifier.fillMaxSize()) {
985+
Row(modifier = Modifier.testTag("Row 1").horizontalScroll(state1)) {
986+
repeat(20) {
987+
Box(modifier = Modifier.size(200.dp))
988+
}
989+
}
990+
Row(modifier = Modifier.testTag("Row 2").horizontalScroll(state2)) {
991+
repeat(20) {
992+
Box(modifier = Modifier.size(200.dp))
993+
}
994+
}
995+
}
996+
}
997+
998+
val tap1 = findNodeWithTag("Row 1").touchDown()
999+
val tap2 = findNodeWithTag("Row 2").touchDown()
1000+
1001+
waitForIdle()
1002+
1003+
// Simulate simultaneous drag of two fingers
1004+
repeat(2) {
1005+
tap1.dragBy(dx = (-25).dp, duration = (0.5).seconds)
1006+
tap2.dragBy(dx = (-50).dp, duration = (0.5).seconds)
1007+
}
1008+
1009+
tap1.up()
1010+
tap2.up()
1011+
1012+
waitForIdle()
1013+
1014+
assertEquals((50 - CUPERTINO_TOUCH_SLOP) * density.density, state1.value.toFloat())
1015+
assertEquals((100 - CUPERTINO_TOUCH_SLOP) * density.density, state2.value.toFloat())
1016+
}
1017+
}
9751018
}
9761019

9771020
@Composable

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import androidx.compose.ui.scene.ComposeHostingViewController
2323
import androidx.compose.ui.test.utils.center
2424
import androidx.compose.ui.test.utils.getTouchesEvent
2525
import androidx.compose.ui.test.utils.moveToLocationOnWindow
26+
import androidx.compose.ui.test.utils.resetTouches
2627
import androidx.compose.ui.test.utils.toCGPoint
2728
import androidx.compose.ui.test.utils.touchDown
2829
import androidx.compose.ui.test.utils.up
@@ -249,19 +250,19 @@ internal class UIKitInstrumentedTest {
249250
*
250251
* @param position The position on the root hosting controller.
251252
*/
252-
fun tap(position: DpOffset): UITouch {
253+
fun tap(position: DpOffset) {
253254
return touchDown(position).up()
254255
}
255256

256257
/**
257258
* Simulates a tap gesture for a given AccessibilityTestNode.
258259
*/
259-
fun AccessibilityTestNode.tap(): UITouch {
260+
fun AccessibilityTestNode.tap() {
260261
val frame = frame ?: error("Internal error. Frame is missing.")
261262
return tap(frame.center())
262263
}
263264

264-
fun AccessibilityTestNode.doubleTap(): UITouch {
265+
fun AccessibilityTestNode.doubleTap() {
265266
val frame = frame ?: error("Internal error. Frame is missing.")
266267
tap(frame.center())
267268
delay(50)
@@ -366,6 +367,7 @@ internal class MockAppDelegate: NSObject(), UIApplicationDelegateProtocol {
366367
window.resignKeyWindow()
367368
}
368369

370+
_window?.resetTouches()
369371
_window?.resignKeyWindow()
370372
_window?.windowScene = null
371373
_window?.rootViewController = UIViewController()
@@ -418,8 +420,8 @@ internal fun UIKitInstrumentedTest.findFocusedUITextInput(): UITextInputProtocol
418420
if (view.isFirstResponder) {
419421
return view
420422
}
421-
view.subviews.forEach {
422-
findFirstResponder(it as UIView)?.let { return it }
423+
view.subviews.forEach { view ->
424+
findFirstResponder(view as UIView)?.let { return it }
423425
}
424426
return null
425427
}

compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/utils/UITouch+Utils.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package androidx.compose.ui.test.utils
1818

19+
import androidx.compose.test.utils.endAllTouches
20+
import androidx.compose.test.utils.endTouch
1921
import androidx.compose.test.utils.getTouchesEvent
2022
import androidx.compose.test.utils.send
2123
import androidx.compose.test.utils.setLocationInWindow
@@ -40,10 +42,16 @@ internal fun UIWindow.touchDown(location: DpOffset): UITouch {
4042
}
4143
}
4244

45+
@OptIn(ExperimentalForeignApi::class)
4346
internal fun UIWindow.getTouchesEvent(): UIEvent {
4447
return UITouch.getTouchesEvent()
4548
}
4649

50+
@OptIn(ExperimentalForeignApi::class)
51+
internal fun UIWindow.resetTouches() {
52+
UITouch.endAllTouches()
53+
}
54+
4755
@OptIn(ExperimentalForeignApi::class)
4856
internal fun UITouch.moveToLocationOnWindow(location: DpOffset) {
4957
setLocationInWindow(location.toCGPoint())
@@ -59,8 +67,8 @@ internal fun UITouch.hold(): UITouch {
5967
}
6068

6169
@OptIn(ExperimentalForeignApi::class)
62-
internal fun UITouch.up(): UITouch {
70+
internal fun UITouch.up() {
6371
setPhase(UITouchPhase.UITouchPhaseEnded)
6472
send()
65-
return this
73+
endTouch()
6674
}

testutils/testutils-xctest/src/iosMain/objc/CMPTestUtils/UITouch+Test.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ NS_ASSUME_NONNULL_BEGIN
2626
fromEdge:(BOOL)fromEdge;
2727

2828
+ (UIEvent *)getTouchesEvent;
29+
+ (void)endAllTouches;
2930

3031
@property (assign) UITouchPhase phase;
3132
@property (assign) CGPoint locationInWindow;
3233

3334
- (void)send;
3435
- (void)updateTimestamp;
36+
- (void)endTouch;
3537

3638
@end
3739

testutils/testutils-xctest/src/iosMain/objc/CMPTestUtils/UITouch+Test.m

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import <objc/runtime.h>
1919
#import "HIDEvent.h"
2020

21+
#pragma mark - UIEvent private methods
2122
@interface UIEvent (CMPTestPrivate)
2223

2324
- (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)arg2;
@@ -26,6 +27,7 @@ - (void)_setHIDEvent:(IOHIDEventPtr)event;
2627

2728
@end
2829

30+
#pragma mark - UIApplication private methods
2931
@interface UIApplication (CMPTestPrivate)
3032

3133
- (UIEvent *)_touchesEvent;
@@ -40,6 +42,39 @@ - (UIEvent *)_touchesEvent;
4042
unsigned int _abandonForwardingRecord:1;
4143
} UITouchFlags;
4244

45+
#pragma mark - ActiveTouchesHolder
46+
47+
@interface CMPActiveTouchesHolder : NSObject
48+
49+
@property (strong, nonatomic) NSMutableArray<UITouch *> *touches;
50+
51+
+ (instancetype)shared;
52+
53+
@end
54+
55+
@implementation CMPActiveTouchesHolder
56+
57+
+ (instancetype)shared {
58+
static CMPActiveTouchesHolder *sharedInstance = nil;
59+
static dispatch_once_t onceToken;
60+
dispatch_once(&onceToken, ^{
61+
sharedInstance = [[self alloc] init];
62+
});
63+
return sharedInstance;
64+
}
65+
66+
- (instancetype)init {
67+
self = [super init];
68+
if (self) {
69+
_touches = [NSMutableArray new];
70+
}
71+
return self;
72+
}
73+
74+
@end
75+
76+
#pragma mark - UITouch extension
77+
4378
@interface UITouch (CMPTestPrivate)
4479

4580
- (void)setWindow:(UIWindow *)window;
@@ -73,6 +108,10 @@ + (UIEvent *)getTouchesEvent {
73108
return [UIApplication.sharedApplication _touchesEvent];
74109
}
75110

111+
+ (void)endAllTouches {
112+
[CMPActiveTouchesHolder.shared.touches removeAllObjects];
113+
}
114+
76115
- (id)initAtPoint:(CGPoint)point inWindow:(UIWindow *)window tapCount:(NSInteger)tapCount fromEdge:(BOOL)fromEdge {
77116
self = [super init];
78117
if (self) {
@@ -95,6 +134,8 @@ - (id)initAtPoint:(CGPoint)point inWindow:(UIWindow *)window tapCount:(NSInteger
95134
IOHIDEventPtr event = HIDEventWithTouches(@[self]);
96135
[self _setHidEvent:event];
97136
CFRelease(event);
137+
138+
[CMPActiveTouchesHolder.shared.touches addObject:self];
98139
}
99140

100141
return self;
@@ -113,8 +154,13 @@ - (void)updateTimestamp {
113154
}
114155

115156
- (void)send {
157+
if (![CMPActiveTouchesHolder.shared.touches containsObject:self]) {
158+
[CMPActiveTouchesHolder.shared.touches addObject:self];
159+
}
160+
116161
UIEvent *event = [[UIApplication sharedApplication] _touchesEvent];
117-
IOHIDEventPtr hidEvent = HIDEventWithTouches(@[self]);
162+
[event _clearTouches];
163+
IOHIDEventPtr hidEvent = HIDEventWithTouches(CMPActiveTouchesHolder.shared.touches);
118164
[event _setHIDEvent:hidEvent];
119165

120166
[self updateTimestamp];
@@ -123,4 +169,8 @@ - (void)send {
123169
[[UIApplication sharedApplication] sendEvent:event];
124170
}
125171

172+
- (void)endTouch {
173+
[CMPActiveTouchesHolder.shared.touches removeObject:self];
174+
}
175+
126176
@end

0 commit comments

Comments
 (0)