| 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;
|
| + }
|
| + }
|
| +}
|
|
|