Send Button

In this example, we are making a button with a fancy loading animation.

The icons used in this example are from Font Awesome.

Note: Although this example contains a complete app screen for context, the focus of this article is only on the button itself.

Making the button

We begin by declaring our SendButton component. It has a boolean property, IsLoading, which controls the animation. The animation begins when IsLoading becomes true, and plays until it becomes false. From here on, we'll refer to these two states as idle (IsLoading="false") and loading (IsLoading="true").

We also declare the PrimaryColor and SecondaryColor properties to allow customizing the colors of the component.

<Panel ux:Class="SendButton" Width="70" Height="70" IsLoading="false" PrimaryColor="#F73859" SecondaryColor="#fff">
    <bool ux:Property="IsLoading" />
    
    <float4 ux:Property="PrimaryColor" />
    <float4 ux:Property="SecondaryColor" />

Our component is made up of a Panel containing several controls. Because of how Panel performs layout on its children, the controls will be layered on top of each other – which is exactly what we want in this case.

The paper plane icon is simply an Image element backed by a MultiDensityImageSource. We give it a name, icon, so we can animate it later on.

<Image ux:Name="icon" StretchMode="Uniform" Width="24" Alignment="Center" Offset="-2,1" Color="{ReadProperty this.SecondaryColor}">
    <MultiDensityImageSource>
        <FileImageSource Density="1" File="Assets/fa-paper-plane@1x.png" />
        <FileImageSource Density="2" File="Assets/fa-paper-plane@2x.png" />
        <FileImageSource Density="4" File="Assets/fa-paper-plane@4x.png" />
    </MultiDensityImageSource>
</Image>

We want to give the user some visual feedback while they are pressing down on the button, but before the animation starts. For that, we use a Circle with a semi-transparent white fill, and a WhilePressed trigger with a Change animator that fades its opacity from 0 to 1.

<Circle ux:Name="tapOverlayCircle" Color="#fff6" Opacity="0" />
<WhilePressed>
    <Change tapOverlayCircle.Opacity="1" Duration="0.05" DurationBack="0.15" />
</WhilePressed>

Now for the good stuff.

We have a Circle named mainCircle, which has a single Stroke and no other brushes. mainCircle is used as both the background while the button is idle, and as the rotating arc while it is loading.

While the button is idle, the Width of this Stroke is larger than the radius of the circle. This results in the circle being completely covered, as if we would have just assigned it a Color. When transitioning to the loading state however, we animate the Width of this stroke to a lower value.

During this transition, we also animate the LengthAngleDegrees property of the Circle to 90. Combined with StartAngleDegrees, this property allows us to draw a slice of the circle instead of the whole 360 degrees.

<Circle ux:Name="mainCircle" StartAngleDegrees="0" LengthAngleDegrees="360">
    <Stroke ux:Name="mainCircleStroke" Width="100" Color="{ReadProperty this.PrimaryColor}"  />
    <Rotation ux:Name="mainCircleRotation" />
</Circle>

The remaining elements are static Circle elements which serve as a background while the button is loading. These are hidden underneath the mainCircle while the button is idle.

The first of these is the "track" underneath the spinning arc. It is scaled with a factor of 0.85 to match the same scaling being performed on mainCircle during the transition to the loading state.

The second Circle is the background that's visible while loading, and also provides the shadow underneath the button.

<Circle>
    <Stroke Width="5" Color="#6662" />
    <Scaling Factor="0.85" />
</Circle>

<Circle Color="{ReadProperty this.SecondaryColor}" Margin="1">
    <Shadow Angle="90" Size="5" Distance="3" Color="#0005" />
</Circle>

Let's move on to animation. We'll add a WhileTrue trigger to play back the animation based on the value of the IsLoading property.

<WhileTrue Value="{ReadProperty this.IsLoading}">
    <PulseForward Target="flightAnimation" />
    <Change icon.Opacity="0" Delay="0.68" DurationBack="0.1" Easing="CubicInOut" />
    
    <Scale Target="mainCircle" Factor="0.85" Duration="0.5" Easing="BackInOut" Delay="0.35" />
    <Change mainCircleStroke.Width="5" Duration="0.2" Delay="0.35" Easing="SinusoidalInOut" />
    <Change mainCircle.LengthAngleDegrees="90" Duration="0.5" Delay="0.75" DelayBack="0" Easing="CubicInOut" />
    <Change spin.Value="true" Delay="1.4" />
</WhileTrue>
<WhileTrue ux:Name="spin">
    <Cycle Target="mainCircleRotation.Degrees" Low="0" High="360" Frequency="0.666" Easing="CubicInOut" Waveform="Sawtooth" />
    <Cycle Target="mainCircleRotation.Degrees" Low="0" High="360" Frequency="0.999" FrequencyBack="-1" Waveform="Sawtooth" MixOp="Add" />
</WhileTrue>

The PulseForward above sets off the animation that makes the plane fly away. By keeping this animation in a separate Timeline we ensure it is played in its entirety, even though IsLoading stays active for shorter than the duration of the animation.

<Timeline ux:Name="flightAnimation">
    <Move Target="icon" RelativeTo="Size" KeyframeInterpolation="Smooth">
        <Keyframe Time="0" X="0" Y="0" />
        <Keyframe TimeDelta="0.38" X="-0.8" Y="0.5" />
        <Keyframe TimeDelta="0.3" X="20" Y="-12" />
    </Move>
    <Rotate Target="icon" Degrees="25" Duration="0.4" Easing="SinusoidalInOut" DurationBack="0" />
    <Scale Target="icon" Factor="0" Easing="CubicIn" Delay="0.38" Duration="0.3" />
    <Change icon.Opacity="0" Duration="0.25" Delay="0.38" Easing="CubicInOut" />
</Timeline>

And that's it! Here is our finished button:

<Panel ux:Class="SendButton" Width="70" Height="70" IsLoading="false" PrimaryColor="#F73859" SecondaryColor="#fff">
    <bool ux:Property="IsLoading" />
    
    <float4 ux:Property="PrimaryColor" />
    <float4 ux:Property="SecondaryColor" />

    <Image ux:Name="icon" StretchMode="Uniform" Width="24" Alignment="Center" Offset="-2,1" Color="{ReadProperty this.SecondaryColor}">
        <MultiDensityImageSource>
            <FileImageSource Density="1" File="Assets/fa-paper-plane@1x.png" />
            <FileImageSource Density="2" File="Assets/fa-paper-plane@2x.png" />
            <FileImageSource Density="4" File="Assets/fa-paper-plane@4x.png" />
        </MultiDensityImageSource>
    </Image>
    
    <Circle ux:Name="tapOverlayCircle" Color="#fff6" Opacity="0" />
    <WhilePressed>
        <Change tapOverlayCircle.Opacity="1" Duration="0.05" DurationBack="0.15" />
    </WhilePressed>
    
    <Circle ux:Name="mainCircle" StartAngleDegrees="0" LengthAngleDegrees="360">
        <Stroke ux:Name="mainCircleStroke" Width="100" Color="{ReadProperty this.PrimaryColor}"  />
        <Rotation ux:Name="mainCircleRotation" />
    </Circle>
    
    <Circle>
        <Stroke Width="5" Color="#6662" />
        <Scaling Factor="0.85" />
    </Circle>

    <Circle Color="{ReadProperty this.SecondaryColor}" Margin="1">
        <Shadow Angle="90" Size="5" Distance="3" Color="#0005" />
    </Circle>
    
    <Timeline ux:Name="flightAnimation">
        <Move Target="icon" RelativeTo="Size" KeyframeInterpolation="Smooth">
            <Keyframe Time="0" X="0" Y="0" />
            <Keyframe TimeDelta="0.38" X="-0.8" Y="0.5" />
            <Keyframe TimeDelta="0.3" X="20" Y="-12" />
        </Move>
        <Rotate Target="icon" Degrees="25" Duration="0.4" Easing="SinusoidalInOut" DurationBack="0" />
        <Scale Target="icon" Factor="0" Easing="CubicIn" Delay="0.38" Duration="0.3" />
        <Change icon.Opacity="0" Duration="0.25" Delay="0.38" Easing="CubicInOut" />
    </Timeline>
    <WhileTrue Value="{ReadProperty this.IsLoading}">
        <PulseForward Target="flightAnimation" />
        <Change icon.Opacity="0" Delay="0.68" DurationBack="0.1" Easing="CubicInOut" />
        
        <Scale Target="mainCircle" Factor="0.85" Duration="0.5" Easing="BackInOut" Delay="0.35" />
        <Change mainCircleStroke.Width="5" Duration="0.2" Delay="0.35" Easing="SinusoidalInOut" />
        <Change mainCircle.LengthAngleDegrees="90" Duration="0.5" Delay="0.75" DelayBack="0" Easing="CubicInOut" />
        <Change spin.Value="true" Delay="1.4" />
    </WhileTrue>
    <WhileTrue ux:Name="spin">
        <Cycle Target="mainCircleRotation.Degrees" Low="0" High="360" Frequency="0.666" Easing="CubicInOut" Waveform="Sawtooth" />
        <Cycle Target="mainCircleRotation.Degrees" Low="0" High="360" Frequency="0.999" FrequencyBack="-1" Waveform="Sawtooth" MixOp="Add" />
    </WhileTrue>
</Panel>

Using the button

Now, the button doesn't automatically play the animation when clicked, since it doesn't know how long to play it for. Instead, we have to change IsLoading to true when the button is clicked, then perform some time-expensive operation, and set it back to false when the operation has completed. Below is an example of how you can use the button in your own app.

<JavaScript>
    var Observable = require("FuseJS/Observable");
    
    var isLoading = Observable(false);
    
    function clicked() {
        isLoading.value = true;
        
        setTimeout(function() {
            isLoading.value = false;
        }, 3000);
    }
    
    module.exports = {
        isLoading: isLoading,
        clicked: clicked
    };
</JavaScript>

<SendButton Clicked="{clicked}" IsLoading="{isLoading}" />