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 ImageSource
property 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
filepublic override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init ();
NControlViewRenderer.Init ();
LoadApplication (new App ());
return base.FinishedLaunching (app, options);
}
Use theALT+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 Action
to 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 codereturn 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 theIsClippedToBounds
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
Download the code here: https://github.com/chrfalch/ImageViewTester
No comments:
Post a Comment