Анимировать переход иглы

Когда я читаю данные с датчика 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);
        }
    }

Однако я немного смущен, как это реализовать.

Вот код для рисования иглы на холсте:

    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 для фильтрации значения. У меня есть класс, откуда я получаю данные, например GPSTracker. GPSTracker предоставляет значение скорости, затем я подписываюсь на EventListener в своей модели HomeViewModel и хочу фильтровать входящее значение.

На основе ответа Адамса:

Может как идея. Не реагировать на само изменение, а с заданным интервалом, например, каждые 100 мс строить среднее из последних 5 данных и показывать это. Затем стрелка обновляется с фиксированной частотой кадров, что делает ее плавной, а средняя еще больше сглаживает положение.

feal 16.03.2022 10:39

@feal, не могли бы вы описать это подробнее. В основном я пытался постепенно увеличивать положение иглы, но пока безуспешно. Вы имеете в виду, что внутри OnDrawNeedle создается цикл для вычисления среднего и увеличения значения?

10101 16.03.2022 21:11

Согласно вашему описанию, эта проблема была вызвана неравномерностью данных, полученных от датчика GPS. Как изменить данные о местоположении (X, Y)?

Liyun Zhang - MSFT 17.03.2022 07:38

@LiyunZhang-MSFT Я использую пакет Plugin.Geolocator; NuGet для отслеживания геолокации. Я также тестировал данные, поступающие по bluetooth от микроконтроллера, и есть небольшая задержка. Я думаю, что нужен гениальный ум, чтобы заставить этот плавный переход работать. Я думаю, что перепробовал все возможные решения, которые пришли мне в голову. Есть такое решение с циклом и задержкой, оно вроде более плавное, но почему-то все же не такое гладкое. Я предполагаю, что предложение Feal с вычислением среднего - это способ пойти дальше. Просто интересно, насколько велика задержка после получения среднего значения?

10101 17.03.2022 09:15

Вот решение с циклом и задержкой: github.com/rgot-org/Xamarin-Forms-RadialGauge

10101 17.03.2022 09:16

@HiFo Небольшая частота кадров может сделать рисунок более плавным. А время задержки — это сумма всего времени кадра. Таким образом, насколько велико время задержки, не важно, а частота кадров является ключевым моментом.

Liyun Zhang - MSFT 17.03.2022 10:23

И, как вы сказали, ценность, которую вы получаете, неравномерна. Таким образом, вы можете установить фиксированное _value и фиксированное _time, а затем установить время задержки, например var delaytime=(value2-value1)/_value*_timeawait Task.Delay(delaytime)

Liyun Zhang - MSFT 17.03.2022 10:40

@LiyunZhang-MSFT Я только начал думать ... лучше сделать это в самом элементе управления или в ViewModel создать какой-то буфер, он же список, который будет принимать, например, 5 последних значений, а затем передавать их в элемент управления? Я сделал все остальные правки, которые хотел сделать для этого элемента управления, также с вашей помощью из моего предыдущего вопроса с нумерацией, но до сих пор не могу понять, как сделать гладкость 0_o. Исходное репо, кажется, больше не активно, так как я не могу связаться с автором. Вы заинтересованы внести свой вклад, если я создам новый публичный репозиторий с текущим контролем? Может и другим поможет

10101 18.03.2022 11:31

Ваша проблема была решена ответом Адама?

Liyun Zhang - MSFT 21.03.2022 06:33

@ LiyunZhang-MSFT Я тестировал метод, который получил вчера, но думаю, что что-то не так, поскольку он не работает должным образом. Я попробую сегодня снова

10101 21.03.2022 11:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
10
200
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Исходя из фона элементов управления, чтобы имитировать поведение аналогового устройства, вы можете использовать экспоненциальный фильтр (он же фильтр нижних частот).

Существует два типа фильтров нижних частот, которые вы можете использовать, в зависимости от того, какой тип поведения вы хотите увидеть: фильтр первого порядка или фильтр второго порядка. Короче говоря, если ваши показания были стабильными на уровне 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 получит 63 % пути к source_value по прошествии time_constant времени и 99% по прошествии 5 раз). Единицы filtered_value будут такими же, как source_value. Единицы time_passed и time_constant должны быть одинаковыми, будь то секунды, микросекунды или миг. Кроме того, 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();

Чтобы использовать эту функцию для фильтра первого порядка, вы должны зациклить следующее, используя таймер или что-то подобное.

double time_passed = stopwatch.ElapsedMilliseconds;
stopwatch.Restart();
Exp_Filt(ref filtered_value, source_value, time_passed, time_constant);

Чтобы использовать эту функцию для фильтра второго порядка, вы должны зациклить следующее, используя таймер или что-то подобное.

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);
}

Фильтр второго порядка работает, принимая во внимание первую производную отфильтрованного значения первого порядка. Также я бы рекомендовал сделать 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;
            }
        }
    }

Благодарю вас! Я начну тестировать его сегодня или завтра!

10101 18.03.2022 17:50

Я тестировал метод, который получил вчера, но он не работал должным образом. Нужно попробовать сегодня, после прочтения вашего ответа. В основном я пытался сделать так, что я передаю две переменные double source_value, double time_constant в метод, и он возвращает filtered_value

10101 21.03.2022 11:41

Следует отметить одну вещь: если вы используете таймер потоковой передачи, Math.Exp(-time_passed / time_constant) должен быть постоянным, так как вы можете использовать период таймера для time_passed. time_constant должно быть постоянным значением, в идеале в пять раз превышающим период обновления с датчика GPS. Следовательно, если датчик GPS обновляется каждые 200 мс, значение time_constant должно быть не менее 1 секунды. Увеличение time_constant сделает его более сглаженным за счет увеличения задержки между данными GPS и выводом пользователя. В этом случае 2-й порядок используется для уменьшения задержки.

Adam 21.03.2022 14:52

О, я только что заметил, что вы сделали полное рабочее решение. Я проверю это сегодня, но для стольких усилий с вашей стороны у меня нет другого выбора, кроме как дать обещанную поддержку с моей стороны.

10101 22.03.2022 12:17

Кстати, может глупый вопрос, что бы вы сказали. Имеет ли смысл фильтровать значение в ViewModel или просто добавить эту функциональность непосредственно в сам Control?

10101 22.03.2022 16:32

Я видел, как это делается в обоих направлениях. Преимущество ViewModel заключается в том, что вы можете напрямую настраивать константы времени (и частоту обновления таймера) по мере необходимости для ваших данных. Преимущество режима «Управление» заключается в том, что, зная период перерисовки, который вы используете, вы можете привязать перерисовку к частоте обновления фильтра. Если вы поместите его в элемент управления, я бы порекомендовал создать параметры, позволяющие пользователю настраивать поведение фильтра (включение/отключение, первый/второй порядок и т. д.), чтобы элемент управления был удобным для пользователя, позволяя вам использовать его в другом месте в будущее.

Adam 22.03.2022 16:44

Ладно, я понял. Думал так же, если это делается в самом элементе управления, добавьте пару BindableProperties, чтобы иметь возможность устанавливать настройки в XAML. Спасибо за помощь! Похоже, сообщество в разделе Xamarin не такое уж большое. Для меня похоже, что React Native и Flutter вступают во владение на основе помеченных сообщений на SO. Однако для парня с некоторым опытом работы с C# Xamarin — самый простой выбор. Надеюсь, что в будущем Xamarin завоюет свои позиции. Еще раз спасибо за вашу поддержку!

10101 22.03.2022 17:16

Другие вопросы по теме