Snackbar in Support Library doesn't include On

2019-01-23 12:21发布

I'd like to implement the new Snackbar included in the latest Design Support Library, but the way it is offered seems counter-intuitive for my, and I assume many others', use.

When the user does an important action, I want to allow them to undo it via the Snackbar, but there seems to be no way to detect when it is dismissed to do the action. It makes sense to me to do it the following way:

  1. User does action.
  2. Show Snackbar and update UI as if the action has been completed (ie it appears that data is sent to the database, but actually isn't yet).
  3. If user pressed "undo," revert the UI changes. If not, when the Snackbar is dismissed, it will then send the data.

But because I don't see any accessable OnDismissListener, I would therefore have to:

  1. User does action.
  2. Send info to database immediately and update UI.
  3. If user presses "undo," send another call to the database to remove the just-added data and revert the UI changes.

I would really like to avoid having to make the two calls to the database, and just send one when the app knows that it's safe (the user has avoided pressing "undo"). I notice there is some implementation of this in a third-party library via an EventListener, but I'd really like to stick to the Google library.

7条回答
Evening l夕情丶
2楼-- · 2019-01-23 13:01
public class CustomCoordinatorLayout extends CoordinatorLayout {

    private boolean mIsSnackBar = false;
    private View mSnakBarView = null;
    private OnSnackBarListener mOnSnackBarListener = null;

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if(mIsSnackBar){
            // Check whether the snackbar is existed.
            // If it is not existed then index of the snackbar is -1.
            if(indexOfChild(mSnakBarView) == -1){
                mSnakBarView = null;
                mIsSnackBar = false;

                if(mOnSnackBarListener != null)
                    mOnSnackBarListener.onDismiss();
                Log.d("NEOSARCHIZO","SnackBar is dismissed!");
            }
        }
    }

    @Override
    public void onMeasureChild(View child, int parentWidthMeasureSpec, int     widthUsed, int parentHeightMeasureSpec, int heightUsed) {
    super.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,     parentHeightMeasureSpec, heightUsed);

        // onMeaureChild is called before onMeasure.
        // The view of SnackBar doesn't have an id.
        if(child.getId() == -1){
            mIsSnackBar = true;
            // Store the view of SnackBar.
            mSnakBarView = child;

            if(mOnSnackBarListener != null)
                mOnSnackBarListener.onShow();
            Log.d("NEOSARCHIZO","SnackBar is showed!");
        }
    }

    public void setOnSnackBarListener(OnSnackBarListener onSnackBarListener){
        mOnSnackBarListener = onSnackBarListener;
    }

    public interface OnSnackBarListener{
        public void onShow();
        public void onDismiss();
    }
}

I use custom coordinatorlayout. When Snackbar is showed then onMeasure and onMeasureChild of CoordinatorLayout are called. So I overrided these methods.

Please note that you must set ids of children of the custom coordinator layout. Because I find the view of SnackBar by id. The id of SnackBar is -1.

CustomCoordinatorLayout layout = (CustomCoordinatorLayout)findViewById(R.id.main_content);
layout.setOnSnackBarListener(this);
Snackbar.make(layout, "Hello!", Snackbar.LENGTH_LONG).setAction("UNDO", new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    //TODO something
                }
            }).show();

Implement OnSnackBarListener in your activity or fragment. When the snackbar is showed then it will call onShow. And the one is dismissed then it will call onDismiss.

查看更多
迷人小祖宗
3楼-- · 2019-01-23 13:03

This was just added in v23.

To be notified when a snackbar has been shown or dismissed, you can provide a Snackbar.Callback via setCallback(Callback).

查看更多
啃猪蹄的小仙女
4楼-- · 2019-01-23 13:03

Francesco's answer (here) is right, but unfortunately it only works on API > 12. I submitted a feature request to the Android Issue Tracker. You can check it here and star it if you're interested. Thanks.

查看更多
倾城 Initia
5楼-- · 2019-01-23 13:17

Now it does

Snackbar.make(getView(), "Hi there!", Snackbar.LENGTH_LONG).setCallback( new Snackbar.Callback() {
                @Override
                public void onDismissed(Snackbar snackbar, int event) {
                    switch(event) {
                        case Snackbar.Callback.DISMISS_EVENT_ACTION:
                            Toast.makeText(getActivity(), "Clicked the action", Toast.LENGTH_LONG).show();
                            break;
                        case Snackbar.Callback.DISMISS_EVENT_TIMEOUT:
                            Toast.makeText(getActivity(), "Time out", Toast.LENGTH_LONG).show();
                            break;
                    }
                }

                @Override
                public void onShown(Snackbar snackbar) {
                    Toast.makeText(getActivity(), "This is my annoying step-brother", Toast.LENGTH_LONG).show();
                }
            }).setAction("Go away!", new View.OnClickListener() {
                @Override
                public void onClick(View v) {

                }
            }).show();
查看更多
冷血范
6楼-- · 2019-01-23 13:21

To improve Hitch.united answer

                boolean mAllowedToRemove = true;
                Snackbar snack = Snackbar.make(mView, mSnackTitle, Snackbar.LENGTH_LONG);
                snack.setAction(getString(R.string.snackbar_undo), new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        mAllowedToRemove = false;

                        // undo
                        ...
                    }
                });
                snack.getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                    @Override
                    public void onViewAttachedToWindow(View v) {

                    }

                    @Override
                    public void onViewDetachedFromWindow(View v) {
                        if(!mAllowedToRemove){
                            // handle actions like http requests
                            ...
                        }
                    }
                });
                snack.show();
查看更多
放荡不羁爱自由
7楼-- · 2019-01-23 13:23

I have the same problem, but I'm providing an 'undo' for deleting data.
This is how I deal with it:

  • Pretend the data is deleted from db (hide from UI, which is a list of items)
  • Wait until the snack bar 'should' have dissapeared
  • Send the delete call to the database
  • IF, the user used the undo action, block the pending DB call

This may not work for you, since you are inserting data and you may (?) need it to be available in the database after the first action, but it will work if 'faking' the data (in the UI) is feasible. My code is not optimal, and I would call it a hack, but its the best I could find while staying within the official libraries.

    // Control objects
    boolean canRemoveData = true;
    Object removedData = getData(id);
    UI.remove(id);

    // Snackbar code
    Snackbar snackbar = Snackbar.make(view, "Data removed", Snackbar.LENGTH_LONG);
    snackbar.setAction("Undo", new View.OnClickListener() {
        @Override
        public void onClick(View v){
            canRemoveData = false;

            DB.remove(id);
        }
    });

    // Handler to time the dismissal of the snackbar
    new Handler(getActivity().getMainLooper()).postDelayed(new Runnable() {
        @Override
        public void run() {
            if(canRemoveData){
                DB.remove(id);
            }
        }
    }, (int)(snackbar.getDuration() * 1.05f)); 
    // Here I am using a slightly longer delay before sending the db delete call,
    // just because I don't trust the accuracy of the Handler timing and I want
    // to be on the safe side. Either way this is dirty code, but the best I could do.

My actual code is more complicated (dealing with the issue of canRemoveData not being accessible inside sub classes without being final, but this is basically how I managed to achieve what you are talking about.
Hope someone can figure out a better solution.

查看更多
登录 后发表回答