gecko/mobile/android/base/gfx/LayerMarginsAnimator.java

317 lines
13 KiB
Java

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