How to disable snackbar's swipe-to-dismiss behavior
Asked Answered
D

8

19

Is there a way to prevent the user from dismissing a snackbar by swiping on it?

I have an app that shows a snack bar during network login, I want to avoid it to be dismissed.

According to Nikola Despotoski suggestion I've experimented both solutions:

private void startSnack(){

    loadingSnack = Snackbar.make(findViewById(R.id.email_login_form), getString(R.string.logging_in), Snackbar.LENGTH_INDEFINITE)
            .setAction("CANCEL", new OnClickListener() {
                @Override
                public void onClick(View view) {
                    getOps().cancelLogin();
                    enableControls();
                }
            });

    loadingSnack.getView().setOnTouchListener(new View.OnTouchListener() {
        public long mInitialTime;
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            if (v instanceof Button) return false; //Action view was touched, proceed normally.
            else {
                switch (MotionEventCompat.getActionMasked(event)) {
                    case MotionEvent.ACTION_DOWN: {
                        Log.i(TAG, "ACTION_DOWN");
                        mInitialTime = System.currentTimeMillis();
                        break;
                    }
                    case MotionEvent.ACTION_UP: {
                        Log.i(TAG, "ACTION_UP");
                        long clickDuration = System.currentTimeMillis() - mInitialTime;
                        if (clickDuration <= ViewConfiguration.getTapTimeout()) {
                            return false;// click event, proceed normally
                        }
                    }
                    case MotionEvent.ACTION_MOVE: {
                        Log.i(TAG, "ACTION_MOVE");
                        return true;
                    }
                }
                return true;
            }
        }
    });

    ViewGroup.LayoutParams lp = loadingSnack.getView().getLayoutParams();
    if (lp != null && lp instanceof CoordinatorLayout.LayoutParams) {
        ((CoordinatorLayout.LayoutParams)lp).setBehavior(new DummyBehavior());
        loadingSnack.getView().setLayoutParams(lp);
        Log.i(TAG, "Dummy behavior assigned to " + lp.toString());

    }

    loadingSnack.show();

}

this is DummyBehavior class:

public class DummyBehavior extends CoordinatorLayout.Behavior<View>{

    /**
     * Debugging tag used by the Android logger.
     */
    protected final static String TAG =
            DummyBehavior.class.getSimpleName();



    public DummyBehavior() {
        Log.i(TAG, "Dummy behavior created");
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        Log.i(TAG, "Method " + stackTrace[2].getMethodName() );

    }

    public DummyBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        Log.i(TAG, "Dummy behavior created");

    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean blocksInteractionBelow(CoordinatorLayout parent, View child) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public boolean isDirty(CoordinatorLayout parent, View child) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onMeasureChild(CoordinatorLayout parent, View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY, boolean consumed) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return false;
    }

    @Override
    public WindowInsetsCompat onApplyWindowInsets(CoordinatorLayout coordinatorLayout, View child, WindowInsetsCompat insets) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return null;
    }

    @Override
    public void onRestoreInstanceState(CoordinatorLayout parent, View child, Parcelable state) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
    }

    @Override
    public Parcelable onSaveInstanceState(CoordinatorLayout parent, View child) {
        Log.i(TAG, "Method " + Thread.currentThread().getStackTrace()[2].getMethodName() );
        return null;
    }
}

But my snackbar still disappears when swiped and this is a typical log:

12-02 22:26:43.864 19598-19598/ I/DummyBehavior: Dummy behavior created
12-02 22:26:43.866 19598-19598/ I/DummyBehavior: Method <init>
12-02 22:26:43.866 19598-19598/ I/LifecycleLoggingActivity: Dummy behavior assigned to android.support.design.widget.CoordinatorLayout$LayoutParams@808c0e9
12-02 22:26:44.755 19598-19598/ I/LifecycleLoggingActivity: ACTION_DOWN
12-02 22:26:44.798 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
12-02 22:26:44.815 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
12-02 22:26:44.832 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
12-02 22:26:44.849 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
12-02 22:26:44.866 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
12-02 22:26:44.883 19598-19598/ I/LifecycleLoggingActivity: ACTION_MOVE
Dysarthria answered 1/12, 2015 at 22:31 Comment(0)
C
19

Snackbar now has actual support for this by using the setBehavior method. The great thing here is that before you would always lose some behaviors which are now preserved.

Note that the package moved so you have to import the "new" Snackbar in the snackbar package.

Snackbar.make(view, stringId, Snackbar.LENGTH_LONG)
    .setBehavior(new NoSwipeBehavior())
    .show();

class NoSwipeBehavior extends BaseTransientBottomBar.Behavior {

    @Override
    public boolean canSwipeDismissView(View child) {
      return false;
    }
}
Clermontferrand answered 16/5, 2018 at 18:22 Comment(0)
S
18

This worked for me:

    Snackbar.SnackbarLayout layout = (Snackbar.SnackbarLayout) snackbar.getView();
    snackbar.setDuration(Snackbar.LENGTH_INDEFINITE);
    snackbar.show();
    layout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            ViewGroup.LayoutParams lp = layout.getLayoutParams();
            if (lp instanceof CoordinatorLayout.LayoutParams) {
                ((CoordinatorLayout.LayoutParams) lp).setBehavior(new DisableSwipeBehavior());
                layout.setLayoutParams(lp);
            }
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                layout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            } else {
                //noinspection deprecation
                layout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
            }
        }
    });

Where DisableSwipeBehavior is:

public class DisableSwipeBehavior extends SwipeDismissBehavior<Snackbar.SnackbarLayout> {
    @Override
    public boolean canSwipeDismissView(@NonNull View view) {
        return false;
    }
}
Stambaugh answered 6/5, 2016 at 12:23 Comment(2)
This can be an accepted answer. Even if we should not use snackbar if we want to disable the swipe gesture.Flan
This works but gives a compiler warning: "SnackbarBaseLayout.setLayoutParams can only be called from within the same library group"Accountable
A
5

This worked for me :

Snackbar snackbar = Snackbar.make(findViewById(container), R.string.offers_refreshed, Snackbar.LENGTH_LONG);
    final View snackbarView = snackbar.getView();
    snackbar.show();

    snackbarView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            snackbarView.getViewTreeObserver().removeOnPreDrawListener(this);
            ((CoordinatorLayout.LayoutParams) snackbarView.getLayoutParams()).setBehavior(null);
            return true;
        }
    });

Good luck! :)

Alboin answered 11/1, 2017 at 1:25 Comment(0)
A
1

You can disable streaming touch events rather than clicks to the Snackbar view.

mSnackBar.getView().setOnTouchListener(new View.OnTouchListener() {
            public long mInitialTime;
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if (v instanceof Button) return false; //Action view was touched, proceed normally.
                else {
                    switch (MotionEventCompat.getActionMasked(event)) {
                        case MotionEvent.ACTION_DOWN: {
                            mInitialTime = System.currentTimeMillis();
                            break;
                        }
                        case MotionEvent.ACTION_UP: {
                            long clickDuration = System.currentTimeMillis() - mInitialTime;
                            if (clickDuration <= ViewConfiguration.getTapTimeout()) {
                                return false;// click event, proceed normally
                            }
                        }
                    }
                    return true;
                }
            });

Or you could just replace the Snackbar behavior with some empty CoordinatorLayout.Behavior:

public CouchPotatoBehavior extends CoordinatorLayout.Behavior<View>{

    //override all methods and don't call super methods. 

}

This is the empty behavior, that does nothing. Default SwipeToDismissBehavior uses ViewDragHelper to process touch events, upon which triggers the dismissal.

 ViewGroup.LayoutParams lp = mSnackbar.getView().getLayoutParams();
 if (lp instanceof CoordinatorLayout.LayoutParams) {
     ((CoordinatorLayout.LayoutParams)lp).setBehavior(new CouchPotatoBehavior());
       mSnackbar.getView().setLayoutParams(lp);              
}
Alvera answered 1/12, 2015 at 22:45 Comment(3)
Or you could just replace the Snackbar behavior with some empty CoordinatorLayout.Behavior I kinda like this idea...can you expand? love the use of ViewConfiguration.getTapTimeout()Albany
@Albany There you go. Just make sure getView() is not null. :)Alvera
Thank for suggestions, but I'm unable to get them working. I modified my question adding code developed according to your proposed solution.Dysarthria
W
1

Better solution here.... Don't provide CoordinatorLayout or any of its child as view in snackbar.

Snackbar.make(ROOT_LAYOUT , "No internet connection", Snackbar.LENGTH_INDEFINITE).show();

Where, the ROOT_LAYOUT should be any layout except coordinatorlayout or its child.

Willemstad answered 23/2, 2016 at 11:50 Comment(1)
Good solution yours, but use it at your own risk. From Android docs: "Having a CoordinatorLayout in your view hierarchy allows Snackbar to enable certain features, such as swipe-to-dismiss and automatically moving of widgets like FloatingActionButton."Sapient
L
0

Here is a solution that does not require you to mess with ViewTreeObserver. Note that the following solution is written in Kotlin based on SDK 26.

BaseTransientBottomBar

final void showView() {
    if (mView.getParent() == null) {
        final ViewGroup.LayoutParams lp = mView.getLayoutParams();

        if (lp instanceof CoordinatorLayout.LayoutParams) {
            // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
            final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;

            final Behavior behavior = new Behavior();
            behavior.setStartAlphaSwipeDistance(0.1f);
            behavior.setEndAlphaSwipeDistance(0.6f);
            behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
            behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
                @Override
                public void onDismiss(View view) {
                    view.setVisibility(View.GONE);
                    dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE);
                }

                @Override
                public void onDragStateChanged(int state) {
                    switch (state) {
                        case SwipeDismissBehavior.STATE_DRAGGING:
                        case SwipeDismissBehavior.STATE_SETTLING:
                            // If the view is being dragged or settling, pause the timeout
                            SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
                            break;
                        case SwipeDismissBehavior.STATE_IDLE:
                            // If the view has been released and is idle, restore the timeout
                            SnackbarManager.getInstance()
                                    .restoreTimeoutIfPaused(mManagerCallback);
                            break;
                    }
                }
            });
            clp.setBehavior(behavior);
            // Also set the inset edge so that views can dodge the bar correctly
            clp.insetEdge = Gravity.BOTTOM;
        }

        mTargetParent.addView(mView);
    }

    ...
}

If you look in to the source code of BaseTransientBottomBar in method showView, it add the behavior if the layoutParams is CoordinatorLayout.LayoutParams. We can undo this by setting the behavior back to null.

As it add the behavior just before the view is shown, we should undo it after the view is shown.

val snackbar = Snackbar.make(coordinatorLayout, "Hello World!", Snackbar.LENGTH_INDEFINITE)
snackbar.show()
snackbar.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
    override fun onShown(transientBottomBar: Snackbar) {
        val layoutParams = transientBottomBar.view.layoutParams as? CoordinatorLayout.LayoutParams
        layoutParams?.let { it.behavior = null }
    }
})
Laurinelaurita answered 13/10, 2017 at 7:6 Comment(0)
C
0

Just override CoordinatorLayout.LayoutParams.getBehaviour() and return null to disable the swiping.

For example:

Snackbar snackBar = Snackbar.make(view, "Enjoy!", Snackbar.LENGTH_INDEFINITE);
CoordinatorLayout.LayoutParams snackBarLayoutParams = new CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT){
                @Override
                public CoordinatorLayout.Behavior getBehavior() {
                    return null;
                }
            };
snackBarLayoutParams.gravity = Gravity.BOTTOM;
snackBar.getView().setLayoutParams(snackBarLayoutParams);
snackBar.show();

Enjoy!

Coenobite answered 8/6, 2021 at 14:37 Comment(0)
R
0

I have a snack bar with transparent elements (as a replacement for a Toast with a custom view which is now deprecated). There are a few challenges to overcome:

  1. The snack bar can be swiped away.
  2. Touching any part of the snack bar (including transparent background sections) refreshes the auto-dismiss timer.
  3. UI elements behind the snack bar (including transparent background sections) cannot be interacted with as the snack bar intercept touch events.

To completely disable all interaction with the snack bar, I use the following.

// Counteract SnackbarBaseLayout's consumeAllTouchListener which prevents
// touches from going through our snack bar, because (a) most of our snack 
// bar is invisible and (b) we don't have any interaction elements
snackbar.view.setOnTouchListener { _, _ -> false }
snackbar.view.isFocusable = false

// Disable swipe-to-dismiss on the snackbar. Also important as if a user 
// clicks on a snackbar (including the transparent parts of it) then the 
// auto-dismiss timer is deferred by default.
snackbar.behavior = object : BaseTransientBottomBar.Behavior() {
    override fun canSwipeDismissView(child: View) = false
    override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: View, event: MotionEvent) = false
}
Rictus answered 16/11, 2021 at 3:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.