31

Although Snackbar is beautiful, it doesn't persist when changing activities. This is a bummer in scenarios where I would like to confirm that a message was sent using a Snackbar, before finishing the activity. I've considered pausing the code before exiting the activity, but have found that to be a bad practice.

If what I describe isn't possible, is there any type of material design toast message? Or a way to make a rectangular toast message; one with rounded edges of a smaller radius?

young_souvlaki
  • 1,886
  • 4
  • 24
  • 28
  • 1
    I'm assuming you want to show a snackbar that says `Message Sent Successfully` but that event happens on an activity that is finished right after the message is sent? You could send a message to the activity shown after it to then show that snackbar. – em_ Oct 21 '15 at 16:01
  • How so? Also, and this is not as important, there are scenarios where I `startActivityForResult()` and use an `Intent.ACTION_GET_CONTENT` in which case I may be in an activity I did not create. – young_souvlaki Oct 21 '15 at 16:08
  • Maybe a Toast is the better choice for this? – Tobi Oct 28 '15 at 15:26
  • Reread my question. Toast messages are not exactly material design. – young_souvlaki Oct 28 '15 at 15:45
  • sorry! check this out, you can customize it http://stackoverflow.com/a/16909532/4130107 – Tobi Oct 28 '15 at 17:59
  • Hey @Tobi sorry for seeing this so late! Very cool tip thank you for bringing this to my attention! – young_souvlaki Nov 26 '15 at 02:43

6 Answers6

30

To create a Snackbar with the application context which is visible across multiple activities:

  1. Get the WindowManager as system service
  2. Create and add a FrameLayout (rootView) with type WindowManager.LayoutParams.TYPE_TOAST and WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL to the WindowManager
  3. Wait until on FrameLayout.onAttachedToWindow() is called in the FrameLayout (rootView)
  4. Get the window token of the FrameLayout (rootView) with View.getWindowToken()
  5. Create a ContextThemeWrapper with the application context and a derived @style/Theme.AppCompat
  6. Use the new context to create an additional FrameLayout (snackbarContainer)
  7. Add this FrameLayout (snackbarContainer) with type WindowManager.LayoutParams.TYPE_APPLICATION_PANEL and flag WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
  8. Wait until on View.onAttachedToWindow() is called in the FrameLayout (snackbarContainer)
  9. Create the Snackbar like normal with the FrameLayout (snackbarContainer)
  10. Set View.onDismissed() callback to the Snackbar and remove the FrameLayouts (rootView and snackbarContainer)
  11. Show the snackbar Snackbar.show()

Here a working wrapper (NOTE: Swipe to dismiss is not working. Maybe some one else find the correct WindowManager.LayoutParams flags to receive touch events Fixed by CoordinatorLayout):

public class SnackbarWrapper
{
    private final CharSequence text;
    private final int duration;
    private final WindowManager windowManager;
    private final Context appplicationContext;
    @Nullable
    private Snackbar.Callback externalCallback;
    @Nullable
    private Action action;

    @NonNull
    public static SnackbarWrapper make(@NonNull Context applicationContext, @NonNull CharSequence text, @Snackbar.Duration int duration)
    {
        return new SnackbarWrapper(applicationContext, text, duration);
    }

    private SnackbarWrapper(@NonNull final Context appplicationContext, @NonNull CharSequence text, @Snackbar.Duration int duration)
    {
        this.appplicationContext = appplicationContext;
        this.windowManager = (WindowManager) appplicationContext.getSystemService(Context.WINDOW_SERVICE);
        this.text = text;
        this.duration = duration;
    }

    public void show()
    {
        WindowManager.LayoutParams layoutParams = createDefaultLayoutParams(WindowManager.LayoutParams.TYPE_TOAST, null);
        windowManager.addView(new FrameLayout(appplicationContext)
        {
            @Override
            protected void onAttachedToWindow()
            {
                super.onAttachedToWindow();
                onRootViewAvailable(this);
            }

        }, layoutParams);
    }

    private void onRootViewAvailable(final FrameLayout rootView)
    {
        final CoordinatorLayout snackbarContainer = new CoordinatorLayout(new ContextThemeWrapper(appplicationContext, R.style.FOL_Theme_SnackbarWrapper))
        {
            @Override
            public void onAttachedToWindow()
            {
                super.onAttachedToWindow();
                onSnackbarContainerAttached(rootView, this);
            }
        };
        windowManager.addView(snackbarContainer, createDefaultLayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, rootView.getWindowToken()));
    }

    private void onSnackbarContainerAttached(final View rootView, final CoordinatorLayout snackbarContainer)
    {
        Snackbar snackbar = Snackbar.make(snackbarContainer, text, duration);
        snackbar.setCallback(new Snackbar.Callback()
        {
            @Override
            public void onDismissed(Snackbar snackbar, int event)
            {
                super.onDismissed(snackbar, event);
                // Clean up (NOTE! This callback can be called multiple times)
                if (snackbarContainer.getParent() != null && rootView.getParent() != null)
                {
                    windowManager.removeView(snackbarContainer);
                    windowManager.removeView(rootView);
                }
                if (externalCallback != null)
                {
                    externalCallback.onDismissed(snackbar, event);
                }
            }

            @Override
            public void onShown(Snackbar snackbar)
            {
                super.onShown(snackbar);
                if (externalCallback != null)
                {
                    externalCallback.onShown(snackbar);
                }
            }
        });
        if (action != null)
        {
            snackbar.setAction(action.text, action.listener);
        }
        snackbar.show();
    }

    private WindowManager.LayoutParams createDefaultLayoutParams(int type, @Nullable IBinder windowToken)
    {
        WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
        layoutParams.format = PixelFormat.TRANSLUCENT;
        layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
        layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        layoutParams.gravity = GravityCompat.getAbsoluteGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM, ViewCompat.LAYOUT_DIRECTION_LTR);
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        layoutParams.type = type;
        layoutParams.token = windowToken;
        return layoutParams;
    }

    @NonNull
    public SnackbarWrapper setCallback(@Nullable Snackbar.Callback callback)
    {
        this.externalCallback = callback;
        return this;
    }

    @NonNull
    public SnackbarWrapper setAction(CharSequence text, final View.OnClickListener listener)
    {
        action = new Action(text, listener);
        return this;
    }

    private static class Action
    {
        private final CharSequence text;
        private final View.OnClickListener listener;

        public Action(CharSequence text, View.OnClickListener listener)
        {
            this.text = text;
            this.listener = listener;
        }
    }
}

EDIT
Once SnackbarWrapper is defined you can use it like this:

final SnackbarWrapper snackbarWrapper = SnackbarWrapper.make(getApplicationContext(),
            "Test snackbarWrapper", Snackbar.LENGTH_LONG);

snackbarWrapper.setAction(R.string.snackbar_text,
            new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Toast.makeText(getApplicationContext(), "Action",
                            Toast.LENGTH_SHORT).show();
                }
            });

snackbarWrapper.show();

If you don't have a theme, you can quickly define one in styles.xml:

<style name="FOL_Theme_SnackbarWrapper" parent="@style/Theme.AppCompat">
    <!--Insert customization here-->
</style>

EDIT
For those on Android Oreo getting Bad Token Exception, change TYPE_TOAST to TYPE_APPLICATION_OVERLAY. This is due to Android Oreo implementing special permissions to draw over applications. You can ask for this permissions using:

    if(!Settings.canDrawOverlays(Activity.this){
        Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, URI.parse("package:" + getPackageName()));
        startActivityForResult(intent, REQ_CODE);
    }
user1185087
  • 4,468
  • 1
  • 30
  • 38
  • In `onDismissed()` are you using `snackBarContainer` as `container`? – young_souvlaki Jun 08 '16 at 18:30
  • 1
    This works, but the ui does not accept input while the `Snackbar` is shown. – young_souvlaki Jun 08 '16 at 18:49
  • You are right on API level 16 snackbar action doesnt work. I edited the solution. Actions should work on API level 16. (Note: Swipe to dismiss still didn't work) – user1185087 Jun 10 '16 at 11:15
  • I appreciate the hard work you're directing towards this question. It would be helpful for you to give an example of how you intend for this `SnackbarWrapper` to be used. Your first answer was clear and straightforward, but I'm a little lost with this one. – young_souvlaki Jun 10 '16 at 14:40
  • The wrapper can be used like the snackbar itself: `final SnackbarWrapper snackbarWrapper = SnackbarWrapper.make(applicationContext, "Test snackbarWrapper", Snackbar.LENGTH_LONG); snackbarWrapper.setAction(R.string.action_message, new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(applicationContext, "Action", Toast.LENGTH_SHORT).show(); } }); snackbarWrapper.show();` – user1185087 Jun 10 '16 at 15:43
  • Okay that worked! Thank you very much! I edited your answer to include the code you commented here in a readable fashion. – young_souvlaki Jun 10 '16 at 18:29
  • Could you shoot me an email? I'm impressed with your code. – young_souvlaki Jun 10 '16 at 18:41
  • Thanks for the code. I got an exception with API 25 though. If on acitivy A you call StartActivity(B), then user clicks in save and B calls finish(), then A calls StartActivity(B) again and user clicks in save, this causes an exception "android.view.WindowManager$BadTokenException: Unable to add window -- window android.view.ViewRootImpl$W@b240ba6 has already been added" – leoneboaventura Apr 27 '17 at 15:02
  • @YoungCoconutCode The UI is blocking any text input and even Back button. To fix this, change: `layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;` – Štarke Jun 06 '17 at 09:54
  • I want to make the snackbar stick to App window only and the global system window. If the user presses home/app is in background mode the snackbar is still visible – beerBear Aug 25 '17 at 03:37
  • 6
    I'm getting "android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?" its in show method with windowManager.addView in Android Oreo, anybody has idea how to fix that? – Jarda Havelik Oct 27 '17 at 14:18
  • Similarly, one of my users has the same error on Android 8 : "Fatal Exception: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?" – Andrew Glukhoff Feb 23 '18 at 07:32
  • Use `WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY` instead of `WindowManager.LayoutParams.TYPE_TOAST` if SDK>=26 and targetSDK>=26 to solve BadTokenException: Unable to add window – Alexey Ozerov Oct 03 '19 at 03:45
  • 2
    I'm upvoting your answer because it is good, but not good for me. I don't want to add another permission request which opens the settings... this is really bad for ui for me :/ Anyway good answer – Z3R0 Jul 27 '20 at 09:14
9

If I understand correctly, you do this:

  1. Activity A launch Activity B to send a message
  2. Once message is send, you display a confirmation message
  3. You go back to Activity A

You can use SnackBar to do that by using an ActivityResult (here is a StackOverflow post with how to use it)

Here are the steps:

  1. Activity A launch Activity B with startActivityForResult
  2. Do your stuff on Activity B
  3. Set your result (check the link above to understand)
  4. Finish Activity
  5. In Activity A, get that code in OnActivityResult and display your SnackBar with the proper message

This allow you do display a Snackar in Activity A corresponding to result of Activity B.

Hopes it can helps your problem

Community
  • 1
  • 1
Damien Belard
  • 285
  • 3
  • 12
  • 1
    IMHO this should be the accepted answer. The other answers may work too but are far trickier and violate several programming principles. – Oke Uwechue Jul 30 '19 at 22:17
  • Thanks. This seems like the way it is intended to be used. – Inti Oct 16 '19 at 13:03
3

Just in case somebody needs to do this in Xamarin I have adapted the accepted answer which I found really helpful.

using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Runtime;
using Android.Support.Design.Widget;
using Android.Views;
using Android.Widget;
using System;

public class SnackbarWrapper
{
    private readonly string text;
    private readonly int duration;
    private readonly IWindowManager windowManager;
    private readonly Context appplicationContext;
    private Snackbar.Callback externalCallback;
    private SnackbarAction action { get; set; }

    public static SnackbarWrapper make(Context applicationContext, string text, int duration)
    {
        return new SnackbarWrapper(applicationContext, text, duration);
    }

    private SnackbarWrapper(Context appplicationContext, string text, int duration)
    {
        this.appplicationContext = appplicationContext;
        var wm = appplicationContext.GetSystemService(Context.WindowService);
        // We have to use JavaCast instead of a normal cast
        this.windowManager = wm.JavaCast<IWindowManager>();
        this.text = text;
        this.duration = duration;
    }

    public void Show()
    {
        WindowManagerLayoutParams layoutParams = createDefaultLayoutParams(WindowManagerTypes.Toast, null);
        var frameLayout = new FrameLayout(appplicationContext);
        frameLayout.ViewAttachedToWindow += delegate
        {
            //this.onAttachedToWindow();
            onRootViewAvailable(frameLayout);
        };

        windowManager.AddView(frameLayout, layoutParams);
    }

    private void onRootViewAvailable(FrameLayout rootView)
    {
        var ctw = new ContextThemeWrapper(appplicationContext, Resource.Style.Base_Theme_AppCompat);
        CoordinatorLayout snackbarContainer = new CoordinatorLayout(ctw);
        snackbarContainer.ViewAttachedToWindow += delegate
        {
            onSnackbarContainerAttached(rootView, snackbarContainer);
        };

        windowManager.AddView(snackbarContainer, createDefaultLayoutParams(WindowManagerTypes.ApplicationPanel, rootView.WindowToken));
    }

    private void onSnackbarContainerAttached(View rootView, CoordinatorLayout snackbarContainer)
    {
        Snackbar snackbar = Snackbar.Make(snackbarContainer, text, duration);

        snackbar.SetCallback(new SnackbarCallbackImpl(rootView, snackbarContainer, windowManager));

        if (action != null)
        {
            snackbar.SetAction(action.Text, action.Listener);
        }
        snackbar.Show();
    }

    private WindowManagerLayoutParams createDefaultLayoutParams(WindowManagerTypes type, IBinder windowToken)
    {
        WindowManagerLayoutParams layoutParams = new WindowManagerLayoutParams();
        layoutParams.Format = Format.Translucent;
        layoutParams.Width = ViewGroup.LayoutParams.MatchParent;
        /* Si ponemos aqui WrapContent en alguna ocasion en la que haya un action largo y el texto tambien, el snackbar puede volverse como loco
         * asi que usamos MatchParent. Aun asi sucede que a veces se puede mostrar en una linea o en dos el mismo texto, pero al menos no hace el temblor loco que de la otra forma*/
        layoutParams.Height = ViewGroup.LayoutParams.MatchParent;
        layoutParams.Gravity = GravityFlags.CenterHorizontal | GravityFlags.Bottom;
        layoutParams.Flags = WindowManagerFlags.NotTouchModal;
        layoutParams.Type = type;
        layoutParams.Token = windowToken;
        return layoutParams;
    }

    public SnackbarWrapper SetCallback(Snackbar.Callback callback)
    {
        this.externalCallback = callback;
        return this;
    }

    public SnackbarWrapper SetAction(string text, Action<View> listener)
    {
        action = new SnackbarAction(text, listener);
        return this;
    }

}//class

internal class SnackbarAction
{
    public string Text { get; set; }
    public Action<View> Listener { get; set; }

    public SnackbarAction(string text, Action<View> listener)
    {
        Text = text;
        Listener = listener;
    }
}

internal class SnackbarCallbackImpl : Snackbar.Callback
{
    public Snackbar.Callback externalCallback { get; set; }

    View rootView;
    CoordinatorLayout snackbarContainer;
    IWindowManager windowManager;

    public SnackbarCallbackImpl(View rootView, CoordinatorLayout snackbarContainer, IWindowManager windowManager)
    {
        this.rootView = rootView;
        this.snackbarContainer = snackbarContainer;
        this.windowManager = windowManager;
    }

    public override void OnShown(Snackbar snackbar)
    {
        base.OnShown(snackbar);
        externalCallback?.OnShown(snackbar);
    }

    public override void OnDismissed(Snackbar snackbar, int evt)
    {
        base.OnDismissed(snackbar, evt);

        // Clean up (NOTE! This callback can be called multiple times)
        if (snackbarContainer.Parent != null && rootView.Parent != null)
        {
            windowManager.RemoveView(snackbarContainer);
            windowManager.RemoveView(rootView);
        }

        externalCallback?.OnDismissed(snackbar, evt);
    }
}
1

To have a rectangular Toast, set a rectangular background for the Toast or just set a different background color for the Toast.

Refer this post where it was posted as a problem. But it your case it is a possible solution.

Community
  • 1
  • 1
Viral Patel
  • 32,418
  • 18
  • 82
  • 110
0

UPDATE: See selected answer.

The best solution to my question is using a Timer after the presenting the Snackbar and then in the run() method of the timer, starting the activity.

Snackbar.show(); // Excluded make for brevity.

Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            Intent chooseVideoIntent = new Intent(Intent.ACTION_GET_CONTENT); // Any type of content/file. Song, doc, video...
            chooseVideoIntent.setType("video/*");
            startActivityForResult(chooseVideoIntent, CHOOSE_VIDEO_REQUEST);
        }
    }, 2 * 1000);

UPDATE: I found that by using findViewById(android.R.id.content) as the view in Snackbar.make() the Snackbar persists among fragment changes.

young_souvlaki
  • 1,886
  • 4
  • 24
  • 28
  • 1
    I tried to use Snackbar.make(findViewById(android.R.id.content)...); but the snackbar isn't showing. – brettbrdls May 22 '16 at 17:59
  • Yup, the snackbar doesn't show up. I'm currently trying to make the snackbar show up to the previous activity when the button clicked on the current activity I have. – brettbrdls May 22 '16 at 18:48
  • Is this not applicable between activity changes? – brettbrdls May 22 '16 at 19:21
  • @brettbrdls no it only worked for me between fragment changes. Let me know if you figure another way. Maybe there's another system view to use. – young_souvlaki May 22 '16 at 19:36
0

Actually I just need to display a message and I don't need a onClickListener on the message. If you just need to show a message look at "Myke Dev" answer in this thread which is the one I needed:

https://stackoverflow.com/a/34640942/9993413

(Don't give upvote to me, give upvote to "Myke Dev" which wrote the answer)

In user1185087 answer you must request permissions from user by opening settings and this, for me, isn't a good thing for user interactions, but I thinks is the only way if you want to show a snackbar with an onClickListener.

(Maybe you can use an activity with no background which launch a snackbar-like dialog, but it won't act just as a window_alert snackbar)

Z3R0
  • 1,011
  • 10
  • 19