Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
michael@0 | 2 | * This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 3 | * License, v. 2.0. If a copy of the MPL was not distributed with this |
michael@0 | 4 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 5 | |
michael@0 | 6 | package org.mozilla.gecko.gfx; |
michael@0 | 7 | |
michael@0 | 8 | import org.mozilla.gecko.GeckoAppShell; |
michael@0 | 9 | import org.mozilla.gecko.GeckoEvent; |
michael@0 | 10 | import org.mozilla.gecko.PrefsHelper; |
michael@0 | 11 | import org.mozilla.gecko.TouchEventInterceptor; |
michael@0 | 12 | import org.mozilla.gecko.util.FloatUtils; |
michael@0 | 13 | import org.mozilla.gecko.util.ThreadUtils; |
michael@0 | 14 | |
michael@0 | 15 | import android.graphics.PointF; |
michael@0 | 16 | import android.graphics.RectF; |
michael@0 | 17 | import android.os.SystemClock; |
michael@0 | 18 | import android.util.Log; |
michael@0 | 19 | import android.view.animation.DecelerateInterpolator; |
michael@0 | 20 | import android.view.MotionEvent; |
michael@0 | 21 | import android.view.View; |
michael@0 | 22 | |
michael@0 | 23 | public class LayerMarginsAnimator implements TouchEventInterceptor { |
michael@0 | 24 | private static final String LOGTAG = "GeckoLayerMarginsAnimator"; |
michael@0 | 25 | // The duration of the animation in ns |
michael@0 | 26 | private static final long MARGIN_ANIMATION_DURATION = 250000000; |
michael@0 | 27 | private static final String PREF_SHOW_MARGINS_THRESHOLD = "browser.ui.show-margins-threshold"; |
michael@0 | 28 | |
michael@0 | 29 | /* This is the proportion of the viewport rect, minus maximum margins, |
michael@0 | 30 | * that needs to be travelled before margins will be exposed. |
michael@0 | 31 | */ |
michael@0 | 32 | private float SHOW_MARGINS_THRESHOLD = 0.20f; |
michael@0 | 33 | |
michael@0 | 34 | /* This rect stores the maximum value margins can grow to when scrolling. When writing |
michael@0 | 35 | * to this member variable, or when reading from this member variable on a non-UI thread, |
michael@0 | 36 | * you must synchronize on the LayerMarginsAnimator instance. */ |
michael@0 | 37 | private final RectF mMaxMargins; |
michael@0 | 38 | /* If this boolean is true, scroll changes will not affect margins */ |
michael@0 | 39 | private boolean mMarginsPinned; |
michael@0 | 40 | /* The task that handles showing/hiding margins */ |
michael@0 | 41 | private LayerMarginsAnimationTask mAnimationTask; |
michael@0 | 42 | /* This interpolator is used for the above mentioned animation */ |
michael@0 | 43 | private final DecelerateInterpolator mInterpolator; |
michael@0 | 44 | /* The GeckoLayerClient whose margins will be animated */ |
michael@0 | 45 | private final GeckoLayerClient mTarget; |
michael@0 | 46 | /* The distance that has been scrolled since either the first touch event, |
michael@0 | 47 | * or since the margins were last fully hidden */ |
michael@0 | 48 | private final PointF mTouchTravelDistance; |
michael@0 | 49 | /* The ID of the prefs listener for the show-marginss threshold */ |
michael@0 | 50 | private Integer mPrefObserverId; |
michael@0 | 51 | |
michael@0 | 52 | public LayerMarginsAnimator(GeckoLayerClient aTarget, LayerView aView) { |
michael@0 | 53 | // Assign member variables from parameters |
michael@0 | 54 | mTarget = aTarget; |
michael@0 | 55 | |
michael@0 | 56 | // Create other member variables |
michael@0 | 57 | mMaxMargins = new RectF(); |
michael@0 | 58 | mInterpolator = new DecelerateInterpolator(); |
michael@0 | 59 | mTouchTravelDistance = new PointF(); |
michael@0 | 60 | |
michael@0 | 61 | // Listen to the dynamic toolbar pref |
michael@0 | 62 | mPrefObserverId = PrefsHelper.getPref(PREF_SHOW_MARGINS_THRESHOLD, new PrefsHelper.PrefHandlerBase() { |
michael@0 | 63 | @Override |
michael@0 | 64 | public void prefValue(String pref, int value) { |
michael@0 | 65 | SHOW_MARGINS_THRESHOLD = (float)value / 100.0f; |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | @Override |
michael@0 | 69 | public boolean isObserver() { |
michael@0 | 70 | return true; |
michael@0 | 71 | } |
michael@0 | 72 | }); |
michael@0 | 73 | |
michael@0 | 74 | // Listen to touch events, for auto-pinning |
michael@0 | 75 | aView.addTouchInterceptor(this); |
michael@0 | 76 | } |
michael@0 | 77 | |
michael@0 | 78 | public void destroy() { |
michael@0 | 79 | if (mPrefObserverId != null) { |
michael@0 | 80 | PrefsHelper.removeObserver(mPrefObserverId); |
michael@0 | 81 | mPrefObserverId = null; |
michael@0 | 82 | } |
michael@0 | 83 | } |
michael@0 | 84 | |
michael@0 | 85 | /** |
michael@0 | 86 | * Sets the maximum values for margins to grow to, in pixels. |
michael@0 | 87 | */ |
michael@0 | 88 | public synchronized void setMaxMargins(float left, float top, float right, float bottom) { |
michael@0 | 89 | ThreadUtils.assertOnUiThread(); |
michael@0 | 90 | |
michael@0 | 91 | mMaxMargins.set(left, top, right, bottom); |
michael@0 | 92 | |
michael@0 | 93 | // Update the Gecko-side global for fixed viewport margins. |
michael@0 | 94 | GeckoAppShell.sendEventToGecko( |
michael@0 | 95 | GeckoEvent.createBroadcastEvent("Viewport:FixedMarginsChanged", |
michael@0 | 96 | "{ \"top\" : " + top + ", \"right\" : " + right |
michael@0 | 97 | + ", \"bottom\" : " + bottom + ", \"left\" : " + left + " }")); |
michael@0 | 98 | } |
michael@0 | 99 | |
michael@0 | 100 | RectF getMaxMargins() { |
michael@0 | 101 | return mMaxMargins; |
michael@0 | 102 | } |
michael@0 | 103 | |
michael@0 | 104 | private void animateMargins(final float left, final float top, final float right, final float bottom, boolean immediately) { |
michael@0 | 105 | if (mAnimationTask != null) { |
michael@0 | 106 | mTarget.getView().removeRenderTask(mAnimationTask); |
michael@0 | 107 | mAnimationTask = null; |
michael@0 | 108 | } |
michael@0 | 109 | |
michael@0 | 110 | if (immediately) { |
michael@0 | 111 | ImmutableViewportMetrics newMetrics = mTarget.getViewportMetrics().setMargins(left, top, right, bottom); |
michael@0 | 112 | mTarget.forceViewportMetrics(newMetrics, true, true); |
michael@0 | 113 | return; |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); |
michael@0 | 117 | |
michael@0 | 118 | mAnimationTask = new LayerMarginsAnimationTask(false, metrics, left, top, right, bottom); |
michael@0 | 119 | mTarget.getView().postRenderTask(mAnimationTask); |
michael@0 | 120 | } |
michael@0 | 121 | |
michael@0 | 122 | /** |
michael@0 | 123 | * Exposes the margin area by growing the margin components of the current |
michael@0 | 124 | * metrics to the values set in setMaxMargins. |
michael@0 | 125 | */ |
michael@0 | 126 | public synchronized void showMargins(boolean immediately) { |
michael@0 | 127 | animateMargins(mMaxMargins.left, mMaxMargins.top, mMaxMargins.right, mMaxMargins.bottom, immediately); |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | public synchronized void hideMargins(boolean immediately) { |
michael@0 | 131 | animateMargins(0, 0, 0, 0, immediately); |
michael@0 | 132 | } |
michael@0 | 133 | |
michael@0 | 134 | public void setMarginsPinned(boolean pin) { |
michael@0 | 135 | if (pin == mMarginsPinned) { |
michael@0 | 136 | return; |
michael@0 | 137 | } |
michael@0 | 138 | |
michael@0 | 139 | mMarginsPinned = pin; |
michael@0 | 140 | } |
michael@0 | 141 | |
michael@0 | 142 | public boolean areMarginsShown() { |
michael@0 | 143 | final ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); |
michael@0 | 144 | return metrics.marginLeft != 0 || |
michael@0 | 145 | metrics.marginRight != 0 || |
michael@0 | 146 | metrics.marginTop != 0 || |
michael@0 | 147 | metrics.marginBottom != 0; |
michael@0 | 148 | } |
michael@0 | 149 | |
michael@0 | 150 | /** |
michael@0 | 151 | * This function will scroll a margin down to zero, or up to the maximum |
michael@0 | 152 | * specified margin size and return the left-over delta. |
michael@0 | 153 | * aMargins are in/out parameters. In specifies the current margin size, |
michael@0 | 154 | * and out specifies the modified margin size. They are specified in the |
michael@0 | 155 | * order of start-margin, then end-margin. |
michael@0 | 156 | * This function will also take into account how far the touch point has |
michael@0 | 157 | * moved and react accordingly. If a touch point hasn't moved beyond a |
michael@0 | 158 | * certain threshold, margins can only be hidden and not shown. |
michael@0 | 159 | * aNegativeOffset can be used if the remaining delta should be determined |
michael@0 | 160 | * by the end-margin instead of the start-margin (for example, in rtl |
michael@0 | 161 | * pages). |
michael@0 | 162 | */ |
michael@0 | 163 | private float scrollMargin(float[] aMargins, float aDelta, |
michael@0 | 164 | float aOverscrollStart, float aOverscrollEnd, |
michael@0 | 165 | float aTouchTravelDistance, |
michael@0 | 166 | float aViewportStart, float aViewportEnd, |
michael@0 | 167 | float aPageStart, float aPageEnd, |
michael@0 | 168 | float aMaxMarginStart, float aMaxMarginEnd, |
michael@0 | 169 | boolean aNegativeOffset) { |
michael@0 | 170 | float marginStart = aMargins[0]; |
michael@0 | 171 | float marginEnd = aMargins[1]; |
michael@0 | 172 | float viewportSize = aViewportEnd - aViewportStart; |
michael@0 | 173 | float exposeThreshold = viewportSize * SHOW_MARGINS_THRESHOLD; |
michael@0 | 174 | |
michael@0 | 175 | if (aDelta >= 0) { |
michael@0 | 176 | float marginDelta = Math.max(0, aDelta - aOverscrollStart); |
michael@0 | 177 | aMargins[0] = marginStart - Math.min(marginDelta, marginStart); |
michael@0 | 178 | if (aTouchTravelDistance < exposeThreshold && marginEnd == 0) { |
michael@0 | 179 | // We only want the margin to be newly exposed after the touch |
michael@0 | 180 | // has moved a certain distance. |
michael@0 | 181 | marginDelta = Math.max(0, marginDelta - (aPageEnd - aViewportEnd)); |
michael@0 | 182 | } |
michael@0 | 183 | aMargins[1] = marginEnd + Math.min(marginDelta, aMaxMarginEnd - marginEnd); |
michael@0 | 184 | } else { |
michael@0 | 185 | float marginDelta = Math.max(0, -aDelta - aOverscrollEnd); |
michael@0 | 186 | aMargins[1] = marginEnd - Math.min(marginDelta, marginEnd); |
michael@0 | 187 | if (-aTouchTravelDistance < exposeThreshold && marginStart == 0) { |
michael@0 | 188 | marginDelta = Math.max(0, marginDelta - (aViewportStart - aPageStart)); |
michael@0 | 189 | } |
michael@0 | 190 | aMargins[0] = marginStart + Math.min(marginDelta, aMaxMarginStart - marginStart); |
michael@0 | 191 | } |
michael@0 | 192 | |
michael@0 | 193 | if (aNegativeOffset) { |
michael@0 | 194 | return aDelta - (marginEnd - aMargins[1]); |
michael@0 | 195 | } |
michael@0 | 196 | return aDelta - (marginStart - aMargins[0]); |
michael@0 | 197 | } |
michael@0 | 198 | |
michael@0 | 199 | /* |
michael@0 | 200 | * Taking maximum margins into account, offsets the margins and then the |
michael@0 | 201 | * viewport origin and returns the modified metrics. |
michael@0 | 202 | */ |
michael@0 | 203 | ImmutableViewportMetrics scrollBy(ImmutableViewportMetrics aMetrics, float aDx, float aDy) { |
michael@0 | 204 | float[] newMarginsX = { aMetrics.marginLeft, aMetrics.marginRight }; |
michael@0 | 205 | float[] newMarginsY = { aMetrics.marginTop, aMetrics.marginBottom }; |
michael@0 | 206 | |
michael@0 | 207 | // Only alter margins if the toolbar isn't pinned |
michael@0 | 208 | if (!mMarginsPinned) { |
michael@0 | 209 | // Make sure to cancel any margin animations when margin-scrolling begins |
michael@0 | 210 | if (mAnimationTask != null) { |
michael@0 | 211 | mTarget.getView().removeRenderTask(mAnimationTask); |
michael@0 | 212 | mAnimationTask = null; |
michael@0 | 213 | } |
michael@0 | 214 | |
michael@0 | 215 | // Reset the touch travel when changing direction |
michael@0 | 216 | if ((aDx >= 0) != (mTouchTravelDistance.x >= 0)) { |
michael@0 | 217 | mTouchTravelDistance.x = 0; |
michael@0 | 218 | } |
michael@0 | 219 | if ((aDy >= 0) != (mTouchTravelDistance.y >= 0)) { |
michael@0 | 220 | mTouchTravelDistance.y = 0; |
michael@0 | 221 | } |
michael@0 | 222 | |
michael@0 | 223 | mTouchTravelDistance.offset(aDx, aDy); |
michael@0 | 224 | RectF overscroll = aMetrics.getOverscroll(); |
michael@0 | 225 | |
michael@0 | 226 | // Only allow margins to scroll if the page can fill the viewport. |
michael@0 | 227 | if (aMetrics.getPageWidth() >= aMetrics.getWidth()) { |
michael@0 | 228 | aDx = scrollMargin(newMarginsX, aDx, |
michael@0 | 229 | overscroll.left, overscroll.right, |
michael@0 | 230 | mTouchTravelDistance.x, |
michael@0 | 231 | aMetrics.viewportRectLeft, aMetrics.viewportRectRight, |
michael@0 | 232 | aMetrics.pageRectLeft, aMetrics.pageRectRight, |
michael@0 | 233 | mMaxMargins.left, mMaxMargins.right, |
michael@0 | 234 | aMetrics.isRTL); |
michael@0 | 235 | } |
michael@0 | 236 | if (aMetrics.getPageHeight() >= aMetrics.getHeight()) { |
michael@0 | 237 | aDy = scrollMargin(newMarginsY, aDy, |
michael@0 | 238 | overscroll.top, overscroll.bottom, |
michael@0 | 239 | mTouchTravelDistance.y, |
michael@0 | 240 | aMetrics.viewportRectTop, aMetrics.viewportRectBottom, |
michael@0 | 241 | aMetrics.pageRectTop, aMetrics.pageRectBottom, |
michael@0 | 242 | mMaxMargins.top, mMaxMargins.bottom, |
michael@0 | 243 | false); |
michael@0 | 244 | } |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | return aMetrics.setMargins(newMarginsX[0], newMarginsY[0], newMarginsX[1], newMarginsY[1]).offsetViewportBy(aDx, aDy); |
michael@0 | 248 | } |
michael@0 | 249 | |
michael@0 | 250 | /** Implementation of TouchEventInterceptor */ |
michael@0 | 251 | @Override |
michael@0 | 252 | public boolean onTouch(View view, MotionEvent event) { |
michael@0 | 253 | return false; |
michael@0 | 254 | } |
michael@0 | 255 | |
michael@0 | 256 | /** Implementation of TouchEventInterceptor */ |
michael@0 | 257 | @Override |
michael@0 | 258 | public boolean onInterceptTouchEvent(View view, MotionEvent event) { |
michael@0 | 259 | int action = event.getActionMasked(); |
michael@0 | 260 | if (action == MotionEvent.ACTION_DOWN && event.getPointerCount() == 1) { |
michael@0 | 261 | mTouchTravelDistance.set(0.0f, 0.0f); |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | return false; |
michael@0 | 265 | } |
michael@0 | 266 | |
michael@0 | 267 | class LayerMarginsAnimationTask extends RenderTask { |
michael@0 | 268 | private float mStartLeft, mStartTop, mStartRight, mStartBottom; |
michael@0 | 269 | private float mTop, mBottom, mLeft, mRight; |
michael@0 | 270 | private boolean mContinueAnimation; |
michael@0 | 271 | |
michael@0 | 272 | public LayerMarginsAnimationTask(boolean runAfter, ImmutableViewportMetrics metrics, |
michael@0 | 273 | float left, float top, float right, float bottom) { |
michael@0 | 274 | super(runAfter); |
michael@0 | 275 | mContinueAnimation = true; |
michael@0 | 276 | this.mStartLeft = metrics.marginLeft; |
michael@0 | 277 | this.mStartTop = metrics.marginTop; |
michael@0 | 278 | this.mStartRight = metrics.marginRight; |
michael@0 | 279 | this.mStartBottom = metrics.marginBottom; |
michael@0 | 280 | this.mLeft = left; |
michael@0 | 281 | this.mRight = right; |
michael@0 | 282 | this.mTop = top; |
michael@0 | 283 | this.mBottom = bottom; |
michael@0 | 284 | } |
michael@0 | 285 | |
michael@0 | 286 | @Override |
michael@0 | 287 | public boolean internalRun(long timeDelta, long currentFrameStartTime) { |
michael@0 | 288 | if (!mContinueAnimation) { |
michael@0 | 289 | return false; |
michael@0 | 290 | } |
michael@0 | 291 | |
michael@0 | 292 | // Calculate the progress (between 0 and 1) |
michael@0 | 293 | float progress = mInterpolator.getInterpolation( |
michael@0 | 294 | Math.min(1.0f, (System.nanoTime() - getStartTime()) |
michael@0 | 295 | / (float)MARGIN_ANIMATION_DURATION)); |
michael@0 | 296 | |
michael@0 | 297 | // Calculate the new metrics accordingly |
michael@0 | 298 | synchronized (mTarget.getLock()) { |
michael@0 | 299 | ImmutableViewportMetrics oldMetrics = mTarget.getViewportMetrics(); |
michael@0 | 300 | ImmutableViewportMetrics newMetrics = oldMetrics.setMargins( |
michael@0 | 301 | FloatUtils.interpolate(mStartLeft, mLeft, progress), |
michael@0 | 302 | FloatUtils.interpolate(mStartTop, mTop, progress), |
michael@0 | 303 | FloatUtils.interpolate(mStartRight, mRight, progress), |
michael@0 | 304 | FloatUtils.interpolate(mStartBottom, mBottom, progress)); |
michael@0 | 305 | PointF oldOffset = oldMetrics.getMarginOffset(); |
michael@0 | 306 | PointF newOffset = newMetrics.getMarginOffset(); |
michael@0 | 307 | newMetrics = |
michael@0 | 308 | newMetrics.offsetViewportByAndClamp(newOffset.x - oldOffset.x, |
michael@0 | 309 | newOffset.y - oldOffset.y); |
michael@0 | 310 | |
michael@0 | 311 | if (progress >= 1.0f) { |
michael@0 | 312 | mContinueAnimation = false; |
michael@0 | 313 | |
michael@0 | 314 | // Force a redraw and update Gecko |
michael@0 | 315 | mTarget.forceViewportMetrics(newMetrics, true, true); |
michael@0 | 316 | } else { |
michael@0 | 317 | mTarget.forceViewportMetrics(newMetrics, false, false); |
michael@0 | 318 | } |
michael@0 | 319 | } |
michael@0 | 320 | return mContinueAnimation; |
michael@0 | 321 | } |
michael@0 | 322 | } |
michael@0 | 323 | |
michael@0 | 324 | } |