История одного прогресс-бара

в 7:23, , рубрики: 2GIS, C#, switchtowindowsphone, windows phone, WP, XAML, Блог компании 2ГИС, интерфейсы, разработка, разработка мобильных приложений, разработка под windows phone

История одного прогресс-бара - 1

Наверное, любому программисту, который разрабатывает пользовательский интерфейс на C#/XAML, приходилось писать нестандартные элементы управления. В нашей веселой команде 2GIS для Windows Phone мы довольно часто делаем это, и такие задачи стали почти рутиной. Но об одном случае мне хочется рассказать подробнее. Все началось с того, что однажды нам понадобилось написать весьма своеобразный прогресс-бар.

Проблема

Как-то раз к нам на внутрикомандную рассылку пришло письмо от нашего коллеги Woodroof. Данил достаточно известен внутри компании: он является талантливым разработчиком, носит странную прическу и прокачал придирчивость внимание к деталям до максимума. Содержание его письма вкратце можно передать примерно так.

  1. У Данила есть жена, ее зовут Саша.
  2. Саша пользуется смартфоном с Windows Phone, и у нее, конечно же, установлен 2GIS.
  3. В 2GIS’е есть стартовый экран загрузки города, на котором есть прогресс-бар.
  4. Саша заметила, что анимация этого прогресс-бара выглядит не круто.

Собственно, анимация тогда была вот такой:

История одного прогресс-бара - 2

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

<Grid>
    <Path Data="..."
          Stroke="Gray" />
    <Path Data="..."
          Stroke="Black">
        <Path.Clip>
            <RectangleGeometry Rect="0,0,100,50" />
        </Path.Clip>
    </Path>
</Grid>

Я намеренно не привожу значения свойств Data у элемента Path, чтобы не загромождать код. Но суть в том, что есть два абсолютно одинаковых векторных пути, расположенных один под другим. При этом часть верхней картинки отсекается маской (элемент Path.Clip). Программно изменяя Rect у маски, мы тем самым регулируем «прогресс» загрузки. В дизайнере VS для этой разметки вы увидите примерно такую картинку.

История одного прогресс-бара - 3

Но анимация сдвига маски для таких сложных кривых, как вы могли заметить, выглядит не очень уместно. Данил писал нам: «Моя жена смотрела на этот лоадер, ждала, когда он доберётся до дерева, и была очень разочарована текущим поведением».

Мы всей командой тогда согласились, что гораздо лучше сделать вот так.

История одного прогресс-бара - 4

Но как же это сделать?

Решение

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

Для начала добавим к нашему векторному пути штриховку.

История одного прогресс-бара - 5
<Grid>
    <Path Data="..."
            Stroke="Gray"
            StrokeThickness="1"
            StrokeDashCap="Flat"
            StrokeStartLineCap="Flat"
            StrokeEndLineCap="Flat">
    </Path>
    <Path Data="..."
            StrokeThickness="2"
            StrokeDashCap="Flat"
            StrokeStartLineCap="Flat"
            StrokeEndLineCap="Flat"
            Stroke="Black"
            StrokeDashArray="9 3">
    </Path>
</Grid>

За штриховку отвечает свойство StrokeDashArray. Оно принимает значения типа DoubleCollection, и в нашем случае первое число — это длина штриха, а второе — длина промежутка между штрихами.

Если очень сильно увеличить длину промежутка между штрихами, например, так StrokeDashArray="9 99999″, то получим вот такую картинку.

История одного прогресс-бара - 6

Теперь становится понятно, что для достижения нужного эффекта нам необходимо изменять длину штриха. Например, для StrokeDashArray="128 99999″ получим вот такую картинку.

История одного прогресс-бара - 7

Давайте для ясности я дам нашему нестандартному прогресс-бару имя — ProgressPathControl и определю его основную характеристику — свойство Progress.

public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
    "Progress", typeof(double), typeof(ProgressPathControl),
    new PropertyMetadata(0.0, OnProgressValueChanged));

public double Progress
{
    get { return (double)GetValue(ProgressProperty); }
    set { SetValue(ProgressProperty, Math.Min(1.0, Math.Max(0.0, value))); }
}

private static void OnProgressValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (e.NewValue != e.OldValue)
    {
        ((ProgressPathControl)d).OnProgressValueChanged((double)e.OldValue, (double)e.NewValue);
    }
}

Пусть Progress меняется от 0 до 1. Очевидно, что для Progress = 0 длина штриха тоже будет равна нулю, а для Progress = 1 длина штриха будет равна длине кривой. Этому числу должен равняться и промежуток между штрихами.

Длину кривой можно элементарно посчитать, приглядевшись к элементу Path. У него есть свойство Data типа Geometry, а в нашем случае это PathGeometry, у которого есть коллекция Figures. Каждый её элемент имеет коллекцию Segments, содержащую элементы различных типов, в том числе BezierSegment и LineSegment. Нужно пробежаться по всем этим штукам и просуммировать длины сегментов. Я ленивый, поэтому ограничусь только BezierSegment и LineSegment, так как в моем случае их достаточно, а длину кривой Безье посчитаю уж совсем простым способом.

Например, так

private double GetGeometryLength(Geometry geometry)
{
    double result = 0;
    var pathGeometry = geometry as PathGeometry;
    if (pathGeometry != null)
    {
        foreach (var figure in pathGeometry.Figures)
        {
            var currentPoint = figure.StartPoint;
            foreach (var segment in figure.Segments)
            {
                var bezier = segment as BezierSegment;
                var line = segment as LineSegment;

                if (bezier != null)
                {
                    result += GetBezierLength(currentPoint, bezier.Point1, bezier.Point2, bezier.Point3);
                    currentPoint = bezier.Point3;
                }
                else if (line != null)
                {
                    result += GetLineLength(currentPoint, line.Point);
                    currentPoint = line.Point;
                }
            }
        }
    }

    return result;
}

private double GetBezierLength(Point p0, Point p1, Point p2, Point p3)
{
    double result = 0;
    Point lastPoint = p0;

    for (double t = 0.001; t <= 1; t += 0.001)
    {
        Point currentPoint;

        // Формула кубической кривой Безье
        // https://ru.wikipedia.org/wiki/Кривая_Безье
        currentPoint.X = Math.Pow(1 - t, 3) * p0.X +
            3 * t * Math.Pow(1 - t, 2) * p1.X +
            3 * t * t * (1 - t) * p2.X +
            Math.Pow(t, 3) * p3.X;

        currentPoint.Y = Math.Pow(1 - t, 3) * p0.Y +
            3 * t * Math.Pow(1 - t, 2) * p1.Y +
            3 * t * t * (1 - t) * p2.Y +
            Math.Pow(t, 3) * p3.Y;

        double dx = currentPoint.X - lastPoint.X;
        double dy = currentPoint.Y - lastPoint.Y;
        result += Math.Sqrt(dx * dx + dy * dy);
        lastPoint = currentPoint;
    }

    return result;
}

private double GetLineLength(Point p0, Point p1)
{
    double dx = p0.X - p1.X;
    double dy = p0.Y - p1.Y;
    return Math.Sqrt(dx * dx + dy * dy);
}

Теперь, когда мы знаем длину кривой, мы можем определить значение свойства StrokeDashArray для любых значений Progress.

double strokeDashLength = progressPathLength / progressPath.StrokeThickness * Progress;
double strokeDashOffset = progressPathLength / progressPath.StrokeThickness;
progressPath.StrokeDashArray = new DoubleCollection { strokeDashLength, strokeDashOffset };

Внимательный читатель заметит, что длина штриха, а также длина промежутка между штрихами задаются не в условных единицах, как это принято в XAML, а в ширинах штриха.

Теперь у нас есть всё необходимое для написания нашего нестандартного прогресс-бара. Для полноты картины весь код ProgressPathControl, изрядно упрощенный, но неплохо иллюстрирующий основные принципы, я привожу ниже.

ProgressPathControl.cs

public class ProgressPathControl : Control
{
    #region ProgressProperty

    public static readonly DependencyProperty ProgressProperty = DependencyProperty.Register(
        "Progress", typeof(double), typeof(ProgressPathControl),
        new PropertyMetadata(0.0, OnProgressValueChanged));

    public double Progress
    {
        get { return (double)GetValue(ProgressProperty); }
        set { SetValue(ProgressProperty, Math.Min(1.0, Math.Max(0.0, value))); }
    }

    private static void OnProgressValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != e.OldValue)
        {
            ((ProgressPathControl)d).OnProgressValueChanged();
        }
    }

    #endregion

    public ProgressPathControl()
    {
        DefaultStyleKey = typeof(ProgressPathControl);
    }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        progressPath = (Path)GetTemplateChild("ProgressPath");
        progressPathLength = GetGeometryLength(progressPath.Data);
        OnProgressValueChanged();
    }

    private double GetGeometryLength(Geometry geometry)
    {
        double result = 0;
        var pathGeometry = geometry as PathGeometry;
        if (pathGeometry != null)
        {
            foreach (var figure in pathGeometry.Figures)
            {
                var currentPoint = figure.StartPoint;
                foreach (var segment in figure.Segments)
                {
                    var bezier = segment as BezierSegment;
                    var line = segment as LineSegment;

                    if (bezier != null)
                    {
                        result += GetBezierLength(currentPoint, bezier.Point1, bezier.Point2, bezier.Point3);
                        currentPoint = bezier.Point3;
                    }
                    else if (line != null)
                    {
                        result += GetLineLength(currentPoint, line.Point);
                        currentPoint = line.Point;
                    }
                }
            }
        }

        return result;
    }

    private double GetBezierLength(Point p0, Point p1, Point p2, Point p3)
    {
        double result = 0;
        Point lastPoint = p0;

        for (double t = 0.001; t <= 1; t += 0.001)
        {
            Point currentPoint;

            // Формула кубической кривой Безье
            // https://ru.wikipedia.org/wiki/Кривая_Безье
            currentPoint.X = Math.Pow(1 - t, 3) * p0.X +
                3 * t * Math.Pow(1 - t, 2) * p1.X +
                3 * t * t * (1 - t) * p2.X +
                Math.Pow(t, 3) * p3.X;

            currentPoint.Y = Math.Pow(1 - t, 3) * p0.Y +
                3 * t * Math.Pow(1 - t, 2) * p1.Y +
                3 * t * t * (1 - t) * p2.Y +
                Math.Pow(t, 3) * p3.Y;

            double dx = currentPoint.X - lastPoint.X;
            double dy = currentPoint.Y - lastPoint.Y;
            result += Math.Sqrt(dx * dx + dy * dy);
            lastPoint = currentPoint;
        }

        return result;
    }

    private double GetLineLength(Point p0, Point p1)
    {
        double dx = p0.X - p1.X;
        double dy = p0.Y - p1.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    private void OnProgressValueChanged()
    {
        if (progressPath != null)
        {
            double strokeDashLength = progressPathLength / progressPath.StrokeThickness * Progress;
            double strokeDashOffset = progressPathLength / progressPath.StrokeThickness;
            progressPath.StrokeDashArray = new DoubleCollection { strokeDashLength, strokeDashOffset };
        }
    }

    private double progressPathLength;
    private Path progressPath;
}

Generic.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:controls="using:DoubleGis.Controls">
    
    <Style TargetType="controls:ProgressPathControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:ProgressPathControl">
                    <Grid>
                        <Canvas Height="50"
                                Width="323">
                            <Path Data="M10.7,45.6C10,47.5,8.1,48.9,6,48.9c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5l0,0h30.3c0.1,0,0.2-0.1,0.2-0.2v-4c0-1.4,0.2-2.3,0.7-2.8c0.3-0.3,0.7-0.5,1.3-0.5H49c0.1,0,0.2-0.1,0.2-0.2v-3.6c0-1.4,0.2-2.3,0.7-2.8c0.3-0.3,0.8-0.5,1.3-0.5h3.5c0.1,0,0.2-0.1,0.2-0.2V14.7c0-2.1,0.4-3.1,1.4-3.3c0.1,0,0.3,0,0.5,0l0.2,0c3.4-0.5,3.6-4,3.6-9.6V0.6H63l0,1.1c0,5.4,0.2,9,3.8,9.6c0,0,0.1,0,0.1,0c0.1,0,0.2,0,0.3,0l0.1,0c1.5,0.4,1.5,2.7,1.5,3.4V29c0,0.1,0.1,0.2,0.3,0.2h3.2c0.6,0,1.1,0.2,1.5,0.6c0.5,0.6,0.8,1.5,0.8,2.7l0,10.6l0,0.4c0,0.1,0.1,0.2,0.2,0.2h16.6c2.4,0,2.8-2.1,3-4.3l0-3.6c0-0.1,0-0.1-0.1-0.2c0,0-0.1-0.1-0.2-0.1l-0.8,0c-3.6,0-6.1-2.3-6.1-5.6c0-1.8,0.9-3.7,2.3-4.7c0.1-0.1,0.1-0.1,0.1-0.2c-0.1-0.5-0.1-0.9-0.1-1.4c0-1.8,0.9-3.5,2.5-4.6c0.1,0,0.1-0.1,0.1-0.2c0-0.1,0-0.3,0-0.4c0-2,1.7-3.5,3.9-3.5c2.3,0,4.2,1.6,4.2,3.5c0,0.2,0,0.3,0,0.4c0,0.1,0,0.2,0.1,0.2c1.6,1.1,2.5,2.8,2.5,4.6c0,0.5,0,0.9-0.1,1.4c0,0.1,0,0.2,0.1,0.2c1.5,1.1,2.4,2.8,2.4,4.5c0,3.4-2.6,5.9-6.1,5.9l-1.2,0c-0.1,0-0.2,0.1-0.2,0.2l0,3.6c0.1,2.2,0.6,4.3,2.9,4.3h9.3c0.1,0,0.2-0.1,0.2-0.2v-2.1c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c1.9,0,1.9,2.3,1.9,3l0,2.2c0,0.1,0,0.1,0.1,0.2c0,0,0.1,0.1,0.2,0.1h2.5c0.1,0,0.1,0,0.2-0.1c0,0,0.1-0.1,0.1-0.2l0-2.2c0-0.7,0-3,1.9-3h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h2.5c0.1,0,0.2-0.2,0.2-0.3l0-2c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7l0,0.4c0,0.1,0.1,0.2,0.2,0.2H148c0.1,0,0.2-0.1,0.2-0.3l0-4.6c0-1.5,0.2-2.5,0.7-3.1c0.3-0.3,0.8-0.5,1.3-0.5h2.2c0.1,0,0.2-0.1,0.2-0.2v-4.6c0-2.4,1.3-3.4,2.5-3.6c3.9-0.6,4.1-4.9,4-11.3l0-0.3l-0.5-2.4c0-0.1,0-0.1-0.1-0.1l-2-1.8l2.8-0.4c0.1,0,0.2-0.1,0.2-0.1l1.1-2.7l1.1,2.7c0,0.1,0.1,0.1,0.2,0.2l2.8,0.3l-2.2,1.9c0,0-0.1,0.1-0.1,0.1l-0.5,2.4c0,0,0,0,0,0.1l0,0.7c0,6.2,0.2,10.2,4.2,10.8c0.6,0.1,2.4,0.5,2.4,3.6v4.6c0,0.1,0.1,0.2,0.2,0.2h2.2c0.4,0,0.7,0.1,1,0.4c0.9,0.9,1,3.1,1,4.3v0.2c0,0.1,0.1,0.2,0.2,0.2h16.2c0.1,0,0.2-0.1,0.2-0.2c0-0.1,0-0.2-0.1-0.3c-1.1-0.8-2.5-1.9-3.6-3c-2.3-2.4-2.8-4.3-2.8-5.5c0-2.8,0.7-6,6-6c1.6,0,2.8,0.7,3.5,2c0.1,0.2,0.4,0.4,0.6,0.4c0.3,0,0.5-0.1,0.6-0.4c0.7-1.3,1.9-2,3.5-2c5.3,0,6,3.2,6,6c0,1.2-0.5,3-2.8,5.5c-1,1.1-2.5,2.2-3.6,3c-0.1,0.1-0.1,0.2-0.1,0.3c0,0.1,0.1,0.2,0.2,0.2l12.2,0c0.1,0,0.2-0.1,0.2-0.2l0-2.3c0.1-2.2,0.6-3.2,1.7-3.5l0.1,0l0.8-0.1c2.5-0.4,3.7-2.3,4-6.4l0-0.7h1.5l0.1,0.7c0.4,3.9,1.8,6,4.4,6.4l0.6,0.1l0.1,0c1.1,0.3,1.8,1.6,1.7,3.5v2.1c0,0.1,0.1,0.2,0.2,0.2h2.2c0.7,0,1.1,0.1,1.4,0.4c0.3,0.3,0.4,0.9,0.4,2.4c0,0,0,0,0,0.1l0,0.1l0,0.1c0,0.1,0.1,0.2,0.3,0.2h11c0.1,0,0.2-0.1,0.2-0.2l0-0.8c0-0.9,0.3-1.7,0.8-2.2c0.3-0.3,0.7-0.4,1.2-0.4c0,0,2.5,0,2.5,0c1.9,0,1.9,2.3,1.9,3l0,2.2c0,0.1,0,0.1,0.1,0.2c0,0,0.1,0.1,0.2,0.1h2.5c0.1,0,0.1,0,0.2-0.1c0,0,0.1-0.1,0.1-0.2l0-2.2c0-0.7,0-3,1.9-3h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h2.5c0.1,0,0.2-0.2,0.3-0.3l0-2c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h11.2l0.1,0c2.3-0.1,2.7-2.2,2.8-4.3l0-3.6c0-0.1-0.1-0.2-0.2-0.2c0,0-0.8,0-0.8,0c-3.6,0-6.1-2.3-6.1-5.6c0-1.8,0.9-3.7,2.3-4.7c0.1-0.1,0.1-0.1,0.1-0.2c-0.1-0.5-0.1-0.9-0.1-1.4c0-1.8,0.9-3.5,2.5-4.6c0.1,0,0.1-0.1,0.1-0.2c0-0.1,0-0.3,0-0.4c0-2,1.7-3.5,3.9-3.5c2.3,0,4.2,1.6,4.2,3.5c0,0.2,0,0.3,0,0.4c0,0.1,0,0.2,0.1,0.2c1.6,1.1,2.5,2.8,2.5,4.6c0,0.5,0,0.9-0.1,1.4c0,0.1,0,0.2,0.1,0.2c1.5,1.1,2.4,2.8,2.4,4.5c0,3.4-2.6,5.9-6.1,5.9l-1.2,0c-0.1,0-0.2,0.1-0.2,0.2l0,3.6c0.1,2.2,0.6,4.3,2.9,4.3H312l0,0.1c0,2.8,2.2,5,5,5c2.8,0,5-2.2,5-5s-2.2-5-5-5c-2.1,0-4,1.4-4.7,3.2"
                                  Height="50"
                                  StrokeThickness="1"
                                  StrokeDashCap="Flat"
                                  StrokeStartLineCap="Flat"
                                  StrokeEndLineCap="Flat"
                                  Stroke="{TemplateBinding Background}"
                                  Canvas.Left="0"
                                  Stretch="None"
                                  Canvas.Top="0"
                                  Width="323" />
                            <Path Data="M10.7,45.6C10,47.5,8.1,48.9,6,48.9c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5l0,0h30.3c0.1,0,0.2-0.1,0.2-0.2v-4c0-1.4,0.2-2.3,0.7-2.8c0.3-0.3,0.7-0.5,1.3-0.5H49c0.1,0,0.2-0.1,0.2-0.2v-3.6c0-1.4,0.2-2.3,0.7-2.8c0.3-0.3,0.8-0.5,1.3-0.5h3.5c0.1,0,0.2-0.1,0.2-0.2V14.7c0-2.1,0.4-3.1,1.4-3.3c0.1,0,0.3,0,0.5,0l0.2,0c3.4-0.5,3.6-4,3.6-9.6V0.6H63l0,1.1c0,5.4,0.2,9,3.8,9.6c0,0,0.1,0,0.1,0c0.1,0,0.2,0,0.3,0l0.1,0c1.5,0.4,1.5,2.7,1.5,3.4V29c0,0.1,0.1,0.2,0.3,0.2h3.2c0.6,0,1.1,0.2,1.5,0.6c0.5,0.6,0.8,1.5,0.8,2.7l0,10.6l0,0.4c0,0.1,0.1,0.2,0.2,0.2h16.6c2.4,0,2.8-2.1,3-4.3l0-3.6c0-0.1,0-0.1-0.1-0.2c0,0-0.1-0.1-0.2-0.1l-0.8,0c-3.6,0-6.1-2.3-6.1-5.6c0-1.8,0.9-3.7,2.3-4.7c0.1-0.1,0.1-0.1,0.1-0.2c-0.1-0.5-0.1-0.9-0.1-1.4c0-1.8,0.9-3.5,2.5-4.6c0.1,0,0.1-0.1,0.1-0.2c0-0.1,0-0.3,0-0.4c0-2,1.7-3.5,3.9-3.5c2.3,0,4.2,1.6,4.2,3.5c0,0.2,0,0.3,0,0.4c0,0.1,0,0.2,0.1,0.2c1.6,1.1,2.5,2.8,2.5,4.6c0,0.5,0,0.9-0.1,1.4c0,0.1,0,0.2,0.1,0.2c1.5,1.1,2.4,2.8,2.4,4.5c0,3.4-2.6,5.9-6.1,5.9l-1.2,0c-0.1,0-0.2,0.1-0.2,0.2l0,3.6c0.1,2.2,0.6,4.3,2.9,4.3h9.3c0.1,0,0.2-0.1,0.2-0.2v-2.1c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c1.9,0,1.9,2.3,1.9,3l0,2.2c0,0.1,0,0.1,0.1,0.2c0,0,0.1,0.1,0.2,0.1h2.5c0.1,0,0.1,0,0.2-0.1c0,0,0.1-0.1,0.1-0.2l0-2.2c0-0.7,0-3,1.9-3h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h2.5c0.1,0,0.2-0.2,0.2-0.3l0-2c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7l0,0.4c0,0.1,0.1,0.2,0.2,0.2H148c0.1,0,0.2-0.1,0.2-0.3l0-4.6c0-1.5,0.2-2.5,0.7-3.1c0.3-0.3,0.8-0.5,1.3-0.5h2.2c0.1,0,0.2-0.1,0.2-0.2v-4.6c0-2.4,1.3-3.4,2.5-3.6c3.9-0.6,4.1-4.9,4-11.3l0-0.3l-0.5-2.4c0-0.1,0-0.1-0.1-0.1l-2-1.8l2.8-0.4c0.1,0,0.2-0.1,0.2-0.1l1.1-2.7l1.1,2.7c0,0.1,0.1,0.1,0.2,0.2l2.8,0.3l-2.2,1.9c0,0-0.1,0.1-0.1,0.1l-0.5,2.4c0,0,0,0,0,0.1l0,0.7c0,6.2,0.2,10.2,4.2,10.8c0.6,0.1,2.4,0.5,2.4,3.6v4.6c0,0.1,0.1,0.2,0.2,0.2h2.2c0.4,0,0.7,0.1,1,0.4c0.9,0.9,1,3.1,1,4.3v0.2c0,0.1,0.1,0.2,0.2,0.2h16.2c0.1,0,0.2-0.1,0.2-0.2c0-0.1,0-0.2-0.1-0.3c-1.1-0.8-2.5-1.9-3.6-3c-2.3-2.4-2.8-4.3-2.8-5.5c0-2.8,0.7-6,6-6c1.6,0,2.8,0.7,3.5,2c0.1,0.2,0.4,0.4,0.6,0.4c0.3,0,0.5-0.1,0.6-0.4c0.7-1.3,1.9-2,3.5-2c5.3,0,6,3.2,6,6c0,1.2-0.5,3-2.8,5.5c-1,1.1-2.5,2.2-3.6,3c-0.1,0.1-0.1,0.2-0.1,0.3c0,0.1,0.1,0.2,0.2,0.2l12.2,0c0.1,0,0.2-0.1,0.2-0.2l0-2.3c0.1-2.2,0.6-3.2,1.7-3.5l0.1,0l0.8-0.1c2.5-0.4,3.7-2.3,4-6.4l0-0.7h1.5l0.1,0.7c0.4,3.9,1.8,6,4.4,6.4l0.6,0.1l0.1,0c1.1,0.3,1.8,1.6,1.7,3.5v2.1c0,0.1,0.1,0.2,0.2,0.2h2.2c0.7,0,1.1,0.1,1.4,0.4c0.3,0.3,0.4,0.9,0.4,2.4c0,0,0,0,0,0.1l0,0.1l0,0.1c0,0.1,0.1,0.2,0.3,0.2h11c0.1,0,0.2-0.1,0.2-0.2l0-0.8c0-0.9,0.3-1.7,0.8-2.2c0.3-0.3,0.7-0.4,1.2-0.4c0,0,2.5,0,2.5,0c1.9,0,1.9,2.3,1.9,3l0,2.2c0,0.1,0,0.1,0.1,0.2c0,0,0.1,0.1,0.2,0.1h2.5c0.1,0,0.1,0,0.2-0.1c0,0,0.1-0.1,0.1-0.2l0-2.2c0-0.7,0-3,1.9-3h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h2.5c0.1,0,0.2-0.2,0.3-0.3l0-2c0-1.3,0.2-2.2,0.7-2.7c0.3-0.3,0.7-0.4,1.2-0.4h2.5c0.5,0,0.9,0.1,1.2,0.4c0.5,0.5,0.7,1.3,0.7,2.7v2.1c0,0.1,0.1,0.2,0.2,0.2h11.2l0.1,0c2.3-0.1,2.7-2.2,2.8-4.3l0-3.6c0-0.1-0.1-0.2-0.2-0.2c0,0-0.8,0-0.8,0c-3.6,0-6.1-2.3-6.1-5.6c0-1.8,0.9-3.7,2.3-4.7c0.1-0.1,0.1-0.1,0.1-0.2c-0.1-0.5-0.1-0.9-0.1-1.4c0-1.8,0.9-3.5,2.5-4.6c0.1,0,0.1-0.1,0.1-0.2c0-0.1,0-0.3,0-0.4c0-2,1.7-3.5,3.9-3.5c2.3,0,4.2,1.6,4.2,3.5c0,0.2,0,0.3,0,0.4c0,0.1,0,0.2,0.1,0.2c1.6,1.1,2.5,2.8,2.5,4.6c0,0.5,0,0.9-0.1,1.4c0,0.1,0,0.2,0.1,0.2c1.5,1.1,2.4,2.8,2.4,4.5c0,3.4-2.6,5.9-6.1,5.9l-1.2,0c-0.1,0-0.2,0.1-0.2,0.2l0,3.6c0.1,2.2,0.6,4.3,2.9,4.3H312l0,0.1c0,2.8,2.2,5,5,5c2.8,0,5-2.2,5-5s-2.2-5-5-5c-2.1,0-4,1.4-4.7,3.2"
                                  x:Name="ProgressPath"
                                  StrokeThickness="2"
                                  StrokeDashCap="Flat"
                                  StrokeStartLineCap="Flat"
                                  StrokeEndLineCap="Flat"
                                  Stroke="{TemplateBinding Foreground}"
                                  Height="50"
                                  Canvas.Left="0"
                                  Stretch="None"
                                  Canvas.Top="0"
                                  Width="323" />
                        </Canvas>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Profit

Подведем итоги. У нас стояла задача сделать необычный прогресс-бар. Для достижения необходимого эффекта мы использовали штриховку векторной кривой и анимацию длины штриха. Если такое решение кажется вам странным, знайте, что в своих чувствах вы не одиноки. Мне тоже так кажется. Но в то же время оно подкупает своей простотой и лаконичностью.

При желании код ProgressPathControl можно доработать, чтобы он научился анимировать произвольные кривые. Например, можно сделать так.

История одного прогресс-бара - 8

Это довольно распространенный в мобильных интерфейсах прогресс-бар, которого «из коробки» в Windows Phone SDK нет. Похожий элемент управления в 2GIS для WP используется, например, при добавлении пользовательских фотографий к геообъектам, и теперь вы тоже знаете, как написать такой самостоятельно.

Автор: 2ГИС

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js