Update Note: This tutorial has been updated to Kotlin by Joe Howard. The original tutorial was written by Megha Bambra.

Material Design with Kotlin!
Google’s material design brings with it exciting ways to delight your users with a visually appealing Android app. But wait—what is material design?
Google has described it as an interface that incorporates “tactile surfaces, bold graphic design and fluid motion to create beautiful, intuitive experiences.” Material design is the “user experience philosophy” for Android apps!
In this tutorial, you’ll integrate material design into an app called Travel Wishlist. Along the way, you’ll learn how to:
- Implement the material theme;
- Build dynamic views using widgets like
RecyclerView
andCardView
; - Use the Palette API to generate color schemes that you can use for text or background views;
- Create delightful interactions using Android animation APIs.
This tutorial assumes you have a basic familiarity with Android programming including Kotlin, XML, Android Studio and Gradle. If you’re completely new to Android, you might want to go through our Beginning Android Development series and Kotlin Introduction first.
To follow along with this tutorial, you’ll need to use Android Studio 3.0 Beta 2 or later and Kotlin 1.1.4-2 or later.
Let’s get started!
Getting Started
Download the starter project, then fire up Android Studio.
To import the starter project, first select Open an existing Android Studio project from the Welcome to Android Studio window:
Then select the downloaded project and click Open:
Travel Wishlist will be a very simple app. Users will see a grid of pictures from locations around the world, and be able to tap each picture to add notes about what to do and see.
Build and run the starter project, and you should see a screen with the most basic of interfaces:
Right now, the world is empty! You’re going to add material components to this existing project, including dynamic views, color schemes and animations that will truly complement the beauty of the photos in your dataset.
Open build.gradle for the app module and add RecyclerView, CardView, Palette, and Picasso to your dependencies:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.0.1'
implementation 'com.android.support:recyclerview-v7:26.0.1'
implementation 'com.android.support:cardview-v7:26.0.1'
implementation 'com.android.support:palette-v7:26.0.1'
implementation 'com.squareup.picasso:picasso:2.5.2'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'
}
Here you’re simply declaring the dependencies that you’ll use throughout the rest of the tutorial. The first few of the added dependencies are Google-provided APIs, but the final one, Picasso, is a fantastic image downloading and caching library provided by the good folks at Square.
With the dependencies declared, it’s time to begin incorporating Material Design into your app!
Setting Up the Theme
Before doing anything else, you will update the theme. Open style.xml under the res/values directory. By default, the theme selected is Theme.AppCompat.Light.DarkActionBar
. Add the following items inside the theme tag:
<item name="android:navigationBarColor">@color/primary_dark</item>
<item name="android:displayOptions">disableHome</item>
Android automatically applies colorPrimary
to the action bar, colorPrimaryDark
to status bar and colorAccent
to UI widgets like text fields and checkboxes.
In the code above, you also alter the color of the navigation bar. For android:displayOptions
, you pass disableHome
to accommodate the screen layouts in this sample app.
Build and run, and you’ll see the new color scheme in the app.
It’s a subtle change, but like every trip on your travel wishlist, upgrading this design begins with a single step!
Using RecyclerView and CardView
To give your users a window into all the cool places they might go, you need a view. You can use RecyclerView
as a replacement for ListView
, but it’s much more versatile than that. Google describes RecyclerView
as “a flexible view for providing a limited window into a large data set.” In this section, you’re going to demonstrate this by switching the view from a list to a custom grid that uses the same data source which supplies the the users locations.
Implementing a Recycler View in XML
First, open activity_main.xml and add the following inside the LinearLayout
tag:
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray"/>
Here you’re adding a RecyclerView
to the activity’s layout, and specifying it should match the entire size of the parent view.
Initializing a Recycler View and Applying a Layout Manager
Before adding Kotlin code, configure Android Studio so that it automatically inserts import statements to save you having to add each one manually.
Go to Preferences\Editor\General\Auto Import and select the Add unambiguous imports on the fly checkbox. In MainActivity.kt, add the following to the top of the class:
lateinit private var staggeredLayoutManager: StaggeredGridLayoutManager
Here you’re simply declaring a property to hold a reference to the LayoutManager
.
Next, add the following to the bottom of onCreate()
:
staggeredLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
list.layoutManager = staggeredLayoutManager
In the code above, you set the layout manager of the RecyclerView
to a StaggeredGridLayoutManager
, which you’ll use to create two types of vertically staggered grids. Here you start with the first type, passing 1
for the span count and StaggeredGridLayoutManager.VERTICAL
for the orientation. A span count of 1 makes this a list rather than a grid, as you’ll soon see. Later, you’ll add a compact grid formation with two columns.
Note that you’re using Kotlin Android Extensions to find list
, so there is no need for a call to findViewById()
. Make sure that the following line is present in your import statements, as it should be automatically added when you type in list
:
import kotlinx.android.synthetic.main.activity_main.*
Creating Rows and Cells Using a Card View
CardView
provides a consistent backdrop for your views, including rounded corners and a drop shadow. You’re going to implement it for the row/cell layout of your RecyclerView
. By default, CardView
extends FrameLayout
and therefore includes the ability to host other child views.
From the res\layout directory, create a new Layout resource file and call it row_places.xml. Press OK to create it.
To create your desired cell layout, replace all the contents of this file with the code below:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:id="@+id/placeCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
card_view:cardCornerRadius="@dimen/card_corner_radius"
card_view:cardElevation="@dimen/card_elevation">
<ImageView
android:id="@+id/placeImage"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop" />
<!-- Used for the ripple effect on touch -->
<LinearLayout
android:id="@+id/placeHolder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:selectableItemBackground"
android:orientation="horizontal" />
<LinearLayout
android:id="@+id/placeNameHolder"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="@+id/placeName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="start"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@android:color/white" />
</LinearLayout>
</android.support.v7.widget.CardView>
By adding xmlns:card_view="http://schemas.android.com/apk/res-auto"
, you can define attributes like card_view:cardCornerRadius
and card_view:cardElevation
that are responsible for giving Material Design enabled Android apps their signature card-like look.
Notice that for mainHolder
, you’ve added ?android:selectableItemBackground
as the background. This enables the ripple effect animation when the user touches a cell, as seen in many Android apps now. You’ll get to see it in action soon.
Implementing an Adapter for a Recycler View
You’re going to use an adapter for the RecyclerView
to bind data to the view. In the main/java folder, right-click on the package com.raywenderlich.android.travelwishlist package and select New\Kotline File/Class. Call the class TravelListAdapter.
Add the following code to the class, taking care to preserve the package statement at the top of the file:
// 1
class TravelListAdapter(private var context: Context) : RecyclerView.Adapter<TravelListAdapter.ViewHolder>() {
override fun getItemCount(): Int {
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
}
override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
}
// 2
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
}
}
A couple of things are happening above:
- You make
TravelListAdapter
extendRecycler.Adapter
so that you can implement logic for the override methods you’ll add soon. You also setup the constructor with aContext
that will be passed in when you create an instance ofTravelListAdapter
inMainActivity
, which you’ll do a bit later in the tutorial. - You create the
ViewHolder
class. Whereas the use of theViewHolder
pattern is optional inListView
,RecyclerView
enforces it. This improves scrolling and performance by avoidingfindViewById()
for each cell.
Update the RecyclerView.Adapter
methods in TravelListAdapter
to the following:
// 1
override fun getItemCount() = PlaceData.placeList().size
// 2
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.row_places, parent, false)
return ViewHolder(itemView)
}
// 3
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val place = PlaceData.placeList()[position]
holder.itemView.placeName.text = place.name
Picasso.with(context).load(place.getImageResourceId(context)).into(holder.itemView.placeImage)
}
Here’s what’s happening:
getItemCount()
returns the number of items from your data array. In this case, you’re using the size of thePlaceData.placeList()
.onCreateViewHolder(...)
returns a new instance of yourViewHolder
by passing an inflated view ofrow_places
.onBindViewHolder(...)
binds thePlace
object to the UI elements inViewHolder
. You’ll use Picasso to cache the images for the list.
Add a field in MainActivity
that will hold a reference to your adapter:
lateinit private var adapter: TravelListAdapter
And then create an instance of your adapter and pass it to the RecyclerView
at the bottom of onCreate()
, just after you configure the layout manager:
adapter = TravelListAdapter(this)
list.adapter = adapter
Now build and run the app, and you’ll see a populated list of places.
Which place is calling your name? I like the look of that turquoise water. But wherever you want to go, you’ll want to cultivate your dream by taking notes about what to do there. First, you need to make the cells respond to a user’s touch.
Implementing a Click Interface for Each Cell
Unlike ListView
, RecyclerView
doesn’t come with an onItemClick
interface, so you have to implement one in the adapter. In TravelListAdapter
, create a property to hold an instance of OnItemClickListener
. Add the following to the top of TravelListAdapter
:
lateinit var itemClickListener: OnItemClickListener
Now implement View.OnClickListener
by adding the interface to the ViewHolder
inner class definition like this:
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
Then add the following method stub to the inner ViewHolder
class:
override fun onClick(view: View) {
}
Hook the two up by adding the following init
block to the top of ViewHolder
:
init {
itemView.placeHolder.setOnClickListener(this)
}
Above, you initiate setOnClickListener
for placeHolder
and implement the onClick
override method.
You need to do a few more things to implement the onClick
interface for the RecyclerView
. First, after the inner ViewHolder
class definition add the following:
interface OnItemClickListener {
fun onItemClick(view: View, position: Int)
}
Next, add the setter method of the onClickListener
to TravelListAdapter
:
fun setOnItemClickListener(itemClickListener: OnItemClickListener) {
this.itemClickListener = itemClickListener
}
Now implement the logic in the empty onClick()
stub within the inner ViewHolder
class:
override fun onClick(view: View) = itemClickListener.onItemClick(itemView, adapterPosition)
In MainActivity
, create an instance of OnItemClickListener
above onCreate()
:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
Toast.makeText(this@MainActivity, "Clicked " + position, Toast.LENGTH_SHORT).show()
}
}
Finally, set the listener to the adapter by adding the following code to the bottom of onCreate()
, just after where you set the adapter:
adapter.setOnItemClickListener(onItemClickListener)
Build and run. Now when you tap a cell you’ll see ripple effect every time you touch a row, and a Toast notification displaying the position of the cell in the list.
From List to Grid and Back
StaggeredLayoutManager
lets you add versatility to your layouts. To change your existing list to a more compact two-column grid, you simply have to change the spanCount
of the StaggeredLayoutManager
in MainActivity
.
In toggle()
, add the following to the top of the showGridView()
:
staggeredLayoutManager.spanCount = 2
And now add the following to the top of showListView()
:
staggeredLayoutManager.spanCount = 1
Here you’re simply switching between single and double span counts, which displays single and double columns respectively.
Build and run and use the action bar button to toggle between list and grid views.
Using the Palette API in the List
Now you can add some interesting Material Design features into the mix, starting with the Palette API. Head back to TravelListAdapter
, where you’ll define a background color for placeNameHolder
that will be determined dynamically using the colors in the image.
Add the following to the bottom of onBindViewHolder(...)
:
val photo = BitmapFactory.decodeResource(context.resources, place.getImageResourceId(context))
Palette.from(photo).generate { palette ->
val bgColor = palette.getMutedColor(ContextCompat.getColor(context, android.R.color.black))
holder.itemView.placeNameHolder.setBackgroundColor(bgColor)
}
The generate(...)
method creates a color palette in the background, and is passed a lambda that is called when the palette has been generated. Here you can access the generated color palette and set the background color of holder.itemView.placeNameHolder
. If the color doesn’t exist, the method will apply a fallback color — in this case, android.R.color.black
.
Build and run to see the Palette API in action!
- Vibrant
- Dark Vibrant
- Light Vibrant
- Muted
- Dark Muted
- Light Muted
I encourage you to experiment with these. Instead of palette.getMutedColor(...)
, try palette.getVibrantColor(...)
, palette.getDarkVibrantColor(...)
and so on.
Using the New Material APIs
In this section, you’ll use DetailActivity
and its corresponding activity_detail
layout, and make them cooler by infusing some of the new Material Design APIs.
First, you’ll want to see how the detail view currently looks in the starter project. To see this, first add the following to the companion object of DetailActivity
:
fun newIntent(context: Context, position: Int): Intent {
val intent = Intent(context, DetailActivity::class.java)
intent.putExtra(EXTRA_PARAM_ID, position)
return intent
}
Then, go to MainActivity
and replace the Toast
in onItemClick(...)
of onItemClickListener with the following:
startActivity(DetailActivity.newIntent(this@MainActivity, position))
You can pass the position of the place object via the intent so that DetailActivity
can retrieve the information and use it to layout the interface. That’s what you’re doing here.
Build and run.
There isn’t anything crazy going on here (yet!), but you’ve got a nice foundation on which to start adding those highly anticipated Material Design APIs. You also see a cool FloatingActionButton
, one of the widgets introduced by Material Design.
Adding a Reveal Animation
Now you want to give your users the ability to add notes about what they’d like to do in each of these stunning places. For this, activity_detail.xml already has an edittext
that is hidden by default. When a user taps the FAB, it reveals itself with a cool animation like below:
Open DetailActivity
. There are two methods you have yet to implement:
revealEditText()
hideEditText()
First, add the following lines inside revealEditText()
:
val cx = view.right - 30
val cy = view.bottom - 60
val finalRadius = Math.max(view.width, view.height)
val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius.toFloat())
view.visibility = View.VISIBLE
isEditTextVisible = true
anim.start()
The two int
values are getting the x
and y
positions of the view with a slight offset. This offset gives the illusion that the reveal is happening from the direction of your FAB.
Next, the radius gives the reveal the circular outline that you can see in the GIF above. All of these values — the x-position, y-position and the radius — you pass into the animation instance. This animation is using ViewAnimationUtils
, which gives you the ability to create this circular reveal.
Since the EditText
view is initially hidden, you set the view’s visibility to VISIBLE
and set your boolean check isEditTextVisible
to true
. Finally, you can call start()
on the animation.
To dismiss the view, add the following to hideEditText()
:
val cx = view.right - 30
val cy = view.bottom - 60
val initialRadius = view.width
val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius.toFloat(), 0f)
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
view.visibility = View.INVISIBLE
}
})
isEditTextVisible = false
anim.start()
Here your goal is to hide the view and show the circular animation in the opposite direction. Therefore, you make the initial radius the width of the view and the ending radius 0, which shrinks the circle.
You want to show the animation first and then hide the view. To do this, you implement an animation listener and hide the view when the animation ends.
Now build and run and see this animation in action!
Note: If the keyboard presents itself, you’ll need to dismiss it explicitly to see the effect without obstruction. Comment out the call to inputManager.showSoftInput(...)
in DetailActivity
, but don’t forget to uncomment it. Oh, and don’t worry that your button doesn’t show the plus icon yet, you’ll fix that soon.
Morphing a Bezier Path for a Floating Action Button
Now that you have your reveal animation hiding and showing the edit text field, you can coordinate the icon on your FAB to look and respond just like the one shown below:
The starter project includes the vector paths for the plus and checkmark icons. You’ll learn how to animate – or morph – the paths from the plus to the checkmark, and vice versa.
Under the res/drawables directory, create a new resource file by going to New\Drawable resource file. Call it icn_morph and define animated-vector
as the root element:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icn_add">
</animated-vector>
animated-vector
requires an existing android:drawable
. In this case, the animated vector will start with a plus sign and morph into a checkmark, so you’ll set the drawable to icn_add
.
Now for the actual morphing, add the following inside the animated-vector
tag:
<target
android:animation="@anim/path_morph"
android:name="sm_vertical_line" />
<target
android:animation="@anim/path_morph_lg"
android:name="lg_vertical_line" />
<target
android:animation="@anim/fade_out"
android:name="horizontal_line" />
With the code above, you are essentially transforming the vertical line of the plus icon into a checkmark while fading out the horizontal line, as the diagram below illustrates:
Furthermore, the vertical line is comprised of two paths, a smaller vertical line and a larger one:
You can see from the diagram above that you can transform the first two targets, sm_vertical_line
and lg_vertical_line
, into a checkmark by drawing their paths at different angles, which is exactly what you do in the previous code block, along with fading out horizontal_line
.
Next, you need to reverse this animation to transform the checkmark back into a plus sign. Create another drawable resource file, this time calling it icn_morph_reverse, and replace it’s contents with the following:
<?xml version="1.0" encoding="utf-8"?>
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/icn_add">
<target
android:animation="@anim/path_morph_reverse"
android:name="sm_vertical_line"/>
<target
android:animation="@anim/path_morph_lg_reverse"
android:name="lg_vertical_line" />
<target
android:animation="@anim/fade_in"
android:name="horizontal_line" />
</animated-vector>
The two lines that make up the final vertical line in the plus icon will now morph back into their original states and the horizontal line will fade into view, creating a smooth effect.
Now, to complete the animation. Open DetailActivity.kt and add the following to onClick()
, at the end of the if
branch before the else:
addButton.setImageResource(R.drawable.icn_morph)
val animatable = addButton.drawable as Animatable
animatable.start()
Here you set the image resource of the button to the icn_morph
drawable you created earlier, extract the animatable from it, and then kick-off the animation.
Finally, add the following to the very bottom of the else
branch:
addButton.setImageResource(R.drawable.icn_morph_reverse)
val animatable = addButton.drawable as Animatable
animatable.start()
Here you’re doing almost exactly the same as the previous step, except you assign icn_morph_reverse
as the image resource so the animation plays out in the opposite direction.
Along with morphing the icon, the user’s click also adds the text from todoText
to the toDoAdapter
and refreshes the place activity list. This is not yet visible because of the white text, but in the next section, you’ll add vibrant color to your views so that the text stands out.
Build and run, and watch the magic unfold before your eyes! The FAB icon now morphs between a checkmark and a plus sign when it’s tapped.
Adding Dynamic Colors to Views Using Palette API
It’s time to add colors to this view using the Palette API. And not just any colors—as before, they will be dynamic colors!
In DetailActivity
, flesh out colorize()
by adding the following:
val palette = Palette.from(photo).generate()
applyPalette(palette)
Just like you did previously, you generate a palette from a photo – although this time you do it synchronously – and then pass that palette onto applyPalette()
. Replace the existing method stub for applyPalette()
with this code:
private fun applyPalette(palette: Palette) {
window.setBackgroundDrawable(ColorDrawable(palette.getDarkMutedColor(defaultColor)))
placeNameHolder.setBackgroundColor(palette.getMutedColor(defaultColor))
revealView.setBackgroundColor(palette.getLightVibrantColor(defaultColor))
}
Here you’re you’re using the dark muted color, the muted color, and the light vibrant color as the background colors of the window, title holder, and reveal view respectively.
Finally, to kick-off this chain of events add the following line to the bottom of getPhoto()
:
colorize(photo)
It’s that time again… build and run your app! You can see the detail activity is now using a color scheme derived from the palette of the associated image.
Activity Transitions With Shared Elements
We’ve all seen and wondered about those nice image and text transitions in Google’s app which have been updated to use Material Design. Well, wait no more—you’re about to learn the intricacies of achieving smooth animation.
Between the places list view, MainActivity
, and the places detail view, DetailActivity
, you’re going to transition the following elements:
- The image of the place;
- The title of the place;
- The background area of the title.
Open row_places.xml and add the following to the declaration of the ImageView
tag with an id of placeImage
:
android:transitionName="tImage"
And then add this to the LinearLayout
tag with an id of placeNameHolder
:
android:transitionName="tNameHolder"
Notice that placeName
doesn’t have a transition name. This is because it is the child of placeNameHolder
, which will transition all of its child views.
In activity_detail.xml, add a transitionName
to the ImageView
tag with the id placeImage
:
android:transitionName="tImage"
And, in a similar fashion, add a transitionName
to the LinearLayout
tag that has an id of placeNameHolder
:
android:transitionName="tNameHolder"
Shared elements between activities that you want to transition should have the same android:transitionName
, which is what you’re setting up here. Also, notice that the size of the image, as well as the height of the placeNameHolder
are much larger in this activity. You’re going to animate all of these layout changes during the activity transition to provide some nice visual continuity.
In onItemClickListener()
found in MainActivity
, update the method to the following:
override fun onItemClick(view: View, position: Int) {
val intent = DetailActivity.newIntent(this@MainActivity, position)
// 1
val placeImage = view.findViewById<ImageView>(R.id.placeImage)
val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
// 2
val imagePair = Pair.create(placeImage as View, "tImage")
val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
// 3
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
imagePair, holderPair)
ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle())
}
After adding this code, you will need to manually add the following import statement to the top of the file as Android Studio cannot automatically determine that this is the intended package.
import android.support.v4.util.Pair
There are a couple of things to highlight here:
- You get an instance of both
placeImage
andplaceNameHolder
for the given position of theRecyclerView
. You’re not relying on Kotlin Android Extensions here since you need theplaceImage
andplaceNameHolder
from the specificview
. - You create a
Pair
containing the view and thetransitionName
for both the image and the text holder view. Note that you will once again have to manually add the import statement to the top of the file:android.support.v4.util.Pair
. - To make the activity scene transition with shared views, you pass in your
Pair
instances and start the activity with youroptions
bundle.
Build and run to see the image transition from the main activity to the detail activity:
However, the animation is a bit jumpy in two areas:
- The FAB button suddenly appears in
DetailActivity
. - If you tap on a row under the action or navigation bar, that row appears to jump a bit before it transitions.
You’ll solve the FAB button issue first. Open DetailActivity.kt and add the following to windowTransition()
:
window.enterTransition.addListener(object : Transition.TransitionListener {
override fun onTransitionEnd(transition: Transition) {
addButton.animate().alpha(1.0f)
window.enterTransition.removeListener(this)
}
override fun onTransitionResume(transition: Transition) { }
override fun onTransitionPause(transition: Transition) { }
override fun onTransitionCancel(transition: Transition) { }
override fun onTransitionStart(transition: Transition) { }
})
The listener you add to the enter transition is triggered when the window transition ends, which you use to fade in the FAB button. For this to be effective, set the alpha
to 0
for the FAB in activity_detail.xml:
android:alpha="0.0"
Build and run! You’ll notice the FAB transition is much smoother!:
As for the action bar and navigation bar issues, begin by updating styles.xml, to set the parent theme to Theme.AppCompat.Light.NoActionBar
:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
Since there is no action bar defined in styles.xml, you’ll have to add it using individual XML views.
Open activity_main.xml and add the following inside LinearLayout
, just above the RecyclerView
tag:
<include layout="@layout/toolbar" />
This simply includes a toolbar layout that's provided as part of the starter project into the current layout. Now you need to make a similar change to the detail activity's layout.
Open activity_detail.xml and add the following at the very bottom of the first FrameLayout
, just below the closing tag of the inner LinearLayout
:
<include layout="@layout/toolbar_detail"/>
Next in MainActivity
, you need to initialize the toolbar. Add the following to the bottom of the onCreate()
method:
setUpActionBar()
Here you assign the result of the findViewById
call to the new field, and then call setUpActionBar()
. At the moment it's just an empty method stub. Fix that now by adding the following to setUpActionBar()
:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(false)
supportActionBar?.setDisplayShowTitleEnabled(true)
supportActionBar?.elevation = 7.0f
Here you set the action bar to be an instance of your custom toolbar, set the visibility of the title, disable the home button, and add a subtle drop shadow by setting the elevation.
Build and run. You'll notice that nothing much has changed, but these changes have laid the foundations of properly being able to transition the toolbar.
Open MainActivity
and replace the existing onItemClickListener
with this one:
private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
// 1
val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)
val placeImage = view.findViewById<ImageView>(R.id.placeImage)
val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
// 2
val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)
val statusBar = findViewById<View>(android.R.id.statusBarBackground)
val imagePair = Pair.create(placeImage as View, "tImage")
val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
// 3
val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)
val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)
val toolbarPair = Pair.create(toolbar as View, "tActionBar")
// 4
val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)
if (navigationBar != null && navPair != null) {
pairs += navPair
}
// 5
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
*pairs.toTypedArray())
ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())
}
}
The differences between the original implementation and this one are thus:
- You've renamed the intent to provide more context;
- You get references to both the navigation bar and status bar;
- You've created three new instances of
Pair
- one for the navigation bar, one for the status bar, and one for the toolbar; - You've protected against an IllegalArgumentException that occurs on certain devices, such as the Galaxy Tab S2, on which
navPair
is null. - And finally you've updated the options that are passed to the new activity to include the references to the new views. You've used the Kotlin spread operator
*
onpairs
, after changing it to a typed array.
Great! Build and run, and you’ll see a much smoother animation:
Now if you tap on a row under the action/toolbar or navigation bar, it doesn't jump before the transition; it transitions with the rest of the shared elements, which is much more pleasing to the eye. Switch to the grid view and you'll notice that the transitions work very nicely with that layout as well.
Ta-da! Here is a video of the final app in action:
Where to Go From Here?
Be proud of yourself: You’ve created a full-blown Android material app! To challenge yourself, try the following:
- Use
StaggeredLayoutManager
to make a grid with three columns instead of two. - Experiment with the
Palette
API in bothMainActivity
andDetailActivity
using different palette options. - Add a button on the place list and transition it to the detail view as a shared element—perhaps a favorite button.
- Make those transitioning animations even cooler—check out Android's Newsstand app and see how it transitions from a grid to a detail view with the reveal animation. You have all the code here to replicate that.
- Try to create all the morphing animations you did here, but using
animated-vectors
.
And of course, apply this knowledge to your apps so that they can be as cool as this one. :]
To find out more about Material Design then be sure to check out Google's recently redesigned Google Design website.
You can get the completed project here.
Feel free to share your feedback or ask any questions in the comments below or in the forums.
The post Android: An Introduction to Material Design with Kotlin appeared first on Ray Wenderlich.