Xamarin Android Part 8
Custom Views in Xamarin Android Applications
This post is part of a series of posts exploring writing apps for Android using Xamarin Android.
I have found that the extra complexity of using fragments, with their similar but subtly different lifecycle from activities, to not really be worth the effort. So I tend to compose activities from custom views instead. One view I have used in multiple apps is a custom view to show the progress of a task.
As we have seen in Xamarin Android, the view layouts use the same XML as writing apps using Kotlin of Java and this custom view is no excpetion. The layout for the control looks 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
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/progress_bar_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:textAlignment="center"
android:background="@color/primary"
android:alpha="0.8"
android:textColor="@color/background"
android:text="@string/placeholder"
android:gravity="center_horizontal" />
<ProgressBar
android:id="@+id/indeterminateBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:background="@color/primary"
android:alpha="0.5"
android:indeterminate="true"
/>
<ProgressBar
android:id="@+id/steppedBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primary"
android:alpha="0.8"
android:max="10"
android:progress="5"
style="?android:attr/progressBarStyleHorizontal"
android:indeterminate="false"
/>
</LinearLayout>
I used the same layout when implementing the view in Kotlin and C#
A progress custom view in Kotlin
The implementation in Kotlin is pretty standard android development
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
class ProgressSpinnerView : LinearLayout {
@BindView(R.id.progress_bar_message)
lateinit var messageView: TextView
@BindView(R.id.indeterminateBar)
lateinit var indeterminateBar: ProgressBar
@BindView(R.id.steppedBar)
lateinit var steppedBar: ProgressBar
constructor(context: Context) : super(context) {
init(context, null, 0)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init(context, attrs, 0)
}
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) {
init(context, attrs, defStyle)
}
var message: String? = null
set(value) {
field = value
messageView.text = value
}
var max: Int = 1
set(value) {
field = value
steppedBar.max = value
}
var progress: Int = 0
set(value) {
field = value
steppedBar.progress = value
}
private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {
val view = inflateView(context)
ButterKnife.bind(this, view)
loadAttributes(attrs, defStyle)
}
private fun loadAttributes(attrs: AttributeSet?, defStyle: Int) {
val a = context.obtainStyledAttributes(
attrs, R.styleable.ProgressSpinnerView, defStyle, 0)
message = a.getString(R.styleable.ProgressSpinnerView_message)
a.recycle()
}
private fun inflateView(context: Context): View {
val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
return inflater.inflate(R.layout.view_progress_spinner, this, true)
}
fun slideDown(indeterminateProgress: Boolean) {
indeterminateBar.visibility = if (indeterminateProgress) View.VISIBLE else View.GONE
steppedBar.visibility = if (indeterminateProgress) View.GONE else View.VISIBLE
visibility = View.VISIBLE
val animate = TranslateAnimation(
0f, // fromXDelta
0f, // toXDelta
-height.toFloat(), // fromYDelta
0f) // toYDelta
animate.duration = 500
clearAnimation()
startAnimation(animate)
}
fun slideUp() {
visibility = View.GONE
val animate = TranslateAnimation(
0f, // fromXDelta
0f, // toXDelta
0f, // fromYDelta
-height.toFloat()) // toYDelta
animate.duration = 500
clearAnimation()
startAnimation(animate)
}
}
A progress custom view in C#
In C# the code is pretty much the same, with the slight differences in the syntax.
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
namespace PodcastUtilities.AndroidLogic.CustomViews;
public class ProgressSpinnerView : LinearLayout
{
private TextView messageView;
private ProgressBar indeterminateBar;
private ProgressBar steppedBar;
public ProgressSpinnerView(Context context) : base(context)
{
Init(context, null, 0);
}
public ProgressSpinnerView(Context context, IAttributeSet attrs) : base(context, attrs)
{
Init(context, attrs, 0);
}
public ProgressSpinnerView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr)
{
Init(context, attrs, defStyleAttr);
}
public string Message
{
set
{
messageView.Text = value;
}
}
public int Max
{
set
{
steppedBar.Max = value;
}
}
public int Progress
{
set
{
steppedBar.Progress = value;
}
}
private void Init(Context context, IAttributeSet attrs, int defStyle)
{
var view = InflateView(context);
messageView = FindViewById<TextView>(Resource.Id.progress_bar_message);
indeterminateBar = FindViewById<ProgressBar>(Resource.Id.indeterminateBar);
steppedBar = FindViewById<ProgressBar>(Resource.Id.steppedBar);
LoadAttributes(attrs, defStyle);
}
private void LoadAttributes(IAttributeSet attrs, int defStyle)
{
var a = Context.ObtainStyledAttributes(attrs, Resource.Styleable.ProgressSpinnerView, defStyle, 0);
Message = a.GetString(Resource.Styleable.ProgressSpinnerView_message);
a.Recycle();
}
private View InflateView(Context context)
{
LayoutInflater inflater = context.GetSystemService(Context.LayoutInflaterService) as LayoutInflater;
return inflater.Inflate(Resource.Layout.view_progress_spinner, this, true);
}
public void SlideDown(bool indeterminateProgress)
{
indeterminateBar.Visibility = indeterminateProgress ? ViewStates.Visible : ViewStates.Gone;
steppedBar.Visibility = indeterminateProgress ? ViewStates.Gone : ViewStates.Visible;
this.Visibility = ViewStates.Visible;
var animate = new TranslateAnimation(
0f, // fromXDelta
0f, // toXDelta
-this.Height, // fromYDelta
0f); // toYDelta
animate.Duration = 500;
ClearAnimation();
StartAnimation(animate);
}
public void SlideUp()
{
this.Visibility = ViewStates.Gone;
var animate = new TranslateAnimation(
0f, // fromXDelta
0f, // toXDelta
0f, // fromYDelta
-this.Height); // toYDelta
animate.Duration = 500;
ClearAnimation();
StartAnimation(animate);
}
}
Using the custom view
We need to mark the message
property as stylable so it can be set in XML. I do this by creating a file called attrs_progress_spinner_view.xml
in the values
folder of the resources.
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressSpinnerView">
<attr name="message" format="string"/>
</declare-styleable>
</resources>
Then to use the custom view we can simply include it in a layout like this
1
2
3
4
5
6
7
8
9
10
<PodcastUtilities.AndroidLogic.CustomViews.ProgressSpinnerView
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:elevation="2dp"
android:visibility="gone"
app:message="@string/finding_podcasts_progress"
>
</PodcastUtilities.AndroidLogic.CustomViews.ProgressSpinnerView>
And it looks like this
There is a slight gotcha when using Xamarin Android, according to stackoverflow the XML element name should be the name of the class including the full namespace. Sometimes this needs to be in lower case and sometimes the case needs to match what was specified in the C# class file. I have not worked out the pattern, if you get an inflation error then try playing with the namespace in the XML element.
Also using custom views tends to blow up the designer in Visual Studio.