![](https://habrastorage.org/files/2bc/027/8e5/2bc0278e5039434a9402cc4e9c529916.png)
Наверное, любому программисту, который разрабатывает пользовательский интерфейс на C#/XAML, приходилось писать нестандартные элементы управления. В нашей веселой команде 2GIS для Windows Phone мы довольно часто делаем это, и такие задачи стали почти рутиной. Но об одном случае мне хочется рассказать подробнее. Все началось с того, что однажды нам понадобилось написать весьма своеобразный прогресс-бар.
Проблема
Как-то раз к нам на внутрикомандную рассылку пришло письмо от нашего коллеги Woodroof. Данил достаточно известен внутри компании: он является талантливым разработчиком, носит странную прическу и прокачал
- У Данила есть жена, ее зовут Саша.
- Саша пользуется смартфоном с Windows Phone, и у нее, конечно же, установлен 2GIS.
- В 2GIS’е есть стартовый экран загрузки города, на котором есть прогресс-бар.
- Саша заметила, что анимация этого прогресс-бара выглядит не круто.
Собственно, анимация тогда была вот такой:
![](https://habrastorage.org/files/221/29a/76a/22129a76a63a4b539902a48327bdd7de.gif)
Такой прогресс-бар уже достаточно необычен, но сделать его можно довольно просто с помощью обычного сдвига маски. Вот разметка на 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 для этой разметки вы увидите примерно такую картинку.
![](https://habrastorage.org/files/b55/184/b81/b55184b810054e9382a6963bef775976.png)
Но анимация сдвига маски для таких сложных кривых, как вы могли заметить, выглядит не очень уместно. Данил писал нам: «Моя жена смотрела на этот лоадер, ждала, когда он доберётся до дерева, и была очень разочарована текущим поведением».
Мы всей командой тогда согласились, что гораздо лучше сделать вот так.
![](https://habrastorage.org/files/aad/1eb/34b/aad1eb34bec642588958f7452b39a813.gif)
Но как же это сделать?
Решение
Мы немного подумали. На первый взгляд, стандартными средствами такого поведения не добиться. Но в нашей команде достаточно опытных разработчиков, настоящих профессионалов своего дела, поэтому через какое-то время мы все же нагуглили решение. Точнее, не готовое решение, конечно, а идею, которую можно взять за основу. Суть вот в чем.
Для начала добавим к нашему векторному пути штриховку.
![](https://habrastorage.org/files/f2e/c12/039/f2ec1203944a42ffaed2e4f978c35223.png)
<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?, то получим вот такую картинку.
![](https://habrastorage.org/files/2b1/e8c/133/2b1e8c133d1b486292341f76076e6693.png)
Теперь становится понятно, что для достижения нужного эффекта нам необходимо изменять длину штриха. Например, для StrokeDashArray="128 99999? получим вот такую картинку.
![](https://habrastorage.org/files/969/a8c/c93/969a8cc9380048919c84e4410fe2922d.png)
Давайте для ясности я дам нашему нестандартному прогресс-бару имя — 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, изрядно упрощенный, но неплохо иллюстрирующий основные принципы, я привожу ниже.
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;
}
<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 можно доработать, чтобы он научился анимировать произвольные кривые. Например, можно сделать так.
![](https://habrastorage.org/files/0b7/1ba/efa/0b71baefa9a345de8eda95a406067e42.gif)
Это довольно распространенный в мобильных интерфейсах прогресс-бар, которого «из коробки» в Windows Phone SDK нет. Похожий элемент управления в 2GIS для WP используется, например, при добавлении пользовательских фотографий к геообъектам, и теперь вы тоже знаете, как написать такой самостоятельно.
Комментарии (40)
Viacheslav01
24.01.2016 19:16+1Единственное добавлю, вы проверяли, что анимация длины штриха выполняется не в UI потоке и не жрет ресурсы?
volokhin
24.01.2016 20:31Это хороший вопрос, я ждал его. Изменение длины штриха конечно же идет через UI поток — никакой магии не происходит. Но сама операция выходит довольно легковесная. Там где у нас используется этот прогресс-бар (загрузка городов, загрузка фотографий) мы не заметили, что его наличие как-то влияет на производительность.
Viacheslav01
24.01.2016 21:07Жаль, что без UI не обошлось, я честно надеялся на магию, такой трюк с заштриховкой мне понравился :)
Беда в том, что даже самая легковесная анимация в UI не прогнозируема по затратам ресурсов, в WP 8.1 Xaml MS из за этого заставил явно указывать при создании таких анимаций, что вы знаете, что делаете.
У меня в проекте тоже не обошлось без таких анимаций, а еще раньше пришлось отказаться от компонентов телерика именно из за того, что их движок анимации почти полностью крутился в UI.
Viacheslav01
24.01.2016 21:12Хотя это все же больше касается именно анимации XAML, а просто установка через UI свойства изредка, а не 60 р/с пожалуй действительно не повредит.
rafuck
Эта фраза прекрасна: