Bug 716403 - Dynamically hide the location bar when scrolling. r=kats

Intercept touch events and use them to pan the location bar on and off the
screen, depending on its current location. Also dynamically show/hide the
location bar when loading pages.
This commit is contained in:
Wes Johnston 2013-03-07 10:17:32 +00:00
parent fb48e81401
commit 6fd755c540
11 changed files with 284 additions and 59 deletions

View File

@ -8,6 +8,7 @@ package org.mozilla.gecko;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.util.UiAsyncTask;
import org.mozilla.gecko.util.GeckoBackgroundThread;
@ -45,6 +46,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import java.io.InputStream;
@ -91,6 +93,12 @@ abstract public class BrowserApp extends GeckoApp
// We'll ask for feedback after the user launches the app this many times.
private static final int FEEDBACK_LAUNCH_COUNT = 15;
// Variables used for scrolling the toolbar on/off the page.
private static final int TOOLBAR_ONLOAD_HIDE_DELAY = 2000;
private float mLastTouchY = 0.0f;
private float mToolbarSubpixelAccumulation = 0.0f;
private boolean mToolbarLocked = true;
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
switch(msg) {
@ -101,10 +109,15 @@ abstract public class BrowserApp extends GeckoApp
// fall through
case SELECTED:
if (Tabs.getInstance().isSelectedTab(tab)) {
if ("about:home".equals(tab.getURL()))
if ("about:home".equals(tab.getURL())) {
showAboutHome();
else
// Show the toolbar immediately.
mBrowserToolbar.animateVisibility(true, 0);
mToolbarLocked = true;
} else {
hideAboutHome();
}
// Dismiss any SiteIdentity Popup
SiteIdentityPopup.getInstance().dismiss();
@ -123,9 +136,23 @@ abstract public class BrowserApp extends GeckoApp
});
}
break;
case LOAD_ERROR:
case START:
if (Tabs.getInstance().isSelectedTab(tab)) {
invalidateOptionsMenu();
// Show the toolbar.
mBrowserToolbar.animateVisibility(true, 0);
}
break;
case LOAD_ERROR:
case STOP:
if (Tabs.getInstance().isSelectedTab(tab)) {
if (!mAboutHomeShowing) {
// Hide the toolbar after a delay.
mBrowserToolbar.animateVisibility(false, TOOLBAR_ONLOAD_HIDE_DELAY);
}
}
// fall through
case MENU_UPDATED:
if (Tabs.getInstance().isSelectedTab(tab)) {
invalidateOptionsMenu();
@ -153,6 +180,93 @@ abstract public class BrowserApp extends GeckoApp
updateAboutHomeTopSites();
}
@Override
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
int action = event.getActionMasked();
int pointerCount = event.getPointerCount();
View toolbarView = mBrowserToolbar.getLayout();
if (action == MotionEvent.ACTION_DOWN ||
action == MotionEvent.ACTION_POINTER_DOWN) {
if (pointerCount == 1) {
mToolbarLocked = false;
mToolbarSubpixelAccumulation = 0.0f;
mLastTouchY = event.getY();
return super.onInterceptTouchEvent(view, event);
}
//
// Lock the toolbar until we're back down to one pointer.
mToolbarLocked = true;
// Animate the toolbar to the fully on/off position.
mBrowserToolbar.animateVisibility(
toolbarView.getScrollY() > toolbarView.getHeight() / 2 ?
false : true, 0);
}
// If more than one pointer has been tracked, let the event pass
// through and be handled by the PanZoomController for zooming.
if (pointerCount > 1) {
return super.onInterceptTouchEvent(view, event);
}
// If a pointer has been lifted so that there's only one pointer left,
// unlock the toolbar and track that remaining pointer.
if (pointerCount == 1 && action == MotionEvent.ACTION_POINTER_UP) {
mLastTouchY = event.getY(1 - event.getActionIndex());
mToolbarLocked = false;
return super.onInterceptTouchEvent(view, event);
}
// Don't bother doing anything with the events if we're loading -
// the toolbar will be permanently visible in this case.
float eventY = event.getY();
if (Tabs.getInstance().getSelectedTab().getState() != Tab.STATE_LOADING) {
int toolbarHeight = toolbarView.getHeight();
if (action == MotionEvent.ACTION_MOVE && !mToolbarLocked) {
// Cancel any ongoing animation before we start moving the toolbar.
mBrowserToolbar.cancelVisibilityAnimation();
// Move the toolbar by the amount the touch event has moved,
// clamping to fully visible or fully hidden.
float deltaY = mLastTouchY - eventY;
// Don't let the toolbar scroll off the top if it's just exposing
// overscroll area.
ImmutableViewportMetrics metrics =
mLayerView.getLayerClient().getViewportMetrics();
float toolbarMaxY = Math.min(toolbarHeight,
Math.max(0, toolbarHeight - (metrics.pageRectTop -
metrics.viewportRectTop)));
int toolbarY = toolbarView.getScrollY();
float newToolbarYf = Math.max(0, Math.min(toolbarMaxY,
toolbarY + deltaY + mToolbarSubpixelAccumulation));
int newToolbarY = Math.round(newToolbarYf);
mToolbarSubpixelAccumulation = (newToolbarYf - newToolbarY);
toolbarView.scrollTo(0, newToolbarY);
// Reset tracking when the toolbar is fully visible or hidden.
if (newToolbarY == 0 || newToolbarY == toolbarHeight) {
mLastTouchY = eventY;
}
} else if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_CANCEL) {
// Animate the toolbar to fully on or off, depending on how much
// of it is hidden.
mBrowserToolbar.animateVisibility(
toolbarView.getScrollY() > toolbarHeight / 2 ? false : true, 0);
}
}
// Update the last recorded y position.
mLastTouchY = eventY;
return super.onInterceptTouchEvent(view, event);
}
void handleReaderAdded(boolean success, final String title, final String url) {
if (!success) {
showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT);
@ -198,7 +312,7 @@ abstract public class BrowserApp extends GeckoApp
super.onCreate(savedInstanceState);
LinearLayout actionBar = (LinearLayout) getActionBarLayout();
mMainLayout.addView(actionBar, 0);
mMainLayout.addView(actionBar, 1);
((GeckoApp.MainLayout) mMainLayout).setOnInterceptTouchListener(new HideTabsTouchListener());
@ -350,6 +464,10 @@ abstract public class BrowserApp extends GeckoApp
mBrowserToolbar.from(actionBar);
mBrowserToolbar.refresh();
if (mAboutHomeContent != null) {
mAboutHomeContent.setPadding(0, mBrowserToolbar.getLayout().getHeight(), 0, 0);
}
// The favicon view is different now, so we need to update the DoorHangerPopup anchor view.
if (mDoorHangerPopup != null)
mDoorHangerPopup.setAnchor(mBrowserToolbar.mFavicon);
@ -419,7 +537,7 @@ abstract public class BrowserApp extends GeckoApp
mBrowserToolbar.adjustForTabsLayout(width);
((LinearLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(width, 0, 0, 0);
((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(width, 0, 0, 0);
mGeckoLayout.requestLayout();
}
@ -644,7 +762,7 @@ abstract public class BrowserApp extends GeckoApp
// Set the gecko layout for sliding.
if (!mTabsPanel.isShown()) {
((LinearLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(0, 0, 0, 0);
((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(0, 0, 0, 0);
mGeckoLayout.scrollTo(mTabsPanel.getWidth() * -1, 0);
mGeckoLayout.requestLayout();
}
@ -684,7 +802,7 @@ abstract public class BrowserApp extends GeckoApp
if (mTabsPanel.isShown()) {
if (hasTabsSideBar()) {
((LinearLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(mTabsPanel.getWidth(), 0, 0, 0);
((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).setMargins(mTabsPanel.getWidth(), 0, 0, 0);
mGeckoLayout.scrollTo(0, 0);
}
@ -802,6 +920,7 @@ abstract public class BrowserApp extends GeckoApp
mAboutHomeStartupTimer.stop();
}
});
mAboutHomeContent.setPadding(0, mBrowserToolbar.getLayout().getHeight(), 0, 0);
} else {
mAboutHomeContent.update(EnumSet.of(AboutHomeContent.UpdateFlags.TOP_SITES,
AboutHomeContent.UpdateFlags.REMOTE_TABS));

View File

@ -41,6 +41,7 @@ import android.widget.ViewSwitcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TimerTask;
public class BrowserToolbar implements ViewSwitcher.ViewFactory,
Tabs.OnTabsChangedListener,
@ -105,11 +106,23 @@ public class BrowserToolbar implements ViewSwitcher.ViewFactory,
private int mCount;
private int mFaviconSize;
private PropertyAnimator mVisibilityAnimator;
private TimerTask mDelayedVisibilityTask;
private enum ToolbarVisibility {
VISIBLE,
HIDDEN,
INCONSISTENT
};
private ToolbarVisibility mVisibility;
private static final int TABS_CONTRACTED = 1;
private static final int TABS_EXPANDED = 2;
private static final int FORWARD_ANIMATION_DURATION = 450;
private static final int VISIBILITY_ANIMATION_DURATION = 250;
public BrowserToolbar(BrowserApp activity) {
// BrowserToolbar is attached to BrowserApp only.
mActivity = activity;
@ -118,6 +131,8 @@ public class BrowserToolbar implements ViewSwitcher.ViewFactory,
sActionItems = new ArrayList<View>();
Tabs.registerOnTabsChangedListener(this);
mAnimateSiteSecurity = true;
mVisibility = ToolbarVisibility.INCONSISTENT;
}
public void from(LinearLayout layout) {
@ -470,6 +485,50 @@ public class BrowserToolbar implements ViewSwitcher.ViewFactory,
}
}
public void animateVisibility(boolean show, long delay) {
// Do nothing if there's a delayed animation pending that does the
// same thing and this request also has a delay.
if (mVisibility != ToolbarVisibility.INCONSISTENT &&
((delay > 0) == (mDelayedVisibilityTask != null)) &&
(show == isVisible())) {
return;
}
cancelVisibilityAnimation();
mVisibility = show ? ToolbarVisibility.VISIBLE : ToolbarVisibility.HIDDEN;
mVisibilityAnimator = new PropertyAnimator(VISIBILITY_ANIMATION_DURATION);
mVisibilityAnimator.attach(mLayout, PropertyAnimator.Property.SCROLL_Y,
show ? 0 : mLayout.getHeight());
if (delay > 0) {
mDelayedVisibilityTask = new TimerTask() {
public void run() {
mVisibilityAnimator.start();
mDelayedVisibilityTask = null;
}
};
mLayout.postDelayed(mDelayedVisibilityTask, delay);
} else {
mVisibilityAnimator.start();
}
}
public void cancelVisibilityAnimation() {
mVisibility = ToolbarVisibility.INCONSISTENT;
if (mDelayedVisibilityTask != null) {
mLayout.removeCallbacks(mDelayedVisibilityTask);
mDelayedVisibilityTask = null;
}
if (mVisibilityAnimator != null) {
mVisibilityAnimator.stop(false);
mVisibilityAnimator = null;
}
}
public boolean isVisible() {
return mVisibility == ToolbarVisibility.VISIBLE;
}
@Override
public void onAnimationStart(Animation animation) {
if (animation.equals(mLockFadeIn)) {

View File

@ -0,0 +1,34 @@
/* -*- 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;
import org.mozilla.gecko.gfx.LayerView;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
public class BrowserToolbarLayout extends LinearLayout {
private static final String LOGTAG = "GeckoToolbarLayout";
public BrowserToolbarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@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() - getScrollY()) {
return false;
}
return super.onTouchEvent(event);
}
}

View File

@ -82,7 +82,6 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.AbsoluteLayout;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.SimpleAdapter;
@ -110,7 +109,8 @@ abstract public class GeckoApp
extends GeckoActivity
implements GeckoEventListener, SensorEventListener, LocationListener,
Tabs.OnTabsChangedListener, GeckoEventResponder,
GeckoMenu.Callback, GeckoMenu.MenuPresenter
GeckoMenu.Callback, GeckoMenu.MenuPresenter,
OnInterceptTouchListener
{
private static final String LOGTAG = "GeckoApp";
@ -148,7 +148,7 @@ abstract public class GeckoApp
static public final int RESTORE_CRASH = 2;
StartupMode mStartupMode = null;
protected LinearLayout mMainLayout;
protected RelativeLayout mMainLayout;
protected RelativeLayout mGeckoLayout;
public View getView() { return mGeckoLayout; }
public SurfaceView cameraView;
@ -185,6 +185,8 @@ abstract public class GeckoApp
private String mPrivateBrowsingSession;
private PointF mInitialTouchPoint = null;
private static Boolean sIsLargeTablet = null;
private static Boolean sIsSmallTablet = null;
@ -1394,7 +1396,7 @@ abstract public class GeckoApp
// setup gecko layout
mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
mMainLayout = (LinearLayout) findViewById(R.id.main_layout);
mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
// setup tabs panel
mTabsPanel = (TabsPanel) findViewById(R.id.tabs_panel);
@ -2425,42 +2427,41 @@ abstract public class GeckoApp
protected void connectGeckoLayerClient() {
mLayerView.getLayerClient().notifyGeckoReady();
mLayerView.setTouchIntercepter(new OnInterceptTouchListener() {
private PointF initialPoint = null;
@Override
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
return false;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
if (event == null)
return true;
int action = event.getAction();
PointF point = new PointF(event.getX(), event.getY());
if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
initialPoint = point;
}
if (initialPoint != null && (action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_MOVE) {
if (PointUtils.subtract(point, initialPoint).length() < PanZoomController.PAN_THRESHOLD) {
// Don't send the touchmove event if if the users finger hasn't move far
// Necessary for Google Maps to work correctlly. See bug 771099.
return true;
} else {
initialPoint = null;
}
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event));
return true;
}
});
mLayerView.setTouchIntercepter(this);
}
public static class MainLayout extends LinearLayout {
@Override
public boolean onInterceptTouchEvent(View view, MotionEvent event) {
return false;
}
@Override
public boolean onTouch(View view, MotionEvent event) {
if (event == null)
return true;
int action = event.getActionMasked();
PointF point = new PointF(event.getX(), event.getY());
if (action == MotionEvent.ACTION_DOWN) {
mInitialTouchPoint = point;
}
if (mInitialTouchPoint != null && action == MotionEvent.ACTION_MOVE) {
if (PointUtils.subtract(point, mInitialTouchPoint).length() <
PanZoomController.PAN_THRESHOLD) {
// Don't send the touchmove event if if the users finger hasn't moved far.
// Necessary for Google Maps to work correctly. See bug 771099.
return true;
} else {
mInitialTouchPoint = null;
}
}
GeckoAppShell.sendEventToGecko(GeckoEvent.createMotionEvent(event));
return true;
}
public static class MainLayout extends RelativeLayout {
private OnInterceptTouchListener mOnInterceptTouchListener;
public MainLayout(Context context, AttributeSet attrs) {

View File

@ -59,6 +59,7 @@ FENNEC_JAVA_FILES = \
BrowserApp.java \
BrowserToolbar.java \
BrowserToolbarBackground.java \
BrowserToolbarLayout.java \
CameraImageResultHandler.java \
CameraVideoResultHandler.java \
CanvasDelegate.java \

View File

@ -141,12 +141,18 @@ public class PropertyAnimator implements Runnable {
}
}
public void stop() {
/**
* Stop the animation, optionally snapping to the end position.
* onPropertyAnimationEnd is only called when snapping to the end position.
*/
public void stop(boolean snapToEndPosition) {
mFramePoster.cancelAnimationFrame();
// Make sure to snap to the end position.
for (ElementHolder element : mElementsList) {
invalidate(element, element.to);
for (ElementHolder element : mElementsList) {
if (snapToEndPosition)
invalidate(element, element.to);
if (shouldEnableHardwareLayer(element))
element.view.setLayerType(View.LAYER_TYPE_NONE, null);
@ -157,11 +163,16 @@ public class PropertyAnimator implements Runnable {
mElementsList.clear();
if (mListener != null) {
mListener.onPropertyAnimationEnd();
if (snapToEndPosition)
mListener.onPropertyAnimationEnd();
mListener = null;
}
}
public void stop() {
stop(true);
}
private boolean shouldEnableHardwareLayer(ElementHolder element) {
if (!mUseHardwareLayer)
return false;

View File

@ -4,7 +4,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res/@ANDROID_PACKAGE_NAME@"
android:id="@+id/browser_toolbar"
style="@style/BrowserToolbar">
@ -165,4 +165,4 @@
</RelativeLayout>
</LinearLayout>
</org.mozilla.gecko.BrowserToolbarLayout>

View File

@ -4,7 +4,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res/@ANDROID_PACKAGE_NAME@"
android:id="@+id/browser_toolbar"
style="@style/BrowserToolbar">
@ -173,4 +173,4 @@
</RelativeLayout>
</LinearLayout>
</org.mozilla.gecko.BrowserToolbarLayout>

View File

@ -4,7 +4,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res/@ANDROID_PACKAGE_NAME@"
android:id="@+id/browser_toolbar"
style="@style/BrowserToolbar">
@ -179,4 +179,4 @@
</RelativeLayout>
</LinearLayout>
</org.mozilla.gecko.BrowserToolbarLayout>

View File

@ -4,7 +4,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res/@ANDROID_PACKAGE_NAME@"
android:id="@+id/browser_toolbar"
style="@style/BrowserToolbar">
@ -163,4 +163,4 @@
</RelativeLayout>
</LinearLayout>
</org.mozilla.gecko.BrowserToolbarLayout>

View File

@ -4,7 +4,7 @@
- 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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<org.mozilla.gecko.BrowserToolbarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res/@ANDROID_PACKAGE_NAME@"
android:id="@+id/browser_toolbar"
style="@style/BrowserToolbar">
@ -172,4 +172,4 @@
</RelativeLayout>
</LinearLayout>
</org.mozilla.gecko.BrowserToolbarLayout>