使用 SwitchMap、Race 和 Timer 进行长按检测
Long Press detection with SwitchMap, Race and Timer
我正在尝试获得一个可以区分常规点击(0-100 毫秒)和长按(恰好在 1000 毫秒)的 Observable。
pseudocode
- 用户点击并按住
mouseup
在 0 - 100 毫秒之间 -> 发出点击
- 直到 1000 毫秒才松开鼠标 -> 发出长按
- (奖励):在用户最终在长按事件
之后的某个时间执行 mouseup
之后,发出名为 longPressFinished 的单独事件(在任何情况下都需要发出点击或长按)
Visual representation
time diagram
reproduction
https://codesandbox.io/s/long-press-p4el0?file=/src/index.ts
到目前为止,我可以使用:
interface UIEventResponse {
type: UIEventType,
event: UIEvent
}
type UIEventType = 'click' | 'longPress'
import { fromEvent, merge, Observable, race, timer } from "rxjs";
import { map, mergeMap, switchMap, take, takeUntil } from "rxjs/operators";
const clickTimeout = 100
const longPressTimeout = 1000
const mouseDown$ = fromEvent<MouseEvent>(window, "mousedown");
const mouseUp$ = fromEvent<MouseEvent>(window, "mouseup");
const click1$ = merge(mouseDown$).pipe(
switchMap((event) => {
return race(
timer(longPressTimeout).pipe(mapTo(true)),
mouseUp$.pipe(mapTo(false))
);
})
);
但是,如果用户一直按下按钮直到可以发出 longPress 事件,它仍然会发出点击事件。
所以我想将点击事件限制在mousedown
之后的0-100ms。如果用户按住 1 秒,它应该立即发出长按。我当前的代码仅适用于常规点击,但随后的长按将被忽略:
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
takeUntil(timer(clickTimeout)),
mapTo({
type: "click",
event
})
)
);
})
);
我认为这是因为 race
的第二个流中的 takeUntil
退订了 race
。如何防止 mouseup
事件忽略 race
中的第一个流,从而仍然发出长按事件?
非常感谢任何帮助。
不是最干净的解决方案,但应该可以帮助您解决问题;您可以自由改进它以避免重复。
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(
clickTimeout,
mouseUp$.pipe(
mapTo({
type: "longPress",
event
})
)
)
)
);
})
);
结果
如果您在 100 毫秒内单击并释放,则为单击。
点击100ms后松开为长按
如果点击不松开,2000ms后就是长按
说明
比赛仍在使用,但我使用 timeoutWith
而不是 takeUntil(timer(...))
;这允许设置超时,如果超时,它会使用另一个可观察对象将 mouseUp 视为长按。
mapTo
用于代替 map
进行清理,但这不是必需的。
注意:根据我的示例,the mouseUp$.pipe
中的第一个 mapTo
必须在 timeoutWith
之前,否则返回的 observable 将始终映射到“click”。
我不确定我的问题是否正确,但在这种情况下,zip
函数可能是你的朋友。
这里是代码
// first create 2 Observables which emit the mousedown and mouseup respectively
// together with a timestamp representing when the event occured
const mouseDown_1$ = fromEvent<MouseEvent>(window, "mousedown").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
const mouseUp_1$ = fromEvent<MouseEvent>(window, "mouseup").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
// then use the zip function to build an Observable which emits a tuple when
// both mouseDown_1$ and mouseUp_1$ notify
const click3$ = zip(mouseDown_1$, mouseUp_1$).pipe(
// then calculate the time difference between the timestamps and decide
// whether it was a click or a longPress
map(([down, up]) => {
return up.ts - down.ts < clickTimeout
? { event: down.event, type: "click" }
: { event: down.event, type: "longPress" };
})
);
感谢@Giovanni Londero 为我指明了正确的方向并帮助我找到了适合我的解决方案!
const click$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(clickTimeout, mouseUp$.pipe(mapTo(undefined)))
)
);
}),
filter((val) => !!val)
);
我很高兴收到一些关于如何改进此代码的建议。
我正在尝试获得一个可以区分常规点击(0-100 毫秒)和长按(恰好在 1000 毫秒)的 Observable。
pseudocode
- 用户点击并按住
mouseup
在 0 - 100 毫秒之间 -> 发出点击- 直到 1000 毫秒才松开鼠标 -> 发出长按
- (奖励):在用户最终在长按事件 之后的某个时间执行
mouseup
之后,发出名为 longPressFinished 的单独事件(在任何情况下都需要发出点击或长按)
Visual representation
time diagram
reproduction
https://codesandbox.io/s/long-press-p4el0?file=/src/index.ts
到目前为止,我可以使用:
interface UIEventResponse {
type: UIEventType,
event: UIEvent
}
type UIEventType = 'click' | 'longPress'
import { fromEvent, merge, Observable, race, timer } from "rxjs";
import { map, mergeMap, switchMap, take, takeUntil } from "rxjs/operators";
const clickTimeout = 100
const longPressTimeout = 1000
const mouseDown$ = fromEvent<MouseEvent>(window, "mousedown");
const mouseUp$ = fromEvent<MouseEvent>(window, "mouseup");
const click1$ = merge(mouseDown$).pipe(
switchMap((event) => {
return race(
timer(longPressTimeout).pipe(mapTo(true)),
mouseUp$.pipe(mapTo(false))
);
})
);
但是,如果用户一直按下按钮直到可以发出 longPress 事件,它仍然会发出点击事件。
所以我想将点击事件限制在mousedown
之后的0-100ms。如果用户按住 1 秒,它应该立即发出长按。我当前的代码仅适用于常规点击,但随后的长按将被忽略:
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
takeUntil(timer(clickTimeout)),
mapTo({
type: "click",
event
})
)
);
})
);
我认为这是因为 race
的第二个流中的 takeUntil
退订了 race
。如何防止 mouseup
事件忽略 race
中的第一个流,从而仍然发出长按事件?
非常感谢任何帮助。
不是最干净的解决方案,但应该可以帮助您解决问题;您可以自由改进它以避免重复。
const click2$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(
clickTimeout,
mouseUp$.pipe(
mapTo({
type: "longPress",
event
})
)
)
)
);
})
);
结果
如果您在 100 毫秒内单击并释放,则为单击。
点击100ms后松开为长按
如果点击不松开,2000ms后就是长按
说明
比赛仍在使用,但我使用 timeoutWith
而不是 takeUntil(timer(...))
;这允许设置超时,如果超时,它会使用另一个可观察对象将 mouseUp 视为长按。
mapTo
用于代替 map
进行清理,但这不是必需的。
注意:根据我的示例,the mouseUp$.pipe
中的第一个 mapTo
必须在 timeoutWith
之前,否则返回的 observable 将始终映射到“click”。
我不确定我的问题是否正确,但在这种情况下,zip
函数可能是你的朋友。
这里是代码
// first create 2 Observables which emit the mousedown and mouseup respectively
// together with a timestamp representing when the event occured
const mouseDown_1$ = fromEvent<MouseEvent>(window, "mousedown").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
const mouseUp_1$ = fromEvent<MouseEvent>(window, "mouseup").pipe(
map((event) => {
const ts = Date.now();
return { event, ts };
})
);
// then use the zip function to build an Observable which emits a tuple when
// both mouseDown_1$ and mouseUp_1$ notify
const click3$ = zip(mouseDown_1$, mouseUp_1$).pipe(
// then calculate the time difference between the timestamps and decide
// whether it was a click or a longPress
map(([down, up]) => {
return up.ts - down.ts < clickTimeout
? { event: down.event, type: "click" }
: { event: down.event, type: "longPress" };
})
);
感谢@Giovanni Londero 为我指明了正确的方向并帮助我找到了适合我的解决方案!
const click$: Observable<UIEventResponse> = mouseDown$.pipe(
switchMap((event) => {
return race<UIEventResponse>(
timer(longPressTimeout).pipe(
mapTo({
type: "longPress",
event
})
),
mouseUp$.pipe(
mapTo({
type: "click",
event
}),
timeoutWith(clickTimeout, mouseUp$.pipe(mapTo(undefined)))
)
);
}),
filter((val) => !!val)
);
我很高兴收到一些关于如何改进此代码的建议。