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