Touch feedback with RecyclerView and CardView
Asked Answered
C

10

67

I would love to enable touch feedback for my Open-Source library.

I've created a RecyclerView and a CardView. The CardView contains different areas with different onClick actions. Now I would love to trigger the Ripple effect if a user clicks anywhere in the card, but I'm not able to achieve this behavior.

This is my listitem, You can find it on GitHub too: https://github.com/mikepenz/AboutLibraries/blob/master/library/src/main/res/layout/listitem_opensource.xml

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true"
    android:background="@drawable/button_rect_normal"/>

<LinearLayout
    android:padding="6dp"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:gravity="center_vertical"
        android:paddingLeft="8dp"
        android:paddingRight="8dp"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/libraryName"
            android:textColor="@color/title_openSource"
            android:textSize="@dimen/textSizeLarge_openSource"
            android:textStyle="normal"
            android:layout_width="0dp"
            android:layout_weight="5"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="1"/>

        <TextView
            android:id="@+id/libraryCreator"
            android:textColor="@color/text_openSource"
            android:textStyle="normal"
            android:layout_width="0dp"
            android:layout_weight="2"
            android:layout_height="wrap_content"
            android:layout_marginTop="2dp"
            android:gravity="right"
            android:maxLines="2"
            android:textSize="@dimen/textSizeSmall_openSource"/>
    </LinearLayout>

    <View
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_marginTop="4dp"
        android:background="@color/dividerLight_openSource"/>

    <TextView
        android:id="@+id/libraryDescription"
        android:textSize="@dimen/textSizeSmall_openSource"
        android:textStyle="normal"
        android:textColor="@color/text_openSource"
        android:padding="8dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxLines="20">
    </TextView>

    <View
        android:id="@+id/libraryBottomDivider"
        android:layout_width="match_parent"
        android:layout_height="1px"
        android:layout_marginTop="4dp"
        android:background="@color/dividerLight_openSource"/>

    <LinearLayout
        android:id="@+id/libraryBottomContainer"
        android:gravity="center_vertical"
        android:paddingLeft="8dp"
        android:paddingTop="4dp"
        android:paddingRight="8dp"
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TextView
            android:id="@+id/libraryVersion"
            android:textColor="@color/text_openSource"
            android:textStyle="normal"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:gravity="left"
            android:maxLines="1"
            android:textSize="@dimen/textSizeSmall_openSource"/>

        <TextView
            android:id="@+id/libraryLicense"
            android:textColor="@color/text_openSource"
            android:textStyle="normal"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:maxLines="1"
            android:textSize="@dimen/textSizeSmall_openSource"/>
    </LinearLayout>
</LinearLayout>

And the onClick is set on 3 parts of this layout. libraryCreator, libraryDescription, and libraryBottomContainer.

I hope someone got an idea what is going wrong here.

Thanks for your help.

Calycine answered 16/11, 2014 at 19:31 Comment(0)
C
10

I was now able to solve the issue.

The problem is that i use onClickListener on the TextViews and this click listener prevents the RippleForeground from being notified about the touch event. So the solution is to implement an TouchListener on those TextViews and pass through the touch event.

The class is really simple, and can be used everywhere:

package com.mikepenz.aboutlibraries.util;

import android.support.v7.widget.CardView;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by mikepenz on 16.04.15.
 */
public class RippleForegroundListener implements View.OnTouchListener {
    CardView cardView;

    public RippleForegroundListener setCardView(CardView cardView) {
        this.cardView = cardView;
        return this;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // Convert to card view coordinates. Assumes the host view is
        // a direct child and the card view is not scrollable.
        float x = event.getX() + v.getLeft();
        float y = event.getY() + v.getTop();

        if (android.os.Build.VERSION.SDK_INT >= 21) {
            // Simulate motion on the card view.
            cardView.drawableHotspotChanged(x, y);
        }

        // Simulate pressed state on the card view.
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                cardView.setPressed(true);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                cardView.setPressed(false);
                break;
        }

        // Pass all events through to the host view.
        return false;
    }
}

It can be used by adding this as TouchListener:

RippleForegroundListener rippleForegroundListener = new RippleForegroundListener();
older.libraryCreator.setOnTouchListener(rippleForegroundListener);

This listener will just pass through the touch event to the CardView and trigger the correct Ripple effect. (You can also modify this to take any view. It should not be limited to a CardView)

Calycine answered 24/4, 2015 at 22:17 Comment(5)
what is older.libraryCreator.setOnTouchListener(rippleForegroundListener); ?Doorway
@MohitKhaitan the view which should pass the Touch to the RippleView. There's also an updated version of this, without keeping the context. github.com/mikepenz/AboutLibraries/blob/master/library/src/main/… Used here: github.com/mikepenz/AboutLibraries/blob/…Calycine
I upvote this because unlike other answers it correctly pointed the reason why my LinearLayout (in my case I used LinearLayout instead of CardView) did not get the selection effect. But your solution does not seem so easy. Is this the way Google intended? Can't this be achieved in the layout XML file alone?Therewith
I created UntouchableTextView by subclassing TextView, and overrode onTouchEvent and returned false. The method body is just "return false" but the boilerplate code (especially the constructors) is kind of annoying. Now I replaced TextView with UntouchableTextView in the layout XML file, the LinearLayout gets the touch event. I wish TextView had a simple attribute like android:hitTest="false" so this could be done in one line...Therewith
now I finally know what was the problem, but your solution has one problem: swipe event is also recognized as click and click effect works outInternal
J
123

Assuming you are using Material/Appcompat theme and Lollipop,I got this to work by making the CardView have the following attributes:

android:focusable="true"
android:clickable="true"
android:foreground="?android:attr/selectableItemBackground"
Janis answered 17/11, 2014 at 15:12 Comment(7)
no this didn't work :/. Still no click feedback. Just on the areas without an onClick listenerCalycine
@Gak2, I think you need to set android:background instead of android:foreground. Setting this made it work for me.Trabeated
using foreground did nothing for me. If there is something to what you suggest, then please explain further.Aletaaletha
I'm going to guess that your problems are because android:foreground only works for FrameLayouts or classes that inherit FrameLayout like CardView. If you try to set it in a RelativeLayout or LinearLayout, it won't workJanis
As @AvinashR said, setting android:background works. However, a better enhancement would be this: android:clickable="true" android:background="?attr/selectableItemBackground" This way, the background uses the default touch selector on pre Lollipop devices and a beautiful ripple on Lollipop and above :-)Beowulf
Changing to android:foreground="?attr/selectableItemBackground" will use fade effect on pre-Lollipop and ripple on Lollipop and above.Sheepfold
Certainly you get a nice feedback if you surly add the theme : android:theme="@style/AppTheme.PopupOverlay" to your root elementFictional
T
52

CardLayout is just a subclass of ViewGroup, So setting:

android:background="?android:attr/selectableItemBackground"

should do the trick.

UPDATE:

(looks like you are trying to use a custom ripple color.)

Try using a ripple drawable with custom color as the background (I haven't used this, So I cannot verify this behavior.) see: the documentation or this question to get you started.

Trabeated answered 26/12, 2014 at 20:40 Comment(3)
How I set it programmatically?Gerrard
I found the answer: #20532016Gerrard
I got this working by also setting android:focusable="true" and android:clickable="true".Phenol
M
22

for me:

android:background="?android:attr/selectableItemBackground"

made it finally work. see background of recyclerView / content behind the item, and also see the ripple effect used with recyclerView.

Mcarthur answered 13/3, 2015 at 13:23 Comment(0)
H
14

None of the above answers worked for me or was either too complicated.

Then I realized I had to set the following properties in THE RECYCLERVIEW ADAPTER LAYOUT.

android:background="?android:attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"

Then I got it to work.

Hormonal answered 13/9, 2015 at 17:16 Comment(5)
that needs a min of API level to be 11Onestep
@war_Hero Is this a problem really?Gladdie
In a way yes, since recyclerview is intended to work till version 7Onestep
On the bright side most of the newer apps support jellybean and above so your answer also worksOnestep
Technically, It's not the case. I was able to fix my problem just by looking for a clue in your snippet. The real issue is you should not be using android:clickable="true" uncesseriliy everywhere. It should only be in CardView. Then it works.Nutting
C
10

I was now able to solve the issue.

The problem is that i use onClickListener on the TextViews and this click listener prevents the RippleForeground from being notified about the touch event. So the solution is to implement an TouchListener on those TextViews and pass through the touch event.

The class is really simple, and can be used everywhere:

package com.mikepenz.aboutlibraries.util;

import android.support.v7.widget.CardView;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by mikepenz on 16.04.15.
 */
public class RippleForegroundListener implements View.OnTouchListener {
    CardView cardView;

    public RippleForegroundListener setCardView(CardView cardView) {
        this.cardView = cardView;
        return this;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        // Convert to card view coordinates. Assumes the host view is
        // a direct child and the card view is not scrollable.
        float x = event.getX() + v.getLeft();
        float y = event.getY() + v.getTop();

        if (android.os.Build.VERSION.SDK_INT >= 21) {
            // Simulate motion on the card view.
            cardView.drawableHotspotChanged(x, y);
        }

        // Simulate pressed state on the card view.
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                cardView.setPressed(true);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                cardView.setPressed(false);
                break;
        }

        // Pass all events through to the host view.
        return false;
    }
}

It can be used by adding this as TouchListener:

RippleForegroundListener rippleForegroundListener = new RippleForegroundListener();
older.libraryCreator.setOnTouchListener(rippleForegroundListener);

This listener will just pass through the touch event to the CardView and trigger the correct Ripple effect. (You can also modify this to take any view. It should not be limited to a CardView)

Calycine answered 24/4, 2015 at 22:17 Comment(5)
what is older.libraryCreator.setOnTouchListener(rippleForegroundListener); ?Doorway
@MohitKhaitan the view which should pass the Touch to the RippleView. There's also an updated version of this, without keeping the context. github.com/mikepenz/AboutLibraries/blob/master/library/src/main/… Used here: github.com/mikepenz/AboutLibraries/blob/…Calycine
I upvote this because unlike other answers it correctly pointed the reason why my LinearLayout (in my case I used LinearLayout instead of CardView) did not get the selection effect. But your solution does not seem so easy. Is this the way Google intended? Can't this be achieved in the layout XML file alone?Therewith
I created UntouchableTextView by subclassing TextView, and overrode onTouchEvent and returned false. The method body is just "return false" but the boilerplate code (especially the constructors) is kind of annoying. Now I replaced TextView with UntouchableTextView in the layout XML file, the LinearLayout gets the touch event. I wish TextView had a simple attribute like android:hitTest="false" so this could be done in one line...Therewith
now I finally know what was the problem, but your solution has one problem: swipe event is also recognized as click and click effect works outInternal
T
9
android:foreground="?android:attr/selectableItemBackground"

If you use foreground instead background, you don't need to use clickable or focusable

Trivial answered 6/5, 2016 at 7:54 Comment(2)
Tried all the other answers, this one works perfectly for me by just adding it to the CardView!Bisulfate
Works perfectly with differently colored cards too. Thanks!Hallo
D
5

In my case, i was need to show custom background and ripple effect as well. So the way I achieved this is by applying these two attributes on the root layout of my recycler-view item:

android:background="@drawable/custom_bg"
android:foreground="?android:attr/selectableItemBackground"
Dander answered 27/5, 2017 at 12:27 Comment(0)
E
4

Had the same problem as Jiang:

I have to set the attributes to the layout-file, which is inflating the RecycleView.

My Main-Layout: (Don't add the properties here!)

<?xml version="1.0" encoding="utf-8"?>

<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">


<android.support.v7.widget.RecyclerView
    android:id="@+id/recycler_view"
    android:scrollbars="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

</RelativeLayout>

My RecycleViewAdapter:

            View v = inflater.inflate(R.layout.list_center_item, parent, false);

My list_center_item.xml (Add the properties here!)

<?xml version="1.0" encoding="utf-8"?>


<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"


android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
/>
Experimental answered 31/8, 2016 at 13:29 Comment(0)
K
0

You have to add following lines in the layout that you are using under CardView

android:clickable="true"
android:focusable="true"
android:background="@drawable/my_ripple"

Example

<android.support.v7.widget.CardView android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:background="@drawable/my_ripple">
Krisha answered 7/2, 2017 at 17:27 Comment(1)
And what is the drawable file for @drawable/my_ripple?Megdal
N
0

The actual silly issue was in my case is the following....

Technically, It's not the case just like everyone suggests here in my case. The real issue is you should not be using android:clickable="true" uncesseriliy everywhere in the LayoutView, RelativeView or TextView.

It should only be in CardView. Then it works like charm

android:focusable="true" Is not really requied.

Just add the following in your CardView

android:clickable="true" android:foreground="?android:attr/selectableItemBackground"


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center"
        android:layout_margin="@dimen/card_margin"
        android:foreground="?android:attr/selectableItemBackground"
        android:clickable="true"
        android:elevation="3dp"
        card_view:cardCornerRadius="@dimen/card_corner_radius">

        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/card_template_item_image"
                android:layout_width="match_parent"
                android:layout_height="@dimen/card_size_height"
                android:scaleType="fitXY" />

            <TextView
                android:id="@+id/card_template_item_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/card_template_item_image"
                android:paddingLeft="@dimen/card_title_padding"
                android:paddingRight="@dimen/card_title_padding"
                android:paddingTop="@dimen/card_title_padding"
                android:textSize="@dimen/card_title_size"
                android:text="@string/place_holder_item_name"/>

            <TextView
                android:id="@+id/card_template_item_sub_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_below="@id/card_template_item_title"
                android:paddingBottom="@dimen/card_sub_title_padding"
                android:paddingLeft="@dimen/card_sub_title_padding"
                android:paddingRight="@dimen/card_sub_title_padding"
                android:textSize="@dimen/card_sub_title_size"
                android:text="@string/place_holder_item_category"/>

        </RelativeLayout>

    </android.support.v7.widget.CardView>

</LinearLayout>
Nutting answered 17/10, 2018 at 11:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.