Index: chrome/android/java/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java b/chrome/android/java/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java |
index 688d122e955d5c961e2706331777500532e66119..ec7b436cf8b0d86154c59746878e26a074351ccb 100644 |
--- a/chrome/android/java/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java |
@@ -15,25 +15,23 @@ import android.view.Window; |
import org.chromium.base.ApiCompatibilityUtils; |
import org.chromium.base.VisibleForTesting; |
import org.chromium.chrome.R; |
-import org.chromium.chrome.browser.ChromeActivity; |
import org.chromium.chrome.browser.device.DeviceClassManager; |
import org.chromium.ui.UiUtils; |
import org.chromium.ui.base.DeviceFormFactor; |
-import java.util.Stack; |
+import java.util.Deque; |
+import java.util.Iterator; |
+import java.util.LinkedList; |
+import java.util.Queue; |
/** |
- * Manager for the snackbar showing at the bottom of activity. |
+ * Manager for the snackbar showing at the bottom of activity. There should be only one |
+ * SnackbarManager and one snackbar in the activity. |
* <p/> |
- * There should be only one SnackbarManager and one snackbar in the activity. The manager maintains |
- * a stack to store all entries that should be displayed. When showing a new snackbar, old entry |
- * will be pushed to stack and text/button will be updated to the newest entry. |
- * <p/> |
- * When action button is clicked, this manager will call |
- * {@link SnackbarController#onAction(Object)} in corresponding listener, and show the next |
- * entry in stack. Otherwise if no action is taken by user during |
- * {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will clear the stack and call |
- * {@link SnackbarController#onDismissNoAction(Object)} to all listeners. |
+ * When action button is clicked, this manager will call {@link SnackbarController#onAction(Object)} |
+ * in corresponding listener, and show the next entry. Otherwise if no action is taken by user |
+ * during {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will call |
+ * {@link SnackbarController#onDismissNoAction(Object)}. |
*/ |
public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener { |
@@ -78,17 +76,18 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
private View mDecor; |
private final Handler mUIThreadHandler; |
- private Stack<Snackbar> mStack = new Stack<Snackbar>(); |
+ private SnackbarCollection mSnackbars = new SnackbarCollection(); |
private SnackbarPopupWindow mPopup; |
private boolean mActivityInForeground; |
private final Runnable mHideRunnable = new Runnable() { |
@Override |
public void run() { |
- dismissAllSnackbars(true); |
+ mSnackbars.removeCurrentDueToTimeout(); |
+ updatePopup(); |
} |
}; |
- // Variables used and reused in local calculations. |
+ // Variables used and reused in popup position calculations. |
private int[] mTempDecorPosition = new int[2]; |
private Rect mTempVisibleDisplayFrame = new Rect(); |
@@ -112,95 +111,31 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
* Notifies the snackbar manager that the activity has been pushed to background. |
*/ |
public void onStop() { |
- dismissAllSnackbars(false); |
+ mSnackbars.clear(); |
+ updatePopup(); |
mActivityInForeground = false; |
} |
/** |
* Shows a snackbar at the bottom of the screen, or above the keyboard if the keyboard is |
- * visible. If the currently displayed snackbar is forcing display, the new snackbar is added as |
- * the next to be displayed on the stack. |
+ * visible. |
*/ |
public void showSnackbar(Snackbar snackbar) { |
if (!mActivityInForeground) return; |
- |
- if (mPopup != null && !mStack.empty() && mStack.peek().getForceDisplay()) { |
- mStack.add(mStack.size() - 1, snackbar); |
- return; |
- } |
- |
- int durationMs = snackbar.getDuration(); |
- if (durationMs == 0) { |
- durationMs = DeviceClassManager.isAccessibilityModeEnabled(mDecor.getContext()) |
- ? sAccessibilitySnackbarDurationMs : sSnackbarDurationMs; |
- } |
- |
- mUIThreadHandler.removeCallbacks(mHideRunnable); |
- mUIThreadHandler.postDelayed(mHideRunnable, durationMs); |
- |
- mStack.push(snackbar); |
- if (mPopup == null) { |
- mPopup = new SnackbarPopupWindow(mDecor, this, snackbar); |
- showPopupAtBottom(); |
- mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this); |
- } else { |
- mPopup.update(snackbar, true); |
- } |
- |
+ mSnackbars.add(snackbar); |
+ updatePopup(); |
mPopup.announceforAccessibility(); |
} |
/** |
- * Warning: Calling this method might cause cascading destroy loop, because you might trigger |
- * callbacks for other {@link SnackbarController}. This method is only meant to be used during |
- * {@link ChromeActivity}'s destruction routine. For other purposes, use |
- * {@link #dismissSnackbars(SnackbarController)} instead. |
- * <p> |
- * Dismisses all snackbars in stack. This will call |
- * {@link SnackbarController#onDismissNoAction(Object)} for every closing snackbar. |
- * |
- * @param isTimeout Whether dismissal was triggered by timeout. |
- */ |
- public void dismissAllSnackbars(boolean isTimeout) { |
- mUIThreadHandler.removeCallbacks(mHideRunnable); |
- |
- if (!mActivityInForeground) return; |
- |
- if (mPopup != null) { |
- mPopup.dismiss(); |
- mPopup = null; |
- } |
- |
- while (!mStack.isEmpty()) { |
- Snackbar snackbar = mStack.pop(); |
- snackbar.getController().onDismissNoAction(snackbar.getActionData()); |
- |
- if (isTimeout && !mStack.isEmpty() && mStack.peek().getForceDisplay()) { |
- showSnackbar(mStack.pop()); |
- return; |
- } |
- } |
- mDecor.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
- } |
- |
- /** |
* Dismisses snackbars that are associated with the given {@link SnackbarController}. |
* |
* @param controller Only snackbars with this controller will be removed. |
*/ |
public void dismissSnackbars(SnackbarController controller) { |
- boolean isFound = false; |
- Snackbar[] snackbars = new Snackbar[mStack.size()]; |
- mStack.toArray(snackbars); |
- for (Snackbar snackbar : snackbars) { |
- if (snackbar.getController() == controller) { |
- mStack.remove(snackbar); |
- isFound = true; |
- } |
+ if (mSnackbars.removeMatchingSnackbars(controller)) { |
+ updatePopup(); |
} |
- if (!isFound) return; |
- |
- finishSnackbarRemoval(); |
} |
/** |
@@ -210,26 +145,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
* @param actionData Only snackbars whose action data is equal to actionData will be removed. |
*/ |
public void dismissSnackbars(SnackbarController controller, Object actionData) { |
- boolean isFound = false; |
- for (Snackbar snackbar : mStack) { |
- if (snackbar.getActionData() != null && snackbar.getActionData().equals(actionData) |
- && snackbar.getController() == controller) { |
- mStack.remove(snackbar); |
- isFound = true; |
- break; |
- } |
- } |
- if (!isFound) return; |
- |
- finishSnackbarRemoval(); |
- } |
- |
- private void finishSnackbarRemoval() { |
- if (mStack.isEmpty()) { |
- dismissAllSnackbars(false); |
- } else { |
- // Refresh the snackbar to let it show top of stack and have full timeout. |
- showSnackbar(mStack.pop()); |
+ if (mSnackbars.removeMatchingSnackbars(controller, actionData)) { |
+ updatePopup(); |
} |
} |
@@ -238,34 +155,15 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
*/ |
@Override |
public void onClick(View v) { |
- assert !mStack.isEmpty(); |
- |
- Snackbar snackbar = mStack.pop(); |
- snackbar.getController().onAction(snackbar.getActionData()); |
- |
- if (!mStack.isEmpty()) { |
- showSnackbar(mStack.pop()); |
- } else { |
- dismissAllSnackbars(false); |
- } |
+ mSnackbars.removeCurrent(true); |
+ updatePopup(); |
} |
- private void showPopupAtBottom() { |
- // When the keyboard is showing, translating the snackbar upwards looks bad because it |
- // overlaps the keyboard. In this case, use an alternative animation without translation. |
- boolean isKeyboardShowing = UiUtils.isKeyboardShowing(mDecor.getContext(), mDecor); |
- mPopup.setAnimationStyle(isKeyboardShowing ? R.style.SnackbarAnimationWithKeyboard |
- : R.style.SnackbarAnimation); |
- |
- mDecor.getLocationInWindow(mTempDecorPosition); |
- mDecor.getWindowVisibleDisplayFrame(mTempVisibleDisplayFrame); |
- int decorBottom = mTempDecorPosition[1] + mDecor.getHeight(); |
- int visibleBottom = Math.min(mTempVisibleDisplayFrame.bottom, decorBottom); |
- int margin = mIsTablet ? mDecor.getResources().getDimensionPixelSize( |
- R.dimen.snackbar_tablet_margin) : 0; |
- |
- mPopup.showAtLocation(mDecor, Gravity.START | Gravity.BOTTOM, margin, |
- decorBottom - visibleBottom + margin); |
+ /** |
+ * @return Whether there is a snackbar on screen. |
+ */ |
+ public boolean isShowing() { |
+ return mPopup != null && mPopup.isShowing(); |
} |
/** |
@@ -296,11 +194,61 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
} |
/** |
- * @return Whether there is a snackbar on screen. |
+ * Updates the snackbar popup window to reflect the value of mSnackbars.currentSnackbar(), which |
+ * may be null. This might show, change, or hide the popup. |
*/ |
- public boolean isShowing() { |
- if (mPopup == null) return false; |
- return mPopup.isShowing(); |
+ private void updatePopup() { |
+ if (!mActivityInForeground) return; |
+ Snackbar currentSnackbar = mSnackbars.getCurrent(); |
+ if (currentSnackbar == null) { |
+ mUIThreadHandler.removeCallbacks(mHideRunnable); |
+ if (mPopup != null) { |
+ mPopup.dismiss(); |
+ mPopup = null; |
+ } |
+ mDecor.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
+ } else { |
+ boolean popupChanged = true; |
+ if (mPopup == null) { |
+ mPopup = new SnackbarPopupWindow(mDecor, this, currentSnackbar); |
+ // When the keyboard is showing, translating the snackbar upwards looks bad because |
+ // it overlaps the keyboard. In this case, use an alternative animation without |
+ // translation. |
+ boolean isKeyboardShowing = UiUtils.isKeyboardShowing(mDecor.getContext(), mDecor); |
+ mPopup.setAnimationStyle(isKeyboardShowing ? R.style.SnackbarAnimationWithKeyboard |
+ : R.style.SnackbarAnimation); |
+ |
+ mDecor.getLocationInWindow(mTempDecorPosition); |
+ mDecor.getWindowVisibleDisplayFrame(mTempVisibleDisplayFrame); |
+ int decorBottom = mTempDecorPosition[1] + mDecor.getHeight(); |
+ int visibleBottom = Math.min(mTempVisibleDisplayFrame.bottom, decorBottom); |
+ int margin = mIsTablet ? mDecor.getResources().getDimensionPixelSize( |
+ R.dimen.snackbar_tablet_margin) : 0; |
+ |
+ mPopup.showAtLocation(mDecor, Gravity.START | Gravity.BOTTOM, margin, |
+ decorBottom - visibleBottom + margin); |
+ mDecor.getViewTreeObserver().addOnGlobalLayoutListener(this); |
+ } else { |
+ popupChanged = mPopup.update(currentSnackbar); |
+ } |
+ |
+ if (popupChanged) { |
+ int durationMs = getDuration(currentSnackbar); |
+ mUIThreadHandler.removeCallbacks(mHideRunnable); |
+ mUIThreadHandler.postDelayed(mHideRunnable, durationMs); |
+ mPopup.announceforAccessibility(); |
+ } |
+ } |
+ |
+ } |
+ |
+ private int getDuration(Snackbar snackbar) { |
+ int durationMs = snackbar.getDuration(); |
+ if (durationMs == 0) { |
+ durationMs = DeviceClassManager.isAccessibilityModeEnabled(mDecor.getContext()) |
+ ? sAccessibilitySnackbarDurationMs : sSnackbarDurationMs; |
+ } |
+ return durationMs; |
} |
/** |
@@ -312,4 +260,105 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
sSnackbarDurationMs = durationMs; |
sAccessibilitySnackbarDurationMs = durationMs; |
} |
+ |
+ /** |
+ * @return The currently showing snackbar. For testing only. |
+ */ |
+ @VisibleForTesting |
+ Snackbar getCurrentSnackbarForTesting() { |
+ return mSnackbars.getCurrent(); |
+ } |
+ |
+ private static class SnackbarCollection { |
+ private Deque<Snackbar> mStack = new LinkedList<>(); |
+ private Queue<Snackbar> mQueue = new LinkedList<>(); |
+ |
+ /** |
+ * Adds a new snackbar to the collection. If the new snackbar is of |
+ * {@link Snackbar#TYPE_ACTION} and current snackbar is of |
+ * {@link Snackbar#TYPE_NOTIFICATION}, the current snackbar will be removed from the |
+ * collection immediately. |
+ */ |
+ public void add(Snackbar snackbar) { |
+ if (snackbar.isTypeAction()) { |
+ if (getCurrent() != null && !getCurrent().isTypeAction()) { |
+ removeCurrent(false); |
+ } |
+ mStack.push(snackbar); |
+ } else { |
+ mQueue.offer(snackbar); |
+ } |
+ } |
+ |
+ /** |
+ * Removes the current snackbar from the collection. |
+ * @param isAction Whether the removal is triggered by user clicking the action button. |
+ */ |
+ public void removeCurrent(boolean isAction) { |
+ Snackbar current = !mStack.isEmpty() ? mStack.pop() : mQueue.poll(); |
+ if (current != null) { |
+ SnackbarController controller = current.getController(); |
+ if (isAction) controller.onAction(current.getActionData()); |
+ else controller.onDismissNoAction(current.getActionData()); |
+ } |
+ } |
+ |
+ /** |
+ * @return The snackbar that is currently displayed. |
+ */ |
+ public Snackbar getCurrent() { |
+ return !mStack.isEmpty() ? mStack.peek() : mQueue.peek(); |
+ } |
+ |
+ public boolean isEmpty() { |
+ return mStack.isEmpty() && mQueue.isEmpty(); |
+ } |
+ |
+ public void clear() { |
+ while (!isEmpty()) { |
+ removeCurrent(false); |
+ } |
+ } |
+ |
+ public void removeCurrentDueToTimeout() { |
+ removeCurrent(false); |
+ Snackbar current; |
+ while ((current = getCurrent()) != null && current.isTypeAction()) { |
+ removeCurrent(false); |
+ } |
+ } |
+ |
+ public boolean removeMatchingSnackbars(SnackbarController controller) { |
+ boolean snackbarRemoved = false; |
+ Iterator<Snackbar> iter = mStack.iterator(); |
+ while (iter.hasNext()) { |
+ Snackbar snackbar = iter.next(); |
+ if (snackbar.getController() == controller) { |
+ iter.remove(); |
+ snackbarRemoved = true; |
+ } |
+ } |
+ return snackbarRemoved; |
+ } |
+ |
+ public boolean removeMatchingSnackbars(SnackbarController controller, Object data) { |
+ boolean snackbarRemoved = false; |
+ Iterator<Snackbar> iter = mStack.iterator(); |
+ while (iter.hasNext()) { |
+ Snackbar snackbar = iter.next(); |
+ if (snackbar.getController() == controller |
+ && objectsAreEqual(snackbar.getActionData(), data)) { |
+ iter.remove(); |
+ snackbarRemoved = true; |
+ } |
+ } |
+ return snackbarRemoved; |
+ } |
+ |
+ private static boolean objectsAreEqual(Object a, Object b) { |
+ if (a == null && b == null) return true; |
+ if (a == null || b == null) return false; |
+ return a.equals(b); |
+ } |
+ } |
} |