|
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/. */ |
|
5 |
|
6 package org.mozilla.gecko.gfx; |
|
7 |
|
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; |
|
14 |
|
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; |
|
22 |
|
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"; |
|
28 |
|
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; |
|
33 |
|
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; |
|
51 |
|
52 public LayerMarginsAnimator(GeckoLayerClient aTarget, LayerView aView) { |
|
53 // Assign member variables from parameters |
|
54 mTarget = aTarget; |
|
55 |
|
56 // Create other member variables |
|
57 mMaxMargins = new RectF(); |
|
58 mInterpolator = new DecelerateInterpolator(); |
|
59 mTouchTravelDistance = new PointF(); |
|
60 |
|
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 } |
|
67 |
|
68 @Override |
|
69 public boolean isObserver() { |
|
70 return true; |
|
71 } |
|
72 }); |
|
73 |
|
74 // Listen to touch events, for auto-pinning |
|
75 aView.addTouchInterceptor(this); |
|
76 } |
|
77 |
|
78 public void destroy() { |
|
79 if (mPrefObserverId != null) { |
|
80 PrefsHelper.removeObserver(mPrefObserverId); |
|
81 mPrefObserverId = null; |
|
82 } |
|
83 } |
|
84 |
|
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(); |
|
90 |
|
91 mMaxMargins.set(left, top, right, bottom); |
|
92 |
|
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 } |
|
99 |
|
100 RectF getMaxMargins() { |
|
101 return mMaxMargins; |
|
102 } |
|
103 |
|
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 } |
|
109 |
|
110 if (immediately) { |
|
111 ImmutableViewportMetrics newMetrics = mTarget.getViewportMetrics().setMargins(left, top, right, bottom); |
|
112 mTarget.forceViewportMetrics(newMetrics, true, true); |
|
113 return; |
|
114 } |
|
115 |
|
116 ImmutableViewportMetrics metrics = mTarget.getViewportMetrics(); |
|
117 |
|
118 mAnimationTask = new LayerMarginsAnimationTask(false, metrics, left, top, right, bottom); |
|
119 mTarget.getView().postRenderTask(mAnimationTask); |
|
120 } |
|
121 |
|
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 } |
|
129 |
|
130 public synchronized void hideMargins(boolean immediately) { |
|
131 animateMargins(0, 0, 0, 0, immediately); |
|
132 } |
|
133 |
|
134 public void setMarginsPinned(boolean pin) { |
|
135 if (pin == mMarginsPinned) { |
|
136 return; |
|
137 } |
|
138 |
|
139 mMarginsPinned = pin; |
|
140 } |
|
141 |
|
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 } |
|
149 |
|
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; |
|
174 |
|
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 } |
|
192 |
|
193 if (aNegativeOffset) { |
|
194 return aDelta - (marginEnd - aMargins[1]); |
|
195 } |
|
196 return aDelta - (marginStart - aMargins[0]); |
|
197 } |
|
198 |
|
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 }; |
|
206 |
|
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 } |
|
214 |
|
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 } |
|
222 |
|
223 mTouchTravelDistance.offset(aDx, aDy); |
|
224 RectF overscroll = aMetrics.getOverscroll(); |
|
225 |
|
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 } |
|
246 |
|
247 return aMetrics.setMargins(newMarginsX[0], newMarginsY[0], newMarginsX[1], newMarginsY[1]).offsetViewportBy(aDx, aDy); |
|
248 } |
|
249 |
|
250 /** Implementation of TouchEventInterceptor */ |
|
251 @Override |
|
252 public boolean onTouch(View view, MotionEvent event) { |
|
253 return false; |
|
254 } |
|
255 |
|
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 } |
|
263 |
|
264 return false; |
|
265 } |
|
266 |
|
267 class LayerMarginsAnimationTask extends RenderTask { |
|
268 private float mStartLeft, mStartTop, mStartRight, mStartBottom; |
|
269 private float mTop, mBottom, mLeft, mRight; |
|
270 private boolean mContinueAnimation; |
|
271 |
|
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 } |
|
285 |
|
286 @Override |
|
287 public boolean internalRun(long timeDelta, long currentFrameStartTime) { |
|
288 if (!mContinueAnimation) { |
|
289 return false; |
|
290 } |
|
291 |
|
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)); |
|
296 |
|
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); |
|
310 |
|
311 if (progress >= 1.0f) { |
|
312 mContinueAnimation = false; |
|
313 |
|
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 } |
|
323 |
|
324 } |