Monthly Archives: February 2020

Move Controls in Xamarin.Forms App with your finger

…or Mouse.. or Pen.. or nose, whatever you like. In WPF this is very easy accomplished, because Drag&Drop is a main UI feature in Windows. Each OS implements this feature slightly different, that’s why there is no such generic solution for all platforms. Microsoft proposes a solution called TouchEffect, which implements a specific Effect for each platform. (https://docs.microsoft.com/de-de/xamarin/xamarin-forms/app-fundamentals/effects/touch-tracking)

But there is still another and quite simple solution, you can use gestures (esp. the PanGesture) for that, with slight limitations.

MoveViewXamarin

The PanGesture

The PanGesture is actually a touch based Drag&Drop. You put the finger down on the screen and moved it around until you raise your finger. The PanGesture will always fire it’s event while the finger is moving around and will be quitted with the coordinates (0,0)

The Implementation

The implementation is quite simple, actually there are only a few lines of code:
1. when panning starts: save current Translation of View
2. while panning: set translation to the starting value + panning value
3. when panning finished: reset variable for starting value

Developers are lazy people, we don’t want to copy that implementation on every View. So the trick is, to use an attached property, so we can use it on every View and even bind it to a ViewModel.

The Code

public static class MoveView
{
    #region attached properties: CanMove
    public static readonly BindableProperty CanMoveProperty =
            BindableProperty.CreateAttached("CanMove", typeof(bool), typeof(View), false,
                propertyChanged: OnCanMoveChanged);

    private static void OnCanMoveChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if ((bool)newValue && bindable is View view)
        {
            PanGestureRecognizer pangest = new PanGestureRecognizer();
            pangest.PanUpdated += Pangest_PanUpdated;
            view.GestureRecognizers.Add(pangest);
        }
    }
    public static bool GetCanMove(BindableObject bindable)
    {
        return (bool)bindable.GetValue(CanMoveProperty);
    }

    public static void SetCanMove(BindableObject bindable, bool value)
    {

        bindable.SetValue(CanMoveProperty, value);
    }
    #endregion

    // saves the translation state on start, it's okay to have it static
    // because we can only move one item at a time
    static Point? _translationStart = null;
    private static void Pangest_PanUpdated(object sender, PanUpdatedEventArgs e)
    {
        if (sender is View view)
        {
            if (e.TotalX == 0 && e.TotalY == 0)
            {
                // movement has end, reset
                _translationStart = null;
            }
            else
            {
                if (_translationStart == null)
                {
                    (view.Parent as Layout)?.RaiseChild(view);
                    _translationStart = new Point(view.TranslationX, view.TranslationY);
                }
                view.TranslationX = e.TotalX + _translationStart.Value.X;
                view.TranslationY = e.TotalY + _translationStart.Value.Y;
            }

        }
    }
}

Usage

 <Frame BorderColor="Red" BackgroundColor="Bisque"
                   dashboard:MoveView.CanMove="True"
                   HeightRequest="100" VerticalOptions="Center"
                 />

Limitations

– only when the parent is a layout, it will raise the View that is moved to the topmost position (only in respect to the parent, not overall)
– you can simply move the View outside of its parent, but then you will not be able to move it back in

Please feel free to use the code and change it as you like. If you find bugs, you may fix them. I would also be happy if you leave me some notes.

Xamarin.Forms FlowLayout

Motivation

I recently faced the challenge to add Items of different sizes to a Page on a Xamarin.Forms App. The result should be some kind of a Dashboard with Diagrams and info boxes on it. Some diagrams are smaller, some bigger.

The first thing, that came in mind, was a FlexLayout, but this looks a little bit odd to me, because the controls on the main axis share the same size in the secondary axis. That is okay if all controls have the same height or width, but this is not the case.

FlexLayout Xamarin.Forms App

But what I actually want is something like this:

FlowLayout - Xamarin.Forms App

It’s very tough to arrange the children of a Layout, when every control has a free size defined. To solve this problem, we need to divide the available area in uniform sections. Using this approach, each control can define it’s size in units, and will be placed on the next available free spot.

Simulator Screen Shot - iPhone 11 - 2020-02-20 at 22.30.18

The Algorithm

  1. The FlowLayout needs a UnitSizeRequested in pixels, we need to calculate how many items can fit vertically and horizontally based on a size of a unit. Afterwards we need to find the actual UnitWidth and UnitHeight so that the whole area of the control is used.
  2. There need to be two attached properties for the child elements, so each element can specify its HorizontalUnits and VerticalUnits.
  3. Finally we create an array as a representation of the FlowLayout so we can arrange the Controls on it.

The Code

using System;

using Xamarin.Forms;

namespace Dashboard
{
    public class FlowLayout : AbsoluteLayout
    {
        #region attached properties: HorizontalUnitsProperty
        public static readonly BindableProperty HorizontalUnitsProperty =
                BindableProperty.CreateAttached("HorizontalUnits", typeof(int), typeof(FlowLayout), 1);
        public static int GetHorizontalUnits(BindableObject view)
        {
            return (int)view.GetValue(HorizontalUnitsProperty);
        }

        public static void SetHorizontalUnits(BindableObject view, int value)
        {
            view.SetValue(HorizontalUnitsProperty, value);
        }
        #endregion
        #region attached properties: VerticalUnitsProperty
        public static readonly BindableProperty VerticalUnitsProperty =
                BindableProperty.CreateAttached("VerticalUnits", typeof(int), typeof(FlowLayout), 1);

        public static int GetVerticalUnits(BindableObject view)
        {
            return (int)view.GetValue(VerticalUnitsProperty);
        }

        public static void SetVerticalUnits(BindableObject view, int value)
        {
            view.SetValue(VerticalUnitsProperty, value);
        }
        #endregion

        #region properties
        public double UnitSizeRequested { get; set; } = 100;
        public double UnitWidth { get; private set; }
        public double UnitHeight { get; private set; }
        #endregion
       

        protected override void OnSizeAllocated(double width, double height)
        {
            // when the control allocates the size, we will arrange the children
            base.OnSizeAllocated(width, height);
            if (width>0 && height>0)
            {
                ArrangeChildren();
            }
        }

        

        public void ArrangeChildren()
        {
            // do the calculation (step 1)
            int horizontalUnitCount = (int)(Width / UnitSizeRequested);
            int verticalUnitCount = (int)(Height / UnitSizeRequested);
            bool[,] dashArray = new bool[horizontalUnitCount, verticalUnitCount];
            UnitWidth = UnitSizeRequested + (Width % UnitSizeRequested) / horizontalUnitCount;
            UnitHeight = UnitSizeRequested + (Height % UnitSizeRequested) / verticalUnitCount;
            foreach (var child in Children)
            {
                // for each child - find the 
                var rect = FindFreeRectangle(dashArray, GetHorizontalUnits(child), GetVerticalUnits(child));
                if (rect != null)
                {
                    AbsoluteLayout.SetLayoutBounds(child, rect.Value);
                }
                else
                {
                    // this control can not be placed ... so just skip it
                    AbsoluteLayout.SetLayoutBounds(child, new Rectangle(0, 0, 0, 0));
                }

            }
        }

        private Rectangle? FindFreeRectangle(bool[,] dashArray, int xCount, int yCount)
        {
            Rectangle res;
            for (int y = 0; y  dashArray.GetLength(1)
                )
                return false;

            for (int xx = x; xx < x + xCount; xx++)
            {
                for (int yy = y; yy < y + yCount; yy++)
                {
                    if (dashArray[xx, yy] == true)
                        return false;
                }
            }
            // now reserve fields
            for (var xx = x; xx < x + xCount; xx++)
            {
                for (var yy = y; yy < y + yCount; yy++)
                {
                    dashArray[xx, yy] = true;
                }
            }
            return true;
        }

      
    }
}

Caution: This code is not complete, perhaps you can not use it with the BindableLayout-Extension. Feel free to copy the code and change it as needed. Be careful in choosing the right UnitSizeRequested, a small unit will probably result in a rather bad performance.

Usage

<FlowLayout BackgroundColor="Black" >
        <Frame BorderColor="Yellow" BackgroundColor="Gray" CornerRadius="0" 
                FlowLayout.HorizontalUnits="1"
                FlowLayout.VerticalUnits="1"
                 />
        <Frame BorderColor="Gray" BackgroundColor="HotPink" CornerRadius="0" 
                FlowLayout.HorizontalUnits="1"
                FlowLayout.VerticalUnits="1"
                 />
        <Frame BorderColor="Blue" BackgroundColor="Honeydew" CornerRadius="0" 
                FlowLayout.HorizontalUnits="1"
                FlowLayout.VerticalUnits="1"
                 />
        <Frame BorderColor="Red" BackgroundColor="Bisque" CornerRadius="0" 
                FlowLayout.HorizontalUnits="4"
                FlowLayout.VerticalUnits="1"
                 />
        <Frame BorderColor="Green" BackgroundColor="CadetBlue" CornerRadius="0" 
                FlowLayout.HorizontalUnits="3"
                FlowLayout.VerticalUnits="3"
                 />
</FlowLayout>

Adding Themes to Xamarin.Forms App

I just want to make some short notes about themes in Xamarin.Forms Apps, since there are already a lot of tutorials available online, that go way deeper.

Themes
Themes are actually ResourceDictionaries. Each atomic type you use in your XAMLs and Styles, will be defined here. Types are f.e. Color, Double (for font sizes), etc.

<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="ShoppingList.Themes.DarkTheme">
    <Color x:Key="BackgroundColor">#333333</Color>
    <Color x:Key="TextColor">#eeeeee</Color>
    <Color x:Key="PlaceholderTextColor">#55ffffff</Color>
    <Color x:Key="ControlBackground">#111111</Color>
    <Color x:Key="ButtonBackgroundColor">#11ffffff</Color>
</ResourceDictionary>

Be sure to have a code behind file for your ResourceDictionary, that calls InitializeComponent(). Otherwise this will only work on UWP (without .NET Toolchain) and no other platforms.

Styles
In most cases the styles are not part of your themes, because using themes just makes you change the look of your pages and controls, not the styling of specific controls. Perhaps styles are using the resources, that you define in your theme. Be sure to use DynamicResource for linking to the key of the resource, otherwise you can not change themes during runtime. (Restart will be needed)

<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="ShoppingList.Resources.Styles">
    <Style TargetType="NavigationPage">
            <Setter Property="BackgroundColor" Value="{DynamicResource BackgroundColor}"/>
            <Setter Property="BarBackgroundColor" Value="{DynamicResource BackgroundColor}"/>
            <Setter Property="BarTextColor" Value="{DynamicResource TextColor}"/>
            
        </Style>
        <Style TargetType="Label">
            <Setter Property="TextColor" Value="{DynamicResource TextColor}" />
        </Style>
        <Style TargetType="Entry">
            <Setter Property="BackgroundColor" Value="{DynamicResource ControlBackground}" />
            <Setter Property="TextColor" Value="{DynamicResource TextColor}" />
            <Setter Property="PlaceholderColor" Value="{DynamicResource PlaceholderTextColor}" />
        </Style>
        <Style TargetType="Button">
            <Setter Property="TextColor" Value="{DynamicResource TextColor}" />
            <Setter Property="BackgroundColor" Value="{DynamicResource ButtonBackgroundColor}" />
            
        </Style>
</ResourceDictionary>

Changing the theme
Now you just need to add the following code to your App.xam.cs, then you can just call App.SetTheme(typeof(DarkTheme)) to change the theme during runtime.

Be sure to add all your style-ResourceDictionaries and a standard theme to your App.xaml in the Application.Resources-Section

internal static void SetTheme(Type themeType)
{
   Preferences.Set("Theme", themeType.FullName);
   var resDict = (ResourceDictionary)Activator.CreateInstance(themeType);
   App.Current.Resources.MergedDictionaries.Add(resDict); // this line replaces all keys of the current dictionary with the value of the selected dictionary, only the keys that are present in the selected dictionary will be replaced
}

In your “OnInitialize” method of your App.xaml.cs you can put the following code, to load the saved theme on startup:

if (Preferences.ContainsKey("Theme"))
{
   var themeTypeName = Preferences.Get("Theme",null);
   var themeType = Type.GetType(themeTypeName);
   if (themeType != null)
   {
      SetTheme(themeType);
   }
}