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 d500a015dca60e6b8b119dbfef596bdc9c3e72e4..dec7e6499a0f5620b215a03713974822d29acd4c 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 |
@@ -4,16 +4,19 @@ |
package org.chromium.chrome.browser.snackbar; |
+import android.graphics.Rect; |
import android.os.Handler; |
import android.view.Gravity; |
import android.view.View; |
import android.view.View.OnClickListener; |
import android.view.ViewTreeObserver.OnGlobalLayoutListener; |
+import android.view.Window; |
import org.chromium.base.ApiCompatibilityUtils; |
import org.chromium.base.VisibleForTesting; |
import org.chromium.chrome.R; |
import org.chromium.chrome.browser.device.DeviceClassManager; |
+import org.chromium.ui.UiUtils; |
import org.chromium.ui.base.DeviceFormFactor; |
import java.util.HashSet; |
@@ -29,7 +32,7 @@ import java.util.Stack; |
* 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_SHOW_DURATION_MS} milliseconds, it will clear the stack and call |
+ * {@link #DEFAULT_SNACKBAR_DURATION_MS} milliseconds, it will clear the stack and call |
* {@link SnackbarController#onDismissNoAction(Object)} to all listeners. |
*/ |
public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener { |
@@ -76,20 +79,18 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
void onDismissForEachType(boolean isTimeout); |
} |
- private static final int DEFAULT_SNACKBAR_SHOW_DURATION_MS = 3000; |
+ private static final int DEFAULT_SNACKBAR_DURATION_MS = 3000; |
private static final int ACCESSIBILITY_MODE_SNACKBAR_DURATION_MS = 6000; |
// Used instead of the constant so tests can override the value. |
- private static int sUndoBarShowDurationMs = DEFAULT_SNACKBAR_SHOW_DURATION_MS; |
- private static int sAccessibilityUndoBarDurationMs = ACCESSIBILITY_MODE_SNACKBAR_DURATION_MS; |
+ private static int sSnackbarDurationMs = DEFAULT_SNACKBAR_DURATION_MS; |
+ private static int sAccessibilitySnackbarDurationMs = ACCESSIBILITY_MODE_SNACKBAR_DURATION_MS; |
private final boolean mIsTablet; |
- private View mParent; |
- // Variable storing current xy position of parent view. |
- private int[] mTempTopLeft = new int[2]; |
+ private View mDecor; |
private final Handler mUIThreadHandler; |
- private Stack<SnackbarEntry> mStack = new Stack<SnackbarEntry>(); |
+ private Stack<Snackbar> mStack = new Stack<Snackbar>(); |
private SnackbarPopupWindow mPopup; |
private final Runnable mHideRunnable = new Runnable() { |
@Override |
@@ -98,19 +99,47 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
} |
}; |
+ // Variables used and reused in local calculations. |
+ private int[] mTempDecorPosition = new int[2]; |
+ private Rect mTempVisibleDisplayFrame = new Rect(); |
+ |
/** |
- * Create an instance of SnackbarManager with the root view of entire activity. |
- * @param parent The view that snackbar anchors to. Since SnackbarManager should be initialized |
- * during activity initialization, parent should always be set to root view of |
- * entire activity. |
+ * Constructs a SnackbarManager to show snackbars in the given window. |
*/ |
- public SnackbarManager(View parent) { |
- mParent = parent; |
+ public SnackbarManager(Window window) { |
+ mDecor = window.getDecorView(); |
mUIThreadHandler = new Handler(); |
- mIsTablet = DeviceFormFactor.isTablet(parent.getContext()); |
+ mIsTablet = DeviceFormFactor.isTablet(mDecor.getContext()); |
} |
/** |
+ * Shows a snackbar at the bottom of the screen, or above the keyboard if the keyboard is |
+ * visible. |
+ */ |
+ public void showSnackbar(Snackbar snackbar) { |
+ 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); |
+ } |
+ |
+ mPopup.announceforAccessibility(); |
+ } |
+ |
+ /** |
+ * TODO(newt): delete this method. Update callers to use {@link #showSnackbar(Snackbar)}. |
* Shows a snackbar with description text and an action button. |
* @param template Teamplate used to compose full description. |
* @param description Text for description showing at start of snackbar. |
@@ -121,17 +150,14 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
*/ |
public void showSnackbar(String template, String description, String actionText, |
Object actionData, SnackbarController controller) { |
- int duration = sUndoBarShowDurationMs; |
- // Duration for snackbars to show is different in normal mode and in accessibility mode. |
- if (DeviceClassManager.isAccessibilityModeEnabled(mParent.getContext())) { |
- duration = sAccessibilityUndoBarDurationMs; |
- } |
- showSnackbar(template, description, actionText, actionData, controller, duration); |
+ showSnackbar(Snackbar.make(description, controller).setTemplateText(template) |
+ .setAction(actionText, actionData)); |
} |
/** |
+ * TODO(newt): delete this method. Update callers to use {@link #showSnackbar(Snackbar)}. |
* Shows a snackbar for the given timeout duration with description text and an action button. |
- * Allows overriding the default timeout of {@link #DEFAULT_SNACKBAR_SHOW_DURATION_MS} with |
+ * Allows overriding the default timeout of {@link #DEFAULT_SNACKBAR_DURATION_MS} with |
* a custom value. |
* @param template Teamplate used to compose full description. |
* @param description Text for description showing at start of snackbar. |
@@ -143,40 +169,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
*/ |
public void showSnackbar(String template, String description, String actionText, |
Object actionData, SnackbarController controller, int timeoutMs) { |
- mUIThreadHandler.removeCallbacks(mHideRunnable); |
- mUIThreadHandler.postDelayed(mHideRunnable, timeoutMs); |
- |
- mStack.push(new SnackbarEntry(template, description, actionText, actionData, controller)); |
- if (mPopup == null) { |
- mPopup = new SnackbarPopupWindow(mParent, this, template, description, actionText); |
- showPopupAtBottom(); |
- mParent.getViewTreeObserver().addOnGlobalLayoutListener(this); |
- } else { |
- mPopup.setTextViews(template, description, actionText, true); |
- } |
- |
- mPopup.announceforAccessibility(); |
- } |
- |
- /** |
- * Convinient function for showSnackbar. Note this method adds passed entry to stack. |
- */ |
- private void showSnackbar(SnackbarEntry entry) { |
- showSnackbar(entry.mTemplate, entry.mDescription, entry.mActionText, entry.mData, |
- entry.mController); |
- } |
- |
- /** |
- * Change parent view of snackbar. This method is likely to be called when a new window is |
- * hiding the snackbar and will dismiss all snackbars. |
- * @param newParent The new parent view snackbar anchors to. |
- */ |
- public void setParentView(View newParent) { |
- if (newParent == mParent) return; |
- mParent.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
- mUIThreadHandler.removeCallbacks(mHideRunnable); |
- dismissSnackbar(false); |
- mParent = newParent; |
+ showSnackbar(Snackbar.make(description, controller).setTemplateText(template) |
+ .setAction(actionText, actionData).setDuration(timeoutMs)); |
} |
/** |
@@ -195,14 +189,14 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
HashSet<SnackbarController> controllers = new HashSet<SnackbarController>(); |
while (!mStack.isEmpty()) { |
- SnackbarEntry entry = mStack.pop(); |
- if (!controllers.contains(entry.mController)) { |
- entry.mController.onDismissForEachType(isTimeout); |
- controllers.add(entry.mController); |
+ Snackbar snackbar = mStack.pop(); |
+ if (!controllers.contains(snackbar.getController())) { |
+ snackbar.getController().onDismissForEachType(isTimeout); |
+ controllers.add(snackbar.getController()); |
} |
- entry.mController.onDismissNoAction(entry.mData); |
+ snackbar.getController().onDismissNoAction(snackbar.getActionData()); |
} |
- mParent.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
+ mDecor.getViewTreeObserver().removeOnGlobalLayoutListener(this); |
} |
/** |
@@ -212,11 +206,11 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
*/ |
public void removeSnackbarEntry(SnackbarController controller) { |
boolean isFound = false; |
- SnackbarEntry[] snackbarEntries = new SnackbarEntry[mStack.size()]; |
- mStack.toArray(snackbarEntries); |
- for (SnackbarEntry entry : snackbarEntries) { |
- if (entry.mController == controller) { |
- mStack.remove(entry); |
+ Snackbar[] snackbars = new Snackbar[mStack.size()]; |
+ mStack.toArray(snackbars); |
+ for (Snackbar snackbar : snackbars) { |
+ if (snackbar.getController() == controller) { |
+ mStack.remove(snackbar); |
isFound = true; |
} |
} |
@@ -235,10 +229,10 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
*/ |
public void removeSnackbarEntry(SnackbarController controller, Object data) { |
boolean isFound = false; |
- for (SnackbarEntry entry : mStack) { |
- if (entry.mData != null && entry.mData.equals(data) |
- && entry.mController == controller) { |
- mStack.remove(entry); |
+ for (Snackbar snackbar : mStack) { |
+ if (snackbar.getActionData() != null && snackbar.getActionData().equals(data) |
+ && snackbar.getController() == controller) { |
+ mStack.remove(snackbar); |
isFound = true; |
break; |
} |
@@ -266,8 +260,8 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
public void onClick(View v) { |
assert !mStack.isEmpty(); |
- SnackbarEntry entry = mStack.pop(); |
- entry.mController.onAction(entry.mData); |
+ Snackbar snackbar = mStack.pop(); |
+ snackbar.getController().onAction(snackbar.getActionData()); |
if (!mStack.isEmpty()) { |
showSnackbar(mStack.pop()); |
@@ -276,40 +270,48 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
} |
} |
- /** |
- * Calculates the show-up position from TOP START corner of parent view as a workaround of an |
- * android bug http://b/17789629 on Lollipop. |
- */ |
private void showPopupAtBottom() { |
- int margin = mIsTablet ? mParent.getResources().getDimensionPixelSize( |
- R.dimen.undo_bar_tablet_margin) : 0; |
- mParent.getLocationInWindow(mTempTopLeft); |
- mPopup.showAtLocation(mParent, Gravity.START | Gravity.TOP, margin, |
- mTempTopLeft[1] + mParent.getHeight() - mPopup.getHeight() - margin); |
+ // 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); |
} |
/** |
- * Resize and re-align popup window when device orientation changes, or soft keyboard shows up. |
+ * Resize and re-position popup window when the device orientation changes or the software |
+ * keyboard appears. Be careful not to let the snackbar overlap the Android navigation bar: |
+ * http://b/17789629. |
*/ |
@Override |
public void onGlobalLayout() { |
if (mPopup == null) return; |
- mParent.getLocationInWindow(mTempTopLeft); |
+ |
+ mDecor.getLocationInWindow(mTempDecorPosition); |
+ mDecor.getWindowVisibleDisplayFrame(mTempVisibleDisplayFrame); |
+ int decorBottom = mTempDecorPosition[1] + mDecor.getHeight(); |
+ int visibleBottom = Math.min(mTempVisibleDisplayFrame.bottom, decorBottom); |
+ |
if (mIsTablet) { |
- int margin = mParent.getResources().getDimensionPixelSize( |
- R.dimen.undo_bar_tablet_margin); |
- int width = mParent.getResources().getDimensionPixelSize( |
- R.dimen.undo_bar_tablet_width); |
- boolean isRtl = ApiCompatibilityUtils.isLayoutRtl(mParent); |
- int startPosition = isRtl ? mParent.getRight() - width - margin |
- : mParent.getLeft() + margin; |
- mPopup.update(startPosition, |
- mTempTopLeft[1] + mParent.getHeight() - mPopup.getHeight() - margin, width, -1); |
+ int margin = mDecor.getResources().getDimensionPixelOffset( |
+ R.dimen.snackbar_tablet_margin); |
+ int width = mDecor.getResources().getDimensionPixelSize(R.dimen.snackbar_tablet_width); |
+ boolean isRtl = ApiCompatibilityUtils.isLayoutRtl(mDecor); |
+ int startPosition = isRtl ? mDecor.getRight() - width - margin |
+ : mDecor.getLeft() + margin; |
+ mPopup.update(startPosition, decorBottom - visibleBottom + margin, width, -1); |
} else { |
- // Phone relayout |
- mPopup.update(mParent.getLeft(), |
- mTempTopLeft[1] + mParent.getHeight() - mPopup.getHeight(), mParent.getWidth(), |
- -1); |
+ mPopup.update(mDecor.getLeft(), decorBottom - visibleBottom, mDecor.getWidth(), -1); |
} |
} |
@@ -322,33 +324,12 @@ public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener |
} |
/** |
- * Allows overriding the default timeout of {@link #DEFAULT_SNACKBAR_SHOW_DURATION_MS} with |
- * a custom value. This is meant to be used by tests. |
- * @param timeoutMs The new timeout to use in ms. |
+ * Overrides the default snackbar duration with a custom value for testing. |
+ * @param durationMs The duration to use in ms. |
*/ |
@VisibleForTesting |
- public static void setTimeoutForTesting(int timeoutMs) { |
- sUndoBarShowDurationMs = timeoutMs; |
- sAccessibilityUndoBarDurationMs = timeoutMs; |
- } |
- |
- /** |
- * Simple data structure representing a single snackbar in stack. |
- */ |
- private static class SnackbarEntry { |
- public String mTemplate; |
- public String mDescription; |
- public String mActionText; |
- public Object mData; |
- public SnackbarController mController; |
- |
- public SnackbarEntry(String template, String description, String actionText, |
- Object actionData, SnackbarController controller) { |
- mTemplate = template; |
- mDescription = description; |
- mActionText = actionText; |
- mData = actionData; |
- mController = controller; |
- } |
+ public static void setDurationForTesting(int durationMs) { |
+ sSnackbarDurationMs = durationMs; |
+ sAccessibilitySnackbarDurationMs = durationMs; |
} |
} |