使用 SwitchMap、Race 和 Timer 进行长按检测

Long Press detection with SwitchMap, Race and Timer

我正在尝试获得一个可以区分常规点击(0-100 毫秒)和长按(恰好在 1000 毫秒)的 Observable。

pseudocode

  1. 用户点击并按住
  2. mouseup 在 0 - 100 毫秒之间 -> 发出点击
  3. 直到 1000 毫秒才松开鼠标 -> 发出长按
    1. (奖励):在用户最终在长按事件
    2. 之后的某个时间执行 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)
);

我很高兴收到一些关于如何改进此代码的建议。