I tried a lot of answer & third party libs, but none was keeping the border and raised effect on pre-lollipop while having the ripple effect on lollipop without drawback. Here is my final solution combining several answers (border/raised are not well rendered on gifs due to grayscale color depth) :
Lollipop
Pre-lollipop
build.gradle
compile 'com.android.support:cardview-v7:23.1.1'
layout.xml
<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/card"
card_view:cardElevation="2dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardMaxElevation="8dp"
android:layout_margin="6dp"
>
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/btn_bg"
android:text="My button"/>
</android.support.v7.widget.CardView>
drawable-v21/btn_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:drawable="?attr/colorPrimary"/>
</ripple>
drawable/btn_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorPrimaryDark" android:state_pressed="true"/>
<item android:drawable="@color/colorPrimaryDark" android:state_focused="true"/>
<item android:drawable="@color/colorPrimary"/>
</selector>
Activity's onCreate
final CardView cardView = (CardView) findViewById(R.id.card);
final Button button = (Button) findViewById(R.id.button);
button.setOnTouchListener(new View.OnTouchListener() {
ObjectAnimator o1 = ObjectAnimator.ofFloat(cardView, "cardElevation", 2, 8)
.setDuration
(80);
ObjectAnimator o2 = ObjectAnimator.ofFloat(cardView, "cardElevation", 8, 2)
.setDuration
(80);
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
o1.start();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
o2.start();
break;
}
return false;
}
});