Wasi's Blog Awesome Blog Stuff

Creating an Android Custom View in Xamarin

Hello there!

I’ve been playing with Xamarin on Android quite a bit recently. If you don’t know what Xamarin is and you’re interested in cross-platform development, you should definitely check it out: Xamarin. Even if you don’t want to use Xamarin, you can directly apply this knowledge to native Android Java.

Custom Views

In this session, I am going to create a simple custom Android view with Xamarin. The final product will look something like below:

final

Why?

The beauty of Xamarin is that it allows you to use Xamarin forms to write UI code once and it will work perfectly on both Android and iOS (and even Windows phone!). However, sometimes when you need to do a really complicated custom view, using forms alone won’t be enough.

What You Need to Get Started

  • Xamarin Studio (or Visual studio with the Xamarin SDK)
  • An Android Phone (ideally with 4.0+ but 3.0+ should work too)
  • Some free time (should take roughly 45min-1hr depending on how fast you want to go through it)

The view itself is a little contrived but the purpose here is to show how seamlessly the native Android code maps so to the Xamarin code. If you’ve ever done custom views in native Java, this will look extremely familiar. If you want to skip everything and just get to the final code, just have a look at the bottom. Be sure to add the names array if you want it to work properly.

Setup

Fire up Xamarin studio and create a new Android project.

step0

Clean up

Lets get rid of the button since we won’t be using it. From the panel on the left, go to Resources -> Layout -> Main.axml and get rid of the button element. So your main.axml should look like this:

1
2
3
4
5
6
7
8
9
	

	<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    	android:orientation="vertical"
    	android:layout_width="fill_parent"
    	android:layout_height="fill_parent">

	</LinearLayout>

Lets also get rid of the button related code in the MainActivity.cs (which can be found at root of the project). So you code should look something like this for now:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	

	using Android.App;
	using Android.OS;

	namespace XamarinDemo
	{
		[Activity (Label = "XamarinDemo", MainLauncher = true, Icon = "@drawable/icon")]
		public class MainActivity : Activity
		{

			protected override void OnCreate (Bundle bundle)
			{
				base.OnCreate (bundle);

				// Set our view from the "main" layout resource
				SetContentView (Resource.Layout.Main);
			
			}
		}
	}

Step 1

Now, let’s create a class that will implement our custom view. Right click on your project and add a new file. Call it whatever you like but make sure you’re consistent thoughout the rest of the tutorial:

step1

Lets extend the View class and put the necessary minimum we need to get the view class working. Don’t be too concerned with the attributeset and style constructor as that is somewhat outside the (time) scope of this tutorial.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	

	using Android.Views;
	using Android.Content;
	using Android.Util;

	namespace XamarinDemo
	{
		public class AwesomeView : View
		{
			Context mContext;
			public AwesomeView(Context context) :
			base(context)
			{
				init (context);
			}
			public AwesomeView(Context context, IAttributeSet attrs) :
			base(context, attrs)
			{
				init (context);
			}

			public AwesomeView(Context context, IAttributeSet attrs, int defStyle) :
			base(context, attrs, defStyle)
			{
				init (context);
			}

			private void init(Context ctx){
				mContext = ctx;
			}
		}
	}

Lets include this view in our layout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	

	<?xml version="1.0" encoding="utf-8"?>
	<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	    android:orientation="vertical"
	    android:layout_width="fill_parent"
	    android:layout_height="fill_parent">

	    <xamarindemo.AwesomeView
	    	android:id="@+id/awesomeview_main"
	    	android:layout_height="wrap_content"
	    	android:layout_width="wrap_content"
	    />
	 
	</LinearLayout>

Hit the play button and you’ll see…Nothing. Well that’s because we aren’t doing any drawing yet.

Step 2

The first thing we need to do to start drawing is to override the onDraw method for the view. We’ll also create two stub functions called drawBigCircle and drawSmallCircle to make our code a little easier to work with. So you should’ve added the following to your view:

1
2
3
4
5
6
7
8
9
10
11
12
13
	


		private void drawBigCircle(Canvas canvas){
		}

		private void drawSmallCircles(Canvas canvas){
		}

		protected override void OnDraw(Canvas canvas){
			drawSmallCircles (canvas);
			drawBigCircle (canvas);
		}

Lets start with drawing the little circles at the bottom. Add the following to drawSmallCircles:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	

const int NUM_BUBBLES = 5;
int radius = 60;
private void drawSmallCircles(Canvas canvas){

	int spacing = Width / NUM_BUBBLES;
	int shift = spacing / 2;
	int bottomMargin = 10;

	var paintCircle = new Paint (){ Color = Color.White};
	for (int i = 0; i < NUM_BUBBLES; i++) {
		int x = i * spacing + shift;
		int y = Height - radius * 2 - bottomMargin;
		canvas.DrawCircle (x, y, radius, paintCircle);
	}	

}

Note that the 0,0 starts at the top left corner (as with most drawing engines). We equally space the 5 circles and show them just above the bottom of the screen. You should see something like this when you hit play:

step2

If you understood that, then drawing the big circle should be even easier.

1
2
3
4
5
6
7
	

		int radius_big = 180;
		private void drawBigCircle(Canvas canvas){
			var paintCircle = new Paint (){ Color = Color.White};
			canvas.DrawCircle (Width/2.0, Height/2.0, radius_big, paintCircle);
		}

step2.1

One thing you might note is that the size of circles looks different on your phone. That is because different phones have different resolutions so feel free to mess around with the values. However, a proper implementation would calculate the pixels using a dpi to pixel conversion. We’ll skip that for now.

Step 3

Now lets add some interactivity to our view. First, we need to keep track of the x,y coordinates of the small circles so we’ll create a list of x,y pairs to hold that. We’ll also optimize a little since right now we’re calculating x and y on every draw. Nothing in terms of drawing should change.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
	

		const int NUM_BUBBLES = 5;
		int radius = 60;
		List <Pair> positions = new List <Pair> ();
		 void initPositions(){

			if (positions.Count == 0) {

				int spacing = Width / NUM_BUBBLES;
				int shift = spacing / 2;
				int bottomMargin = 10;

				for (int i = 0; i < NUM_BUBBLES; i++) {
					int x = i * spacing + shift;
					int y = Height - radius * 2 - bottomMargin;
					positions.Add (new Pair (x, y));
				}
			}
		}

		 void drawSmallCircles(Canvas canvas){

			initPositions ();
			var paintCircle = new Paint (){ Color = Color.White};
			for (int i = 0; i < NUM_BUBBLES; i++) {
				int x = (int)positions [i].First;
				int y = (int)positions [i].Second;
				canvas.DrawCircle (x, y, radius, paintCircle);
			}
		}
	

Next we’ll override the ontouchevent function and also check if the event is inside any of the given circles. The is InsideCircle function will basically return the index of the circle that was tapped/touched.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	


		public override bool OnTouchEvent(MotionEvent e) {

			int indexHit = isInsideCircle (e.GetX (), e.GetY ());
			if (indexHit > -1) {
				Toast.MakeText (mContext, "Got index" + indexHit, ToastLength.Long).Show ();
			}

			return false;
		}

		int isInsideCircle(float x, float y){

			for (int i = 0; i < positions.Count; i++) {

				int centerX = (int)positions [i].First;
				int centerY = (int)positions [i].Second;

				if (System.Math.Pow (x - centerX, 2) + System.Math.Pow (y - centerY, 2) < System.Math.Pow (radius, 2)) {
					return i;	
				}
			}

			return -1;
		}

Tap the small circles and you should see the toast show up with the correct index.

Step 4

Now lets add some cool animations to this view. Remember we want the small circle to move to the center and we also want it to scale up. We’ll start by adding some ValueAnimators and also the values that they will animate.

1
2
3
4
5
6
7
8
9
10
	

		//Important properties for the large bubble
		int activeIndex = -1;
		float activeX = 0;
		float activeY = 0;
		float activeRadius = 60;
		ValueAnimator animatorX;
		ValueAnimator animatorY;
		ValueAnimator animatorRadius;

By active, I just mean the big circle. Now let’s initialize them by putting the following in the init function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	

		animatorX = new ValueAnimator ();
		animatorY = new ValueAnimator ();
		animatorRadius = new ValueAnimator ();
		animatorX.SetDuration(1000);
		animatorY.SetDuration (1000);
		animatorRadius.SetDuration (1000);
		animatorX.SetInterpolator (new DecelerateInterpolator());
		animatorY.SetInterpolator (new BounceInterpolator());


		//These are called everytime an update happens in the animator.
		animatorRadius.SetIntValues (new [] { radius, radius_big });
		animatorRadius.Update += (sender, e) => {
			activeRadius = (float) e.Animation.AnimatedValue;
			Invalidate();
		};

		animatorX.Update += (sender, e) => {
			activeX = (float) e.Animation.AnimatedValue;
			Invalidate();
		};
		animatorY.Update += (sender, e) => {
			activeY = (float) e.Animation.AnimatedValue;
			Invalidate();
		};

This is just saying that each animation will last 1 second and we added a fancy interpolator to make the animation look cool (feel free to change it up). We also subscribe to the update event to get the latest value and then we call invalidate which basically clear the canvas and call the onDraw method again.

Now lets add the start and end values for each animator and actually start the animations. Modify the onTouchEvent, drawBigCircle and drawLittleCircle function as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
	

		 public override bool OnTouchEvent(MotionEvent e) {

			float centerScreenX = Width / 2.0f;
			float centerScreenY = Height / 2.0f;
			activeIndex = isInsideCircle (e.GetX (), e.GetY ());
			if (activeIndex > -1) {
				Toast.MakeText (mContext, "Got index" + activeIndex, ToastLength.Long).Show ();
				animatorX.SetFloatValues (new [] {(float)positions[activeIndex].First, centerScreenX});
				animatorY.SetFloatValues (new [] {(float)positions[activeIndex].Second, centerScreenY});
				animatorX.Start ();
				animatorY.Start ();
				animatorRadius.Start ();
			}

			return false;
		}


	int radius_big = 180;
	void drawBigCircle(Canvas canvas){
			if (activeIndex > -1) {
				var paintCircle = new Paint (){ Color = Color.White };
				canvas.DrawCircle (activeX, activeY, activeRadius, paintCircle);
			}
	}

	void drawSmallCircles(Canvas canvas){

		initPositions ();
		var paintCircle = new Paint (){ Color = Color.White};
		for (int i = 0; i < NUM_BUBBLES; i++) {
			if (i == activeIndex) {
				continue;
			}
			int x = (int)positions [i].First;
			int y = (int)positions [i].Second;
			canvas.DrawCircle (x, y, radius, paintCircle);
		}
	}

In the ontouchevent function, now set the activeindex and all the starting and end values for the interpolator. In the drawSmallCircles function, we skip the index if its the big circle and in the drawBigCircle function we only draw something has actually been tapped (i.e. the index is greated than -1)

Hit play and should see the small circle move to the center and grow when tapped.

Step 5

Lets finish this off by adding some colors and text to our circles. Add the following colors array (and again you can choose to do different colors).

1
2
	
	Color []colors = new []{Color.Red, Color.LightBlue, Color.Green, Color.Yellow, Color.Orange};

And as you might’ve guessed, we just replace the white color in paint with the colors[index]. Replace paintCircle with the following in the small circle function and move it inside the for loop

1
2
	
	var paintCircle = new Paint (){ Color = colors[i]};

And with this in the big circle function:

1
2
	
	var paintCircle = new Paint (){ Color = colors[activeIndex]};

Fire up the app and you should see something like this:

step5

Lets add an array to hold some names to display in the circles.

1
2
	
		public string [] names {get;set;}

And then in your MainActivity.cs add the following names (or anything names you want) but make sure you have at least 5.

1
2
3
	
	var awesomeview = FindViewById<AwesomeView> (Resource.Id.awesomeview_main);
	awesomeview.names = new []{"Bob", "John", "Paul", "Wasi", "Mark"};

Finally lets show the names on the big circle. Modify the big circle function as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	

	void drawBigCircle(Canvas canvas){
		if (activeIndex > -1) {
			var paintCircle = new Paint (){ Color = colors[activeIndex]};
			canvas.DrawCircle (activeX, activeY, activeRadius, paintCircle);

			var paintText = new Paint(){Color = Color.Black};
			//  the screen's density scale
			var scale = mContext.Resources.DisplayMetrics.Density;
			// Convert the dps to pixels, based on density scale
			var textSizePx = (int) (20f * scale);
			var name = names [activeIndex];
			paintText.TextSize = textSizePx;
			paintText.TextAlign = Paint.Align.Center;
			canvas.DrawText (name, activeX, activeY + radius/2, paintText);

		}
	}

Lets also show the first letter of each name on the little circle.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	

	void drawSmallCircles(Canvas canvas){

		initPositions ();

		var paintText = new Paint (){ Color = Color.Black };
		// Get the screen's density scale
		var scale = mContext.Resources.DisplayMetrics.Density;
		// Convert the dps to pixels, based on density scale
		var textSizePx = (int) (30f * scale);
		paintText.TextSize = textSizePx;
		paintText.TextAlign = Paint.Align.Center;

		for (int i = 0; i < NUM_BUBBLES; i++) {
			if (i == activeIndex) {
				continue;
			}

			var paintCircle = new Paint (){ Color = colors[i]};
			int x = (int)positions [i].First;
			int y = (int)positions [i].Second;
			canvas.DrawCircle (x, y, radius, paintCircle);
			canvas.DrawText (""+names [i][0], x, y + radius/2, paintText);
		}
	}

The final code for the entire AwesomeView should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
using Android.Views;
using Android.Content;
using Android.Util;
using Android.Graphics;
using System.Collections.Generic;
using Android.Widget;
using Android.Animation;
using Android.Views.Animations;

namespace XamarinDemo
{
	public class AwesomeView : View
	{
		
		Context mContext;

		//Important properties for the large bubble
		int activeIndex = -1;
		float activeX = 0;
		float activeY = 0;
		float activeRadius = 60;
		ValueAnimator animatorX;
		ValueAnimator animatorY;
		ValueAnimator animatorRadius;

		Color []colors = new []{Color.Red, Color.LightBlue, Color.Green, Color.Yellow, Color.Orange};

		public string [] names {get;set;}

		public AwesomeView(Context context) :
		base(context)
		{
			init (context);
		}
		public AwesomeView(Context context, IAttributeSet attrs) :
		base(context, attrs)
		{
			init (context);
		}

		public AwesomeView(Context context, IAttributeSet attrs, int defStyle) :
		base(context, attrs, defStyle)
		{
			init (context);
		}

		private void init(Context ctx){
			mContext = ctx;
			animatorX = new ValueAnimator ();
			animatorY = new ValueAnimator ();
			animatorRadius = new ValueAnimator ();
			animatorX.SetDuration(1000);
			animatorY.SetDuration (1000);
			animatorRadius.SetDuration (1000);
			animatorX.SetInterpolator (new DecelerateInterpolator());
			animatorY.SetInterpolator (new BounceInterpolator());
		
			animatorRadius.SetIntValues (new [] { radius, radius_big });
			animatorRadius.Update += (sender, e) => {
				activeRadius = (float) e.Animation.AnimatedValue;
				Invalidate();
			};

			animatorX.Update += (sender, e) => {
				activeX = (float) e.Animation.AnimatedValue;
				Invalidate();
			};
			animatorY.Update += (sender, e) => {
				activeY = (float) e.Animation.AnimatedValue;
				Invalidate();
			};

		}

		public override bool OnTouchEvent(MotionEvent e) {

			float centerScreenX = Width / 2.0f;
			float centerScreenY = Height / 2.0f;
			activeIndex = isInsideCircle (e.GetX (), e.GetY ());
			if (activeIndex > -1) {
				Toast.MakeText (mContext, "Got index" + activeIndex, ToastLength.Long).Show ();
				animatorX.SetFloatValues (new [] {(float)positions[activeIndex].First, centerScreenX});
				animatorY.SetFloatValues (new [] {(float)positions[activeIndex].Second, centerScreenY});
				animatorX.Start ();
				animatorY.Start ();
				animatorRadius.Start ();
			}

			return false;
		}

		int isInsideCircle(float x, float y){

			for (int i = 0; i < positions.Count; i++) {

				int centerX = (int)positions [i].First;
				int centerY = (int)positions [i].Second;

				if (System.Math.Pow (x - centerX, 2) + System.Math.Pow (y - centerY, 2) < System.Math.Pow (radius, 2)) {
					return i;	
				}
			}

			return -1;
		}


		const int NUM_BUBBLES = 5;
		int radius = 60;
		List <Pair> positions = new List <Pair> ();
		 void initPositions(){

			if (positions.Count == 0) {

				int spacing = Width / NUM_BUBBLES;
				int shift = spacing / 2;
				int bottomMargin = 10;

				for (int i = 0; i < NUM_BUBBLES; i++) {
					int x = i * spacing + shift;
					int y = Height - radius * 2 - bottomMargin;
					positions.Add (new Pair (x, y));
				}
			}
		}

		 void drawSmallCircles(Canvas canvas){

			initPositions ();

			var paintText = new Paint (){ Color = Color.Black };
			// Get the screen's density scale
			var scale = mContext.Resources.DisplayMetrics.Density;
			// Convert the dps to pixels, based on density scale
			var textSizePx = (int) (30f * scale);
			paintText.TextSize = textSizePx;
			paintText.TextAlign = Paint.Align.Center;

			for (int i = 0; i < NUM_BUBBLES; i++) {
				if (i == activeIndex) {
					continue;
				}

				var paintCircle = new Paint (){ Color = colors[i]};
				int x = (int)positions [i].First;
				int y = (int)positions [i].Second;
				canvas.DrawCircle (x, y, radius, paintCircle);
				canvas.DrawText (""+names [i][0], x, y + radius/2, paintText);
			}
		}

		int radius_big = 180;
		private void drawBigCircle(Canvas canvas){
			if (activeIndex > -1) {
				var paintCircle = new Paint (){ Color = colors[activeIndex]};
				canvas.DrawCircle (activeX, activeY, activeRadius, paintCircle);

				var paintText = new Paint(){Color = Color.Black};
				//  the screen's density scale
				var scale = mContext.Resources.DisplayMetrics.Density;
				// Convert the dps to pixels, based on density scale
				var textSizePx = (int) (20f * scale);
				var name = names [activeIndex];
				paintText.TextSize = textSizePx;
				paintText.TextAlign = Paint.Align.Center;
				canvas.DrawText (name, activeX, activeY + radius/2, paintText);

			}
		}

		protected override void OnDraw(Canvas canvas){
			drawSmallCircles (canvas);
			drawBigCircle (canvas);
		}

	}
}

Done

And we’re done! This just shows you a taste of what you can accomplish with custom views.