Introduction
I like the game Battlefield 4.
The game is extremely well designed in almost every aspect.
I especially like the idea that you visually start the game from a Terminal where you see some flickering images (distorted) of the map being loaded while a prompt is awaiting input in the top right corner.
The prompt is also used as a progress indicator when pages are loaded on Battlelog.
Design
The prompt is designed so it show up like an old terminal prompt with 10 lines or so in height. There is a clear gap between each line to indicate a very old monitor of some sort.
There is also an artifact showing when the prompt is raised to full prompt, like a halo indicating a slow refresh rate.
The prompt is shown in 3 different modes depending on the state of the game. On Battlelog, it's always the same mode.
Progress Control
I decided to create the prompt as a progress control that could be used as any other progress control.
This would allow me and others that like Battlefield to show progress in our otherwise dull financial apps with a smile on our lips.
The progress indicator can be used like this:
//
// Using Bf4ProgressIndicator
//
<Grid>
<controls1:Bf4ProgressIndicator Margin="5" Value="{Binding ...}" PromptColor="White"/>
</Grid>
The control has the following properties:
The indicator will switch between 3 modes:
- Mode 1) Value is below 1/3 of maximum. A full prompt is just blinking.
- Mode 2) Value between 1/3 of maximum and below 2/3 of maximum. Underscore moving upward.
- Mode 3) Value above 1/3 of maximum. Switching between full prompt and underscore.
Each mode has its own style.
The style is set when the value changes and according to the rules above.
Here is the ProgressChanged
method:
protected void ProgressChanged(Object sender, DependencyPropertyChangedEventArgs e)
{
OnValueChanged(new RoutedPropertyChangedEventArgs<double>(
e.OldValue != null?(double)e.OldValue:0,
e.NewValue!=null?(double)e.NewValue:0));
var oldStyle = Style;
var diff = Maximum - Minimum;
Style = Value < Minimum + diff/3 ? style1 : Value > Maximum - diff/3 ? style3 : style2;
if (oldStyle == Style) return;
if (promptStoryboard != null && partPrompt != null)
promptStoryboard.Stop(partPrompt);
if (promptStoryboard1 != null && partPrompt1 != null)
promptStoryboard1.Stop(partPrompt1);
if (promptStoryboard2 != null && partPrompt2 != null)
promptStoryboard2.Stop(partPrompt2);
if (promptStoryboard3 != null && partPrompt3 != null)
promptStoryboard3.Stop(partPrompt3);
promptStoryboard = GetTemplateChild("PART_StoryBoard") as Storyboard;
promptStoryboard1 = GetTemplateChild("PART_StoryBoard1") as Storyboard;
promptStoryboard2 = GetTemplateChild("PART_StoryBoard2") as Storyboard;
promptStoryboard3 = GetTemplateChild("PART_StoryBoard3") as Storyboard;
partPrompt = GetTemplateChild("PART_Prompt") as Rectangle;
partPrompt1 = GetTemplateChild("PART_Prompt1") as Rectangle;
partPrompt2 = GetTemplateChild("PART_Prompt2") as Rectangle;
partPrompt3 = GetTemplateChild("PART_Prompt3") as Rectangle;
if (promptStoryboard != null && partPrompt != null) {
promptStoryboard.Begin(partPrompt, true); }
if (promptStoryboard1 != null && partPrompt1 != null) {
promptStoryboard1.Begin(partPrompt1, true); }
if (promptStoryboard2 != null && partPrompt2 != null) {
promptStoryboard2.Begin(partPrompt2, true); }
if (promptStoryboard3 != null && partPrompt3 != null) {
promptStoryboard3.Begin(partPrompt3, true); }
}
Switching between different styles with running animations will require the storyboards to be stopped and started manually.
The visual part of the progress indicator is created as a style.
Here is an example of the style for mode 1:
<Style x:Key="Bf4ProgressIndicatorStyle1" TargetType="{x:Type local:Bf4ProgressIndicator}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Bf4ProgressIndicator}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="9*" MinHeight="{TemplateBinding MinHeight}"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="9*" MinWidth="{TemplateBinding MinWidth}"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Rectangle x:Name="PART_Prompt" Grid.Row="1" Grid.Column="2"
Width="Auto" Height="Auto" Stretch="Fill">
<Rectangle.Resources>
<SolidColorBrush x:Key="PromptBrush"
Color="{Binding Path=PromptColor,
RelativeSource={RelativeSource AncestorType={x:Type local:Bf4ProgressIndicator}}}" />
</Rectangle.Resources>
<Rectangle.Fill>
...
</Rectangle.Fill>
<Rectangle.Effect>
<DropShadowEffect BlurRadius="10" Color="{Binding Path=PromptColor,
RelativeSource={RelativeSource AncestorType={x:Type local:Bf4ProgressIndicator}}}"
Opacity="1" ShadowDepth="0" />
</Rectangle.Effect>
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard x:Name="PART_StoryBoard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_Prompt"
Storyboard.TargetProperty="Opacity" AutoReverse="False"
RepeatBehavior="Forever">
<LinearDoubleKeyFrame Value="1" KeyTime="0:0:.1" />
<LinearDoubleKeyFrame Value="1" KeyTime="0:0:.2" />
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:.3" />
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:.7" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
<Rectangle x:Name="PART_HighLight" Grid.Row="1" Grid.Column="1"
Grid.ColumnSpan="3" Width="Auto" Height="Auto"
Stretch="Fill" Opacity="0">
<Rectangle.Fill>
...
</Rectangle.Fill>
<Rectangle.Triggers>
<EventTrigger RoutedEvent="Loaded">
<BeginStoryboard>
<Storyboard x:Name="PART_StoryBoard2">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_HighLight"
Storyboard.TargetProperty="Opacity" AutoReverse="False"
RepeatBehavior="Forever">
<LinearDoubleKeyFrame Value=".25" KeyTime="0:0:.1" />
<LinearDoubleKeyFrame Value=".25" KeyTime="0:0:.15" />
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:.2" />
<LinearDoubleKeyFrame Value="0" KeyTime="0:0:.7" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Sample Application
The sample application illustrates how the progress indicator is used and the different modes.
In the game, it's mostly used as white on black but the control allows any color to be used.
Points of Interest
I tried to keep the used images (Geometry
) as static resources, but I did not find a way to dynamically change the color.
I ended up adding the geometry to each element in the visual tree so I could bind to a dependency property on the control.
History
- 15th December, 2013: Version 1.0.0.0
- 16th December, 2013: Added link to Nuget package