michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.gfx; michael@0: michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.PrefsHelper; michael@0: import org.mozilla.gecko.TouchEventInterceptor; michael@0: import org.mozilla.gecko.util.FloatUtils; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.graphics.PointF; michael@0: import android.graphics.RectF; michael@0: import android.os.SystemClock; michael@0: import android.util.Log; michael@0: import android.view.animation.DecelerateInterpolator; michael@0: import android.view.MotionEvent; michael@0: import android.view.View; michael@0: michael@0: public class LayerMarginsAnimator implements TouchEventInterceptor { michael@0: private static final String LOGTAG = "GeckoLayerMarginsAnimator"; michael@0: // The duration of the animation in ns michael@0: private static final long MARGIN_ANIMATION_DURATION = 250000000; michael@0: private static final String PREF_SHOW_MARGINS_THRESHOLD = "browser.ui.show-margins-threshold"; michael@0: michael@0: /* This is the proportion of the viewport rect, minus maximum margins, michael@0: * that needs to be travelled before margins will be exposed. michael@0: */ michael@0: private float SHOW_MARGINS_THRESHOLD = 0.20f; michael@0: michael@0: /* This rect stores the maximum value margins can grow to when scrolling. When writing michael@0: * to this member variable, or when reading from this member variable on a non-UI thread, michael@0: * you must synchronize on the LayerMarginsAnimator instance. */ michael@0: private final RectF mMaxMargins; michael@0: /* If this boolean is true, scroll changes will not affect margins */ michael@0: private boolean mMarginsPinned; michael@0: /* The task that handles showing/hiding margins */ michael@0: private LayerMarginsAnimationTask mAnimationTask; michael@0: /* This interpolator is used for the above mentioned animation */ michael@0: private final DecelerateInterpolator mInterpolator; michael@0: /* The GeckoLayerClient whose margins will be animated */ michael@0: private final GeckoLayerClient mTarget; michael@0: /* The distance that has been scrolled since either the first touch event, michael@0: * or since the margins were last fully hidden */ michael@0: private final PointF mTouchTravelDistance; michael@0: /* The ID of the prefs listener for the show-marginss threshold */ michael@0: private Integer mPrefObserverId; michael@0: michael@0: public LayerMarginsAnimator(GeckoLayerClient aTarget, LayerView aView) { michael@0: // Assign member variables from parameters michael@0: mTarget = aTarget; michael@0: michael@0: // Create other member variables michael@0: mMaxMargins = new RectF(); michael@0: mInterpolator = new DecelerateInterpolator(); michael@0: mTouchTravelDistance = new PointF(); michael@0: michael@0: // Listen to the dynamic toolbar pref michael@0: mPrefObserverId = PrefsHelper.getPref(PREF_SHOW_MARGINS_THRESHOLD, new PrefsHelper.PrefHandlerBase() { michael@0: @Override michael@0: public void prefValue(String pref, int value) { michael@0: SHOW_MARGINS_THRESHOLD = (float)value / 100.0f; michael@0: } michael@0: michael@0: @Override michael@0: public boolean isObserver() { michael@0: return true; michael@0: } michael@0: }); michael@0: michael@0: // Listen to touch events, for auto-pinning michael@0: aView.addTouchInterceptor(this); michael@0: } michael@0: michael@0: public void destroy() { michael@0: if (mPrefObserverId != null) { michael@0: PrefsHelper.removeObserver(mPrefObserverId); michael@0: mPrefObserverId = null; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Sets the maximum values for margins to grow to, in pixels. michael@0: */ michael@0: public synchronized void setMaxMargins(float left, float top, float right, float bottom) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: mMaxMargins.set(left, top, right, bottom); michael@0: michael@0: // Update the Gecko-side global for fixed viewport margins. michael@0: GeckoAppShell.sendEventToGecko( michael@0: GeckoEvent.createBroadcastEvent("Viewport:FixedMarginsChanged", michael@0: "{ \"top\" : " + top + ", \"right\" : " + right michael@0: + ", \"bottom\" : " + bottom + ", \"left\" : " + left + " }")); michael@0: } michael@0: michael@0: RectF getMaxMargins() { michael@0: return mMaxMargins; michael@0: } michael@0: michael@0: private void animateMargins(final float left, final float top, final float right, final float bottom, boolean immediately) { michael@0: if (mAnimationTask != null) { michael@0: mTarget.getView().removeRenderTask(mAnimationTask); michael@0: mAnimationTask = null; michael@0: } michael@0: michael@0: if (immediately) { michael@0: ImmutableViewportMetrics newMetrics = mTarget.getViewportMetrics().setMargins(left, top, right, bottom); michael@0: mTarget.forceViewportMetrics(newMetrics, true, true); michael@0: return; michael@0: } michael@0: michael@0: ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); michael@0: michael@0: mAnimationTask = new LayerMarginsAnimationTask(false, metrics, left, top, right, bottom); michael@0: mTarget.getView().postRenderTask(mAnimationTask); michael@0: } michael@0: michael@0: /** michael@0: * Exposes the margin area by growing the margin components of the current michael@0: * metrics to the values set in setMaxMargins. michael@0: */ michael@0: public synchronized void showMargins(boolean immediately) { michael@0: animateMargins(mMaxMargins.left, mMaxMargins.top, mMaxMargins.right, mMaxMargins.bottom, immediately); michael@0: } michael@0: michael@0: public synchronized void hideMargins(boolean immediately) { michael@0: animateMargins(0, 0, 0, 0, immediately); michael@0: } michael@0: michael@0: public void setMarginsPinned(boolean pin) { michael@0: if (pin == mMarginsPinned) { michael@0: return; michael@0: } michael@0: michael@0: mMarginsPinned = pin; michael@0: } michael@0: michael@0: public boolean areMarginsShown() { michael@0: final ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); michael@0: return metrics.marginLeft != 0 || michael@0: metrics.marginRight != 0 || michael@0: metrics.marginTop != 0 || michael@0: metrics.marginBottom != 0; michael@0: } michael@0: michael@0: /** michael@0: * This function will scroll a margin down to zero, or up to the maximum michael@0: * specified margin size and return the left-over delta. michael@0: * aMargins are in/out parameters. In specifies the current margin size, michael@0: * and out specifies the modified margin size. They are specified in the michael@0: * order of start-margin, then end-margin. michael@0: * This function will also take into account how far the touch point has michael@0: * moved and react accordingly. If a touch point hasn't moved beyond a michael@0: * certain threshold, margins can only be hidden and not shown. michael@0: * aNegativeOffset can be used if the remaining delta should be determined michael@0: * by the end-margin instead of the start-margin (for example, in rtl michael@0: * pages). michael@0: */ michael@0: private float scrollMargin(float[] aMargins, float aDelta, michael@0: float aOverscrollStart, float aOverscrollEnd, michael@0: float aTouchTravelDistance, michael@0: float aViewportStart, float aViewportEnd, michael@0: float aPageStart, float aPageEnd, michael@0: float aMaxMarginStart, float aMaxMarginEnd, michael@0: boolean aNegativeOffset) { michael@0: float marginStart = aMargins[0]; michael@0: float marginEnd = aMargins[1]; michael@0: float viewportSize = aViewportEnd - aViewportStart; michael@0: float exposeThreshold = viewportSize * SHOW_MARGINS_THRESHOLD; michael@0: michael@0: if (aDelta >= 0) { michael@0: float marginDelta = Math.max(0, aDelta - aOverscrollStart); michael@0: aMargins[0] = marginStart - Math.min(marginDelta, marginStart); michael@0: if (aTouchTravelDistance < exposeThreshold && marginEnd == 0) { michael@0: // We only want the margin to be newly exposed after the touch michael@0: // has moved a certain distance. michael@0: marginDelta = Math.max(0, marginDelta - (aPageEnd - aViewportEnd)); michael@0: } michael@0: aMargins[1] = marginEnd + Math.min(marginDelta, aMaxMarginEnd - marginEnd); michael@0: } else { michael@0: float marginDelta = Math.max(0, -aDelta - aOverscrollEnd); michael@0: aMargins[1] = marginEnd - Math.min(marginDelta, marginEnd); michael@0: if (-aTouchTravelDistance < exposeThreshold && marginStart == 0) { michael@0: marginDelta = Math.max(0, marginDelta - (aViewportStart - aPageStart)); michael@0: } michael@0: aMargins[0] = marginStart + Math.min(marginDelta, aMaxMarginStart - marginStart); michael@0: } michael@0: michael@0: if (aNegativeOffset) { michael@0: return aDelta - (marginEnd - aMargins[1]); michael@0: } michael@0: return aDelta - (marginStart - aMargins[0]); michael@0: } michael@0: michael@0: /* michael@0: * Taking maximum margins into account, offsets the margins and then the michael@0: * viewport origin and returns the modified metrics. michael@0: */ michael@0: ImmutableViewportMetrics scrollBy(ImmutableViewportMetrics aMetrics, float aDx, float aDy) { michael@0: float[] newMarginsX = { aMetrics.marginLeft, aMetrics.marginRight }; michael@0: float[] newMarginsY = { aMetrics.marginTop, aMetrics.marginBottom }; michael@0: michael@0: // Only alter margins if the toolbar isn't pinned michael@0: if (!mMarginsPinned) { michael@0: // Make sure to cancel any margin animations when margin-scrolling begins michael@0: if (mAnimationTask != null) { michael@0: mTarget.getView().removeRenderTask(mAnimationTask); michael@0: mAnimationTask = null; michael@0: } michael@0: michael@0: // Reset the touch travel when changing direction michael@0: if ((aDx >= 0) != (mTouchTravelDistance.x >= 0)) { michael@0: mTouchTravelDistance.x = 0; michael@0: } michael@0: if ((aDy >= 0) != (mTouchTravelDistance.y >= 0)) { michael@0: mTouchTravelDistance.y = 0; michael@0: } michael@0: michael@0: mTouchTravelDistance.offset(aDx, aDy); michael@0: RectF overscroll = aMetrics.getOverscroll(); michael@0: michael@0: // Only allow margins to scroll if the page can fill the viewport. michael@0: if (aMetrics.getPageWidth() >= aMetrics.getWidth()) { michael@0: aDx = scrollMargin(newMarginsX, aDx, michael@0: overscroll.left, overscroll.right, michael@0: mTouchTravelDistance.x, michael@0: aMetrics.viewportRectLeft, aMetrics.viewportRectRight, michael@0: aMetrics.pageRectLeft, aMetrics.pageRectRight, michael@0: mMaxMargins.left, mMaxMargins.right, michael@0: aMetrics.isRTL); michael@0: } michael@0: if (aMetrics.getPageHeight() >= aMetrics.getHeight()) { michael@0: aDy = scrollMargin(newMarginsY, aDy, michael@0: overscroll.top, overscroll.bottom, michael@0: mTouchTravelDistance.y, michael@0: aMetrics.viewportRectTop, aMetrics.viewportRectBottom, michael@0: aMetrics.pageRectTop, aMetrics.pageRectBottom, michael@0: mMaxMargins.top, mMaxMargins.bottom, michael@0: false); michael@0: } michael@0: } michael@0: michael@0: return aMetrics.setMargins(newMarginsX[0], newMarginsY[0], newMarginsX[1], newMarginsY[1]).offsetViewportBy(aDx, aDy); michael@0: } michael@0: michael@0: /** Implementation of TouchEventInterceptor */ michael@0: @Override michael@0: public boolean onTouch(View view, MotionEvent event) { michael@0: return false; michael@0: } michael@0: michael@0: /** Implementation of TouchEventInterceptor */ michael@0: @Override michael@0: public boolean onInterceptTouchEvent(View view, MotionEvent event) { michael@0: int action = event.getActionMasked(); michael@0: if (action == MotionEvent.ACTION_DOWN && event.getPointerCount() == 1) { michael@0: mTouchTravelDistance.set(0.0f, 0.0f); michael@0: } michael@0: michael@0: return false; michael@0: } michael@0: michael@0: class LayerMarginsAnimationTask extends RenderTask { michael@0: private float mStartLeft, mStartTop, mStartRight, mStartBottom; michael@0: private float mTop, mBottom, mLeft, mRight; michael@0: private boolean mContinueAnimation; michael@0: michael@0: public LayerMarginsAnimationTask(boolean runAfter, ImmutableViewportMetrics metrics, michael@0: float left, float top, float right, float bottom) { michael@0: super(runAfter); michael@0: mContinueAnimation = true; michael@0: this.mStartLeft = metrics.marginLeft; michael@0: this.mStartTop = metrics.marginTop; michael@0: this.mStartRight = metrics.marginRight; michael@0: this.mStartBottom = metrics.marginBottom; michael@0: this.mLeft = left; michael@0: this.mRight = right; michael@0: this.mTop = top; michael@0: this.mBottom = bottom; michael@0: } michael@0: michael@0: @Override michael@0: public boolean internalRun(long timeDelta, long currentFrameStartTime) { michael@0: if (!mContinueAnimation) { michael@0: return false; michael@0: } michael@0: michael@0: // Calculate the progress (between 0 and 1) michael@0: float progress = mInterpolator.getInterpolation( michael@0: Math.min(1.0f, (System.nanoTime() - getStartTime()) michael@0: / (float)MARGIN_ANIMATION_DURATION)); michael@0: michael@0: // Calculate the new metrics accordingly michael@0: synchronized (mTarget.getLock()) { michael@0: ImmutableViewportMetrics oldMetrics = mTarget.getViewportMetrics(); michael@0: ImmutableViewportMetrics newMetrics = oldMetrics.setMargins( michael@0: FloatUtils.interpolate(mStartLeft, mLeft, progress), michael@0: FloatUtils.interpolate(mStartTop, mTop, progress), michael@0: FloatUtils.interpolate(mStartRight, mRight, progress), michael@0: FloatUtils.interpolate(mStartBottom, mBottom, progress)); michael@0: PointF oldOffset = oldMetrics.getMarginOffset(); michael@0: PointF newOffset = newMetrics.getMarginOffset(); michael@0: newMetrics = michael@0: newMetrics.offsetViewportByAndClamp(newOffset.x - oldOffset.x, michael@0: newOffset.y - oldOffset.y); michael@0: michael@0: if (progress >= 1.0f) { michael@0: mContinueAnimation = false; michael@0: michael@0: // Force a redraw and update Gecko michael@0: mTarget.forceViewportMetrics(newMetrics, true, true); michael@0: } else { michael@0: mTarget.forceViewportMetrics(newMetrics, false, false); michael@0: } michael@0: } michael@0: return mContinueAnimation; michael@0: } michael@0: } michael@0: michael@0: }