动画针过渡

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_valuetime_passed 是自上次调用此函数过滤后经过的时间 filtered_value,以及time_constant 是滤波器的时间常数(仅供参考,对阶跃变化作出反应,filtered_value 将在 time_constant 时间过去后获得 63% 的朝向 source_value 和 99 % 当 5 倍已经过去时)。 filtered_value 的单位将与 source_value 相同。 time_passedtime_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;
            }
        }
    }