gecko-dev/mobile/android/base/toolbar/BrowserToolbar.java

1463 lines
50 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.toolbar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import org.json.JSONObject;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.MenuPopup;
import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnStopListener;
import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.OnTitleChangeListener;
import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.MenuUtils;
import org.mozilla.gecko.widget.ThemedImageButton;
import org.mozilla.gecko.widget.ThemedImageView;
import org.mozilla.gecko.widget.ThemedRelativeLayout;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextMenu;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
/**
* {@code BrowserToolbar} is single entry point for users of the toolbar
* subsystem i.e. this should be the only import outside the 'toolbar'
* package.
*
* {@code BrowserToolbar} serves at the single event bus for all
* sub-components in the toolbar. It tracks tab events and gecko messages
* and update the state of its inner components accordingly.
*
* It has two states, display and edit, which are controlled by
* ToolbarEditLayout and ToolbarDisplayLayout. In display state, the toolbar
* displays the current state for the selected tab. In edit state, it shows
* a text entry for searching bookmarks/history. {@code BrowserToolbar}
* provides public API to enter, cancel, and commit the edit state as well
* as a set of listeners to allow {@code BrowserToolbar} users to react
* to state changes accordingly.
*/
public class BrowserToolbar extends ThemedRelativeLayout
implements Tabs.OnTabsChangedListener,
GeckoMenu.ActionItemBarPresenter,
GeckoEventListener {
private static final String LOGTAG = "GeckoToolbar";
public interface OnActivateListener {
public void onActivate();
}
public interface OnCommitListener {
public void onCommit();
}
public interface OnDismissListener {
public void onDismiss();
}
public interface OnFilterListener {
public void onFilter(String searchText, AutocompleteHandler handler);
}
public interface OnStartEditingListener {
public void onStartEditing();
}
public interface OnStopEditingListener {
public void onStopEditing();
}
private enum UIMode {
EDIT,
DISPLAY
}
enum ForwardButtonAnimation {
SHOW,
HIDE
}
private ToolbarDisplayLayout urlDisplayLayout;
private ToolbarEditLayout urlEditLayout;
private View urlBarEntry;
private RelativeLayout.LayoutParams urlBarEntryDefaultLayoutParams;
private RelativeLayout.LayoutParams urlBarEntryShrunkenLayoutParams;
private ImageView urlBarTranslatingEdge;
private boolean isSwitchingTabs;
private ShapedButton tabsButton;
private ImageButton backButton;
private ImageButton forwardButton;
private ToolbarProgressView progressBar;
private TabCounter tabsCounter;
private ThemedImageButton menuButton;
private ThemedImageView menuIcon;
private LinearLayout actionItemBar;
private MenuPopup menuPopup;
private List<View> focusOrder;
private final ThemedImageView editCancel;
private final View[] tabletDisplayModeViews;
private boolean hidForwardButtonOnStartEditing = false;
private boolean shouldShrinkURLBar = false;
private OnActivateListener activateListener;
private OnFocusChangeListener focusChangeListener;
private OnStartEditingListener startEditingListener;
private OnStopEditingListener stopEditingListener;
private final PropertyAnimator.PropertyAnimationListener showEditingPhoneAnimationListener;
private final PropertyAnimator.PropertyAnimationListener stopEditingPhoneAnimationListener;
private final BrowserApp activity;
private boolean hasSoftMenuButton;
private UIMode uiMode;
private boolean isAnimatingEntry;
private int urlBarViewOffset;
private int defaultForwardMargin;
private static final Interpolator buttonsInterpolator = new AccelerateInterpolator();
private static final int FORWARD_ANIMATION_DURATION = 450;
private final LightweightTheme theme;
public BrowserToolbar(Context context) {
this(context, null);
}
public BrowserToolbar(Context context, AttributeSet attrs) {
super(context, attrs);
theme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
// BrowserToolbar is attached to BrowserApp only.
activity = (BrowserApp) context;
// Inflate the content.
LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this);
Tabs.registerOnTabsChangedListener(this);
isSwitchingTabs = true;
isAnimatingEntry = false;
EventDispatcher.getInstance().registerGeckoThreadListener(this,
"Reader:Click",
"Reader:LongClick");
final Resources res = getResources();
urlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
defaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
urlBarEntry = findViewById(R.id.url_bar_entry);
urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
urlBarEntryDefaultLayoutParams = (RelativeLayout.LayoutParams) urlBarEntry.getLayoutParams();
// API level 19 adds a RelativeLayout.LayoutParams copy constructor, so we explicitly cast
// to ViewGroup.MarginLayoutParams to ensure consistency across platforms.
urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams(
(ViewGroup.MarginLayoutParams) urlBarEntryDefaultLayoutParams);
// Note: a shrunken phone layout is not displayed on any known devices,
// and thus shrunken layout params for phone are not maintained.
if (HardwareUtils.isTablet()) {
urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.edit_layout);
urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_LEFT, R.id.edit_layout);
urlBarEntryShrunkenLayoutParams.leftMargin = 0;
}
// This will clip the translating edge's image at 60% of its width
urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
if (urlBarTranslatingEdge != null) {
urlBarTranslatingEdge.getDrawable().setLevel(6000);
}
tabsButton = (ShapedButton) findViewById(R.id.tabs);
tabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
if (Build.VERSION.SDK_INT >= 11) {
tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
backButton = (ImageButton) findViewById(R.id.back);
setButtonEnabled(backButton, false);
forwardButton = (ImageButton) findViewById(R.id.forward);
setButtonEnabled(forwardButton, false);
menuButton = (ThemedImageButton) findViewById(R.id.menu);
menuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
actionItemBar = (LinearLayout) findViewById(R.id.menu_items);
hasSoftMenuButton = !HardwareUtils.hasMenuButton();
editCancel = (ThemedImageView) findViewById(R.id.edit_cancel);
// We use different layouts on phones and tablets, so adjust the focus
// order appropriately.
focusOrder = new ArrayList<View>();
if (HardwareUtils.isTablet()) {
focusOrder.addAll(Arrays.asList(tabsButton, backButton, forwardButton, this));
focusOrder.addAll(urlDisplayLayout.getFocusOrder());
focusOrder.addAll(Arrays.asList(actionItemBar, menuButton));
} else {
focusOrder.add(this);
focusOrder.addAll(urlDisplayLayout.getFocusOrder());
focusOrder.addAll(Arrays.asList(tabsButton, menuButton));
}
setUIMode(UIMode.DISPLAY);
// Create these listeners here, once, to avoid constructing new listeners
// each time they are set on an animator (i.e. each time the url bar is clicked).
showEditingPhoneAnimationListener = new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() { /* Do nothing */ }
@Override
public void onPropertyAnimationEnd() {
isAnimatingEntry = false;
}
};
stopEditingPhoneAnimationListener = new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() { /* Do nothing */ }
@Override
public void onPropertyAnimationEnd() {
urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
if (shouldShrinkURLBar) {
urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
}
PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator);
buttonsAnimator.start();
isAnimatingEntry = false;
// Trigger animation to update the tabs counter once the
// tabs button is back on screen.
updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
}
};
tabletDisplayModeViews = new View[] {
actionItemBar,
backButton,
menuButton,
menuIcon,
tabsButton,
tabsCounter,
};
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
if (activateListener != null) {
activateListener.onActivate();
}
}
});
setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
// We don't the context menu while editing
if (isEditing()) {
return;
}
// NOTE: Use MenuUtils.safeSetVisible because some actions might
// be on the Page menu
MenuInflater inflater = activity.getMenuInflater();
inflater.inflate(R.menu.titlebar_contextmenu, menu);
String clipboard = Clipboard.getText();
if (TextUtils.isEmpty(clipboard)) {
menu.findItem(R.id.pasteandgo).setVisible(false);
menu.findItem(R.id.paste).setVisible(false);
}
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
String url = tab.getURL();
if (url == null) {
menu.findItem(R.id.copyurl).setVisible(false);
menu.findItem(R.id.add_to_launcher).setVisible(false);
}
MenuUtils.safeSetVisible(menu, R.id.subscribe, tab.hasFeeds());
MenuUtils.safeSetVisible(menu, R.id.add_search_engine, tab.hasOpenSearch());
} else {
// if there is no tab, remove anything tab dependent
menu.findItem(R.id.copyurl).setVisible(false);
menu.findItem(R.id.add_to_launcher).setVisible(false);
MenuUtils.safeSetVisible(menu, R.id.subscribe, false);
MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false);
}
}
});
urlDisplayLayout.setOnStopListener(new OnStopListener() {
@Override
public Tab onStop() {
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.doStop();
return tab;
}
return null;
}
});
urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() {
@Override
public void onTitleChange(CharSequence title) {
final String contentDescription;
if (title != null) {
contentDescription = title.toString();
} else {
contentDescription = activity.getString(R.string.url_bar_default_text);
}
// The title and content description should
// always be sync.
setContentDescription(contentDescription);
}
});
urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
// This will select the url bar when entering editing mode.
setSelected(hasFocus);
if (focusChangeListener != null) {
focusChangeListener.onFocusChange(v, hasFocus);
}
}
});
tabsButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View v) {
toggleTabs();
}
});
tabsButton.setImageLevel(0);
backButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
Tabs.getInstance().getSelectedTab().doBack();
}
});
backButton.setOnLongClickListener(new Button.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return Tabs.getInstance().getSelectedTab().showBackHistory();
}
});
forwardButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
Tabs.getInstance().getSelectedTab().doForward();
}
});
forwardButton.setOnLongClickListener(new Button.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
return Tabs.getInstance().getSelectedTab().showForwardHistory();
}
});
editCancel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// If we exit editing mode during the animation,
// we're put into an inconsistent state (bug 1017276).
if (!isAnimatingEntry) {
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
TelemetryContract.Method.ACTIONBAR,
getResources().getResourceEntryName(editCancel.getId()));
cancelEdit();
}
}
});
if (hasSoftMenuButton) {
menuButton.setVisibility(View.VISIBLE);
menuIcon.setVisibility(View.VISIBLE);
menuButton.setOnClickListener(new Button.OnClickListener() {
@Override
public void onClick(View view) {
activity.openOptionsMenu();
}
});
}
}
public void setProgressBar(ToolbarProgressView progressBar) {
this.progressBar = progressBar;
}
public void refresh() {
urlDisplayLayout.dismissSiteIdentityPopup();
}
public boolean onBackPressed() {
// If we exit editing mode during the animation,
// we're put into an inconsistent state (bug 1017276).
if (isEditing() && !isAnimatingEntry) {
Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL,
TelemetryContract.Method.BACK);
cancelEdit();
return true;
}
return urlDisplayLayout.dismissSiteIdentityPopup();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// If the motion event has occured below the toolbar (due to the scroll
// offset), let it pass through to the page.
if (event != null && event.getY() > getHeight() + ViewHelper.getTranslationY(this)) {
return false;
}
return super.onTouchEvent(event);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (h != oldh) {
// Post this to happen outside of onSizeChanged, as this may cause
// a layout change and relayouts within a layout change don't work.
post(new Runnable() {
@Override
public void run() {
activity.refreshToolbarHeight();
}
});
}
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
Log.d(LOGTAG, "onTabChanged: " + msg);
final Tabs tabs = Tabs.getInstance();
// These conditions are split into three phases:
// * Always do first
// * Handling specific to the selected tab
// * Always do afterwards.
switch (msg) {
case ADDED:
case CLOSED:
updateTabCount(tabs.getDisplayCount());
break;
case RESTORED:
// TabCount fixup after OOM
case SELECTED:
urlDisplayLayout.dismissSiteIdentityPopup();
updateTabCount(tabs.getDisplayCount());
isSwitchingTabs = true;
break;
}
if (tabs.isSelectedTab(tab)) {
final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class);
// Progress-related handling
switch (msg) {
case START:
updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT);
// Fall through.
case ADDED:
case LOCATION_CHANGE:
case LOAD_ERROR:
case LOADED:
case STOP:
flags.add(UpdateFlags.PROGRESS);
if (progressBar.getVisibility() == View.VISIBLE) {
progressBar.animateProgress(tab.getLoadProgress());
}
break;
case SELECTED:
flags.add(UpdateFlags.PROGRESS);
updateProgressVisibility();
break;
}
switch (msg) {
case STOP:
// Reset the title in case we haven't navigated
// to a new page yet.
flags.add(UpdateFlags.TITLE);
// Fall through.
case START:
case CLOSED:
case ADDED:
updateBackButton(tab);
updateForwardButton(tab);
break;
case SELECTED:
flags.add(UpdateFlags.PRIVATE_MODE);
setPrivateMode(tab.isPrivate());
// Fall through.
case LOAD_ERROR:
flags.add(UpdateFlags.TITLE);
// Fall through.
case LOCATION_CHANGE:
// A successful location change will cause Tab to notify
// us of a title change, so we don't update the title here.
flags.add(UpdateFlags.FAVICON);
flags.add(UpdateFlags.SITE_IDENTITY);
updateBackButton(tab);
updateForwardButton(tab);
break;
case TITLE:
flags.add(UpdateFlags.TITLE);
break;
case FAVICON:
flags.add(UpdateFlags.FAVICON);
break;
case SECURITY_CHANGE:
flags.add(UpdateFlags.SITE_IDENTITY);
break;
}
if (!flags.isEmpty()) {
updateDisplayLayout(tab, flags);
}
}
switch (msg) {
case SELECTED:
case LOAD_ERROR:
case LOCATION_CHANGE:
isSwitchingTabs = false;
}
}
private void updateProgressVisibility() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
}
private void updateProgressVisibility(Tab selectedTab, int progress) {
if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) {
progressBar.setProgress(progress);
progressBar.setVisibility(View.VISIBLE);
} else {
progressBar.setVisibility(View.GONE);
}
}
private boolean isVisible() {
return ViewHelper.getTranslationY(this) == 0;
}
@Override
public void setNextFocusDownId(int nextId) {
super.setNextFocusDownId(nextId);
tabsButton.setNextFocusDownId(nextId);
backButton.setNextFocusDownId(nextId);
forwardButton.setNextFocusDownId(nextId);
urlDisplayLayout.setNextFocusDownId(nextId);
menuButton.setNextFocusDownId(nextId);
}
/**
* Returns the number of pixels the url bar translating edge
* needs to translate to the right to enter its editing mode state.
* A negative value means the edge must translate to the left.
*/
private int getUrlBarEntryTranslation() {
// Find the distance from the right-edge of the url bar (where we're translating from) to
// the left-edge of the cancel button (where we're translating to; note that the cancel
// button must be laid out, i.e. not View.GONE).
return editCancel.getLeft() - urlBarEntry.getRight();
}
private int getUrlBarCurveTranslation() {
return getWidth() - tabsButton.getLeft();
}
private boolean canDoBack(Tab tab) {
return (tab.canDoBack() && !isEditing());
}
private boolean canDoForward(Tab tab) {
return (tab.canDoForward() && !isEditing());
}
private void addTab() {
activity.addTab();
}
private void toggleTabs() {
if (activity.areTabsShown()) {
if (activity.hasTabsSideBar())
activity.hideTabs();
} else {
// hide the virtual keyboard
InputMethodManager imm =
(InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
if (!tab.isPrivate())
activity.showNormalTabs();
else
activity.showPrivateTabs();
}
}
}
private void updateTabCountAndAnimate(int count) {
// Don't animate if the toolbar is hidden.
if (!isVisible()) {
updateTabCount(count);
return;
}
// If toolbar is in edit mode on a phone, this means the entry is expanded
// and the tabs button is translated offscreen. Don't trigger tabs counter
// updates until the tabs button is back on screen.
// See stopEditing()
if (!isEditing() || HardwareUtils.isTablet()) {
tabsCounter.setCount(count);
tabsButton.setContentDescription((count > 1) ?
activity.getString(R.string.num_tabs, count) :
activity.getString(R.string.one_tab));
}
}
private void updateTabCount(int count) {
// If toolbar is in edit mode on a phone, this means the entry is expanded
// and the tabs button is translated offscreen. Don't trigger tabs counter
// updates until the tabs button is back on screen.
// See stopEditing()
if (isEditing() && !HardwareUtils.isTablet()) {
return;
}
// Set TabCounter based on visibility
if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) {
tabsCounter.setCountWithAnimation(count);
} else {
tabsCounter.setCount(count);
}
// Update A11y information
tabsButton.setContentDescription((count > 1) ?
activity.getString(R.string.num_tabs, count) :
activity.getString(R.string.one_tab));
}
private void updateDisplayLayout(Tab tab, EnumSet<UpdateFlags> flags) {
if (isSwitchingTabs) {
flags.add(UpdateFlags.DISABLE_ANIMATIONS);
}
urlDisplayLayout.updateFromTab(tab, flags);
if (flags.contains(UpdateFlags.TITLE)) {
if (!isEditing()) {
urlEditLayout.setText(tab.getURL());
}
}
if (flags.contains(UpdateFlags.PROGRESS)) {
updateFocusOrder();
}
}
private void updateFocusOrder() {
View prevView = null;
// If the element that has focus becomes disabled or invisible, focus
// is given to the URL bar.
boolean needsNewFocus = false;
for (View view : focusOrder) {
if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) {
if (view.hasFocus()) {
needsNewFocus = true;
}
continue;
}
if (view == actionItemBar) {
final int childCount = actionItemBar.getChildCount();
for (int child = 0; child < childCount; child++) {
View childView = actionItemBar.getChildAt(child);
if (prevView != null) {
childView.setNextFocusLeftId(prevView.getId());
prevView.setNextFocusRightId(childView.getId());
}
prevView = childView;
}
} else {
if (prevView != null) {
view.setNextFocusLeftId(prevView.getId());
prevView.setNextFocusRightId(view.getId());
}
prevView = view;
}
}
if (needsNewFocus) {
requestFocus();
}
}
public void onEditSuggestion(String suggestion) {
if (!isEditing()) {
return;
}
urlEditLayout.onEditSuggestion(suggestion);
}
public void setTitle(CharSequence title) {
urlDisplayLayout.setTitle(title);
}
public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) {
if (!tabsAreShown) {
PropertyAnimator buttonsAnimator =
new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
buttonsAnimator.attach(tabsCounter,
PropertyAnimator.Property.ALPHA,
1.0f);
if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
buttonsAnimator.attach(menuIcon,
PropertyAnimator.Property.ALPHA,
1.0f);
}
buttonsAnimator.start();
return;
}
ViewHelper.setAlpha(tabsCounter, 0.0f);
if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
ViewHelper.setAlpha(menuIcon, 0.0f);
}
}
public void finishTabsAnimation(boolean tabsAreShown) {
if (tabsAreShown) {
return;
}
PropertyAnimator animator = new PropertyAnimator(150);
animator.attach(tabsCounter,
PropertyAnimator.Property.ALPHA,
1.0f);
if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
animator.attach(menuIcon,
PropertyAnimator.Property.ALPHA,
1.0f);
}
animator.start();
}
public void setOnActivateListener(OnActivateListener listener) {
activateListener = listener;
}
public void setOnCommitListener(OnCommitListener listener) {
urlEditLayout.setOnCommitListener(listener);
}
public void setOnDismissListener(OnDismissListener listener) {
urlEditLayout.setOnDismissListener(listener);
}
public void setOnFilterListener(OnFilterListener listener) {
urlEditLayout.setOnFilterListener(listener);
}
public void setOnFocusChangeListener(OnFocusChangeListener listener) {
focusChangeListener = listener;
}
public void setOnStartEditingListener(OnStartEditingListener listener) {
startEditingListener = listener;
}
public void setOnStopEditingListener(OnStopEditingListener listener) {
stopEditingListener = listener;
}
private void showUrlEditLayout() {
setUrlEditLayoutVisibility(true, null);
}
private void showUrlEditLayout(PropertyAnimator animator) {
setUrlEditLayoutVisibility(true, animator);
}
private void hideUrlEditLayout() {
setUrlEditLayoutVisibility(false, null);
}
private void hideUrlEditLayout(PropertyAnimator animator) {
setUrlEditLayoutVisibility(false, animator);
}
private void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
if (showEditLayout) {
urlEditLayout.prepareShowAnimation(animator);
}
if (animator == null) {
final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
viewToHide.setVisibility(View.GONE);
viewToShow.setVisibility(View.VISIBLE);
final int cancelVisibility = (showEditLayout ? View.VISIBLE : View.INVISIBLE);
setCancelVisibility(cancelVisibility);
return;
}
animator.addPropertyAnimationListener(new PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
if (!showEditLayout) {
urlEditLayout.setVisibility(View.GONE);
urlDisplayLayout.setVisibility(View.VISIBLE);
setCancelVisibility(View.INVISIBLE);
}
}
@Override
public void onPropertyAnimationEnd() {
if (showEditLayout) {
urlDisplayLayout.setVisibility(View.GONE);
urlEditLayout.setVisibility(View.VISIBLE);
setCancelVisibility(View.VISIBLE);
}
}
});
}
private void setCancelVisibility(final int visibility) {
editCancel.setVisibility(visibility);
}
/**
* Disables all toolbar elements which are not
* related to editing mode.
*/
private void updateChildrenEnabledStateForEditing() {
// Disable toolbar elements while in editing mode
final boolean enabled = !isEditing();
if (!enabled) {
tabsCounter.onEnterEditingMode();
}
tabsButton.setEnabled(enabled);
menuButton.setEnabled(enabled);
final int actionItemsCount = actionItemBar.getChildCount();
for (int i = 0; i < actionItemsCount; i++) {
actionItemBar.getChildAt(i).setEnabled(enabled);
}
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
setButtonEnabled(backButton, canDoBack(tab));
setButtonEnabled(forwardButton, canDoForward(tab));
// Once the editing mode is finished, we have to ensure that the
// forward button slides away if necessary. This is because we might
// have only disabled it (without hiding it) when the toolbar entered
// editing mode.
if (!isEditing()) {
animateForwardButton(canDoForward(tab) ?
ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
}
}
}
private void setUIMode(final UIMode uiMode) {
this.uiMode = uiMode;
urlEditLayout.setEnabled(uiMode == UIMode.EDIT);
}
/**
* Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new
* tab button). Note that selection state is independent of editing mode.
*/
public boolean isEditing() {
return (uiMode == UIMode.EDIT);
}
public boolean isAnimating() {
return isAnimatingEntry;
}
public void startEditing(String url, PropertyAnimator animator) {
if (isEditing()) {
return;
}
urlEditLayout.setText(url != null ? url : "");
setUIMode(UIMode.EDIT);
updateProgressVisibility();
if (startEditingListener != null) {
startEditingListener.onStartEditing();
}
if (HardwareUtils.isTablet()) {
showEditingOnTablet();
} else {
urlBarTranslatingEdge.setVisibility(View.VISIBLE);
final int curveTranslation = getUrlBarCurveTranslation();
final int entryTranslation = getUrlBarEntryTranslation();
shouldShrinkURLBar = (entryTranslation < 0);
if (shouldShrinkURLBar) {
urlBarEntry.setLayoutParams(urlBarEntryShrunkenLayoutParams);
}
if (Build.VERSION.SDK_INT < 11) {
showEditingOnPreHoneycomb(entryTranslation, curveTranslation);
} else {
showEditingWithPhoneAnimation(animator, entryTranslation, curveTranslation);
}
}
}
private void showEditingOnPreHoneycomb(final int entryTranslation,
final int curveTranslation) {
showUrlEditLayout();
// Prevent taps through the editing mode cancel button (bug 1001243).
tabsButton.setEnabled(false);
ViewHelper.setTranslationX(urlBarTranslatingEdge, entryTranslation);
ViewHelper.setTranslationX(tabsButton, curveTranslation);
ViewHelper.setTranslationX(tabsCounter, curveTranslation);
ViewHelper.setTranslationX(actionItemBar, curveTranslation);
if (hasSoftMenuButton) {
// Prevent tabs through the editing mode cancel button (bug 1001243).
menuButton.setEnabled(false);
ViewHelper.setTranslationX(menuButton, curveTranslation);
ViewHelper.setTranslationX(menuIcon, curveTranslation);
}
}
private void showEditingOnTablet() {
urlBarEntry.setLayoutParams(urlBarEntryShrunkenLayoutParams);
// Hide display elements.
updateChildrenEnabledStateForEditing();
for (final View v : tabletDisplayModeViews) {
v.setVisibility(View.INVISIBLE);
}
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null && selectedTab.canDoForward()) {
hidForwardButtonOnStartEditing = true;
forwardButton.setVisibility(View.INVISIBLE);
} else {
hidForwardButtonOnStartEditing = false;
}
// Show editing elements.
showUrlEditLayout();
}
private void showEditingWithPhoneAnimation(final PropertyAnimator animator,
final int entryTranslation, final int curveTranslation) {
if (isAnimatingEntry)
return;
urlDisplayLayout.prepareStartEditingAnimation();
// Slide toolbar elements.
animator.attach(urlBarTranslatingEdge,
PropertyAnimator.Property.TRANSLATION_X,
entryTranslation);
animator.attach(tabsButton,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
animator.attach(tabsCounter,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
animator.attach(actionItemBar,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
if (hasSoftMenuButton) {
animator.attach(menuButton,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
animator.attach(menuIcon,
PropertyAnimator.Property.TRANSLATION_X,
curveTranslation);
}
showUrlEditLayout(animator);
animator.addPropertyAnimationListener(showEditingPhoneAnimationListener);
isAnimatingEntry = true; // To be correct, this should be called last.
}
/**
* Exits edit mode without updating the toolbar title.
*
* @return the url that was entered
*/
public String cancelEdit() {
Telemetry.stopUISession(TelemetryContract.Session.AWESOMESCREEN);
return stopEditing();
}
/**
* Exits edit mode, updating the toolbar title with the url that was just entered.
*
* @return the url that was entered
*/
public String commitEdit() {
final String url = stopEditing();
if (!TextUtils.isEmpty(url)) {
setTitle(url);
}
return url;
}
private String stopEditing() {
final String url = urlEditLayout.getText();
if (!isEditing()) {
return url;
}
setUIMode(UIMode.DISPLAY);
updateChildrenEnabledStateForEditing();
if (stopEditingListener != null) {
stopEditingListener.onStopEditing();
}
updateProgressVisibility();
// The animation looks cleaner if the text in the URL bar is
// not selected so clear the selection by clearing focus.
urlEditLayout.clearFocus();
if (Build.VERSION.SDK_INT < 11) {
stopEditingOnPreHoneycomb();
} else if (HardwareUtils.isTablet()) {
stopEditingOnTablet();
} else {
stopEditingWithPhoneAnimation();
}
return url;
}
private void stopEditingOnPreHoneycomb() {
hideUrlEditLayout();
updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
if (shouldShrinkURLBar) {
urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
}
tabsButton.setEnabled(true);
urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
ViewHelper.setTranslationX(urlBarTranslatingEdge, 0);
ViewHelper.setTranslationX(tabsButton, 0);
ViewHelper.setTranslationX(tabsCounter, 0);
ViewHelper.setTranslationX(actionItemBar, 0);
if (hasSoftMenuButton) {
menuButton.setEnabled(true);
ViewHelper.setTranslationX(menuButton, 0);
ViewHelper.setTranslationX(menuIcon, 0);
}
}
private void stopEditingOnTablet() {
urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
// Show display elements.
updateChildrenEnabledStateForEditing();
for (final View v : tabletDisplayModeViews) {
v.setVisibility(View.VISIBLE);
}
if (hidForwardButtonOnStartEditing) {
forwardButton.setVisibility(View.VISIBLE);
}
// Hide editing elements.
hideUrlEditLayout();
}
private void stopEditingWithPhoneAnimation() {
final PropertyAnimator contentAnimator = new PropertyAnimator(250);
contentAnimator.setUseHardwareLayer(false);
// Slide the toolbar back to its original size.
contentAnimator.attach(urlBarTranslatingEdge,
PropertyAnimator.Property.TRANSLATION_X,
0);
contentAnimator.attach(tabsButton,
PropertyAnimator.Property.TRANSLATION_X,
0);
contentAnimator.attach(tabsCounter,
PropertyAnimator.Property.TRANSLATION_X,
0);
contentAnimator.attach(actionItemBar,
PropertyAnimator.Property.TRANSLATION_X,
0);
if (hasSoftMenuButton) {
contentAnimator.attach(menuButton,
PropertyAnimator.Property.TRANSLATION_X,
0);
contentAnimator.attach(menuIcon,
PropertyAnimator.Property.TRANSLATION_X,
0);
}
hideUrlEditLayout(contentAnimator);
contentAnimator.addPropertyAnimationListener(stopEditingPhoneAnimationListener);
isAnimatingEntry = true;
contentAnimator.start();
}
private void setButtonEnabled(ImageButton button, boolean enabled) {
final Drawable drawable = button.getDrawable();
if (drawable != null) {
drawable.setAlpha(enabled ? 255 : 61);
}
button.setEnabled(enabled);
}
public void updateBackButton(Tab tab) {
setButtonEnabled(backButton, canDoBack(tab));
}
private void animateForwardButton(final ForwardButtonAnimation animation) {
// If the forward button is not visible, we must be
// in the phone UI.
if (forwardButton.getVisibility() != View.VISIBLE) {
return;
}
final boolean showing = (animation == ForwardButtonAnimation.SHOW);
// if the forward button's margin is non-zero, this means it has already
// been animated to be visible¸ and vice-versa.
MarginLayoutParams fwdParams = (MarginLayoutParams) forwardButton.getLayoutParams();
if ((fwdParams.leftMargin > defaultForwardMargin && showing) ||
(fwdParams.leftMargin == defaultForwardMargin && !showing)) {
return;
}
// We want the forward button to show immediately when switching tabs
final PropertyAnimator forwardAnim =
new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
final int width = forwardButton.getWidth() / 2;
forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
@Override
public void onPropertyAnimationStart() {
if (!showing) {
// Set the margin before the transition when hiding the forward button. We
// have to do this so that the favicon isn't clipped during the transition
MarginLayoutParams layoutParams =
(MarginLayoutParams) urlDisplayLayout.getLayoutParams();
layoutParams.leftMargin = 0;
// Do the same on the URL edit container
layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
layoutParams.leftMargin = 0;
requestLayout();
// Note, we already translated the favicon, site security, and text field
// in prepareForwardAnimation, so they should appear to have not moved at
// all at this point.
}
}
@Override
public void onPropertyAnimationEnd() {
if (showing) {
MarginLayoutParams layoutParams =
(MarginLayoutParams) urlDisplayLayout.getLayoutParams();
layoutParams.leftMargin = urlBarViewOffset;
layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
layoutParams.leftMargin = urlBarViewOffset;
}
urlDisplayLayout.finishForwardAnimation();
MarginLayoutParams layoutParams = (MarginLayoutParams) forwardButton.getLayoutParams();
layoutParams.leftMargin = defaultForwardMargin + (showing ? width : 0);
ViewHelper.setTranslationX(forwardButton, 0);
requestLayout();
}
});
prepareForwardAnimation(forwardAnim, animation, width);
forwardAnim.start();
}
public void updateForwardButton(Tab tab) {
final boolean enabled = canDoForward(tab);
if (forwardButton.isEnabled() == enabled)
return;
// Save the state on the forward button so that we can skip animations
// when there's nothing to change
setButtonEnabled(forwardButton, enabled);
animateForwardButton(enabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
}
private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
if (animation == ForwardButtonAnimation.HIDE) {
anim.attach(forwardButton,
PropertyAnimator.Property.TRANSLATION_X,
-width);
anim.attach(forwardButton,
PropertyAnimator.Property.ALPHA,
0);
} else {
anim.attach(forwardButton,
PropertyAnimator.Property.TRANSLATION_X,
width);
anim.attach(forwardButton,
PropertyAnimator.Property.ALPHA,
1);
}
urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
}
@Override
public boolean addActionItem(View actionItem) {
actionItemBar.addView(actionItem);
return true;
}
@Override
public void removeActionItem(View actionItem) {
actionItemBar.removeView(actionItem);
}
@Override
public void setPrivateMode(boolean isPrivate) {
super.setPrivateMode(isPrivate);
tabsButton.setPrivateMode(isPrivate);
menuButton.setPrivateMode(isPrivate);
menuIcon.setPrivateMode(isPrivate);
urlEditLayout.setPrivateMode(isPrivate);
if (backButton instanceof BackButton) {
((BackButton) backButton).setPrivateMode(isPrivate);
}
if (forwardButton instanceof ForwardButton) {
((ForwardButton) forwardButton).setPrivateMode(isPrivate);
}
}
public void show() {
setVisibility(View.VISIBLE);
}
public void hide() {
setVisibility(View.GONE);
}
public View getDoorHangerAnchor() {
return urlDisplayLayout.getDoorHangerAnchor();
}
public void onDestroy() {
Tabs.unregisterOnTabsChangedListener(this);
EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
"Reader:Click",
"Reader:LongClick");
}
public boolean openOptionsMenu() {
if (!hasSoftMenuButton) {
return false;
}
// Initialize the popup.
if (menuPopup == null) {
View panel = activity.getMenuPanel();
menuPopup = new MenuPopup(activity);
menuPopup.setPanelView(panel);
menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
activity.onOptionsMenuClosed(null);
}
});
}
GeckoAppShell.getGeckoInterface().invalidateOptionsMenu();
if (!menuPopup.isShowing()) {
menuPopup.showAsDropDown(menuButton);
}
return true;
}
public boolean closeOptionsMenu() {
if (!hasSoftMenuButton) {
return false;
}
if (menuPopup != null && menuPopup.isShowing()) {
menuPopup.dismiss();
}
return true;
}
@Override
public void handleMessage(String event, JSONObject message) {
Log.d(LOGTAG, "handleMessage: " + event);
if (event.equals("Reader:Click")) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.toggleReaderMode();
}
} else if (event.equals("Reader:LongClick")) {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.addToReadingList();
}
}
}
@Override
public void onLightweightThemeChanged() {
Drawable drawable = theme.getDrawable(this);
if (drawable == null)
return;
StateListDrawable stateList = new StateListDrawable();
stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
stateList.addState(EMPTY_STATE_SET, drawable);
setBackgroundDrawable(stateList);
editCancel.onLightweightThemeChanged();
}
@Override
public void onLightweightThemeReset() {
setBackgroundResource(R.drawable.url_bar_bg);
editCancel.onLightweightThemeReset();
}
}