Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3598)

Unified Diff: chrome/android/java_staging/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java

Issue 1141283003: Upstream oodles of Chrome for Android code into Chromium. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: final patch? Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/android/java_staging/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java
diff --git a/chrome/android/java_staging/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java b/chrome/android/java_staging/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..8e1746304cf42bbf3d5dcfec903afe91fa901f28
--- /dev/null
+++ b/chrome/android/java_staging/src/org/chromium/chrome/browser/snackbar/SnackbarManager.java
@@ -0,0 +1,338 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.snackbar;
+
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import com.google.android.apps.chrome.R;
+
+import org.chromium.base.ApiCompatibilityUtils;
+import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.browser.device.DeviceClassManager;
+import org.chromium.ui.base.DeviceFormFactor;
+
+import java.util.HashSet;
+import java.util.Stack;
+
+/**
+ * Manager for the snackbar showing at the bottom of 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_SHOW_DURATION_MS} milliseconds, it will clear the stack and call
+ * {@link SnackbarController#onDismissNoAction(Object)} to all listeners.
+ */
+public class SnackbarManager implements OnClickListener, OnGlobalLayoutListener {
+
+ /**
+ * Interface that shows the ability to provide a unified snackbar manager.
+ */
+ public interface SnackbarManageable {
+ /**
+ * @return The snackbar manager that has a proper anchor view.
+ */
+ SnackbarManager getSnackbarManager();
+ }
+
+ /**
+ * Controller that post entries to snackbar manager and interact with snackbar manager during
+ * dismissal and action click event.
+ */
+ public static interface SnackbarController {
+ /**
+ * Callback triggered when user clicks on button at end of snackbar. This method is only
+ * called for controller having posted the entry the user clicked on; other controllers are
+ * not notified. Also once this {@link #onAction(Object)} is called,
+ * {@link #onDismissNoAction(Object)} and {@link #onDismissForEachType(boolean)} will not be
+ * called.
+ * @param actionData Data object passed when showing this specific snackbar.
+ */
+ void onAction(Object actionData);
+
+ /**
+ * Callback triggered when the snackbar is dismissed by either timeout or UI environment
+ * change. This callback will be called for each entry a controller has posted, _except_ for
+ * entries which the user has done action with, by clicking the action button.
+ * @param actionData Data object associated with the dismissed snackbar entry.
+ */
+ void onDismissNoAction(Object actionData);
+
+ /**
+ * Notify each SnackbarControllers instance only once immediately before the snackbar is
+ * dismissed. This function is likely to be used for controllers to do user metrics for
+ * dismissal.
+ * @param isTimeout Whether this dismissal is triggered by timeout.
+ */
+ void onDismissForEachType(boolean isTimeout);
+ }
+
+ private static final int DEFAULT_SNACKBAR_SHOW_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 final boolean mIsTablet;
+
+ private View mParent;
+ // Variable storing current xy position of parent view.
+ private int[] mTempTopLeft = new int[2];
+ private final Handler mUIThreadHandler;
+ private Stack<SnackbarEntry> mStack = new Stack<SnackbarEntry>();
+ private SnackbarPopupWindow mPopup;
+ private final Runnable mHideRunnable = new Runnable() {
+ @Override
+ public void run() {
+ dismissSnackbar(true);
+ }
+ };
+
+ /**
+ * 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.
+ */
+ public SnackbarManager(View parent) {
+ mParent = parent;
+ mUIThreadHandler = new Handler();
+ mIsTablet = DeviceFormFactor.isTablet(parent.getContext());
+ }
+
+ /**
+ * 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.
+ * @param actionText Text for action button to show.
+ * @param actionData Data bound to this snackbar entry. Will be returned to listeners when
+ * action be clicked or snackbar be dismissed.
+ * @param controller Listener for this snackbar entry.
+ */
+ public void showSnackbar(String template, String description, String actionText,
+ Object actionData, SnackbarController controller) {
+ mUIThreadHandler.removeCallbacks(mHideRunnable);
+ int duration = sUndoBarShowDurationMs;
+ // Duration for snackbars to show is different in normal mode and in accessibility mode.
+ if (DeviceClassManager.isAccessibilityModeEnabled(mParent.getContext())) {
+ duration = sAccessibilityUndoBarDurationMs;
+ }
+ mUIThreadHandler.postDelayed(mHideRunnable, duration);
+
+ 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;
+ }
+
+ /**
+ * Dismisses snackbar, clears out all entries in stack and prevents future remove callbacks from
+ * happening. This method also unregisters this class from global layout notifications.
+ * @param isTimeout Whether dismissal was triggered by timeout.
+ */
+ public void dismissSnackbar(boolean isTimeout) {
+ mUIThreadHandler.removeCallbacks(mHideRunnable);
+
+ if (mPopup != null) {
+ mPopup.dismiss();
+ mPopup = null;
+ }
+
+ 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);
+ }
+ entry.mController.onDismissNoAction(entry.mData);
+ }
+ mParent.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+
+ /**
+ * Removes all entries for certain type of controller. This method is used when a controller
+ * wants to remove all entries it posted to snackbar manager before.
+ * @param controller This method only removes entries posted by this controller.
+ */
+ 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);
+ isFound = true;
+ }
+ }
+ if (!isFound) return;
+
+ finishSnackbarEntryRemoval(controller);
+ }
+
+ /**
+ * Removes all entries for certain type of controller and with specified data. This method is
+ * used when a controller wants to remove some entries it posted to snackbar manager before.
+ * However it does not affect other controllers' entries. Note that this method assumes
+ * different types of snackbar controllers are not sharing the same instance.
+ * @param controller This method only removes entries posted by this controller.
+ * @param data Identifier of an entry to be removed from stack.
+ */
+ 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);
+ isFound = true;
+ break;
+ }
+ }
+ if (!isFound) return;
+
+ finishSnackbarEntryRemoval(controller);
+ }
+
+ private void finishSnackbarEntryRemoval(SnackbarController controller) {
+ controller.onDismissForEachType(false);
+
+ if (mStack.isEmpty()) {
+ dismissSnackbar(false);
+ } else {
+ // Refresh the snackbar to let it show top of stack and have full timeout.
+ showSnackbar(mStack.pop());
+ }
+ }
+
+ /**
+ * Handles click event for action button at end of snackbar.
+ */
+ @Override
+ public void onClick(View v) {
+ assert !mStack.isEmpty();
+
+ SnackbarEntry entry = mStack.pop();
+ entry.mController.onAction(entry.mData);
+
+ if (!mStack.isEmpty()) {
+ showSnackbar(mStack.pop());
+ } else {
+ dismissSnackbar(false);
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Resize and re-align popup window when device orientation changes, or soft keyboard shows up.
+ */
+ @Override
+ public void onGlobalLayout() {
+ if (mPopup == null) return;
+ mParent.getLocationInWindow(mTempTopLeft);
+ 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);
+ } else {
+ // Phone relayout
+ mPopup.update(mParent.getLeft(),
+ mTempTopLeft[1] + mParent.getHeight() - mPopup.getHeight(), mParent.getWidth(),
+ -1);
+ }
+ }
+
+ /**
+ * @return Whether there is a snackbar on screen.
+ */
+ public boolean isShowing() {
+ if (mPopup == null) return false;
+ return mPopup.isShowing();
+ }
+
+ /**
+ * 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.
+ */
+ @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;
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698