How to handle the Ripple effect on 9-patch and CardView, and have control over the states of the selector?
Asked Answered
O

5

25

Background

I wish to add a simple ripple effect for listView items from Android Lollipop and above.

First I'd like to set it for simple rows, and then to 9-patch rows and even CardView.

The problem

I was sure this one is going to be very easy, as it doesn't even require me to define the normal selector. I failed to do so even for simple rows. For some reason, the ripple effect goes beyond the row's boundaries:

enter image description here

Not only that, but on some cases, the background of the item gets stuck on the color I've set it to be.

The code

This is what I've tried:

MainActivity.java

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final ListView listView = (ListView) findViewById(android.R.id.list); 
        final LayoutInflater inflater = LayoutInflater.from(this);
        listView.setAdapter(new BaseAdapter() {

            @Override
            public View getView(final int position, final View convertView, final ViewGroup parent) {
                View rootView = convertView;
                if (rootView == null) {
                    rootView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false);
                    ((TextView) rootView.findViewById(android.R.id.text1)).setText("Test");
                }
                return rootView;
            }

            @Override
            public long getItemId(final int position) {
                return 0;
            }

            @Override
            public Object getItem(final int position) {
                return null;
            }

            @Override
            public int getCount() {
                return 2;
            }
        });
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:cacheColorHint="@android:color/transparent"
    android:divider="@null"
    android:dividerHeight="0px"
    android:fadeScrollbars="false"
    android:fastScrollEnabled="true"
    android:listSelector="@drawable/listview_selector"
    android:scrollingCache="false"
    android:verticalScrollbarPosition="right" />

res/drawable-v21/listview_selector.xml (I have a normal selector for other Android versions)

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" />

What I've tried

Aside from the simple code above, I've also tried setting the selector per item's background property, instead of using "listSelector" on the ListView, but it didn't help.

Another thing I've tried is to set the foreground of the items, but it also had the same result.

The question

How do I fix this issue? Why does it occur? What did I do wrong?

How do I go further, to support 9-patch and even CardView ?

Also, how can I set a state for the new background, like being checked/selected ?


Update: The drawing outside of the view is fixed using something like this:

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight" >

    <item android:id="@android:id/mask">
        <color android:color="@color/listview_pressed" />
    </item>

</ripple>

Still, it has the issue of background being stuck, and I can't find how to handle the rest of the missing features (9-patch, cardView,...) .

I think the color-being-stuck has something to do with using it as the foreground of views.


EDIT: I see some people don't understand what the question here is about.

It's about handling the new ripple effect, while still having the older selector/CardView.

For example, here's a selector-drawble:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="..." android:state_selected="true"/>
    <item android:drawable="..." android:state_activated="true"/>
    <item android:drawable="..." android:state_focused="true" android:state_pressed="true"/>
    <item android:drawable="..." android:state_pressed="true"/>
    <item android:drawable="..."/>
</selector>

This can be used as a list-selector or a background of a single view.

However, I can't find how to use it along with the ripple drawable.

I know that the ripple already takes care of some of the states, but for some, it doesn't. Plus, I can't find out how to make it handle 9-patch and CardView.

I hope now it's easier to understand the problem I have.


About the issue of the color of the ripple gets "stucked", I think it's because of how I made the layout. I wanted a layout which can be checked (when I decide to) and also have the effect of clicking, so this is what I made (based on this website and another that I can't find) :

public class CheckableRelativeLayout extends RelativeLayout implements Checkable {
    private boolean mChecked;
    private static final String TAG = CheckableRelativeLayout.class.getCanonicalName();
    private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
    private Drawable mForegroundDrawable;

    public CheckableRelativeLayout(final Context context) {
        this(context, null, 0);
    }

    public CheckableRelativeLayout(final Context context, final AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CheckableRelativeLayout(final Context context, final AttributeSet attrs, final int defStyle) {
        super(context, attrs, defStyle);
        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CheckableRelativeLayout, defStyle,
                0);
        setForeground(a.getDrawable(R.styleable.CheckableRelativeLayout_foreground));
        a.recycle();
    }

    public void setForeground(final Drawable drawable) {
        this.mForegroundDrawable = drawable;
    }

    public Drawable getForeground() {
        return this.mForegroundDrawable;
    }

    @Override
    protected int[] onCreateDrawableState(final int extraSpace) {
        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
        if (isChecked()) {
            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
        }
        return drawableState;
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        final Drawable drawable = getBackground();
        boolean needRedraw = false;
        final int[] myDrawableState = getDrawableState();
        if (drawable != null) {
            drawable.setState(myDrawableState);
            needRedraw = true;
        }
        if (mForegroundDrawable != null) {
            mForegroundDrawable.setState(myDrawableState);
            needRedraw = true;
        }
        if (needRedraw)
            invalidate();
    }

    @Override
    protected void onSizeChanged(final int width, final int height, final int oldwidth, final int oldheight) {
        super.onSizeChanged(width, height, oldwidth, oldheight);
        if (mForegroundDrawable != null)
            mForegroundDrawable.setBounds(0, 0, width, height);
    }

    @Override
    protected void dispatchDraw(final Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mForegroundDrawable != null)
            mForegroundDrawable.draw(canvas);
    }

    @Override
    public boolean isChecked() {
        return mChecked;
    }

    @Override
    public void setChecked(final boolean checked) {
        setChecked(checked, true);
    }

    public void setChecked(final boolean checked, final boolean alsoRecursively) {
        mChecked = checked;
        refreshDrawableState();
        if (alsoRecursively)
            ViewUtil.setCheckedRecursively(this, checked);
    }

    @Override
    public void toggle() {
        setChecked(!mChecked);
    }

    @Override
    public Parcelable onSaveInstanceState() {
        // Force our ancestor class to save its state
        final Parcelable superState = super.onSaveInstanceState();
        final SavedState savedState = new SavedState(superState);
        savedState.checked = isChecked();
        return savedState;
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void drawableHotspotChanged(final float x, final float y) {
        super.drawableHotspotChanged(x, y);
        if (mForegroundDrawable != null) {
            mForegroundDrawable.setHotspot(x, y);
        }
    }

    @Override
    public void onRestoreInstanceState(final Parcelable state) {
        final SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        setChecked(savedState.checked);
        requestLayout();
    }

    // /////////////
    // SavedState //
    // /////////////

    private static class SavedState extends BaseSavedState {
        boolean checked;

        SavedState(final Parcelable superState) {
            super(superState);
        }

        private SavedState(final Parcel in) {
            super(in);
            checked = (Boolean) in.readValue(null);
        }

        @Override
        public void writeToParcel(final Parcel out, final int flags) {
            super.writeToParcel(out, flags);
            out.writeValue(checked);
        }

        @Override
        public String toString() {
            return TAG + ".SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " checked=" + checked
                    + "}";
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(final Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(final int size) {
                return new SavedState[size];
            }
        };
    }
}

EDIT: the fix was to add the next lines for the layout I've made:

@SuppressLint("ClickableViewAccessibility")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean onTouchEvent(final MotionEvent e) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && //
            e.getActionMasked() == MotionEvent.ACTION_DOWN && //
            mForeground != null)
        mForeground.setHotspot(e.getX(), e.getY());
    return super.onTouchEvent(e);
}
Ozonize answered 27/11, 2014 at 12:32 Comment(0)
K
20

RippleDrawable extends LayerDrawable. Touch feedback drawable may contain multiple child layers, including a special mask layer that is not drawn to the screen. A single layer may be set as the mask by specifying its android:id value as mask. The second layer can be StateListDrawable.

For example, here is our StateListDrawable resource with name item_selectable.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="..." android:state_selected="true"/>
    <item android:drawable="..." android:state_activated="true"/>
    <item android:drawable="..." android:state_focused="true" android:state_pressed="true"/>
    <item android:drawable="..." android:state_pressed="true"/>
    <item android:drawable="..."/>
</selector>

To achieve ripple effect along with selectors we can set drawable above as a layer of RippleDrawable with name list_selector_ripple.xml:

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/colorControlHighlight">
    <item android:id="@android:id/mask">
        <color android:color="@android:color/white"/>
    </item>
    <item android:drawable="@drawable/item_selectable"/>
</ripple>

UPD:

1) To use this drawable with CardView just set it as android:foreground, like this:

<android.support.v7.widget.CardView
    ...
    android:foreground="@drawable/list_selector_ripple"
    />

2) To make the ripple effect works within the bounds of the 9-patch we should set this 9-patch drawable as mask of ripple drawable (list_selector_ripple_nine_patch.xml):

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
        android:color="@color/colorControlHighlight">
    <item android:id="@android:id/mask" android:drawable="@drawable/your_nine_patch" />
    <item android:drawable="@drawable/your_nine_patch" />
</ripple>

Then set the background of view:

<LinearLayout
    ...
    android:background="@drawable/list_selector_ripple_nine_patch"
    />
Katey answered 5/12, 2014 at 14:16 Comment(18)
Nice. Thank you! Will the ripple work just within the bounds of the 9-patch? Or will it go further to the whole size of the View? Is it even possible to customize the ripple (other than colors)? Also, how would you also handle CardView? For example, suppose I want the CardView to be clickable (using a ripple effect), and when I mark it, it's in "checked" (or "selected") state, so its content changes to the color I've chosen .Ozonize
Thank you! So the mask is used to restrict the drawing of the ripple? If so, what does it do when you choose a color or a drawable that isn't 9-patch for it? About "foreground", I think there is a bug on v5.0 that doesn't put the ripple where you've touched (meaning it only starts on the center) : code.google.com/p/android-developer-preview/issues/… . Was it fixed?Ozonize
The mask defines the bounds for the ripple animation. It can be any drawable (shape, 9-patch, color etc.). I don't know about issue you post. I have a Nexus 4 with Lollipop and it hasn't this bug.Katey
But what does it do when you choose a color as the mask, if it defines the bounds of the ripple? About the bug, I've now checked, and I think it got fixed (plus the website says it got fixed). However, now I don't know how to handle a CardView selection. Maybe I will make a new question for this.Ozonize
I'm now away from my laptop and writing from my phone. There is a quote from documentation: If a mask layer is set, the ripple effect will be masked against that layer before it is drawn over the composite of the remaining child layers. If no mask layer is set, the ripple effect is masked against the composite of the child layers.Katey
But what does it do when you choose a color for the mask? Even their example has a color there...Ozonize
The ripple effect will be where nontransparent pixels are arranged. It does not matter what kind of color you set for the mask. If a mask set as color, the ripple effect will be masked against whole View.Katey
so why bother putting a mask at all in this case?Ozonize
Suppose we use color with alpha (for example, #88000000, android:color/transparent etc.) for pressed state in item_selectable.xml. If we don't set mask in list_selector_ripple.xml the ripple effect is masked against the child layer, i.e. drawable for pressed state from item_selectable.xml. This drawable is color with alpha, so the ripple effect will have zero sided bounds. To see this effect we should set the mask manually. If we use color without alpha for pressed state (#fff, android:color/white etc.) setting the mask is optional.Katey
Not sure I understand, so I've tried it now, and got the effect I had on the question (ripple gets outside of the view). When I add a 9-patch, the effect seems to work fine on both cases (with and without a mask), unless I set an alpha, which causes the ripple (and its background) become more transparent. Can you please post an example of what happens (with screenshots)? As you've answered everything, I will now mark your answer so that you get the bounty, and I assume I will learn the rest by myself, but I still don't quite get the mask purpose and behavior.Ozonize
Maybe I can't explain this because my English is bad. But rippledrawable is very easy to understand. All you need to know: "The ripple effect will be where non transparent pixels of mask are arranged. If a mask layer is set, the ripple effect will be masked against that layer before it is drawn over the composite of the remaining child layers. If no mask layer is set, the ripple effect is masked against the composite of the child layers". It is not necessarily what you use as mask (color, 9-patch, shapes etc.)Katey
Just try to compose various cases (with/without mask, 9-patch/color/shapes with/without alpha). Please post an example where rules above is not working.Katey
I think I understand about the layers: if I use a 9-patch, I should also set the same one for the mask, so that the ripple will be bound to the content area of the 9-patch. Is this correct? However, for all other cases (color and shape, for example), I don't get what is supposed to happen. I also don't get why I can get the ripple effect to be shown outside of the View's bounds (as I've shown on the question) ... BTW, about English, that's not my main language either.Ozonize
Ripple effect outside View's bounds is a normal behavior. There is another quote from documentation: "If no child layers or mask is specified and the ripple is set as a View background, the ripple will be drawn atop the first available parent background within the View's hierarchy. In this case, the drawing region may extend outside of the Drawable bounds". Example: <ripple android:color="@color/colorControlHighlight" />.Katey
But why would you want something that works this way? When I tell a view to have a certain background, I expect it to draw only within itself... Anyway, about the mask used as a color (or any other drawable that doesn't have bounds), what will it do? suppose I choose #5500FF00 for the mask, what will happen exactly?Ozonize
I work on an app which has a cardView implementation (not using the CardView class) using a simple background and foreground drawables. the foreground is a selector (now also a ripple which simply has a mask), and the background is a simple 9-patch image (showing the card). When using this technique, sometimes the view gets stuck on the color of the ripple (for example, if the ripple has a red color, the whole view's will be red). How could it be?Ozonize
I've found another way to set ripples on any view : https://mcmap.net/q/539399/-ripple-effect-on-top-of-image-androidOzonize
Hi, I realize by applying selector (or ripple) on CardView's foreground, the solid color in selector will block children in CardView itself - #33763903 May I know do you encounter the same prob as I did? Or, do I miss out anything? Thanks.Television
C
5

Simple way to create a ripple make a xml in drawable-v21 folder and use this code for xml.

android:backgroung="@drawable/ripple_xyz"

And if, Through java / dynamically use.

View.setBackgroundResource(R.drawable.ripple_xyz);

Here is the ripple_xyz.xml.

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#228B22" >
// ^ THIS IS THE COLOR FOR RIPPLE
<item>
    <shape
        android:shape="rectangle"
        android:useLevel="false" >
        <solid android:color="#CCFFFFFF" />
// ^ THIS IS THE COLOR FOR BACK GROUND
    </shape>
</item>

Christianchristiana answered 18/12, 2014 at 6:47 Comment(1)
That's nice, but I already marked an answer (and a good one actually). However, I will give you +1 for the effort.Ozonize
E
2

You have to set a mask layer in the ripple.

Something like this:

<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight">
    <item android:id="@id/mask">
        <color android:color="@color/myColor" />
    </item>
</ripple>
Extinctive answered 27/11, 2014 at 17:42 Comment(2)
The mask id is not recognizable . Also, after putting the second color, it seems that even after I stop selecting an item, its background is different from the others. In addition, how should I handle 9-patch backgrounds and also CardView ?Ozonize
Can you please help? I've set android:id="@android:id/mask" , but on some cases, when I long click an item, and continue touching to other areas, and then stop touching, the background of the view stays filled... Is it perhaps because I tried setting the ripple drawable as a foreground?Ozonize
N
2

Check this tutorial. ripple effect is implement and its working fine. Ripple Effect on RecyclerView

Necropolis answered 5/12, 2014 at 9:21 Comment(1)
It just says to use android:background="?android:attr/selectableItemBackground" , but how do I use selectors in addition to that, and on 9-patch and CardViews ?Ozonize
R
2

You can handle 9 patch images by something like below code:

 <?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="?attr/colorControlHighlight" >

    <item android:id="@android:id/mask"
     android:drawable="@drawable/comment_background">
    </item>

</ripple>

where comment_background is a 9 patch image

Runck answered 5/12, 2014 at 9:54 Comment(1)
How do you make the background to also have a "selected" (or "checked") state? Can I put a selector inside somehow? Also, how do you set it on a CardView?Ozonize

© 2022 - 2024 — McMap. All rights reserved.