使用 Observables 检测点击和双击

问题描述 投票:0回答:1

我有一个点击事件流,我想从该流中确定用户是否执行了单击或双击。目前,我正在尝试通过缓冲指定的

doubleClickTime
(例如 300 毫秒)内的值并计算这些值的数量来实现此目的。

所以从这个开始

IObservable<Click> clicks = ...

我正在尝试将 buffer 运算符与打开选择器和关闭选择器一起使用

IDisposable subscription = clicks.Buffer(clicks, _ => Observable.Timer(doubleClickTime))
.Subscribe(items =>
 {
    if(items.Count > 1)
    {
      Debug.WriteLine($"Double Click");
    }
    else
    {
      Debug.WriteLine($"Single Click");
    }
 });

但是,上述问题是双击时我得到以下输出

Double Click
Single Click

即,它记录双击和单击。我认为这是因为我的打开选择器也是单击事件,因此双击时会启动一个新的缓冲区窗口。

如何正确组合打开选择器,使其仅在没有缓冲发生时触发?

有更好的方法吗?

更新

大概与用 rxjs 编写的example类似,但不太清楚如何在 C# 中翻译下面的内容

const clickOrDoubleClick$ = click$.pipe(
buffer(click$.pipe(switchMap(() => timer(DOUBLE_CLICK_INTERVAL)))),
map((clicksArr) =>
clicksArr.length > 1 ? createDoubleClick() : createClick() ));
c# observable reactive-programming system.reactive
1个回答
0
投票

您可以使用

Delay()
运算符创建一个新的可观察量,其中每个项目都比原始可观察量延迟固定时间。当您将原始可观察值和“延迟”可观察值与
CombineLatest()
结合起来时,您将看到原始可观察值或“延迟”可观察值的变化。下面的大理石图说明了这种情况:

orig : ---1----2--3-----4------5--6----7----8----
delay: -----1----2--3-----4------5--6----7----8--
comb :      ^--^-^^-^---^-^----^-^^-^--^-^--^-^--

^
指示
CombineLatest()
可观察量将在何处发出新值。

要查看/识别实际的“变化”,您必须使用

CombineLatest()
可观察到的值并将之前的值保存在“状态”中。这样您将看到“原始”值是否已更改或“延迟”值是否已更改。根据这些信息,您将能够确定是快速双击(“原始”值已更改)还是缓慢的单击(“延迟”值已更改)。我想出了以下解决方案:

首先我们构建一个可观察的值,其中的值有一个索引计数器,这样我们就可以认识到它绝对是一个新值。这是必需的,因为发出时的值可能等于先前的值(基于

Equals()
实现),并且我们不会看到变化。创建带有索引计数器的容器类似于
Timestamp()
TimeInterval()
正在做的事情。我构建了自己的容器类和可观察的:

public class IndexedValue
{
    public string Value {get; set;}
    public int Index {get; set;}

    public override string ToString()
    {
        return $"V={Value}, Index={Index}";
    }
}

ISubject<string> clicksSource = new Subject<string>();

IObservable<IndexedValue> indexedClicksSource = clicksSource.Select((v, i) => {
        return new IndexedValue
        {
            Value = v,
            Index = i
        };
    });

(我在开发/测试期间使用

string
类型作为发出的值,但它可能是带有
T
或您的特定类型
Click
的通用解决方案。另外,回想起来,我可以使用
Timestamp()
它的工作原理很可能是一样的)

现在我们构建

CombineLatest()
可观察的:

indexedClicksSource.CombineLatest(
        indexedClicksSource.Delay(
            TimeSpan.FromMilliseconds(doubleClickRangeInMs))
            .StartWith(default(IndexedValue)),
        (original, delayed) => {
            return new 
            {
                Original = original,
                Delayed = delayed
            };
        })

Delay()
observable 将以
NULL
值开始,因此即使“延迟”的 observable 尚未生成值,
CombineLatest()
调用也会立即触发。这很重要,以防原始可观察对象通过两次短按或双击立即开始。大理石图现在可能看起来像这样:

orig : ---1----2--3---...
delay: ---N-1----2--3-...
comb :    ^-^--^-^^-^-...

我们使用

Scan()
运算符来处理“状态”,其中保存有关先前
CombineLatest()
组合的信息以及有关活动的单击/双击检查的任何信息。状态类定义如下:

public class DelayedStateInfo
{
    public IndexedValue PreviousOriginalValue {get; set;}
    public IndexedValue PreviousDelayedValue {get; set;}
    public IndexedValue StartValue {get; set;}
    public IndexedValue EndValue {get; set;}
}

PreviousOriginalValue
PreviousDelayedValue
属性是来自
CombineLatest()
调用的两个值。
StartValue
EndValue
属性用于决定我们是双击还是单击来构建/查看建筑物。
StartValue
将是第一次单击,
EndValue
将是第二次单击。根据它们保存的值(更准确地说是它们保存的
Index
值),我们将确定是双击还是单击。
Scan()
调用可以如下所示:

.Scan(default(DelayedStateInfo), (oldState, current) => {
            DelayedStateInfo newState = new()
            {
                PreviousOriginalValue = current.Original,
                PreviousDelayedValue = current.Delayed
            };
            if (oldState == default)
            {
                // the actual seed
                newState.StartValue = current.Original;
                return newState;
            }
            bool originalUpdated = oldState.PreviousOriginalValue.Index != current.Original.Index;
            if (originalUpdated)
            {
                // the original value is updated, must be a double click or a new sequence
                if (oldState.EndValue == null)
                {
                    // it's a double click
                    newState.StartValue = oldState.StartValue;
                    newState.EndValue = current.Original;
                }
                else 
                {
                    // there already was a click, so start a new sequence
                    newState.StartValue = current.Original;
                }
                return newState;
            }
            else
            {
                // the delayed value is updated, must be a single click or a catch up
                if (oldState.EndValue == null && oldState.StartValue.Index <= current.Delayed.Index)
                {
                    // single click
                    newState.StartValue = oldState.StartValue;
                    newState.EndValue = current.Delayed;
                    return newState;
                }
                else 
                {
                    // a catch up, but there was already a single or double click sent, so do nothing
                    newState.StartValue = oldState.StartValue;
                    newState.EndValue = oldState.EndValue;
                    return newState;
                }
            }
        })

这将在发出原始(和“延迟”)值时生成

DelayedStateInfo
实例,如果单击或双击,我们将使用
Select()
运算符来构建元信息。我使用以下类来保存此信息:

public class DoubleClickInfo {
    public string Value {get; set;}
    public bool IsDoubleClick {get; set;}

    public override string ToString() {
        return $"V={Value}, doubleClick={IsDoubleClick}";
    }
}

Select()
调用可以如下所示:

        .DistinctUntilChanged(it =>
        {
            int startIndex = it.StartValue.Index;
            int? endIndex = it.EndValue?.Index;
            return $"{startIndex}|{endIndex}";
        })
        .Select(info => {
            if (info.EndValue == null)
            {
                return null; // no click yet
            }
            if (info.StartValue.Index == info.EndValue.Index)
            {
                // a single click
                return new DoubleClickInfo {
                    Value = info.EndValue.Value,
                    IsDoubleClick = false
                };
            }
            // a double click
            return new DoubleClickInfo {
                Value = info.EndValue.Value,
                IsDoubleClick = true
            };
        })
        .Where(it => it != null)

DistinctUntilChanged()
调用用于过滤掉实际上没有显示变化的状态。最后的
Where()
调用用于防止我们还无法识别点击(
EndValue
null
)。

使用所有这些代码片段,我们可以构建以下示例程序:

sw = Stopwatch.StartNew();
ISubject<string> clicksSource = new Subject<string>();

IObservable<IndexedValue> indexedClicksSource = clicksSource.Select((v, i) => {
    return new IndexedValue
    {
        Value = v,
        Index = i
    };
});

IObservable<DoubleClickInfo> aggregatedSource = indexedClicksSource.CombineLatest(
    indexedClicksSource.Delay(
        TimeSpan.FromMilliseconds(doubleClickRangeInMs))
        .StartWith(default(IndexedValue)),
    (original, delayed) => {
        return new 
        {
            Original = original,
            Delayed = delayed
        };
    })
    .Scan(default(DelayedStateInfo), (oldState, current) => {
        DelayedStateInfo newState = new()
        {
            PreviousOriginalValue = current.Original,
            PreviousDelayedValue = current.Delayed
        };
        if (oldState == default)
        {
            DebugScanDelegate("Start seed");
            // the actual seed
            newState.StartValue = current.Original;
            return newState;
        }
        bool originalUpdated = oldState.PreviousOriginalValue.Index != current.Original.Index;
        if (originalUpdated)
        {
            // the original value is updated, must be a double click or a new sequence
            if (oldState.EndValue == null)
            {
                // it's a double click
                newState.StartValue = oldState.StartValue;
                newState.EndValue = current.Original;
            }
            else 
            {
                // there already was a click, so start a new sequence
                newState.StartValue = current.Original;
            }
            return newState;
        }
        else
        {
            // the delayed value is updated, must be a single click or a catch up
            if (oldState.EndValue == null && oldState.StartValue.Index <= current.Delayed.Index)
            {
                // single click
                newState.StartValue = oldState.StartValue;
                newState.EndValue = current.Delayed;
                return newState;
            }
            else 
            {
                // a catch up, but there was already a single or double click sent, so do nothing
                newState.StartValue = oldState.StartValue;
                newState.EndValue = oldState.EndValue;
                return newState;
            }
        }
    })
    .DistinctUntilChanged(it =>
    {
        int startIndex = it.StartValue.Index;
        int? endIndex = it.EndValue?.Index;
        return $"{startIndex}|{endIndex}";
    })
    .Select(info => {
        if (info.EndValue == null)
        {
            return null; // no click yet
        }
        if (info.StartValue.Index == info.EndValue.Index)
        {
            // a single click
            return new DoubleClickInfo {
                Value = info.EndValue.Value,
                IsDoubleClick = false
            };
        }
        // a double click
        return new DoubleClickInfo {
            Value = info.EndValue.Value,
            IsDoubleClick = true
        };
    })
    .Where(it => it != null);

aggregatedSource.Subscribe(it => {
    Console.WriteLine($"{sw.ElapsedMilliseconds:00000}ms| Subscribe called with: {it}");
});

EmitValue(clicksSource, "str_11");
SleepWithOutput(500);
EmitValue(clicksSource, "str_12");
SleepWithOutput(150);
EmitValue(clicksSource, "str_13");
SleepWithOutput(450);
EmitValue(clicksSource, "str_14");
SleepWithOutput(400);
EmitValue(clicksSource, "str_15");
SleepWithOutput(350);
EmitValue(clicksSource, "str_16");
SleepWithOutput(300);
EmitValue(clicksSource, "str_17");
SleepWithOutput(250);
EmitValue(clicksSource, "str_18");
SleepWithOutput(200);
EmitValue(clicksSource, "str_19");
SleepWithOutput(500);
EmitValue(clicksSource, "str_20");
SleepWithOutput(100);
EmitValue(clicksSource, "str_21");
SleepWithOutput(350);

这将生成以下调试输出:

00057ms| Emit value str_11
00076ms| Sleep for 500ms
00386ms| Subscribe called with: V=str_11, doubleClick=False
00576ms| Emit value str_12
00576ms| Sleep for 150ms
00726ms| Emit value str_13
00726ms| Subscribe called with: V=str_13, doubleClick=True
00726ms| Sleep for 450ms
01177ms| Emit value str_14
01177ms| Sleep for 400ms
01476ms| Subscribe called with: V=str_14, doubleClick=False
01577ms| Emit value str_15
01577ms| Sleep for 350ms
01876ms| Subscribe called with: V=str_15, doubleClick=False
01927ms| Emit value str_16
01927ms| Sleep for 300ms
02227ms| Subscribe called with: V=str_16, doubleClick=False
02227ms| Emit value str_17
02227ms| Sleep for 250ms
02477ms| Emit value str_18
02477ms| Subscribe called with: V=str_18, doubleClick=True
02477ms| Sleep for 200ms
02677ms| Emit value str_19
02677ms| Sleep for 500ms
02976ms| Subscribe called with: V=str_19, doubleClick=False
03177ms| Emit value str_20
03177ms| Sleep for 100ms
03277ms| Emit value str_21
03278ms| Subscribe called with: V=str_21, doubleClick=True
03278ms| Sleep for 350ms

这向您展示了它如何根据发出原始事件/值的时间识别单击和双击。

© www.soinside.com 2019 - 2024. All rights reserved.