Ga naar content
Zoek op onderwerpen, blogs, diensten etc.

Business Apps keuken – popup animatie

Blogs
25-4-2016

'In your face'

Een popup is een klein schermpje dat over een deel van het huidige scherm valt. Meestal wordt dit gebruikt om de gebruiker op de hoogte te stellen van een belangrijke gebeurtenis (lees: een fout) of om te vragen om input (weet u het zeker ja/nee). Daar kun je native alerts voor gebruiken (waarover in een latere blog post meer). Zeker als het om complexere informatie gaat, of de gebruiker keuzes moet maken, is het echter mooier is het om iets als dit te maken:

popup

Voordeel van deze benadering is ook dat er een meer consistente gebruikers ervaring is over de diverse platformen, hetgeen voor Business Apps een voordeel is. Consistentie maakt training, documentatie en tech support eenvoudiger en overzichtelijker - en dus kosten effectiever.

Het probleem van zo'n popup is echter dat deze 'ineens' over je gebruikers interface wordt gegooid. Dit noemen onze designers 'in your face': een abrupte, weinig verfijnde overgang in de gebruikerservaring die als niet erg vloeiend of fraai wordt ervaren. Dat valt niet binnen onze design filosofie voor Business Apps, zoals eerder beschreven. Dat kan wat subtieler, zoals bijvoorbeeld in onderstaande video

[video width="1100" height="600" mp4="https://www.wortell.nl/wp-content/uploads/2016/04/PopupBehavior.mp4" loop="true"][/video]

 

Wederom: een behavior

Ook hier maken we weer gebruik van een behavior. Nu al het grondwerk voor animatie behaviors al gelegd is in de vorige blog post, is de implementatie verrassend weinig code:

using Wortell.XamarinForms.Behaviors.Base;
using Xamarin.Forms;

namespace Wortell.XamarinForms.Behaviors
{
public class AnimateScaleBehavior : AnimateFoldBehaviorBase
{
protected override void Init(bool newValue)
{
if (newValue)
{
FoldOutPosition = 1;
FoldInPosition = 0;
AssociatedObject.Scale = FoldInPosition;
}
}

protected override void ExecuteAnimation(double start, double end,
uint runningTime)
{
var animation = new Animation(
d => AssociatedObject.Scale = d, start, end, Easing.SinOut);

animation.Commit(AssociatedObject, "Unfold", length: runningTime,
finished: (d, b) =>
{
if (AssociatedObject.Scale.Equals(FoldInPosition))
{
AssociatedObject.IsVisible = false;
}
});
}
}
}

In plaats van de TranslationX en TranslationY, zoals in het vorige artikel, wordt nu het Scale property geanimeerd. Gevolg is dat de popup niet vanuit het niets opkomt, maar vanuit het midden naar je toekomt - en daar ook weer naartoe verdwijnt als je op Ok of Cancel tapt. Niet 'in your face', maar een visuele inleiding op wat gaat gebeuren.

... en nog een beetje meer

Oplettende kijkers hebben ook gezien dat op het moment dat de popup begint te verschijnen, er een grijze waas over de rest van het scherm valt, dat pas verdwijnt als de popup helemaal verdwenen is. Dit heeft een driedubbele functie. Ten eerste de visuele - je wilt de gebruiker duidelijk maken waar op dit moment de focus voor zijn/haar activiteiten ligt. Ten tweede is er een technische reden - je wilt voorkomen dat de gebruiker - al dan niet per ongeluk - gebruikers interface element activeert die zich niet op de popup bevinden, met alle mogelijke gevolgen van dien. Ten derde is er een functionele reden - het is gebruikelijk in een mobiele applicatie dat popups en menu's verdwijnen als je er buiten tapt. Het grijze vlak is daarom tevens een 'ontvanger' voor een 'annuleer' actie.

Als je het voorbeeld code project (nu uitgebreid met de nieuwe behavior en de nieuwe demo pagina) van GibHub ophaalt, kun je dat heel simpel zelf 'naspelen', maar je kunt het ook al in de video boven zien.

Alles wordt geregeld binnen één user control, dat op een slimme manier gebruik maakt van de dynamische opzet van Xamarin Forms, alsmede wat mogelijkheden van de data binding techniek

<?xml version="1.0" encoding="utf-8" ?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:base="clr-namespace:Wortell.XamarinForms.Behaviors.Base;assembly=Wortell.XamarinForms"
xmlns:behaviors="clr-namespace:Wortell.XamarinForms.Behaviors;assembly=Wortell.XamarinForms"
x:Class="XamarinFormsDemos.Views.Controls.PopupControl"
IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}">
<ContentView BackgroundColor="{StaticResource SeeThrough}"
HorizontalOptions="Fill" VerticalOptions="Fill" >

<ContentView Padding="20,0,20,0" x:Name="AnimatedGrid" IsVisible="False">
<ContentView.Behaviors>
<behaviors:AnimateScaleBehavior IsVisible="{Binding IsMenuVisible}"
ViewIsInitialized="{Binding ViewIsInitialized}"/>
</ContentView.Behaviors>

<ContentView Style="{StaticResource MenuStyle}" HorizontalOptions="Fill"
VerticalOptions="CenterAndExpand" Padding="0,0,0,10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>

<ContentView Grid.Row="0" Style="{StaticResource PopupHeaderStyle}">
<Label Style="{StaticResource PopupHeaderTextStyle}" Text="Popup header" VerticalOptions="Center"/>
</ContentView>

<StackLayout Orientation="Vertical" Grid.Row="1">

<StackLayout Style="{StaticResource ContentStyle}" Orientation="Vertical">
<Label Style="{StaticResource MenuTextStyle}" Text="Here be text" />
<Label Style="{StaticResource MenuTextStyle}" Text="Here be more text" VerticalOptions="Center"/>
</StackLayout>

<ContentView Style="{StaticResource Separator}"></ContentView>

<StackLayout Style="{StaticResource ContentStyle}" Orientation="Vertical">
<Label Style="{StaticResource MenuTextStyle}" Text="Here be text" />
<Label Style="{StaticResource MenuTextStyle}" Text="Here be more text" />
<Label Style="{StaticResource MenuTextStyle}" Text="Here be more whatever UI elements you want" />
</StackLayout>

<Grid HeightRequest="10"></Grid>

<Grid Style="{StaticResource ContentStyle}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="10"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Text="Ok" Command="{Binding CloseMenuCommand}"></Button>
<Button Text="Cancel" Grid.Column="2" Command="{Binding CloseMenuCommand}"></Button>

</Grid>
</StackLayout>
</Grid>
</ContentView>
</ContentView>

<ContentView.GestureRecognizers>
<TapGestureRecognizer Command="{Binding CloseMenuCommand}"></TapGestureRecognizer>
</ContentView.GestureRecognizers>

</ContentView>

</Grid

Dit ziet er heel indrukwekkend uit, maar het is vooral de inhoud van de popup. De interessante delen staan in rood/vet aangegeven.

Bovenaan staat "IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}". Dit betekent dat de zichtbaarheid van het gehele control afhankelijk is van de zichtbaarheid van een grid genaamd "AnimatedGrid". Dit heet "element binding" - in plaats van een property van een gebruikers interface aan een view model te binden, binden we aan een property van een ander gebruikers interface element. Laat dat nu net het grid zijn waaraan de behavior is gehangen. Netto effect - zodra de animatie begint, wordt meteen de half doorzichtige achtergrond getoond over het gehele scherm, en die verdwijnt pas als de animatie ook helemaal klaar is. Geheel automatisch, zonder dat daar code voor nodig is. De grijze achtergrond wordt overigens verzorgd door een zogenaamde ContentView, van de kleur "SeeTrough". Deze is in App.Xaml gedefinieerd als #B2000000 - wat designers "70% zwart" noemen. Dit is een zwart vlak dat dus voor 30% doorzichtig is.

De behavior verzorgd de animatie en - indirect middels element binding - het tonen en verbergen van de grijze achtergrond. Beide buttons roepen het CloseMenuCommand aan dat - niet geheel onverwacht - zorgt dat de popup weer verdwijnt (zie view model hieronder). Hetzelfde geldt voor het onderste deel van de XAML - hier maken we gebruik van een zogenaamde TapGestureRecognizer. Als de gebruiker tapt op het grijze waas, wordt het CloseMenuCommand ook aangeroepen en verdwijnt de popup ook, zoals gewenst.

Het viewmodel

Het is bijna te simpel voor woorden, maar dit stuurt het geheel aan vanuit de 'business logic'

using Xamarin.Forms;

namespace XamarinFormsDemos.ViewModels
{
public class PopupViewModel : MenuViewModelBase
{
public Command CloseMenuCommand { get; private set; }

public PopupViewModel() : base()
{
CloseMenuCommand = new Command(() => IsMenuVisible = false);
}
}
}

maar voor de volledigheid laten we het toch maar zien.

Eenmaal gemaakt, simpel te gebruiken

In de pagina PopupPage wordt het menu als volgt gebruikt:

<?xml version="1.0" encoding="utf-8" ?>
<demoViewFramework:BaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:demoViewFramework="clr-namespace:DemoViewFramework;assembly=DemoViewFramework"
xmlns:controls="clr-namespace:XamarinFormsDemos.Views.Controls;assembly=XamarinFormsDemos"
x:Class="XamarinFormsDemos.Views.PopupPage" Style="{StaticResource PageStyle}">
<Grid>
<!-- Page content -->
<Grid.RowDefinitions>
<RowDefinition Height="25*"></RowDefinition>
<RowDefinition Height="75*"></RowDefinition>
</Grid.RowDefinitions>
<Grid Row="0" Style="{StaticResource HeaderOutsideStyle}">
<ContentView Style="{StaticResource ContentStyle}">
<Label Text="Popup menu" Style="{StaticResource HeaderStyle}"
VerticalOptions="CenterAndExpand"></Label>
</ContentView>
</Grid>

<ContentView Grid.Row="1" Style="{StaticResource ContentStyle}" HorizontalOptions="Fill">
<Label Text="Here be page content" Style="{StaticResource ContentTextStyle}"></Label>

<ContentView VerticalOptions="Start" Style="{StaticResource ContentStyle}" >
<Button Text="Open popup menu" Command="{Binding ToggleMenuCommand}"
HorizontalOptions="Fill" VerticalOptions="Start"></Button>
</ContentView>
</ContentView>


<!-- Menu -->
<controls:PopupControl Grid.Row="0" Grid.RowSpan="2"></controls:PopupControl>

</Grid>
</demoViewFramework:BaseContentPage;/Grid>

Ook hier is het weer belangrijk om te zien dat het gebruikers interface element dat over de rest heen moet vallen na de standaard gebruikers interface in de XAML wordt vermeldt, zodat het menu ook echt over de standaard interface heen komt.

Ten slotte

Wederom hebben we laten zien hoe je eenvoudig Business Apps die zowel op Android, iOS als Windows 10 draaien met een behavior van vloeiend lopende animaties kunt voorzien. Deze animaties voldoen ook nog eens netjes aan de architectuur standaards: er is geen harde koppeling tussen gebruikers interface en business logic (hoe beperkt die ook is in het voorbeeld project). Het is wel belangrijk om te weten waar je mee bezig bent en dit goed cross platform te testen. Zo is er bijvoorbeeld tijdens het maken van dit demo project vastgesteld dat als de code bovenin wijzigt van

<Grid
//Namespaces verwijderd
IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}">
<ContentView BackgroundColor="{StaticResource SeeThrough}"
HorizontalOptions="Fill" VerticalOptions="Fill" >

naar

<Grid
//Namespaces verwijderd
>
<ContentView IsVisible="{Binding IsVisible, Source={x:Reference AnimatedGrid}}"
BackgroundColor="{StaticResource SeeThrough}"
HorizontalOptions="Fill" VerticalOptions="Fill" >

je hetzelfde resultaat lijkt te krijgen. Er is echter een subtiel verschil - het buitenste Grid blijft nu altijd zichtbaar, maar omdat alles wat erin staat onzichtbaar is, zie je het verschil niet. Functioneel is er ook geen verschil - totdat je de app op iOS probeert de draaien. Leeg of niet, blijkbaar vangt het Grid toch alle tap events af en je kunt de knop "Open popup menu" niet meer aanklikken. Maar alleen op iOS. Het leven van een cross platform ontwikkelaar zit vol met dit soort verrassingen, maar met een gedegen test aanpak komen dit soort zaken snel te voorschijn. En bij voldoende ervaren developers treden ze niet eens op.