Temporal edge navigation feature
- User can swipe from left and right edge to go
back and forward in history of a tab
Change-Id: I941f91122510b004bdcb6a718eb6c05730694960
diff --git a/src/com/android/browser/EdgeSwipeController.java b/src/com/android/browser/EdgeSwipeController.java
new file mode 100644
index 0000000..87ad2e7
--- /dev/null
+++ b/src/com/android/browser/EdgeSwipeController.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2015, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package com.android.browser;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.os.CountDownTimer;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.View;
+
+import org.codeaurora.swe.WebHistoryItem;
+import org.codeaurora.swe.util.Activator;
+import org.codeaurora.swe.util.Observable;
+
+public class EdgeSwipeController extends ViewDragHelper.Callback {
+ private ViewDragHelper mDragHelper;
+ private int mState = ViewDragHelper.STATE_IDLE;
+ private int mFromEdge = ViewDragHelper.EDGE_LEFT;
+ private boolean mbNavigated = false;
+ private int mOldX = 0;
+ private int mOldDx = 0;
+ private Observable mPageLoadTarget;
+ private Observable mPageLoadObservable;
+
+ private boolean mbCurrBMSynced = false;
+
+ private Tab mActiveTab;
+ private TitleBar mTitleBar;
+
+ private static final float mMinAlpha = 0.5f;
+ private static final int mMinProgress = 85;
+ private static final int mProgressWaitMS = 1000;
+ private static final int EDGE_SWIPE_INVALID_INDEX = -2;
+
+ private CountDownTimer mLoadTimer, mCommitTimer;
+
+ private int mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+ private int mPrevIndex;
+ private int mNextIndex;
+ private int mMaxIndex;
+
+ private EdgeSwipeModel mModel;
+ private EdgeSwipeView mView;
+
+ public EdgeSwipeController(View container,
+ int stationaryViewId,
+ int slidingViewId,
+ int slidingViewShadowId,
+ int opacityViewId,
+ int liveViewId,
+ int viewGroupId,
+ BaseUi ui) {
+ DraggableFrameLayout viewGroup = (DraggableFrameLayout)
+ container.findViewById(viewGroupId);
+
+ mActiveTab = ui.getActiveTab();
+ mTitleBar = ui.getTitleBar();
+
+ mModel = new EdgeSwipeModel(mActiveTab, mTitleBar);
+ mView = new EdgeSwipeView(
+ container,
+ stationaryViewId,
+ slidingViewId,
+ slidingViewShadowId,
+ opacityViewId,
+ liveViewId,
+ viewGroupId,
+ mTitleBar);
+
+ mPageLoadTarget = mActiveTab.getTabHistoryUpdateObservable();
+ mPageLoadObservable = Activator.activate(
+ new Observable.Observer() {
+ @Override
+ public void onChange(Object... params) {
+ if (mDragHelper == null ||
+ mPageLoadTarget == null) {
+ return;
+ }
+
+ synchronized (this) {
+ int index = (int) params[0];
+ if (mState == ViewDragHelper.STATE_IDLE && index == mCurrIndex) {
+ monitorProgressAtHistoryUpdate(index);
+ }
+ }
+ }
+ },
+ mPageLoadTarget
+ );
+
+ mDragHelper = ViewDragHelper.create(viewGroup, 0.5f, this);
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT | ViewDragHelper.EDGE_RIGHT);
+ viewGroup.setDragHelper(mDragHelper);
+ }
+
+ private void swipeSessionCleanup() {
+ mView.goLive();
+ mModel.cleanup();
+ mCurrIndex = EDGE_SWIPE_INVALID_INDEX;
+ mState = ViewDragHelper.STATE_IDLE;
+ }
+
+ private boolean setState(int curState, int newState) {
+ if (mState == curState) {
+ mState = newState;
+ return true;
+ }
+ return false;
+ }
+
+ public void cleanup() {
+ if (mPageLoadObservable != null) {
+ mPageLoadObservable.onOff(false);
+ synchronized (this) {
+ mDragHelper.cancel();
+ swipeSessionCleanup();
+ }
+ }
+ }
+
+ public void onConfigurationChanged() {
+ synchronized (this) {
+ swipeSessionCleanup();
+ }
+ }
+
+ private void showCurrBMInStationaryView() {
+ if (!mbCurrBMSynced) {
+ Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+ if (currBM != null) {
+ mView.setStationaryViewBitmap(currBM);
+ mbCurrBMSynced = true;
+ }
+ }
+ }
+
+ private void showCurrBMInSlidingView() {
+ if (!mbCurrBMSynced) {
+ Bitmap currBM = mModel.readSnapshot(mCurrIndex);
+ mView.setSlidingViewBitmap(currBM);
+ if (currBM != null) {
+ mbCurrBMSynced = true;
+ }
+ }
+ }
+
+ private Bitmap getGrayscale(Bitmap bitmap)
+ {
+ if (bitmap == null)
+ return null;
+
+ int height = bitmap.getHeight();
+ int width = bitmap.getWidth();
+
+ Bitmap gray = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(gray);
+ Paint paint = new Paint();
+ ColorMatrix cm = new ColorMatrix();
+
+ cm.setSaturation(0);
+
+ ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
+
+ paint.setColorFilter(f);
+
+ c.drawBitmap(bitmap, 0, 0, paint);
+ return gray;
+ }
+
+ private void monitorProgressAtLoad(final int pageIndex) {
+ if (mLoadTimer != null) {
+ mLoadTimer.cancel();
+ }
+
+ mLoadTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+ boolean mGrayBM = false;
+
+ public void onTick(long msRemain) {
+ if (msRemain > mProgressWaitMS * 4) {
+ return;
+ }
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+
+ }
+ cancel();
+ } else {
+ if (mGrayBM) {
+ return;
+ }
+ mView.setStationaryViewBitmap(
+ getGrayscale(getSnapshotOrFavicon(pageIndex)));
+ mGrayBM = true;
+ }
+ }
+ }
+
+ public void onFinish() {
+ mGrayBM = false;
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+ }
+ cancel();
+ }
+ }
+ }
+ }.start();
+ }
+
+ private void monitorProgressAtHistoryUpdate(final int pageIndex) {
+ if (mCommitTimer != null) {
+ mCommitTimer.cancel();
+ }
+
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress
+ && mActiveTab.getWebView().getLastCommittedHistoryIndex() == pageIndex) {
+ swipeSessionCleanup();
+ return;
+ }
+
+ mCommitTimer = new CountDownTimer(mProgressWaitMS * 5, mProgressWaitMS) {
+ public void onTick(long msRemain) {
+ synchronized (this) {
+ if (mTitleBar.getProgressView().getProgressPercent() >= mMinProgress) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+
+ }
+ cancel();
+ }
+ }
+ }
+
+ public void onFinish() {
+ synchronized (this) {
+ if (mState == ViewDragHelper.STATE_IDLE && pageIndex == mCurrIndex) {
+ swipeSessionCleanup();
+ }
+ }
+ }
+ }.start();
+ }
+
+ private boolean isPortrait(Bitmap bitmap) {
+ return (bitmap.getHeight() < bitmap.getWidth());
+ }
+
+ private Bitmap getSnapshotOrFavicon(int index) {
+ Bitmap bm = mModel.readSnapshot(index);
+ if (bm == null || mView.isPortrait() != isPortrait(bm)) {
+ WebHistoryItem item = mActiveTab.getWebView()
+ .copyBackForwardList().getItemAtIndex(index);
+ if (item != null) {
+ bm = item.getFavicon();
+ }
+ }
+ return bm;
+ }
+
+ public void onViewDragStateChanged(int state) {
+ synchronized (this) {
+ if (mState != ViewDragHelper.STATE_SETTLING || state != ViewDragHelper.STATE_IDLE) {
+ return;
+ }
+
+ mView.hideSlidingViews();
+
+ if (mView.isLive()) {
+ return;
+ }
+
+ if (mbNavigated) {
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mCurrIndex));
+ mView.setStationaryViewAlpha(1.0f);
+ } else {
+ swipeSessionCleanup();
+ }
+
+ mView.invalidate();
+
+ setState(ViewDragHelper.STATE_SETTLING, ViewDragHelper.STATE_IDLE);
+ }
+ }
+
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ synchronized (this) {
+ if (!setState(ViewDragHelper.STATE_DRAGGING, ViewDragHelper.STATE_SETTLING)) {
+ mOldX = 0;
+ mOldDx = 0;
+ return;
+ }
+
+ mbNavigated = true;
+
+ boolean bCrossedEventHorizon = Math.abs(mOldX) > mView.getWidth() / 2;
+
+ if (mCurrIndex >= 0) {
+ if ((xvel > 0 || (xvel == 0 && mOldX > 0 && bCrossedEventHorizon))
+ && mFromEdge == ViewDragHelper.EDGE_LEFT
+ && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex - 1)) {
+ mCurrIndex -= 1;
+ mActiveTab.getWebView().stopLoading();
+ mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+ monitorProgressAtLoad(mCurrIndex);
+ mDragHelper.settleCapturedViewAt(
+ releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ } else if ((xvel < 0 || (xvel == 0 && mOldX < 0 && bCrossedEventHorizon))
+ && mFromEdge == ViewDragHelper.EDGE_RIGHT
+ && mActiveTab.getWebView().canGoToHistoryIndex(mCurrIndex + 1)) {
+ mCurrIndex += 1;
+ mActiveTab.getWebView().stopLoading();
+ mActiveTab.getWebView().goToHistoryIndex(mCurrIndex);
+ monitorProgressAtLoad(mCurrIndex);
+ mDragHelper.settleCapturedViewAt(
+ -releasedChild.getMeasuredWidth(),
+ releasedChild.getTop());
+ mView.goDormant();
+ } else {
+ mbNavigated = false;
+ mDragHelper.settleCapturedViewAt(0, releasedChild.getTop());
+ }
+ }
+ mOldX = 0;
+ mOldDx = 0;
+
+ mView.invalidate();
+ }
+ }
+
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ float alpha = ((float) Math.abs(left)) / mView.getMeasuredWidth();
+
+ synchronized (this) {
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ if (mView.isLive()) {
+ return;
+ }
+
+ if (mState != ViewDragHelper.STATE_IDLE) {
+ mView.moveShadowView(left);
+ }
+
+ showCurrBMInSlidingView();
+
+ if (mPrevIndex >= 0) {
+ if (!mView.stationaryViewHasImage()) {
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+ }
+
+ if (mActiveTab.getWebView().canGoToHistoryIndex(mPrevIndex)) {
+ mView.setStationaryViewAlpha(mMinAlpha + alpha * (1 - mMinAlpha));
+ }
+ }
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ if (mState != ViewDragHelper.STATE_IDLE) {
+ mView.moveShadowView(mView.getMeasuredWidth() + left);
+
+ if (!mView.slidingViewHasImage() && mNextIndex < mMaxIndex) {
+ mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+ }
+
+ showCurrBMInStationaryView();
+ if (mbCurrBMSynced) {
+ mView.goDormant();
+ }
+ }
+ if (mNextIndex < mMaxIndex &&
+ mActiveTab.getWebView().canGoToHistoryIndex(mNextIndex)) {
+ mView.setStationaryViewAlpha(mMinAlpha + (1 - alpha) * (1 - mMinAlpha));
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+ synchronized (this) {
+ if (mActiveTab.isPrivateBrowsingEnabled()) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE ||
+ !setState(ViewDragHelper.STATE_IDLE, ViewDragHelper.STATE_DRAGGING)) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if ((edgeFlags & mFromEdge) != mFromEdge || mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+ onEdgeTouched(edgeFlags, pointerId);
+ }
+
+ mbCurrBMSynced = false;
+
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ mView.showSlidingViews();
+ mView.goDormant();
+ mPrevIndex = mCurrIndex - 1;
+ mView.setStationaryViewBitmap(getSnapshotOrFavicon(mPrevIndex));
+ showCurrBMInSlidingView();
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ mView.showSlidingViews();
+ mNextIndex = mCurrIndex + 1;
+ mView.setSlidingViewBitmap(getSnapshotOrFavicon(mNextIndex));
+ showCurrBMInStationaryView();
+ if (mbCurrBMSynced)
+ mView.goDormant();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ public int getOrderedChildIndex(int index) {
+ return mView.slidingViewIndex();
+ }
+
+ public void onEdgeTouched (int edgeFlags, int pointerId) {
+ synchronized (this) {
+ if (mActiveTab.isPrivateBrowsingEnabled()) {
+ mDragHelper.abort();
+ return;
+ }
+
+ if (mState != ViewDragHelper.STATE_IDLE && mCurrIndex != EDGE_SWIPE_INVALID_INDEX) {
+ mDragHelper.abort();
+ return;
+ }
+
+ mView.init();
+
+ if (mCurrIndex == EDGE_SWIPE_INVALID_INDEX) {
+ mCurrIndex = mActiveTab.getWebView().getLastCommittedHistoryIndex();
+ }
+
+ mMaxIndex = mActiveTab.getWebView().copyBackForwardList().getSize() - 1;
+ mModel.updateSnapshot(mCurrIndex);
+
+ if (ViewDragHelper.EDGE_LEFT == (edgeFlags & ViewDragHelper.EDGE_LEFT)) {
+ mFromEdge = ViewDragHelper.EDGE_LEFT;
+ mView.slidingViewTouched(mFromEdge);
+ if (mCurrIndex > 0) {
+ mModel.fetchSnapshot(mCurrIndex - 1);
+ }
+ } else if (ViewDragHelper.EDGE_RIGHT == (edgeFlags & ViewDragHelper.EDGE_RIGHT)) {
+ mFromEdge = ViewDragHelper.EDGE_RIGHT;
+ mView.slidingViewTouched(mFromEdge);
+ if (mCurrIndex < mMaxIndex) {
+ mModel.fetchSnapshot(mCurrIndex + 1);
+ }
+ }
+ }
+ }
+
+ public int getViewHorizontalDragRange(View child) {
+ return child.getMeasuredWidth();
+ }
+
+ public boolean tryCaptureView(View child, int pointerId) {
+ return (mState == ViewDragHelper.STATE_DRAGGING && mView.allowCapture(child));
+ }
+
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ if (mOldX != 0 && Math.signum(dx) != Math.signum(mOldDx)) {
+ mOldDx = dx;
+ return mOldX;
+ }
+
+ switch (mFromEdge) {
+ case ViewDragHelper.EDGE_LEFT:
+ if (left < 0) {
+ mOldDx = dx;
+ return mOldX;
+ }
+ if (!mActiveTab.getWebView().canGoToHistoryIndex(mPrevIndex)) {
+ if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+ return child.getMeasuredWidth() / 3;
+ }
+ }
+ break;
+ case ViewDragHelper.EDGE_RIGHT:
+ if (left > 0) {
+ mOldDx = dx;
+ return mOldX;
+ }
+ if (!mActiveTab.getWebView().canGoToHistoryIndex(mNextIndex)) {
+ if (Math.abs(left) >= child.getMeasuredWidth() / 3) {
+ return -child.getMeasuredWidth() / 3;
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ mOldX = left;
+ mOldDx = dx;
+ return left;
+ }
+}
+