Когда я читаю данные с датчика 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 и хочу фильтровать входящее значение.
На основе ответа Адамса:
@feal, не могли бы вы описать это подробнее. В основном я пытался постепенно увеличивать положение иглы, но пока безуспешно. Вы имеете в виду, что внутри OnDrawNeedle создается цикл для вычисления среднего и увеличения значения?
Согласно вашему описанию, эта проблема была вызвана неравномерностью данных, полученных от датчика GPS. Как изменить данные о местоположении (X, Y)?
@LiyunZhang-MSFT Я использую пакет Plugin.Geolocator;
NuGet для отслеживания геолокации. Я также тестировал данные, поступающие по bluetooth от микроконтроллера, и есть небольшая задержка. Я думаю, что нужен гениальный ум, чтобы заставить этот плавный переход работать. Я думаю, что перепробовал все возможные решения, которые пришли мне в голову. Есть такое решение с циклом и задержкой, оно вроде более плавное, но почему-то все же не такое гладкое. Я предполагаю, что предложение Feal с вычислением среднего - это способ пойти дальше. Просто интересно, насколько велика задержка после получения среднего значения?
Вот решение с циклом и задержкой: github.com/rgot-org/Xamarin-Forms-RadialGauge
@HiFo Небольшая частота кадров может сделать рисунок более плавным. А время задержки — это сумма всего времени кадра. Таким образом, насколько велико время задержки, не важно, а частота кадров является ключевым моментом.
И, как вы сказали, ценность, которую вы получаете, неравномерна. Таким образом, вы можете установить фиксированное _value
и фиксированное _time
, а затем установить время задержки, например var delaytime=(value2-value1)/_value*_time
await Task.Delay(delaytime)
@LiyunZhang-MSFT Я только начал думать ... лучше сделать это в самом элементе управления или в ViewModel создать какой-то буфер, он же список, который будет принимать, например, 5 последних значений, а затем передавать их в элемент управления? Я сделал все остальные правки, которые хотел сделать для этого элемента управления, также с вашей помощью из моего предыдущего вопроса с нумерацией, но до сих пор не могу понять, как сделать гладкость 0_o. Исходное репо, кажется, больше не активно, так как я не могу связаться с автором. Вы заинтересованы внести свой вклад, если я создам новый публичный репозиторий с текущим контролем? Может и другим поможет
Ваша проблема была решена ответом Адама?
@ LiyunZhang-MSFT Я тестировал метод, который получил вчера, но думаю, что что-то не так, поскольку он не работает должным образом. Я попробую сегодня снова
Исходя из фона элементов управления, чтобы имитировать поведение аналогового устройства, вы можете использовать экспоненциальный фильтр (он же фильтр нижних частот).
Существует два типа фильтров нижних частот, которые вы можете использовать, в зависимости от того, какой тип поведения вы хотите увидеть: фильтр первого порядка или фильтр второго порядка. Короче говоря, если ваши показания были стабильными на уровне 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;
}
}
}
Благодарю вас! Я начну тестировать его сегодня или завтра!
Я тестировал метод, который получил вчера, но он не работал должным образом. Нужно попробовать сегодня, после прочтения вашего ответа. В основном я пытался сделать так, что я передаю две переменные double source_value, double time_constant
в метод, и он возвращает filtered_value
Следует отметить одну вещь: если вы используете таймер потоковой передачи, Math.Exp(-time_passed / time_constant)
должен быть постоянным, так как вы можете использовать период таймера для time_passed
. time_constant
должно быть постоянным значением, в идеале в пять раз превышающим период обновления с датчика GPS. Следовательно, если датчик GPS обновляется каждые 200 мс, значение time_constant должно быть не менее 1 секунды. Увеличение time_constant
сделает его более сглаженным за счет увеличения задержки между данными GPS и выводом пользователя. В этом случае 2-й порядок используется для уменьшения задержки.
О, я только что заметил, что вы сделали полное рабочее решение. Я проверю это сегодня, но для стольких усилий с вашей стороны у меня нет другого выбора, кроме как дать обещанную поддержку с моей стороны.
Кстати, может глупый вопрос, что бы вы сказали. Имеет ли смысл фильтровать значение в ViewModel или просто добавить эту функциональность непосредственно в сам Control?
Я видел, как это делается в обоих направлениях. Преимущество ViewModel заключается в том, что вы можете напрямую настраивать константы времени (и частоту обновления таймера) по мере необходимости для ваших данных. Преимущество режима «Управление» заключается в том, что, зная период перерисовки, который вы используете, вы можете привязать перерисовку к частоте обновления фильтра. Если вы поместите его в элемент управления, я бы порекомендовал создать параметры, позволяющие пользователю настраивать поведение фильтра (включение/отключение, первый/второй порядок и т. д.), чтобы элемент управления был удобным для пользователя, позволяя вам использовать его в другом месте в будущее.
Ладно, я понял. Думал так же, если это делается в самом элементе управления, добавьте пару BindableProperties, чтобы иметь возможность устанавливать настройки в XAML. Спасибо за помощь! Похоже, сообщество в разделе Xamarin не такое уж большое. Для меня похоже, что React Native и Flutter вступают во владение на основе помеченных сообщений на SO. Однако для парня с некоторым опытом работы с C# Xamarin — самый простой выбор. Надеюсь, что в будущем Xamarin завоюет свои позиции. Еще раз спасибо за вашу поддержку!
Может как идея. Не реагировать на само изменение, а с заданным интервалом, например, каждые 100 мс строить среднее из последних 5 данных и показывать это. Затем стрелка обновляется с фиксированной частотой кадров, что делает ее плавной, а средняя еще больше сглаживает положение.