我有一个点击事件流,我想从该流中确定用户是否执行了单击或双击。目前,我正在尝试通过缓冲指定的
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() ));
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
这向您展示了它如何根据发出原始事件/值的时间识别单击和双击。