RippleDrawable NOT drawing over Views

2019-08-03 03:35发布

问题:

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?

回答1:

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:

  1. 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.
  2. wrap your KenBurnsView with a FrameLayout and set the ripple using setForeground on the FrameLayout.
  3. 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.


回答2:

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.



回答3:

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.