动画针过渡
Animate needle transition
当我从 GPS 传感器读取数据时,它有轻微的延迟。您没有得到像 0,1 0,2 0,3 0,4 0,5 等值,但它们像 1 一样出现,然后突然变成 5 或 9 或 12。在这种情况下,指针会来回跳动。有人知道如何使针头平稳移动吗?我想需要某种延迟?
类似的东西,取自另一个控件:
async void animateProgress(int progress)
{
sweepAngle = 1;
// Looping at data interval of 5
for (int i = 0; i < progress; i=i+5)
{
sweepAngle = i;
await Task.Delay(3);
}
}
但是我有点困惑如何实现它。
这是在 canvas 上画针的代码:
private void OnDrawNeedle()
{
using (var needlePath = new SKPath())
{
//first set up needle pointing towards 0 degrees (or 6 o'clock)
var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
var needleOffset = ScaleToSize(NeedleOffset);
var needleStart = _center.Y - needleOffset;
var needleLength = ScaleToSize(NeedleLength);
needlePath.MoveTo(_center.X - widthOffset, needleStart);
needlePath.LineTo(_center.X + widthOffset, needleStart);
needlePath.LineTo(_center.X, needleStart + needleLength);
needlePath.LineTo(_center.X - widthOffset, needleStart);
needlePath.Close();
//then calculate needle position in degrees
var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
//finally rotate needle to actual value
needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
using (var needlePaint = new SKPaint())
{
needlePaint.IsAntialias = true;
needlePaint.Color = NeedleColor.ToSKColor();
needlePaint.Style = SKPaintStyle.Fill;
_canvas.DrawPath(needlePath, needlePaint);
}
}
}
编辑:
仍然很难理解这个过程。
假设我不想将此筛选器应用于控件,而是将其放在 ViewModel 中以筛选值。我有一个 Class 从我获取数据的地方,例如 GPSTracker。 GPSTracker 提供速度值,然后我在我的 HomeViewModel 中订阅 EventListener 并想过滤传入的值。
基于亚当斯的回答:
来自控件背景,为了模仿模拟设备的行为,您可以使用指数(又名 low-pass)过滤器。
您可以使用两种类型的 low-pass 过滤器,具体取决于您希望看到的行为类型:first-order 或 second-order 过滤器。简而言之,如果您的读数稳定在 0,然后突然变为 10 并稳定在 10(阶跃变化),则第一个订单会慢慢变为 10,永远不会超过它,然后保持在 10,而第二个订单订单将加速其向 10 的进展,通过它,然后向 10 振荡。
指数滤波器的函数很简单:
public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
{
if (time_passed > 0.0)
{
if (time_constant > 0.0)
{
source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
}
filtered_value = source_value;
}
}
filtered_value
是源的过滤版本 source_value
,time_passed
是自上次调用此函数过滤后经过的时间 filtered_value
,以及time_constant
是滤波器的时间常数(仅供参考,对阶跃变化作出反应,filtered_value
将在 time_constant
时间过去后获得 63% 的朝向 source_value
和 99 % 当 5 倍已经过去时)。 filtered_value
的单位将与 source_value
相同。 time_passed
和 time_constant
的单位必须相同,无论是秒、微秒还是 jiffy。此外,time_passed
应始终明显小于 time_constant
,否则过滤器行为将变为 non-deterministic。 time_passed
有多种获取方式,比如Stopwatch
,见How can I calculate how much time have been passed?
在使用过滤器功能之前,您需要初始化 filtered_value
以及用于获取 time_passed
的任何内容。对于这个例子,我将使用 stopwatch
.
var stopwatch = new System.Diagnostics.Stopwatch();
double filtered_value, filtered_dot_value;
...
filtered_value = source_value;
filtered_dot_value = 0.0;
stopwatch.Start();
要将此函数用于 first-order 过滤器,您可以使用计时器或类似的东西循环以下内容
double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
要将此函数用于 second-order 过滤器,您可以使用计时器或类似的东西循环以下内容
double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
if (time_passed > 0.0)
{
double last_value = filtered_value;
filtered_value += filtered_dot_value * time_passed;
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
Exp_Filt(ref filtered_dot_value, (filtered_value - last_value) / time_passed, time_passed, dot_time_constant);
}
second-order 过滤器通过考虑 first-order 过滤值的一阶导数来工作。另外,我建议制作 time_constant < dot_time_constant
- 首先,我会设置 dot_time_constant = 2 * time_constant
就我个人而言,我会在由 threading timer 控制的后台线程中调用此过滤器,并让 time_passed
一个等于计时器周期的常量,但我会将具体实现留给您。
编辑:
下面是创建一阶和二阶滤波器的示例 class。为了操作过滤器,我使用线程计时器设置为每 100 毫秒处理一次。由于这个计时器相当一致,使 time_passed 不变,我通过 pre-calculating Math.Exp(-time_passed / time_constant)
优化了滤波器方程,而不是通过 [=84= dividing/multiplying 'dot' 项优化了滤波器方程].
对于 first-order 过滤器,使用 var filter = new ExpFilter(initial_value, time_constant)
。对于 second-order 过滤器,使用 var filter = new ExpFilter(initial_value, time_constant, dot_time_constant)
。然后,要读取最新的过滤值,请调用 double value = filter.Value
。要设置要过滤的值,请调用 filter.Value = value
.
public class ExpFilter : IDisposable
{
private double _input, _output, _dot;
private readonly double _tc, _tc_dot;
private System.Threading.Timer _timer;
/// <summary>
/// Initializes first-order filter
/// </summary>
/// <param name="value">initial value of filter</param>
/// <param name="time_constant">time constant of filter, in seconds</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> must be positive</exception>
public ExpFilter(double value, double time_constant)
{
// time constant must be positive
if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
// initialize filter
_output = _input = value;
_dot = 0.0;
// calculate gain from time constant
_tc = CalcTC(time_constant);
// disable second-order
_tc_dot = -1.0;
// start filter timer
StartTimer();
}
/// <summary>
/// Initializes second-order filter
/// </summary>
/// <param name="value">initial value of filter</param>
/// <param name="time_constant">time constant of primary filter, in seconds</param>
/// <param name="dot_time_constant">time constant of secondary filter, in seconds</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> and <paramref name="dot_time_constant"/> must be positive</exception>
public ExpFilter(double value, double time_constant, double dot_time_constant)
{
// time constant must be positive
if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
if (dot_time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(dot_time_constant));
// initialize filter
_output = _input = value;
_dot = 0.0;
// calculate gains from time constants
_tc = CalcTC(time_constant);
_tc_dot = CalcTC(dot_time_constant);
// start filter timer
StartTimer();
}
// the following two functions must share the same time period
private double CalcTC(double time_constant)
{
// time period = 0.1 s (100 ms)
return Math.Exp(-0.1 / time_constant);
}
private void StartTimer()
{
// time period = 100 ms
_timer = new System.Threading.Timer(Filter_Timer, this, 100, 100);
}
~ExpFilter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_timer.Dispose();
}
}
/// <summary>
/// Get/Set filter value
/// </summary>
public double Value
{
get => _output;
set => _input = value;
}
private static void Filter_Timer(object stateInfo)
{
var _filter = (ExpFilter)stateInfo;
// get values
double _input = _filter._input;
double _output = _filter._output;
double _dot = _filter._dot;
// if second-order, adjust _output (no change if first-order as _dot = 0)
// then use filter function to calculate new filter value
_input += (_output + _dot - _input) * _filter._tc;
_filter._output = _input;
if (_filter._tc_dot >= 0.0)
{
// calculate second-order portion of filter
_output = _input - _output;
_output += (_dot - _output) * _filter._tc_dot;
_filter._dot = _output;
}
}
}
当我从 GPS 传感器读取数据时,它有轻微的延迟。您没有得到像 0,1 0,2 0,3 0,4 0,5 等值,但它们像 1 一样出现,然后突然变成 5 或 9 或 12。在这种情况下,指针会来回跳动。有人知道如何使针头平稳移动吗?我想需要某种延迟?
类似的东西,取自另一个控件:
async void animateProgress(int progress)
{
sweepAngle = 1;
// Looping at data interval of 5
for (int i = 0; i < progress; i=i+5)
{
sweepAngle = i;
await Task.Delay(3);
}
}
但是我有点困惑如何实现它。
这是在 canvas 上画针的代码:
private void OnDrawNeedle()
{
using (var needlePath = new SKPath())
{
//first set up needle pointing towards 0 degrees (or 6 o'clock)
var widthOffset = ScaleToSize(NeedleWidth / 2.0f);
var needleOffset = ScaleToSize(NeedleOffset);
var needleStart = _center.Y - needleOffset;
var needleLength = ScaleToSize(NeedleLength);
needlePath.MoveTo(_center.X - widthOffset, needleStart);
needlePath.LineTo(_center.X + widthOffset, needleStart);
needlePath.LineTo(_center.X, needleStart + needleLength);
needlePath.LineTo(_center.X - widthOffset, needleStart);
needlePath.Close();
//then calculate needle position in degrees
var needlePosition = StartAngle + ((Value - RangeStart) / (RangeEnd - RangeStart) * SweepAngle);
//finally rotate needle to actual value
needlePath.Transform(SKMatrix.CreateRotationDegrees(needlePosition, _center.X, _center.Y));
using (var needlePaint = new SKPaint())
{
needlePaint.IsAntialias = true;
needlePaint.Color = NeedleColor.ToSKColor();
needlePaint.Style = SKPaintStyle.Fill;
_canvas.DrawPath(needlePath, needlePaint);
}
}
}
编辑:
仍然很难理解这个过程。
假设我不想将此筛选器应用于控件,而是将其放在 ViewModel 中以筛选值。我有一个 Class 从我获取数据的地方,例如 GPSTracker。 GPSTracker 提供速度值,然后我在我的 HomeViewModel 中订阅 EventListener 并想过滤传入的值。
基于亚当斯的回答:
来自控件背景,为了模仿模拟设备的行为,您可以使用指数(又名 low-pass)过滤器。
您可以使用两种类型的 low-pass 过滤器,具体取决于您希望看到的行为类型:first-order 或 second-order 过滤器。简而言之,如果您的读数稳定在 0,然后突然变为 10 并稳定在 10(阶跃变化),则第一个订单会慢慢变为 10,永远不会超过它,然后保持在 10,而第二个订单订单将加速其向 10 的进展,通过它,然后向 10 振荡。
指数滤波器的函数很简单:
public void Exp_Filt(ref double filtered_value, double source_value, double time_passed, double time_constant)
{
if (time_passed > 0.0)
{
if (time_constant > 0.0)
{
source_value += (filtered_value - source_value) * Math.Exp(-time_passed / time_constant);
}
filtered_value = source_value;
}
}
filtered_value
是源的过滤版本 source_value
,time_passed
是自上次调用此函数过滤后经过的时间 filtered_value
,以及time_constant
是滤波器的时间常数(仅供参考,对阶跃变化作出反应,filtered_value
将在 time_constant
时间过去后获得 63% 的朝向 source_value
和 99 % 当 5 倍已经过去时)。 filtered_value
的单位将与 source_value
相同。 time_passed
和 time_constant
的单位必须相同,无论是秒、微秒还是 jiffy。此外,time_passed
应始终明显小于 time_constant
,否则过滤器行为将变为 non-deterministic。 time_passed
有多种获取方式,比如Stopwatch
,见How can I calculate how much time have been passed?
在使用过滤器功能之前,您需要初始化 filtered_value
以及用于获取 time_passed
的任何内容。对于这个例子,我将使用 stopwatch
.
var stopwatch = new System.Diagnostics.Stopwatch();
double filtered_value, filtered_dot_value;
...
filtered_value = source_value;
filtered_dot_value = 0.0;
stopwatch.Start();
要将此函数用于 first-order 过滤器,您可以使用计时器或类似的东西循环以下内容
double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
要将此函数用于 second-order 过滤器,您可以使用计时器或类似的东西循环以下内容
double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
if (time_passed > 0.0)
{
double last_value = filtered_value;
filtered_value += filtered_dot_value * time_passed;
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
Exp_Filt(ref filtered_dot_value, (filtered_value - last_value) / time_passed, time_passed, dot_time_constant);
}
second-order 过滤器通过考虑 first-order 过滤值的一阶导数来工作。另外,我建议制作 time_constant < dot_time_constant
- 首先,我会设置 dot_time_constant = 2 * time_constant
就我个人而言,我会在由 threading timer 控制的后台线程中调用此过滤器,并让 time_passed
一个等于计时器周期的常量,但我会将具体实现留给您。
编辑:
下面是创建一阶和二阶滤波器的示例 class。为了操作过滤器,我使用线程计时器设置为每 100 毫秒处理一次。由于这个计时器相当一致,使 time_passed 不变,我通过 pre-calculating Math.Exp(-time_passed / time_constant)
优化了滤波器方程,而不是通过 [=84= dividing/multiplying 'dot' 项优化了滤波器方程].
对于 first-order 过滤器,使用 var filter = new ExpFilter(initial_value, time_constant)
。对于 second-order 过滤器,使用 var filter = new ExpFilter(initial_value, time_constant, dot_time_constant)
。然后,要读取最新的过滤值,请调用 double value = filter.Value
。要设置要过滤的值,请调用 filter.Value = value
.
public class ExpFilter : IDisposable
{
private double _input, _output, _dot;
private readonly double _tc, _tc_dot;
private System.Threading.Timer _timer;
/// <summary>
/// Initializes first-order filter
/// </summary>
/// <param name="value">initial value of filter</param>
/// <param name="time_constant">time constant of filter, in seconds</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> must be positive</exception>
public ExpFilter(double value, double time_constant)
{
// time constant must be positive
if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
// initialize filter
_output = _input = value;
_dot = 0.0;
// calculate gain from time constant
_tc = CalcTC(time_constant);
// disable second-order
_tc_dot = -1.0;
// start filter timer
StartTimer();
}
/// <summary>
/// Initializes second-order filter
/// </summary>
/// <param name="value">initial value of filter</param>
/// <param name="time_constant">time constant of primary filter, in seconds</param>
/// <param name="dot_time_constant">time constant of secondary filter, in seconds</param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="time_constant"/> and <paramref name="dot_time_constant"/> must be positive</exception>
public ExpFilter(double value, double time_constant, double dot_time_constant)
{
// time constant must be positive
if (time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(time_constant));
if (dot_time_constant <= 0.0) throw new ArgumentOutOfRangeException(nameof(dot_time_constant));
// initialize filter
_output = _input = value;
_dot = 0.0;
// calculate gains from time constants
_tc = CalcTC(time_constant);
_tc_dot = CalcTC(dot_time_constant);
// start filter timer
StartTimer();
}
// the following two functions must share the same time period
private double CalcTC(double time_constant)
{
// time period = 0.1 s (100 ms)
return Math.Exp(-0.1 / time_constant);
}
private void StartTimer()
{
// time period = 100 ms
_timer = new System.Threading.Timer(Filter_Timer, this, 100, 100);
}
~ExpFilter()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
_timer.Dispose();
}
}
/// <summary>
/// Get/Set filter value
/// </summary>
public double Value
{
get => _output;
set => _input = value;
}
private static void Filter_Timer(object stateInfo)
{
var _filter = (ExpFilter)stateInfo;
// get values
double _input = _filter._input;
double _output = _filter._output;
double _dot = _filter._dot;
// if second-order, adjust _output (no change if first-order as _dot = 0)
// then use filter function to calculate new filter value
_input += (_output + _dot - _input) * _filter._tc;
_filter._output = _input;
if (_filter._tc_dot >= 0.0)
{
// calculate second-order portion of filter
_output = _input - _output;
_output += (_dot - _output) * _filter._tc_dot;
_filter._dot = _output;
}
}
}