Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New error Marshmallow (only?) - "Underflow in restore - more restores than saves" #74

Closed
conzorz opened this issue Aug 24, 2015 · 20 comments

Comments

@conzorz
Copy link

conzorz commented Aug 24, 2015

Error at:

com.andexert.library.RippleView.draw (RippleView.java:166)

Not sure if this helps? http://stackoverflow.com/questions/18220084/underflow-in-restore-in-android-4-3

@Possimpible
Copy link

If it means anything, I can confirm this one. Crashes Marshmallow only for me.

@pnickv
Copy link

pnickv commented Aug 31, 2015

+1

3 similar comments
@nickaein-a
Copy link

+1

@arsalancode
Copy link

+1

@matthewrice345
Copy link

+1

@matthewrice345
Copy link

I ended up wrapping the insides of the draw method with a try/catch and if the exception is thrown cause the onCompleteionListener to be triggered. While a try/catch is not ideal it does function like pre-M.

`public void draw(Canvas canvas) {
try {
...
} catch (IllegalStateException e) {
if (onCompletionListener != null) onCompletionListener.onComplete(this);
}
}``

@ben-zhong
Copy link

+1

@esiek
Copy link

esiek commented Sep 24, 2015

+1

@Possimpible
Copy link

@traex with M rolling out soon, any update on the issue please?

@n4sthetics
Copy link

Wrapping the whole onDraw method doesn't call the onRippleCompleteListener class,
Here's a modified version of the RippleView class which you can use which wraps a try-catch block around canvas.restore();
Just add this to your project, and use this class instead of your regular RippleView class

EDIT: It doesn't seem necessary in Marshmallow to call canvas.restore(). So instead of using a try-catch block, adding a simple condition to check whether the device's running Marshmallow will suffice

if(Build.VERSION.SDK_INT != Build.VERSION_CODES.M)
    canvas.restore();
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.os.Handler;
import android.support.annotation.ColorRes;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.animation.Animation;
import android.view.animation.ScaleAnimation;
import android.widget.AdapterView;
import android.widget.RelativeLayout;


/**
 * RippleView custom layout
 *
 * Custom Layout that allows to use Ripple UI pattern above API 21
 *
 * @author Chutaux Robin
 * @version 2015.0512
 */
public class TryRippleView extends RelativeLayout {

    private int WIDTH;
    private int HEIGHT;
    private int frameRate = 10;
    private int rippleDuration = 400;
    private int rippleAlpha = 90;
    private Handler canvasHandler;
    private float radiusMax = 0;
    private boolean animationRunning = false;
    private int timer = 0;
    private int timerEmpty = 0;
    private int durationEmpty = -1;
    private float x = -1;
    private float y = -1;
    private int zoomDuration;
    private float zoomScale;
    private ScaleAnimation scaleAnimation;
    private Boolean hasToZoom;
    private Boolean isCentered;
    private Integer rippleType;
    private Paint paint;
    private Bitmap originBitmap;
    private int rippleColor;
    private int ripplePadding;
    private GestureDetector gestureDetector;
    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };

    private OnRippleCompleteListener onCompletionListener;

    public TryRippleView(Context context) {
        super(context);
    }

    public TryRippleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public TryRippleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    /**
     * Method that initializes all fields and sets listeners
     *
     * @param context Context used to create this view
     * @param attrs Attribute used to initialize fields
     */
    private void init(final Context context, final AttributeSet attrs) {
        if (isInEditMode())
            return;

        final TypedArray typedArray = context.obtainStyledAttributes(attrs, com.andexert.library.R.styleable.RippleView);
        rippleColor = typedArray.getColor(com.andexert.library.R.styleable.RippleView_rv_color, getResources().getColor(com.andexert.library.R.color.rippelColor));
        rippleType = typedArray.getInt(com.andexert.library.R.styleable.RippleView_rv_type, 0);
        hasToZoom = typedArray.getBoolean(com.andexert.library.R.styleable.RippleView_rv_zoom, false);
        isCentered = typedArray.getBoolean(com.andexert.library.R.styleable.RippleView_rv_centered, false);
        rippleDuration = typedArray.getInteger(com.andexert.library.R.styleable.RippleView_rv_rippleDuration, rippleDuration);
        frameRate = typedArray.getInteger(com.andexert.library.R.styleable.RippleView_rv_framerate, frameRate);
        rippleAlpha = typedArray.getInteger(com.andexert.library.R.styleable.RippleView_rv_alpha, rippleAlpha);
        ripplePadding = typedArray.getDimensionPixelSize(com.andexert.library.R.styleable.RippleView_rv_ripplePadding, 0);
        canvasHandler = new Handler();
        zoomScale = typedArray.getFloat(com.andexert.library.R.styleable.RippleView_rv_zoomScale, 1.03f);
        zoomDuration = typedArray.getInt(com.andexert.library.R.styleable.RippleView_rv_zoomDuration, 200);
        typedArray.recycle();
        paint = new Paint();
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(rippleColor);
        paint.setAlpha(rippleAlpha);
        this.setWillNotDraw(false);

        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public void onLongPress(MotionEvent event) {
                super.onLongPress(event);
                animateRipple(event);
                sendClickEvent(true);
            }

            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                return true;
            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }
        });

        this.setDrawingCacheEnabled(true);
        this.setClickable(true);
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        if (animationRunning) {
            if (rippleDuration <= timer * frameRate) {
                animationRunning = false;
                timer = 0;
                durationEmpty = -1;
                timerEmpty = 0;
                try {
                    canvas.restore();
                }catch(Exception e){
                    e.printStackTrace();
                }
                invalidate();
                if (onCompletionListener != null) onCompletionListener.onComplete(this);
                return;
            } else
                canvasHandler.postDelayed(runnable, frameRate);

            if (timer == 0)
                canvas.save();


            canvas.drawCircle(x, y, (radiusMax * (((float) timer * frameRate) / rippleDuration)), paint);

            paint.setColor(Color.parseColor("#ffff4444"));

            if (rippleType == 1 && originBitmap != null && (((float) timer * frameRate) / rippleDuration) > 0.4f) {
                if (durationEmpty == -1)
                    durationEmpty = rippleDuration - timer * frameRate;

                timerEmpty++;
                final Bitmap tmpBitmap = getCircleBitmap((int) ((radiusMax) * (((float) timerEmpty * frameRate) / (durationEmpty))));
                canvas.drawBitmap(tmpBitmap, 0, 0, paint);
                tmpBitmap.recycle();
            }

            paint.setColor(rippleColor);

            if (rippleType == 1) {
                if ((((float) timer * frameRate) / rippleDuration) > 0.6f)
                    paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timerEmpty * frameRate) / (durationEmpty)))));
                else
                    paint.setAlpha(rippleAlpha);
            } else
                paint.setAlpha((int) (rippleAlpha - ((rippleAlpha) * (((float) timer * frameRate) / rippleDuration))));

            timer++;
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        WIDTH = w;
        HEIGHT = h;

        scaleAnimation = new ScaleAnimation(1.0f, zoomScale, 1.0f, zoomScale, w / 2, h / 2);
        scaleAnimation.setDuration(zoomDuration);
        scaleAnimation.setRepeatMode(Animation.REVERSE);
        scaleAnimation.setRepeatCount(1);
    }

    /**
     * Launch Ripple animation for the current view with a MotionEvent
     *
     * @param event MotionEvent registered by the Ripple gesture listener
     */
    public void animateRipple(MotionEvent event) {
        createAnimation(event.getX(), event.getY());
    }

    /**
     * Launch Ripple animation for the current view centered at x and y position
     *
     * @param x Horizontal position of the ripple center
     * @param y Vertical position of the ripple center
     */
    public void animateRipple(final float x, final float y) {
        createAnimation(x, y);
    }

    /**
     * Create Ripple animation centered at x, y
     *
     * @param x Horizontal position of the ripple center
     * @param y Vertical position of the ripple center
     */
    private void createAnimation(final float x, final float y) {
        if (this.isEnabled() && !animationRunning) {
            if (hasToZoom)
                this.startAnimation(scaleAnimation);

            radiusMax = Math.max(WIDTH, HEIGHT);

            if (rippleType != 2)
                radiusMax /= 2;

            radiusMax -= ripplePadding;

            if (isCentered || rippleType == 1) {
                this.x = getMeasuredWidth() / 2;
                this.y = getMeasuredHeight() / 2;
            } else {
                this.x = x;
                this.y = y;
            }

            animationRunning = true;

            if (rippleType == 1 && originBitmap == null)
                originBitmap = getDrawingCache(true);

            invalidate();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (gestureDetector.onTouchEvent(event)) {
            animateRipple(event);
            sendClickEvent(false);
        }
        return super.onTouchEvent(event);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        this.onTouchEvent(event);
        return super.onInterceptTouchEvent(event);
    }

    /**
     * Send a click event if parent view is a Listview instance
     *
     * @param isLongClick Is the event a long click ?
     */
    private void sendClickEvent(final Boolean isLongClick) {
        if (getParent() instanceof AdapterView) {
            final AdapterView adapterView = (AdapterView) getParent();
            final int position = adapterView.getPositionForView(this);
            final long id = adapterView.getItemIdAtPosition(position);
            if (isLongClick) {
                if (adapterView.getOnItemLongClickListener() != null)
                    adapterView.getOnItemLongClickListener().onItemLongClick(adapterView, this, position, id);
            } else {
                if (adapterView.getOnItemClickListener() != null)
                    adapterView.getOnItemClickListener().onItemClick(adapterView, this, position, id);
            }
        }
    }

    private Bitmap getCircleBitmap(final int radius) {
        final Bitmap output = Bitmap.createBitmap(originBitmap.getWidth(), originBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(output);
        final Paint paint = new Paint();
        final Rect rect = new Rect((int)(x - radius), (int)(y - radius), (int)(x + radius), (int)(y + radius));

        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        canvas.drawCircle(x, y, radius, paint);

        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(originBitmap, rect, rect, paint);

        return output;
    }

    /**
     * Set Ripple color, default is #FFFFFF
     *
     * @param rippleColor New color resource
     */
    @ColorRes
    public void setRippleColor(int rippleColor) {
        this.rippleColor = getResources().getColor(rippleColor);
    }

    public int getRippleColor() {
        return rippleColor;
    }

    public RippleType getRippleType()
    {
        return RippleType.values()[rippleType];
    }

    /**
     * Set Ripple type, default is RippleType.SIMPLE
     *
     * @param rippleType New Ripple type for next animation
     */
    public void setRippleType(final RippleType rippleType)
    {
        this.rippleType = rippleType.ordinal();
    }

    public Boolean isCentered()
    {
        return isCentered;
    }

    /**
     * Set if ripple animation has to be centered in its parent view or not, default is False
     *
     * @param isCentered
     */
    public void setCentered(final Boolean isCentered)
    {
        this.isCentered = isCentered;
    }

    public int getRipplePadding()
    {
        return ripplePadding;
    }

    /**
     * Set Ripple padding if you want to avoid some graphic glitch
     *
     * @param ripplePadding New Ripple padding in pixel, default is 0px
     */
    public void setRipplePadding(int ripplePadding)
    {
        this.ripplePadding = ripplePadding;
    }

    public Boolean isZooming()
    {
        return hasToZoom;
    }

    /**
     * At the end of Ripple effect, the child views has to zoom
     *
     * @param hasToZoom Do the child views have to zoom ? default is False
     */
    public void setZooming(Boolean hasToZoom)
    {
        this.hasToZoom = hasToZoom;
    }

    public float getZoomScale()
    {
        return zoomScale;
    }

    /**
     * Scale of the end animation
     *
     * @param zoomScale Value of scale animation, default is 1.03f
     */
    public void setZoomScale(float zoomScale)
    {
        this.zoomScale = zoomScale;
    }

    public int getZoomDuration()
    {
        return zoomDuration;
    }

    /**
     * Duration of the ending animation in ms
     *
     * @param zoomDuration Duration, default is 200ms
     */
    public void setZoomDuration(int zoomDuration)
    {
        this.zoomDuration = zoomDuration;
    }

    public int getRippleDuration()
    {
        return rippleDuration;
    }

    /**
     * Duration of the Ripple animation in ms
     *
     * @param rippleDuration Duration, default is 400ms
     */
    public void setRippleDuration(int rippleDuration)
    {
        this.rippleDuration = rippleDuration;
    }

    public int getFrameRate()
    {
        return frameRate;
    }

    /**
     * Set framerate for Ripple animation
     *
     * @param frameRate New framerate value, default is 10
     */
    public void setFrameRate(int frameRate)
    {
        this.frameRate = frameRate;
    }

    public int getRippleAlpha()
    {
        return rippleAlpha;
    }

    /**
     * Set alpha for ripple effect color
     *
     * @param rippleAlpha Alpha value between 0 and 255, default is 90
     */
    public void setRippleAlpha(int rippleAlpha)
    {
        this.rippleAlpha = rippleAlpha;
    }

    public void setOnRippleCompleteListener(OnRippleCompleteListener listener) {
        this.onCompletionListener = listener;
    }

    /**
     * Defines a callback called at the end of the Ripple effect
     */
    public interface OnRippleCompleteListener {
        void onComplete(TryRippleView rippleView);
    }

    public enum RippleType {
        SIMPLE(0),
        DOUBLE(1),
        RECTANGLE(2);

        int type;

        RippleType(int type)
        {
            this.type = type;
        }
    }
}

@Frequencies
Copy link

+1

1 similar comment
@EliasMazz
Copy link

+1

@FranzBos
Copy link

+1

1 similar comment
@iRYO400
Copy link

iRYO400 commented Feb 23, 2016

+1

@traex
Copy link
Owner

traex commented Feb 24, 2016

Please see #92. It will be released asap

@traex traex closed this as completed Feb 24, 2016
@traex traex reopened this Feb 24, 2016
@traex traex closed this as completed Feb 24, 2016
@DDsix
Copy link

DDsix commented Mar 26, 2016

Issue still persists on Android M using versino 1.3 of the library

@GirishBhalerao
Copy link

There are two solutions for this Problem.

  1. As I mentioned before - "Just downgrade your - targetSdkVersion to 22
    It works for all versions" - This works fine if you have not launched your app with targetSdkVersion 23 yet.
  2. If you already have launched your app with targetSdkVersion 23 then,
    instead of adding compile 'com.github.traex.rippleeffect:library:1.3' in dependancies,
    Download the project by clicking on link - https://codeload.github.com/traex/RippleEffect/zip/master
    Get library from that and use that in your app. It works great...!!! :)

@duke79
Copy link

duke79 commented Jan 1, 2017

Facing the same problem. Haven't tried @GirishBhalerao's suggestions though.

@ism33ail
Copy link

this solves the issue #124 (comment)

@patrickpissurno
Copy link

Please try this fixed version: https://github.com/patrickpissurno/RippleEffect/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests