Ever since Google Now introduced the playful cards UI on Android, cards have become a core UI component of the platform. With Material Design, cards were officially incorporated into Android's UI toolkit, accompanied by Matias Duarte's lovingly-crafted guidelines on exactly how they should look and feel.
The CardView
implementation takes care of ensuring that cards adhere to the spec, but it doesn't implement one crucial interaction out-of-the-box: the lift-on-touch effect specified in the Responsive Interaction section:
Strangely none of Google's own apps care: I checked the latest versions of Now, Play Music, Play Store, Newsstand. Why throw away a simple opportunity for responsive interaction? Here's how to achieve that effect on API 21+ (Lollipop and above only). It really is quite simple.
Code
First, add this StateListAnimator to your res/animator-v21
directory (let's call it lift_on_touch.xml
):
<?xml version="1.0" encoding="utf-8"?>
<!-- animate the translationZ property of a view when pressed -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:state_enabled="true"
android:state_pressed="true">
<set>
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="8dp"
android:valueType="floatType"/>
</set>
</item>
<item>
<set>
<objectAnimator
android:duration="@android:integer/config_shortAnimTime"
android:propertyName="translationZ"
android:valueTo="0"
android:valueType="floatType"/>
</set>
</item>
</selector>
This animator moves the view it's applied to 6dp
8dp
up along the Z-axis, which is visually seen as the shadow underneath it expanding. Why UPDATE: elevation changed to 6dp
? Because it's the value used by the Google I/O 2014 app and also mentioned in the documentation at one point.8dp
as per the latest Material Design guidelines. Credits to Vibin in the comments for pointing this out!
Next, in your layout XML, you just have to specify this StateListAnimator:[1]
<android.support.v7.widget.CardView
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"
android:foreground="?attr/selectableItemBackground"
android:stateListAnimator="@anim/lift_on_touch"
android:clickable="true"
app:cardPreventCornerOverlap="false"
app:cardUseCompatPadding="true">
<!-- card content goes here -->
</android.support.v7.widget.CardView>
(Thanks to Ben Carver and Fatih Coşkun in the comments thread, for pointing out the occasional need for android:clickable
; it is not needed if you call setOnClickListener
on the CardView
as shown below, which is going to be the case most of the time).
Or you can do it in code if you prefer:
CardView card = findViewById(R.id.card);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
StateListAnimator stateListAnimator = AnimatorInflater
.loadStateListAnimator(context, R.anim.lift_on_touch);
card.setStateListAnimator(stateListAnimator);
}
// add a click handler to ensure the CardView handles touch events
// otherwise the animation won't work
card.setOnClickListener(/* ... */);
Note that you can use the same StateListAnimator to add the lift-on-touch effect to any View
on API 21+. In fact I just sent in a Github pull request to add this missing piece of interaction to Melnykov Oleksandr's awesome FloatingActionButton library. Nice eh?
Feel free to post a comment below if you have anything to add!
You might be wondering, if StateListAnimator is only available on API 21+, how are we referring to it directly in our layout? Here's the trick: "When parsing XML resources, Android ignores XML attributes that aren’t supported by the current device. So you can safely use XML attributes that are only supported by newer versions without worrying about older versions breaking when they encounter that code." ↩︎