Custom Views with Constraint Layout and Kotlin

With Constraint Layout (if you don’t know what it is, check out my previous blog post here), it’s now possible in Android to create any interface that was previously possible with any of the other layouts with just a single layout. And with Kotlin we get great language features like higher order functions and extensions. So we can use these together to create custom views easily and populate our Activity with reusable views.

First just create the xml for your Constraint Layout based view. Here’s one which is just a button and a progress bar I did super quickly:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/button"
app:layout_constraintStart_toStartOf="@+id/button"
app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>

Now to create the Kotlin class for this view. Create a new Kotlin file and add the constructors:

Screen Shot 2018-03-27 at 17.20.04.png

(correction: this originally used the @JvmOverloads method, but this has been shown to be a bad idea)

So now we have our constructor set up, we can call the init {} block and inflate our view. When we inflate our view we need to add it to ‘this’, where ‘this’ is the constraint layout. However, because the view will have no constraints it won’t have any positioning at runtime and we won’t see our view. So instead we have to manually add constraints to our view after we’ve added it through code. Now luckily, programatically adding constraints in Constraint Layout isn’t nearly as weird looking as with Auto Layout in iOS, but even so, we can use Kotlin’s extensions to do this in a single method call.

Screen Shot 2017-11-30 at 12.12.36.png.png

So now we just call this on a ConstraintSet object in our init block, and you should get something like this:

Screen Shot 2017-11-30 at 12.13.33.png.png

(correction: alternately you can avoid this and just call LayoutInflater.from(context).inflate(R.layout.custom_view, this, true without adding it later on or using the match extension function above)

Cool, so now our view is added. Now you can go ahead and add this new custom view to your xml in your activity and build and run and voila! You should see your custom view!

The last thing I want to take a look at is using Kotlin’s higher order functions to notify your activity when something happens. We could do this with an interface and make our activity implement that interface, but if you only have one or two things you want to notify your activity of, it’s a lot of code. So instead we can use higher order functions.

Create a new property on your custom view like this:

Screen Shot 2017-11-30 at 12.17.39.png.png

This is just a property that is a function that takes no parameters and returns unit. It’s optional because it might not be set when we call .invoke() on it. Now in our custom view we can just invoke this block when our button is tapped:

Screen Shot 2017-11-30 at 12.19.24.png.png

Obviously now if you build and run and tap the button, nothing’s gonna happen, but at least it doesn’t crash! That’s why we made buttonTapped an optional property. Ok so now let’s actually set that buttonTapped property. In your activity that holds this custom view, just set that property to do whatever you want when it’s invoked.

Screen Shot 2017-11-30 at 12.21.00.png.png

Now every time your button is tapped, your activity should print “button was tapped”. If you’re more familiar with higher order functions you could also pass a value back from your view to your activity this way.

In case I went too fast, I’ll include the project files for you to look at in more detail.

Main Activity:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mubaloo.custview.CustView
android:id="@+id/customView"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>


class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
customView.buttonTapped = {
println("button was tapped")
}
}
}

view raw

MainActivity.kt

hosted with ❤ by GitHub

Custom View:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/button"
app:layout_constraintStart_toStartOf="@+id/button"
app:layout_constraintTop_toBottomOf="@+id/button" />
</android.support.constraint.ConstraintLayout>

view raw

custom_view.xml

hosted with ❤ by GitHub


class CustView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: ConstraintLayout(context, attrs, defStyleAttr) {
var buttonTapped: (() -> Unit)? = null
init {
val view = LayoutInflater.from(context).inflate(R.layout.custom_view, this, false)
val set = ConstraintSet()
addView(view)
set.clone(this)
set.match(view, this)
button.setOnClickListener {
buttonTapped?.invoke()
}
}
}
fun ConstraintSet.match(view: View, parentView: View) {
this.connect(view.id, ConstraintSet.TOP, parentView.id, ConstraintSet.TOP)
this.connect(view.id, ConstraintSet.START, parentView.id, ConstraintSet.START)
this.connect(view.id, ConstraintSet.END, parentView.id, ConstraintSet.END)
this.connect(view.id, ConstraintSet.BOTTOM, parentView.id, ConstraintSet.BOTTOM)
}

view raw

CustView.kt

hosted with ❤ by GitHub

Advertisement

One thought on “Custom Views with Constraint Layout and Kotlin”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s