Buttons with image and StackLayout in Xamarin.Forms Apps

Here’s what we’re going to build with Xamarin Forms:

Image for post
Image for post
Figure 1

If you just want the source code, it’s in this repository.

In this sample app, on the bottom of the screen is a button named “Retry”. The button extends across the width of the screen. The button has a text and image to its right. The use case is simple. When the app is first launched, if the app is not connected to the network or the server is not accessible, the screen will display an error icon and a button for the users to retry connecting. That’s shown in Figure 1. While connecting, the ActivityIndicator should be displayed and nothing else as shown in Figure 2:

I’d like to add an image next to the text inside the “Retry” button because, well, images are everywhere in apps nowadays. What you see in the picture is exactly how I’d like to arrange the error icon and the button.

Why not out-of-the-box Xamarin button? Doesn’t it already have an Image property? Yes, but because it is very limited and does not allow any flexibility for us to position the image and the text inside the button the way we want. The button’s image is always to the left of the text. That’s not a big deal, but if we want to have a button that has its width equal to the width of the screen, then it does not look right. This is how it looks.

Image for post
Image for post

Ignore the color because that can be easily fixed, the image is way in the far left, while the text is centered. That is a not a good look.

If you find yourself not having a good grasp of using StackLayout in Xamarin Forms and want to learn more, you follow this article, you will have a recipe to follow in most cases. You will be able to arrange and position anything and everything.

This app will only have one page. I will name this page as MainPage, so the XAML file is MainPage.xaml and the code behind is MainPage.cs.

We want the button to be at the bottom of the page and extend all the way, so let’s have the page with the button:

Gist 1

Line 18 is actually a bunch of lines, but don’t worry for now as we’ll fill them in later. The outer most StackLayout takes the entire page. It starts at line 17 and closes at line 27. We want the “button” (simulated button, not Xamarin button) to at the bottom of the screen, so we wrap the “button” (again that’s simulated button, not Xamarin button) in a StackLayout with VerticalOptions of End. This wrapper StackLayout is between lines 19 and 26.

The button consists of everything inside and including the StackLayout between lines 20 and 25. So when the user touches anywhere inside this area, we want that to respond like a button tap event. That’s why we give it an x:Name, because we will add gesture to it later. Let’s put the text (which is really a label) and an image inside this StackLayout which is x:Named ButtonStackLayout.

But first, let’s see my recipe for dealing with VerticalOptions and HorizontalOptions; it’s actually simple. Take any area (in other words any space or StackLayout) and no matter how big (of course the whole screen is the biggest) or how small, we can always mentally divide it into 3 parts: beginning, middle and end. In Xamarin Forms terminology, that’s Start, Center and End. For each of these, there’s also a counter part StartAndExpand, CenterAndExpand and EndAndExpand. There’s also Fill and FillAndExpand, but if I find that with my recipe I don’t have to use Fill or FillAndExpand too often. By the way, you can refer to http://stackoverflow.com/questions/25338533/what-is-the-difference-between-xamarin-forms-layoutoptions-especially-fill-and.

But for now, hear me out. In my way, I always have 1 or more Start and/or StartAndExpand, 1 or more Center and/or CenterAndExpand, 1 or more End and/or EndAndExpand. The button we want to create has a label and an image, and if you look at the Figure 1 again, we want both of them to be centered. Therefore they should both get HorizontalOptions of Center. But we don’t want these 2 controls (views is the official term) to get shifted toward either end, so we’ll have an empty StackLayout with StartAndExpand and an empty StackLayout with EndAndExpand. Any StackLayout with the …AndExpand option holds the space, therefore prevents anything to get shifted toward its direction. The code for the button is from line 21 through line 24.

For a moment, let’s say we omit or comment out line 24, this will become:

Image for post
Image for post
Figure 3

If we replace the HorizontalOptions of EndAndExpand in line 24

<StackLayout HorizontalOptions=”EndAndExpand”></StackLayout>

with simply End

<StackLayout HorizontalOptions=”End”></StackLayout>

we’ll end up with Figure 3 as well.

Likewise, if line 21 is removed or the StartAndExpand in HorizontalOptions is replaced by Start, we’ll end up with

Image for post
Image for post
Figure 4

Now let’s say we want:

Image for post
Image for post
Figure 5

Then lines 19 through 26 would become

which seems like I’m not following my own recipe because I don’t have any StackLayout with Center or CenterAndExpand options.

If I was going to follow my recipe, then I would do:

Now I have 1 Start (line 3 through 5), 1 CenterAndExpand (line 6) and 1 End (line 7 through 9).

We’ll circle back to layout for the rest of the page, but for now, let’s add action when the button is tapped.

Remember if users tap anywhere inside the StackLayout x:Named ButtonStackLayout, or tap on the label or tap on the image, we want to the action to be called.

We define 3 variables:

private Label _btnLabel;
private Image _btnImage;
private StackLayout _btnStackLayout;

then in the constructor of MainPage:

public MainPage ()
{
InitializeComponent ();

_btnLabel = this.FindByName<Label>(“ButtonLabel”);

_btnLabel.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(async () => await OnConnect()),
});

_btnImage = this.FindByName<Image>(“ButtonImage”);
_btnImage.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(async () => await OnConnect()),
});

_btnStackLayout = this.FindByName<StackLayout(“ButtonStackLayout”);
_btnStackLayout.GestureRecognizers.Add(new TapGestureRecognizer
{
Command = new Command(async () => await OnConnect()),
});
}

In the OnConnect(), we want to give the button some animation and also carry out some action. Here’s how the OnConnect look like:

private async Task OnConnect()
{
await _btnImage.ScaleTo(0.9, 50, Easing.Linear);
await Task.Delay(100);
await _btnImage.ScaleTo(1, 50, Easing.Linear);

animateButtonTouched(_btnStackLayout, 1500, “#F8F8F8”, “#E3E3E3”, 1);
animateButtonTouched(_btnLabel, 1500, “#F8F8F8”, “#E3E3E3”, 1);
animateButtonTouched(_btnImage, 1500, “#F8F8F8”, “#E3E3E3”, 1);

await Connect();
}

There are 2 animation taking place when the button is tapped: Scale the image to 90% and back quickly (100 ms). We also change the background color of the label, the image and StackLayout quickly and change it back (1.5 sec). This is no ripple effect of the native button, but at least it gives the visual feedback to the users.

Here’s the aimateButtonTouched()’s which handles the animation of background color.

private void animateButtonTouched(View view, uint duration, string hexColorInitial, string hexColorFinal, int repeatCountMax)
{
var repeatCount = 0;
view.Animate (“changedBG”, new Animation ((val) => {
if (repeatCount == 0)
{
view.BackgroundColor = Color.FromHex(hexColorInitial);
}
else
{
view.BackgroundColor = Color.FromHex(hexColorFinal);
}
}), duration, finished: (val, b) => {
repeatCount++;
}, repeat: () => {
return repeatCount < repeatCountMax;
});
}

The Connect() simply delays by a short time (1.5 s) to simulate the network connection time.

Back to layout for the entire MainPage, we already completed the StackLayout at the End which is the button, we need to have 1 or more StackLayout with Start (or StartAndExpand) option and 1 or more Center (or CenterAndExpand).

The entire code is shown below:

Gist 2

Lines 26 through 33 were discussed at length above.

The purpose of line 25, a StackLayout with VerticalOptions of CenterAndExpand is to prevent something like Figure 6 from happening.

Image for post
Image for post
Figure 6

Similarly, if line 18, which defines a StackLayout with VerticalOptions of StartAndExpand is to prevent something like in Figure 7 from happening,

Image for post
Image for post
Figure 7

And if both lines 18 and 25 were missing, then it looks totally messed up like in Figure 8.

Image for post
Image for post
Figure 8

Now let’s say we want to have a label right under the error icon that says something like “ Network not reachable. Please try again.” and a label under the ActivityIndicator that says “Connecting…”. That’s easy. Just add 2 additional StackLayout, each with VerticalOptions of Center. Here’s the gist for that:

Summary

  1. Xamarin Forms out-of-the-box Button does not give us too much flexibility. We can mimic by combining label and image and arrange them in a StackLayout.
  2. Then we write code to trigger the action handler when the label, image or StackLayout is tapped on. In the action handler, we add animation to change background color and scale the image temporarily and briefly.
  3. Mentally divide a StackLayout area in 3 imaginary parts: Start, Center and End. There’s no clear dividing line or percentage of each. Have at least one StackLayout for each area. Use an empty StackLayout with the AndExpand option as a placeholder.

Written by

Driven by passion and patience. Read my shorter posts https://dev.to/codeprototype (possibly duplicated from here but not always)

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store