The platform-independent shared code
The Ball element expands across the whole MainPage. The ball is red and centered using the position (0.5, 0.5). (Since we will exchange coordinates between shared and platform-specific code, we would need to convert between device-independent and -dependent pixels. Therefore, it is often useful to work with floating point coordinates relative to the element size. Thus, (0, 0) is the top left corner, (1, 1) is the bottom right corner.)
MainPage = new ContentPage {
Content = new Ball {
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
Position = new Point(0.5, 0.5),
Color = Color.Red,
},
};
The Ball class is derived from the Xamarin.Forms BoxView.
public class Ball: BoxView
The only additional public property is the ball Position.
public Point Position { get; set; }
Another Point stores the offset between ball and touching finger. It will be used for computing the new ball position depending on the current finger location.
Point offset;
The Ball class has two public methods for handling finger gestures. The first one, Down, is called when a new finger touches the screen and computes the offset from current ball and finger coordinates.
public void Down(double x, double y)
{
offset = new Point(x - Position.X, y - Position.Y);
}
Whenever an already touching finger moves, the second method Pan is called. Given the new finger position and the previously stored offset, it computes the new ball Position. Afterwards, we trigger OnPropertyChanged. On the platform-specific side this will cause a redraw of the UI element.
public void Pan(double x, double y)
{
Position = new Point(x - offset.X, y - offset.Y);
OnPropertyChanged();
}
So far we have implemented the platform-independent part of our new UI element. Now we create new custom renderers for each platform. They define how to draw a Ball and call the gesture handling methods.
The iOS renderer
First we add a new class BallRenderer to the iOS project. It is a partial class and derives from BoxRenderer.
public partial class BallRenderer: BoxRenderer
The partial keyword allows to spread the class implementation across multiple source code files. In our example we split the rendering part and the gesture recognition.
To link the shared UI element Ball with its iOS renderer, we add the following assembly attribute above the namespace definition.
[assembly:ExportRenderer(typeof(Ball), typeof(BallRenderer))]
We override the OnElementChanged method and add a line to trigger the drawing method whenever an element property changed. This will be the case when a touching finger moves and the ball position changes. The background color White clears the canvas. The screen would be red otherwise, since Ballderives from BoxView with a Color property set to Red.
protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
{
base.OnElementChanged(e);
Element.PropertyChanged += (s_, e_) => SetNeedsDisplay();
BackgroundColor = UIColor.White;
}
Finally, we override the Draw method. Using the Width and Height of the associated UI ball we compute the iOS screen coordinates. (Remember that we defined Position to contain relative coordinates.) Then, within the current drawing context, we set the fill color, create a small rectangle around the ball position, add an elliptical path defined by that rectangle and draw the path with drawing mode Fill.
public override void Draw(CGRect rect)
{
var ball = Element as Ball;
var x = ball.Position.X * ball.Width;
var y = ball.Position.Y * ball.Height;
using (var context = UIGraphics.GetCurrentContext()) {
context.SetFillColor(ball.Color.ToCGColor());
var ballRect = new CGRect((float)x - 10, (float)y - 10, 20, 20);
context.AddEllipseInRect(ballRect);
context.DrawPath(CGPathDrawingMode.Fill);
}
}
After implementing – and testing – the rendering part, we add another file named “BallGesture” to the iOS project. But instead of creating a new class, we continue the partial class BallRenderer. (Note that we don’t need another assembly attribute nor specifying the inheritance.)
public partial class BallRenderer
The touch gestures are handled with two separate methods. First, we override TouchesBegan, basically converting the touch location to relative coordinates and calling the platform-independent method Down.
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
var touch = touches.AnyObject as UITouch;
if (touch != null) {
var location = touch.LocationInView(this);
(Element as Ball).Down(location.X / Element.Width, location.Y / Element.Height);
}
}
Furthermore, we override TouchesMoved. The implementation is almost identical, but calls Pan in this case.
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
var touch = touches.AnyObject as UITouch;
if (touch != null) {
var location = touch.LocationInView(this);
(Element as Ball).Pan(location.X / Element.Width, location.Y / Element.Height);
}
}
Note that iOS provides two more methods for handling touch events: TouchesEnded is called when the finger is raised from the screen and TouchesCancelled is called when it leaves the screen, e.g. via the edges of the touchable area. In this example, however, we restrict to the two above-mentioned methods.
The Android renderer
On Android the custom BallRenderer is conceptually equal to iOS, but differs in many details. As on iOS, we need a new partial class BallRenderer.
public sealed partial class BallRenderer: BoxRenderer
The sealed keyword prevents other classes from deriving from BallRenderer and fixes a compiler warning when adding the constructor code given below. But before implementing the renderer, we make sure to add the assembly attribute to link the renderer with the shared UI element Ball.
[assembly:ExportRenderer(typeof(Ball), typeof(BallRenderer))]
For a custom renderer on Android, we always need to decide whether we will draw the element. Therefore, we set:
public BallRenderer()
{
SetWillNotDraw(false);
}
Again, we need to trigger the drawing procedure when the element changed. For this purpose, there is the Invalidate command on Android.
protected override void OnElementChanged(ElementChangedEventArgs<BoxView> e)
{
base.OnElementChanged(e);
Element.PropertyChanged += (s_, e_) => Invalidate();
}
To draw the ball, we convert the position from relative coordinates to screen coordinates, clear the screen by drawing a large black rectangle and draw a circle with the Element’s Color. (Note that we don’t compute the circle radius from a device-independent measure. Consequently, its size might differ between iOS and Android.)
public override void Draw(Canvas canvas)
{
var ball = Element as Ball;
var x = ball.Position.X * ball.Width;
var y = ball.Position.Y * ball.Height;
canvas.DrawRect(canvas.ClipBounds, new Paint{ Color = Android.Graphics.Color.Black });
canvas.DrawCircle((float)x, (float)y, 20, new Paint { Color = ball.Color.ToAndroid() });
}
For handling touch events, we create a new file “BallGesture” within the Android project and continue the partial class BallRenderer. As in iOS, we don’t need another assembly attribute, the inheritance nor the sealed keyword.
public partial class BallRenderer
In contrast to iOS, we can handle all touch events in one overridden method OnTouchEvent. Depending on the Action, Down or Move, we call the shared methods Down or Pan with the computed relative touch coordinates.
public override bool OnTouchEvent(MotionEvent e)
{
var x = e.RawX / Element.Width;
var y = e.RawY / Element.Height;
if (e.Action == MotionEventActions.Down)
(Element as Ball).Down(x, y);
if (e.Action == MotionEventActions.Move)
(Element as Ball).Pan(x, y);
return true;
}