Карта города и справочник предприятий
101,34
рейтинг
20 января в 10:23

Разработка → История одного прогресс-бара



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


Проблема


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

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

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

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

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

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

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

Решение


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

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


<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″, то получим вот такую картинку.

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

Давайте для ясности я дам нашему нестандартному прогресс-бару имя — 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 можно доработать, чтобы он научился анимировать произвольные кривые. Например, можно сделать так.

Это довольно распространенный в мобильных интерфейсах прогресс-бар, которого «из коробки» в Windows Phone SDK нет. Похожий элемент управления в 2GIS для WP используется, например, при добавлении пользовательских фотографий к геообъектам, и теперь вы тоже знаете, как написать такой самостоятельно.
Автор: @volokhin
2ГИС
рейтинг 101,34
Карта города и справочник предприятий

Похожие публикации

Комментарии (40)

  • +10
    Вы бы лучше сделали нормальный интерфейс, потому как ваш текущий, который не использует никаких нативных элементов

    1) жутко медленно работает даже на топовом железе
    2) выглядит инородно и просто некрасиво
    3) плохо выглядит на высоком разрешении: пользуюсь телефоном с экраном QHD, 5.7' весь софт выглядит нормально кроме такого же яндекс.навигатора, но там-то изначально все крупнее чем у вас, и стало просто чуть мельче. В 2gis же все надписи надо рассматривать в очках

    P.S. Когда в прошлый раз отписался с критикой в вашей гуглогруппе бета-тестеров мне заявили, что я не разработчик и вообще несу чушь и не понимаю о чем говорю, у вас все отлажено и супер оптимизировано, и еще забанили на всякий случай.

    Забыл указать, все описанное относится к android версии
    • 0
      Как правило такие вещи сразу заигнорят, Вы же не программист, А у них все там круто и оптимизировано ))
      SOLON7@habr~: Sarcasm mode off
    • +4
      Если вы в гуглогруппе критиковали так же, как здесь, то реакция вполне логичная. На месте разработчиков я бы тоже такой фидбек проигнорировал.

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

      BTW, я не работаю в 2ГИСе и никак не аффилирован с этой компанией. Разве что пользуюсь их приложением для Windows 10 Mobile.
      • +2
        Ок, я буду благодарен, если вы мне поможете сформулировать ту же самую мысль но так, чтобы она не оскорбляла чувств разработчиков. Если кого-то обидел мой комментарий — прошу прощения. Я за адекватные отзывы и хочу помочь.
        • +12
          Давайте по порядку.

          жутко медленно работает даже на топовом железе

          • Как проявляется медленность? Если приложение долго грузится, хотя бы примерно уточните, насколько долго. Если анимация интерфейса отображается рывками, уточните, какие именно элементы интерфейса тормозят. Если имеете в виду что-то другое, напишите об этом.
          • На каком телефоне воспроизводятся проблемы? «Топовое железо» — это конкретно что?


          выглядит инородно и просто некрасиво

          • Если интерфейс сделан не по гайдлайнам Андроида, будет полезно указать на конкретные расхождения. Еще лучше — со ссылкой на гайдлайны и скриншотами. Важно учесть, что в разных версиях и вариантах Андроида разный дизайн, поэтому укажите, какой именно у вас Андроид.
          • Некрасиво — субъективная характеристика. Формализуйте ее в виде конкретных косяков: тут буквы скачут, тут тексту душно из-за маленьких полей, тут желтое на белом не читается и т. д.


          плохо выглядит на высоком разрешении… В 2gis же все надписи надо рассматривать в очках

          • Уточните разрешение, приведите скриншоты конкретных мелких надписей. Если мелкие не только надписи, но и элементы интерфейса, снимите и их тоже. Покажите, как они должны выглядеть, прикрепив скриншот из другого приложения.
          • +2
            * жутко медленно работает даже на топовом железе

            — CPU Snapdragon 808 MSM8992
            — GPU Adreno 418
            — RAM 3 GB
            — Storage 32 GB
            Motorola Moto X Style (2015) Android 6.0
            Торможение проявляется в общей медлительности интерфейса, переходов, анимаций. Иногда с рывками. Проявляется только на сабже, весь остальной софт работает без замечаний.

            * выглядит инородно и просто некрасиво

            Интерфейс не по гайдлайнам, начиная от иконок меню (три точки) стилизованных под Android 4, заканчивая размерами элементов, стилями всплывающих окон (при копировании текста высплывает попап стилизованный по гайдлайнам Android 4), заканчивая анимацией нажатий и шрифтами

            * плохо выглядит на высоком разрешении… В 2gis же все надписи надо рассматривать в очках

            Разрешение, как я уже писал QHD 1440 x 2560 pixels (520 ppi)

            Скриншот
            http://s14.postimg.org/rfk8i96dt/Screenshot_20160120_112311.png
            • 0
              * плохо выглядит на высоком разрешении… В 2gis же все надписи надо рассматривать в очках
              Разрешение, как я уже писал QHD 1440 x 2560 pixels (520 ppi)


              Кстати, в настройках 2гис можно включить увеличенные шрифт, хотя он и ненамного больше.
              • 0
                там есть такая настройка, только звучит она как «Smaller fonts and buttons». Страшно подумать, что будет если я ее включу :(
                • +3
                  Забавно, там разные настройки, если открывать из главного меню и с самой карты.
                  С главного меню Smaller fonts and buttons, а если открыть настройки с карты, то там уже как раз «Large fonts on the map»
            • +2
              По поводу замечаний (справедливых) — расскажу что знаю. Ребята из команды 2GIS для Android сейчас активно работают над новой версией, которую мы анонсировали ранее. А скриншот у вас из старой версии. Уверен, в новой версии все будет гораздо лучше. Морально устаревший дизайн в ней полностью обновили, нет путаницы с настройками, номера домов нормально читаются на высоком разрешении. Я как WP разработчик не очень часто держу в руках Android с новым 2GIS на борту, но те версии, которые я видел, с точки зрения производительности вели себя вполне достойно.
              • +2
                12 мая 2015 в 09:47
                Релиз нового мобильного 2ГИС

                С ума сойти, уже больше полугода прошло. А вы прогресбары правите. Помогли бы коллегам :)
                • 0
                  с ума сойти, это после разработки под WP начать писать под Android. Причём сойдут с ума и windows разработчики, и команда android.
      • +11
        Извините, но задача поддержки — добиться от пользователя внятного описания и исправить косяки, а не банить за то, что он не умеет внятно излагать замечания. Мало кто умеет. Это не отмазка.
        • 0
          Конечно, вы правы. Однако с возрастанием сложности разбора фидбека снижается вероятность того, что поддержка дойдет до конца. У поддержки ограниченный ресурс, и углубиться до упора в каждый комментарий нет возможности.

          Конечно, банить за мнение недопустимо. Тем не менее, я не знаю, что там был за тред, и правда ли человека забанили именно за это. Тут только представитель 2ГИСа может дать какие-то осмысленные комментарии, я не буду гадать и защищать ни поддержку, ни пользователя.
          • +1
            Представитель 2ГИСа предусмотрительно не суется в этот тред )
    • +1
      Вообще, у нас случаи бана очень редки. И причины обычно две: реклама или мат. Скажите, пожалуйста, своё имя в G+, если произошло какое-то недоразумение, то мы вас обязательно разбаним.
  • +1
    Ну вот на мой взгляд можно очень легко добиться подобного поведения, чтобы прогресс двигался как надо. Нарисовать путь градиентом (если недостаточно одного цвета, двумя, тремя). И просто закрашивать все пиксели там, где значение цвета в градиенте меньше текущего. Значение каждый кадр растёт, прогрессбар будет двигаться. На разных участках такого прогрессбара можно и скорость заполнения тоже варьировать так же, крася одним цветом не 1, а сразу 2-3 пикселя, которые будут за 1 кадр сразу закрашиваться.
    ИМХО будет куда быстрее работать и куда проще в реализации.
    Работая с комп.графикой в реалтайме такие решения приходят естественным путём.
    Кстати весь прогрессбар в таком случае можно реализовать одним простейшим шейдером.
    • 0
      На самом деле мы первое время тоже думали в сторону похожего варианта. Но все-таки работа с компьютерной графикой – отдельная история. Решение на теплом ламповом XAML'е, напротив, показалось нам проще и роднее.
    • 0
      Закраска градиентом в XAML даст в результате первый прогресс бар. Я пробовал использовать эту идею для простой отрисовки треков на картах с градиентной закраской, не вышло.
      • 0
        Не понимаю как оно даст первый. Либо Вы не понимаете суть описанного мной метода.
        • 0
          Понимаю, но путь в XAML будет закрашен градиентом не следуя за закрашиваемым путем, а градиентом плоскости накладываемой на путь.
  • +1
    Всегда обращаю внимание на этот прогрессбар и всегда радуюсь именно такому его поведению :-) Такое внимание к деталям — это очень круто, спасибо!
    • 0
      Приятно слышать :) Спасибо, что пользуетесь 2GIS на вашем телефоне с Windows 10 Mobile!
      • 0
        Не планируете сделать UWP-приложение?
        • 0
          Желание есть, но конкретных планов или сроков нет. Мы пока осторожно так присматриваемся к UWP и к Windows 10 (Mobile).
  • +1
    Ребята, простите, что пишу и сюда (в поддержку письмо уже писал, а тут оно оффтоп), но, пожалуйста, сделайте ночную, а в идеале вообще чёрную тему оформления для вашего приложения под Android, очень тяжело глазам.
  • +1
    Из-за этого прогресс-бара и медленного интернета несколько раз закрывал приложение, пока понял, что оно не просто зависло, а что-то делает и это прогресс-бар.
    • 0
      Спасибо за фидбэк, проблема понятная. Подумаем с дизайнерами как можно сделать процесс загрузки при медленном интернете более явным.
      • +3
        Могу предложить целых два способа. Дешевый костыль: добавить текстом проценты или даже мегабайты (скачано/всего). И правильное решение: сделать так, чтобы с приложением можно было бы работать, не скачивая каждый раз по двести мегов.
  • +8
    Отличная статья. Эта штука называется «Path Tracing», почти готовые имплементации довольно легко гуглятся.
    • 0
      Теперь я знаю как это называется, спасибо! Статья по ссылке классная, как раз то что надо (хоть и для Android).
  • +12
    А мне первый вариант больше понравился — в нём чётко видно, что происходит «заполнение», слева направо. Второй вариант больше похож на просто красивую анимацию, а не прогресс-бар. Особенно смущает финальная стадия, когда описывается маленький круг и некоторое время движение происходит в обратную сторону (справа налево).
  • +3
    Красивый метод, причем универсальный — подойдёт для любой платформы, умеющий рисовать векторные картинки, а не только Windows Phone.
  • +12
    Первый вариант анимации был намного лучше.
    Занимаетесь ерундой.
    • +5
      Второй вариант лучше, но занимаетесь вы всё-таки ерундой.
  • +7
    Эта фраза прекрасна:
    «Но в нашей команде достаточно опытных разработчиков, настоящих профессионалов своего дела, поэтому через какое-то время мы все же нагуглили решение.»
  • +1
    Единственное добавлю, вы проверяли, что анимация длины штриха выполняется не в UI потоке и не жрет ресурсы?
    • 0
      Это хороший вопрос, я ждал его. Изменение длины штриха конечно же идет через UI поток — никакой магии не происходит. Но сама операция выходит довольно легковесная. Там где у нас используется этот прогресс-бар (загрузка городов, загрузка фотографий) мы не заметили, что его наличие как-то влияет на производительность.
      • 0
        Жаль, что без UI не обошлось, я честно надеялся на магию, такой трюк с заштриховкой мне понравился :)
        Беда в том, что даже самая легковесная анимация в UI не прогнозируема по затратам ресурсов, в WP 8.1 Xaml MS из за этого заставил явно указывать при создании таких анимаций, что вы знаете, что делаете.

        У меня в проекте тоже не обошлось без таких анимаций, а еще раньше пришлось отказаться от компонентов телерика именно из за того, что их движок анимации почти полностью крутился в UI.
      • 0
        Хотя это все же больше касается именно анимации XAML, а просто установка через UI свойства изредка, а не 60 р/с пожалуй действительно не повредит.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое Разработка