问题
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:
- User does action.
- 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).
- 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:
- User does action.
- Send info to database immediately and update UI.
- 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.
回答1:
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();
回答2:
Check out my helper class for snackbars.
public class SnackbarUtils {
private static final String LOG_TAG = SnackbarUtils.class.getSimpleName();
private SnackbarUtils() {
}
public interface SnackbarDismissListener {
void onSnackbarDismissed();
}
public static void showSnackbar(View rootView, String message, String actionMessage, View.OnClickListener callbacks, final SnackbarDismissListener dismissListener){
Snackbar snackbar = Snackbar.make(rootView,message,Snackbar.LENGTH_LONG);
snackbar.setAction(actionMessage,callbacks);
if (dismissListener != null){
snackbar.getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
dismissListener.onSnackbarDismissed();
}
});
}
snackbar.show();
}
}
回答3:
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.
回答4:
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.
回答5:
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();
回答6:
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)
.
回答7:
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.
来源:https://stackoverflow.com/questions/30639470/snackbar-in-support-library-doesnt-include-ondismisslistener