1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/gfx/LayerMarginsAnimator.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,324 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.gfx; 1.10 + 1.11 +import org.mozilla.gecko.GeckoAppShell; 1.12 +import org.mozilla.gecko.GeckoEvent; 1.13 +import org.mozilla.gecko.PrefsHelper; 1.14 +import org.mozilla.gecko.TouchEventInterceptor; 1.15 +import org.mozilla.gecko.util.FloatUtils; 1.16 +import org.mozilla.gecko.util.ThreadUtils; 1.17 + 1.18 +import android.graphics.PointF; 1.19 +import android.graphics.RectF; 1.20 +import android.os.SystemClock; 1.21 +import android.util.Log; 1.22 +import android.view.animation.DecelerateInterpolator; 1.23 +import android.view.MotionEvent; 1.24 +import android.view.View; 1.25 + 1.26 +public class LayerMarginsAnimator implements TouchEventInterceptor { 1.27 + private static final String LOGTAG = "GeckoLayerMarginsAnimator"; 1.28 + // The duration of the animation in ns 1.29 + private static final long MARGIN_ANIMATION_DURATION = 250000000; 1.30 + private static final String PREF_SHOW_MARGINS_THRESHOLD = "browser.ui.show-margins-threshold"; 1.31 + 1.32 + /* This is the proportion of the viewport rect, minus maximum margins, 1.33 + * that needs to be travelled before margins will be exposed. 1.34 + */ 1.35 + private float SHOW_MARGINS_THRESHOLD = 0.20f; 1.36 + 1.37 + /* This rect stores the maximum value margins can grow to when scrolling. When writing 1.38 + * to this member variable, or when reading from this member variable on a non-UI thread, 1.39 + * you must synchronize on the LayerMarginsAnimator instance. */ 1.40 + private final RectF mMaxMargins; 1.41 + /* If this boolean is true, scroll changes will not affect margins */ 1.42 + private boolean mMarginsPinned; 1.43 + /* The task that handles showing/hiding margins */ 1.44 + private LayerMarginsAnimationTask mAnimationTask; 1.45 + /* This interpolator is used for the above mentioned animation */ 1.46 + private final DecelerateInterpolator mInterpolator; 1.47 + /* The GeckoLayerClient whose margins will be animated */ 1.48 + private final GeckoLayerClient mTarget; 1.49 + /* The distance that has been scrolled since either the first touch event, 1.50 + * or since the margins were last fully hidden */ 1.51 + private final PointF mTouchTravelDistance; 1.52 + /* The ID of the prefs listener for the show-marginss threshold */ 1.53 + private Integer mPrefObserverId; 1.54 + 1.55 + public LayerMarginsAnimator(GeckoLayerClient aTarget, LayerView aView) { 1.56 + // Assign member variables from parameters 1.57 + mTarget = aTarget; 1.58 + 1.59 + // Create other member variables 1.60 + mMaxMargins = new RectF(); 1.61 + mInterpolator = new DecelerateInterpolator(); 1.62 + mTouchTravelDistance = new PointF(); 1.63 + 1.64 + // Listen to the dynamic toolbar pref 1.65 + mPrefObserverId = PrefsHelper.getPref(PREF_SHOW_MARGINS_THRESHOLD, new PrefsHelper.PrefHandlerBase() { 1.66 + @Override 1.67 + public void prefValue(String pref, int value) { 1.68 + SHOW_MARGINS_THRESHOLD = (float)value / 100.0f; 1.69 + } 1.70 + 1.71 + @Override 1.72 + public boolean isObserver() { 1.73 + return true; 1.74 + } 1.75 + }); 1.76 + 1.77 + // Listen to touch events, for auto-pinning 1.78 + aView.addTouchInterceptor(this); 1.79 + } 1.80 + 1.81 + public void destroy() { 1.82 + if (mPrefObserverId != null) { 1.83 + PrefsHelper.removeObserver(mPrefObserverId); 1.84 + mPrefObserverId = null; 1.85 + } 1.86 + } 1.87 + 1.88 + /** 1.89 + * Sets the maximum values for margins to grow to, in pixels. 1.90 + */ 1.91 + public synchronized void setMaxMargins(float left, float top, float right, float bottom) { 1.92 + ThreadUtils.assertOnUiThread(); 1.93 + 1.94 + mMaxMargins.set(left, top, right, bottom); 1.95 + 1.96 + // Update the Gecko-side global for fixed viewport margins. 1.97 + GeckoAppShell.sendEventToGecko( 1.98 + GeckoEvent.createBroadcastEvent("Viewport:FixedMarginsChanged", 1.99 + "{ \"top\" : " + top + ", \"right\" : " + right 1.100 + + ", \"bottom\" : " + bottom + ", \"left\" : " + left + " }")); 1.101 + } 1.102 + 1.103 + RectF getMaxMargins() { 1.104 + return mMaxMargins; 1.105 + } 1.106 + 1.107 + private void animateMargins(final float left, final float top, final float right, final float bottom, boolean immediately) { 1.108 + if (mAnimationTask != null) { 1.109 + mTarget.getView().removeRenderTask(mAnimationTask); 1.110 + mAnimationTask = null; 1.111 + } 1.112 + 1.113 + if (immediately) { 1.114 + ImmutableViewportMetrics newMetrics = mTarget.getViewportMetrics().setMargins(left, top, right, bottom); 1.115 + mTarget.forceViewportMetrics(newMetrics, true, true); 1.116 + return; 1.117 + } 1.118 + 1.119 + ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); 1.120 + 1.121 + mAnimationTask = new LayerMarginsAnimationTask(false, metrics, left, top, right, bottom); 1.122 + mTarget.getView().postRenderTask(mAnimationTask); 1.123 + } 1.124 + 1.125 + /** 1.126 + * Exposes the margin area by growing the margin components of the current 1.127 + * metrics to the values set in setMaxMargins. 1.128 + */ 1.129 + public synchronized void showMargins(boolean immediately) { 1.130 + animateMargins(mMaxMargins.left, mMaxMargins.top, mMaxMargins.right, mMaxMargins.bottom, immediately); 1.131 + } 1.132 + 1.133 + public synchronized void hideMargins(boolean immediately) { 1.134 + animateMargins(0, 0, 0, 0, immediately); 1.135 + } 1.136 + 1.137 + public void setMarginsPinned(boolean pin) { 1.138 + if (pin == mMarginsPinned) { 1.139 + return; 1.140 + } 1.141 + 1.142 + mMarginsPinned = pin; 1.143 + } 1.144 + 1.145 + public boolean areMarginsShown() { 1.146 + final ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); 1.147 + return metrics.marginLeft != 0 || 1.148 + metrics.marginRight != 0 || 1.149 + metrics.marginTop != 0 || 1.150 + metrics.marginBottom != 0; 1.151 + } 1.152 + 1.153 + /** 1.154 + * This function will scroll a margin down to zero, or up to the maximum 1.155 + * specified margin size and return the left-over delta. 1.156 + * aMargins are in/out parameters. In specifies the current margin size, 1.157 + * and out specifies the modified margin size. They are specified in the 1.158 + * order of start-margin, then end-margin. 1.159 + * This function will also take into account how far the touch point has 1.160 + * moved and react accordingly. If a touch point hasn't moved beyond a 1.161 + * certain threshold, margins can only be hidden and not shown. 1.162 + * aNegativeOffset can be used if the remaining delta should be determined 1.163 + * by the end-margin instead of the start-margin (for example, in rtl 1.164 + * pages). 1.165 + */ 1.166 + private float scrollMargin(float[] aMargins, float aDelta, 1.167 + float aOverscrollStart, float aOverscrollEnd, 1.168 + float aTouchTravelDistance, 1.169 + float aViewportStart, float aViewportEnd, 1.170 + float aPageStart, float aPageEnd, 1.171 + float aMaxMarginStart, float aMaxMarginEnd, 1.172 + boolean aNegativeOffset) { 1.173 + float marginStart = aMargins[0]; 1.174 + float marginEnd = aMargins[1]; 1.175 + float viewportSize = aViewportEnd - aViewportStart; 1.176 + float exposeThreshold = viewportSize * SHOW_MARGINS_THRESHOLD; 1.177 + 1.178 + if (aDelta >= 0) { 1.179 + float marginDelta = Math.max(0, aDelta - aOverscrollStart); 1.180 + aMargins[0] = marginStart - Math.min(marginDelta, marginStart); 1.181 + if (aTouchTravelDistance < exposeThreshold && marginEnd == 0) { 1.182 + // We only want the margin to be newly exposed after the touch 1.183 + // has moved a certain distance. 1.184 + marginDelta = Math.max(0, marginDelta - (aPageEnd - aViewportEnd)); 1.185 + } 1.186 + aMargins[1] = marginEnd + Math.min(marginDelta, aMaxMarginEnd - marginEnd); 1.187 + } else { 1.188 + float marginDelta = Math.max(0, -aDelta - aOverscrollEnd); 1.189 + aMargins[1] = marginEnd - Math.min(marginDelta, marginEnd); 1.190 + if (-aTouchTravelDistance < exposeThreshold && marginStart == 0) { 1.191 + marginDelta = Math.max(0, marginDelta - (aViewportStart - aPageStart)); 1.192 + } 1.193 + aMargins[0] = marginStart + Math.min(marginDelta, aMaxMarginStart - marginStart); 1.194 + } 1.195 + 1.196 + if (aNegativeOffset) { 1.197 + return aDelta - (marginEnd - aMargins[1]); 1.198 + } 1.199 + return aDelta - (marginStart - aMargins[0]); 1.200 + } 1.201 + 1.202 + /* 1.203 + * Taking maximum margins into account, offsets the margins and then the 1.204 + * viewport origin and returns the modified metrics. 1.205 + */ 1.206 + ImmutableViewportMetrics scrollBy(ImmutableViewportMetrics aMetrics, float aDx, float aDy) { 1.207 + float[] newMarginsX = { aMetrics.marginLeft, aMetrics.marginRight }; 1.208 + float[] newMarginsY = { aMetrics.marginTop, aMetrics.marginBottom }; 1.209 + 1.210 + // Only alter margins if the toolbar isn't pinned 1.211 + if (!mMarginsPinned) { 1.212 + // Make sure to cancel any margin animations when margin-scrolling begins 1.213 + if (mAnimationTask != null) { 1.214 + mTarget.getView().removeRenderTask(mAnimationTask); 1.215 + mAnimationTask = null; 1.216 + } 1.217 + 1.218 + // Reset the touch travel when changing direction 1.219 + if ((aDx >= 0) != (mTouchTravelDistance.x >= 0)) { 1.220 + mTouchTravelDistance.x = 0; 1.221 + } 1.222 + if ((aDy >= 0) != (mTouchTravelDistance.y >= 0)) { 1.223 + mTouchTravelDistance.y = 0; 1.224 + } 1.225 + 1.226 + mTouchTravelDistance.offset(aDx, aDy); 1.227 + RectF overscroll = aMetrics.getOverscroll(); 1.228 + 1.229 + // Only allow margins to scroll if the page can fill the viewport. 1.230 + if (aMetrics.getPageWidth() >= aMetrics.getWidth()) { 1.231 + aDx = scrollMargin(newMarginsX, aDx, 1.232 + overscroll.left, overscroll.right, 1.233 + mTouchTravelDistance.x, 1.234 + aMetrics.viewportRectLeft, aMetrics.viewportRectRight, 1.235 + aMetrics.pageRectLeft, aMetrics.pageRectRight, 1.236 + mMaxMargins.left, mMaxMargins.right, 1.237 + aMetrics.isRTL); 1.238 + } 1.239 + if (aMetrics.getPageHeight() >= aMetrics.getHeight()) { 1.240 + aDy = scrollMargin(newMarginsY, aDy, 1.241 + overscroll.top, overscroll.bottom, 1.242 + mTouchTravelDistance.y, 1.243 + aMetrics.viewportRectTop, aMetrics.viewportRectBottom, 1.244 + aMetrics.pageRectTop, aMetrics.pageRectBottom, 1.245 + mMaxMargins.top, mMaxMargins.bottom, 1.246 + false); 1.247 + } 1.248 + } 1.249 + 1.250 + return aMetrics.setMargins(newMarginsX[0], newMarginsY[0], newMarginsX[1], newMarginsY[1]).offsetViewportBy(aDx, aDy); 1.251 + } 1.252 + 1.253 + /** Implementation of TouchEventInterceptor */ 1.254 + @Override 1.255 + public boolean onTouch(View view, MotionEvent event) { 1.256 + return false; 1.257 + } 1.258 + 1.259 + /** Implementation of TouchEventInterceptor */ 1.260 + @Override 1.261 + public boolean onInterceptTouchEvent(View view, MotionEvent event) { 1.262 + int action = event.getActionMasked(); 1.263 + if (action == MotionEvent.ACTION_DOWN && event.getPointerCount() == 1) { 1.264 + mTouchTravelDistance.set(0.0f, 0.0f); 1.265 + } 1.266 + 1.267 + return false; 1.268 + } 1.269 + 1.270 + class LayerMarginsAnimationTask extends RenderTask { 1.271 + private float mStartLeft, mStartTop, mStartRight, mStartBottom; 1.272 + private float mTop, mBottom, mLeft, mRight; 1.273 + private boolean mContinueAnimation; 1.274 + 1.275 + public LayerMarginsAnimationTask(boolean runAfter, ImmutableViewportMetrics metrics, 1.276 + float left, float top, float right, float bottom) { 1.277 + super(runAfter); 1.278 + mContinueAnimation = true; 1.279 + this.mStartLeft = metrics.marginLeft; 1.280 + this.mStartTop = metrics.marginTop; 1.281 + this.mStartRight = metrics.marginRight; 1.282 + this.mStartBottom = metrics.marginBottom; 1.283 + this.mLeft = left; 1.284 + this.mRight = right; 1.285 + this.mTop = top; 1.286 + this.mBottom = bottom; 1.287 + } 1.288 + 1.289 + @Override 1.290 + public boolean internalRun(long timeDelta, long currentFrameStartTime) { 1.291 + if (!mContinueAnimation) { 1.292 + return false; 1.293 + } 1.294 + 1.295 + // Calculate the progress (between 0 and 1) 1.296 + float progress = mInterpolator.getInterpolation( 1.297 + Math.min(1.0f, (System.nanoTime() - getStartTime()) 1.298 + / (float)MARGIN_ANIMATION_DURATION)); 1.299 + 1.300 + // Calculate the new metrics accordingly 1.301 + synchronized (mTarget.getLock()) { 1.302 + ImmutableViewportMetrics oldMetrics = mTarget.getViewportMetrics(); 1.303 + ImmutableViewportMetrics newMetrics = oldMetrics.setMargins( 1.304 + FloatUtils.interpolate(mStartLeft, mLeft, progress), 1.305 + FloatUtils.interpolate(mStartTop, mTop, progress), 1.306 + FloatUtils.interpolate(mStartRight, mRight, progress), 1.307 + FloatUtils.interpolate(mStartBottom, mBottom, progress)); 1.308 + PointF oldOffset = oldMetrics.getMarginOffset(); 1.309 + PointF newOffset = newMetrics.getMarginOffset(); 1.310 + newMetrics = 1.311 + newMetrics.offsetViewportByAndClamp(newOffset.x - oldOffset.x, 1.312 + newOffset.y - oldOffset.y); 1.313 + 1.314 + if (progress >= 1.0f) { 1.315 + mContinueAnimation = false; 1.316 + 1.317 + // Force a redraw and update Gecko 1.318 + mTarget.forceViewportMetrics(newMetrics, true, true); 1.319 + } else { 1.320 + mTarget.forceViewportMetrics(newMetrics, false, false); 1.321 + } 1.322 + } 1.323 + return mContinueAnimation; 1.324 + } 1.325 + } 1.326 + 1.327 +}