动画针过渡

101*_*101 5 c# xamarin xamarin.forms skiasharp

当我从 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是我有点困惑如何实现它。

以下是在画布上绘制针的代码:

    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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

编辑:

仍然很难理解这个过程。

假设我不想应用此过滤器进行控制,而是将其放在 ViewModel 中来过滤值。我有一个从中获取数据的类,例如 GPSTracker。GPSTracker 提供速度值,然后我在 HomeViewModel 中订阅 EventListener 并想要过滤传入值。

基于亚当斯的回答:

Ada*_*dam 3

来自控制背景,为了模仿模拟设备的行为,您可以使用指数(也称为低通)滤波器。

您可以使用两种类型的低通滤波器,具体取决于您想要看到的行为类型:一阶滤波器或二阶滤波器。简而言之,如果您的读数稳定在 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

filtered_value是源的过滤版本source_valuetime_passed是从上次调用此函数到过滤器经过的时间filtered_valuetime_constant是过滤器的时间常数(仅供参考,对阶跃变化做出反应,filtered_value将获得63%的时间source_valuetime_constant时间已过,5x 已过时为 99%)。的单位filtered_value将与 相同source_valuetime_passed和的单位time_constant必须相同,无论是秒、微秒还是 jiffy。此外,time_passed应明显小于time_constant任何时候,否则过滤器行为将变得不确定。有多种方法可以获取time_passed,例如Stopwatch,请参阅如何计算已经过去了多少时间?

在使用过滤器函数之前,您需要初始化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();
Run Code Online (Sandbox Code Playgroud)

要将此函数用于一阶滤波器,您可以使用计时器或类似的东西循环以下内容

double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);
Run Code Online (Sandbox Code Playgroud)

要将此函数用于二阶滤波器,您可以使用计时器或类似的东西循环以下内容

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);
}
Run Code Online (Sandbox Code Playgroud)

二阶滤波器的工作原理是考虑一阶滤波值的一阶导数。另外,我建议制作time_constant < dot_time_constant- 首先,我会设置dot_time_constant = 2 * time_constant

就个人而言,我会在由线程计时器控制的后台线程中调用此过滤器,并有time_passed一个等于计时器周期的常量,但我将把实现细节留给您。

编辑:

下面是创建一阶和二阶滤波器的示例类。为了操作过滤器,我使用了一个线程计时器,设置为每 100 毫秒处理一次。由于该计时器相当一致,使得 time_passed 恒定,因此我通过预先计算Math.Exp(-time_passed / time_constant)而不是将“点”项除以/乘以 time_passed 来优化滤波器方程。

对于一阶滤波器,使用var filter = new ExpFilter(initial_value, time_constant). 对于二阶滤波器,使用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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)