I have a layout with a KenBurnsView and an ImageView over it (just a toggle button). When I click on the button a Ripple is generated but is drawn below the KenBurnsView.
Previously, when I had an Image view in replacement to the KenBurnsView the Ripple was drawn above the ImageView on the top.
Here is my layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background"
android:clickable="true"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_drawer_header_height">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.flaviofaria.kenburnsview.KenBurnsView
android:id="@+id/header_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cover_1" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/header_toggle"
android:layout_width="50dp"
android:layout_height="30dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp"
android:padding="10dp"
android:src="@drawable/toggle_down" />
</RelativeLayout>
</FrameLayout>
</RelativeLayout>
<RelativeLayout
android:id="@+id/nav_toggle_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"></RelativeLayout>
</LinearLayout>
This is my ripple drawable XML:
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@android:color/white"
android:drawSelectorOnTop="true"> <!-- ripple color -->
</ripple>
This is how I am adding the ripple:
toggle.setBackground(getResources().getDrawable(R.drawable.ripple));
What is the problem because of which the Ripple gets drwan below the KenBurnsView? It used to work perfectly when there was an ImageView in place of the KenBurnsView?
as you can see from your own code ('setBackground') , you're setting the ripple as a BACKGROUND that's why it's being drawn on the background.
ImageView on android API 21 added this "hack" for the ripple android:drawSelectorOnTop="true"
. But the library you're using didn't add the same hack to it.
There's nothing wrong itself on your code. But this type of behavior cannot be guaranteed by the Android team for 3rd party libraries.
You have a few of options here that will vary on cleanliness, effort and performance:
- check ImageView source code, clone the library, add the same hack that imageview used on it for the ripple. After it's working fine, make sure to pull request back to the library.
- wrap your
KenBurnsView
with a FrameLayout and set the ripple using setForeground
on the FrameLayout.
- clone the library, add option to foreground drawable to it (similar to this How to set foreground attribute to other non FrameLayout view). Also make sure to pull request this valueable code back to the library.
Unbounded ripples are projected on to the first available background of an ancestor view. Trace up the view hierarchy from the ImageView
that's hosting the ripple and you will find that the first available background is on the LinearLayout
with identifier drawer
.
If you set a transparent background on the RelativeLayout
containing your ImageView
, the first available background will be one that's rendered above the sibling view and you will get the desired effect.
Side note, that RelativeLayout
should be replaced with a FrameLayout
, which will provide the same effect with a less expensive layout pass.
I took inspiration and help from the answer by @Budius and created a much better solution.
The ImageView uses a hack which allows RippleDrawable to draw over it even when you set it as background (using setBackgroundDrawable()).
I created a modified ImageView with the capability of drawing foreground,
public class ForegroundImageView extends ImageView {
private Drawable foreground;
public ForegroundImageView(Context context) {
this(context, null);
}
public ForegroundImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundImageView);
Drawable foreground = a.getDrawable(R.styleable.ForegroundImageView_android_foreground);
if (foreground != null) {
setForeground(foreground);
}
a.recycle();
}
/**
* Supply a drawable resource that is to be rendered on top of all of the child
* views in the frame layout.
*
* @param drawableResId The drawable resource to be drawn on top of the children.
*/
public void setForegroundResource(int drawableResId) {
setForeground(getContext().getResources().getDrawable(drawableResId));
}
/**
* Supply a Drawable that is to be rendered on top of all of the child
* views in the frame layout.
*
* @param drawable The Drawable to be drawn on top of the children.
*/
public void setForeground(Drawable drawable) {
if (foreground == drawable) {
return;
}
if (foreground != null) {
foreground.setCallback(null);
unscheduleDrawable(foreground);
}
foreground = drawable;
if (drawable != null) {
drawable.setCallback(this);
if (drawable.isStateful()) {
drawable.setState(getDrawableState());
}
}
requestLayout();
invalidate();
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || who == foreground;
}
@Override
public void jumpDrawablesToCurrentState() {
super.jumpDrawablesToCurrentState();
if (foreground != null) foreground.jumpToCurrentState();
}
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (foreground != null && foreground.isStateful()) {
foreground.setState(getDrawableState());
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (foreground != null) {
foreground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight());
invalidate();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (foreground != null) {
foreground.setBounds(0, 0, w, h);
invalidate();
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (foreground != null) {
foreground.draw(canvas);
}
}
}
Then I put my toggle image in this image view and programmatically applied the ripple,
toggle.setForegroundResource(R.drawable.ripple);
This will make the ripple draw within the bounds of this particular view. To make the ripple draw over other views,
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
You need to add false to both clipChildren and clipToPadding.
Hope this helps someone stuck with the problem.