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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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:
(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.
So now we just call this on a ConstraintSet object in our init block, and you should get something like this:
(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:
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:
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.
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MainActivity : AppCompatActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
customView.buttonTapped = { | |
println("button was tapped") | |
} | |
} | |
} |
Custom View:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
perfect
LikeLike