Friday, September 25, 2015

Creating an Image Viewer In Xamarin.Forms with NControls

This blog post is about how to create custom controls within the Xamarin.Forms platform using the NControl library (which you can read more about here).
The scope for this article is to show how to create a control that supports simple scaling and panning (create gesture controls is something we'll save for later.) and has a bindable Source property that can be used in a view to connect with an ImageSourceproperty in your viewmodel. 
What we'll do in this blog post is:
  • Create a new Cross Platform Xamarin.Forms project
  • Install NControl from NuGet
  • Create a new custom control (our Image Viewer control)
  • Add a bindable property for the Image source property in our control
  • Add an inner image in our control
  • Handle touch events to add panning

New Project

Create a new Cross Platform App in Xamarin Studio by opening the File > New Solution menu and follow the wizard so you end up with a solution containing an iOS, Android and shared project (and maybe a project for UI testing as well).

NuGet Packages

To build our control we need to add the NControl libraries using the NuGet package manager included in Xamarin Studio (I'll skip this part from this blog post) - make sure you install the NuGet package NControl in the Android, iOS and Shared PCL project.
Make sure you add the correct initialization of the library:

Android

In your MainActivity.cs file:
protected override void OnCreate (Bundle bundle)
{
    base.OnCreate (bundle);

    global::Xamarin.Forms.Forms.Init (this, bundle);
    NControlViewRenderer.Init ();

    LoadApplication (new App ());
}

iOS

In your AppDelegate.cs file
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
    global::Xamarin.Forms.Forms.Init ();
    NControlViewRenderer.Init ();

    LoadApplication (new App ());

    return base.FinishedLaunching (app, options);
}
Use the ALT+ENTER keyboard shortcut to get Xamarin Studio to insert the correct using statements for the new lines you've added.

Create a new Control

Add a new class to your shared PCL project called ImageViewer.cs. This is the starting point for our container control. Make sure it inherits from NControlView:
using System;
using NControl.Abstractions;
using Xamarin.Forms;
using System.Linq;

namespace DraggableImageTest
{
    public class ImageViewer: ContentView
    {
    }
}

Adding a Bindable Property

Next up we need to give our control a property that can be bound to a view model to display images in the file ImageViewer.cs:
public static BindableProperty SourceProperty = 
    BindableProperty.Create<ImageViewer, ImageSource> (p => p.Source, null,
        BindingMode.TwoWay, propertyChanged: (bindable, oldValue, newValue) => {
            var ctrl = (ImageViewer)bindable;
            ctrl.Source = newValue;
        });

public ImageSource Source {
    get{ return (ImageSource)GetValue (SourceProperty); }
    set {
        SetValue (SourceProperty, value);
    }
}

Adding a Test Image

No lets add an image for testing! Find your favourite image and add it to the PCL project. Right-click on the file and set Build Actionto EmbeddedResource. This way the image will be included in the assembly as a resource you can load during runtime.
To be able to inspect and retreive a resource from our assembly we need to add the System.Reflection namespace to our using list:
using System.Reflection;
Now lets update our main page to show our control. Open the startup file in your project (usually the csharp file in your PCL project with the same name as the project itself):
public App ()
{
    // The root page of your application
    MainPage = new ContentPage {
        Content = new StackLayout {
            VerticalOptions = LayoutOptions.Center,
            Children = {
                new Label {
                    XAlign = TextAlignment.Center,
                    Text = "Image Container:"
                },
                new ContentView{
                    BackgroundColor = Color.Gray,
                    Padding = 25,
                    HeightRequest = 200,
                    Content = new ImageViewer{
                        Source = ImageSource.FromStream(() => {
                            var assembly = this.GetType().GetTypeInfo().Assembly;
                            return assembly.GetManifestResourceStream ("Your namespace.Your image filename");
                        }),
                    }                           
                }
            }
        }
    };
}
As you can see we're wrapping the image viewer in a ContentView to enable some padding and color to make our control stand out. 
Make sure you update the line with the code return assembly.GetManifestResourceStream ("Your namespace.Your image filename") with your own namespace and filename.

Add Inner Image Control

To display an image and be able to move and scale it, we'll add an inner Image control in our ImageViewer control. Declare a private field in your ImageViewer class:
private Image _image;
In the ImageViewer constructor we'll instantiate the Image view:
public ImageViewer ()
{
    IsClippedToBounds = true;

    _image = new Image ();
    _image.Scale = 2.0;
    _image.InputTransparent = true;

    Content = _image;
}
The key to get the displayed image to scale and pan inside our viewport is to set the IsClippedToBounds property of the ImageViewer control to true to hide everything outside our control's bounds.
Update the setter for the Source property to update the Image view:
public ImageSource Source {
    get{ return (ImageSource)GetValue (SourceProperty); }
    set {
        SetValue (SourceProperty, value);
        _image.Source = value;
    }
}
Now you should be able to run the app and see if it shows your image zoomed in and inside your control's bounds.

Add Panning

NControlView has support for touch handling, and as an exercise we'll add simple panning to our ImageViewer control.
Add a new field in the ImageViewer class that we can use to calculate deltas when handling touches:
private NGraphics.Point _startPoint;
Now override the TouchesBegan method in the ImageViewer class to begin handling touches:
public override bool TouchesBegan (System.Collections.Generic.IEnumerable<NGraphics.Point> points)
{
    _startPoint = points.FirstOrDefault ();
    _startPoint.X -= _image.TranslationX;
    _startPoint.Y -= _image.TranslationY;
    return true;
}
First of all we'll save the location when touches starts. Next we add the current translation for the image to be able to pan multiple times. Remember to return true in the TouchesBegan event to tell NControlView that you have handled the touch.
When touches moves on the screen, we'll just calculate the delta and update the image's translation:
public override bool TouchesMoved (System.Collections.Generic.IEnumerable<NGraphics.Point> points)
{
    var newPoint = points.FirstOrDefault ();
    var diff = new NGraphics.Point (newPoint.X - _startPoint.X, newPoint.Y - _startPoint.Y);

    _image.TranslationX = diff.X;
    _image.TranslationY = diff.Y;

    return true;
}
Read the current location, calculate the diff and update the image's translation. Easy?
Lastly we'll add TouchesCancelled and TouchesEnded as well:
public override bool TouchesCancelled (System.Collections.Generic.IEnumerable<NGraphics.Point> points)
{           
    return true;
}

public override bool TouchesEnded (System.Collections.Generic.IEnumerable<NGraphics.Point> points)
{           
    return true;
}
Run the app and see if you can pan the image around.

Next Step

The next step would be to create some kind of Gesture class where we can abstract away the inner logic of gestures to be able to implement zooming by pitching and double/tripple tapping to zoom in/out.

Code

Tuesday, September 01, 2015

Playing Sound in Xamarin.Forms

Background

Xamarin.Forms does not have built-in way of playing sound, but it is easy to implement this using platform specific code on Android and iOS. This blog post describes how to build a project that implements a sound playing service with Xamarin.Forms.
We'll begin this walkthrough with a new project in Xamarin Studio using the Cross Platform App - Blank Xamarin.Forms App template as a starting point. 

Code

First of all we'll have to define the interface that should be implemented in our platform projects. Add a new interface to the Forms project in your solution called ISoundProvider:
public interface ISoundProvider{
  public Task PlaySoundAsync(string filename);
}
Remember that you can press ALT+ENTER on classes that the Xamarin Studio editor doesn't know to bring up the refactoring dropdown that automatically inserts the correct using statements in your source file.*
Next lets create the abstraction class that will be used in the Xamarin.Forms app to play sound in our Forms project:
public class SoundService{ 
  private readonly ISoundProvider _soundProvider;
  public SoundService(){
    
  }
  public async Task PlaySoundAsync(string filename){    
    return _soundProvider.PlaySoundAsync(filename);
  }
}
We'll get back to how you look up the platform implementation to get an instance that you can assign to the _soundProvidermember.
Now we should be ready to create the platform specific providers!

Android

Lets start with Android first - add the following code to your Droid project in a file called AndroidSoundProvider.cs:
public class AndroidSoundProvider: ISoundProvider
{
    public Task PlaySoundAsync (string filename)
    {
        // Create media player
        var player = new MediaPlayer();     

        // Create task completion source to support async/await
        var tcs = new TaskCompletionSource<bool> ();

        // Open the resource
        var fd = Xamarin.Forms.Forms.Context.Assets.OpenFd (filename);

        // Hook up some events
        player.Prepared += (s, e) => {            
            player.Start();
        };

        player.Completion += (sender, e) => {
            tcs.SetResult(true);
        };

        // Start playing
        player.SetDataSource (fd.FileDescriptor);           player.Prepare ();

        return tcs.Task;
    }
}
As you can see we're using the async/await pattern in our little app to make it possible to play a sound and wait for it to return. This is done by creating a TaskCompletionSource that can be used to signal back to the caller that we are done playing our sound. 
Since we'll be adding the sound file as an asset to our Android project, we're using the asset system in Android to load the file. After we've hooked up some events to be able mark the task as completed we'll set the data source and ask the player to prepare before we return the awaitable task object.

iOS

Next we'll create the iOS provider:
public class TouchSoundProvider: NSObject, ISoundProvider
{
    private AVAudioPlayer _player;

    public Task PlaySoundAsync (string filename)
    {
        var tcs = new TaskCompletionSource<bool> ();

        string path = NSBundle.MainBundle.PathForResource(Path.GetFileNameWithoutExtension(filename), 
            Path.GetExtension(filename));

        var url = NSUrl.FromString (path);
        _player = AVAudioPlayer.FromUrl(url);

        _player.FinishedPlaying += (object sender, AVStatusEventArgs e) => {
            _player = null;
            tcs.SetResult(true);
        };

        _player.Play();

        return tcs.Task;
    }
}
This code is as you can see almost identical to the code on Android, except that we're using the iOS Frameworks to play the sound andthe iOS resource system to load the sound file.

Connecting the Dots

Now we need to tie these classes together. The abstract SoundService class does not yet know about our platform specific implementations. In Xamarin.Forms we have access to a simple dependency service that helps us make platform specific classes defined by interfaces available through a lookup mechanism. 
To tell the dependency service that we have an implementations of the ISoundProvider interface we add an assembly attribute to the classes in our platform projects:
Add the following assembly attributes to Android implementations (above the namespace line):
[assembly: Dependency (typeof (AndroidSoundProvider))]
and to the iOS implementation:
[assembly: Dependency (typeof (TouchSoundProvider))]
Now we should be ready to use the classes through the dependecy service in Xamarin.Forms. Update the abstraction class constructor to:
public SoundService(){
    _soundProvider = DependencyService.Get();
}
You can read more about the Xamarin Forms Dependency Service here: https://developer.xamarin.com/guides/cross-platform/xamarin-forms/dependency-service/

Adding the Sound Resources

There are different ways of adding the sound resources - in this sample I've decided to add them as native resources to the platform projects. 
On iOS you can add your sound file to the Resources folder - this will make the file a Bundle Resource (right-click on the sound file and ensure its build action is set to Bundle Resource). 
In your Android project you'll add the sound file to the Asset folder and right-click and set the build action to AndroidAsset.

Add a Button to Play the Sound

In your Forms project you can now add a button that lets you play the sound you added (in this example it is called sound.mp3). Update your App class' constructor to:
public App ()
{
    // The root page of your application
    MainPage = new ContentPage {
        Content = new StackLayout {
            VerticalOptions = LayoutOptions.Center,
            Children = {
                new Label {
                    XAlign = TextAlignment.Center,
                    Text = "Click to play a sound!"
                },
                new Button{
                    Text = "Play",
                    Command = new Command(async () => {
                        var soundService = new SoundService();
                        await soundService.PlaySoundAsync("sound.mp3");
                    })
                }
            }
        }
    };
}
Notice how the code uses a Command to execute when the user taps the button, and how the command object supports the async/await pattern that we've already implemented in our code.
Now run and test your apps - they should play your sound when you tap the play button!
Full code can be downloaded from Github.