chromium/android_webview/java/src/org/chromium/android_webview/OverScrollGlow.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.android_webview;

import android.content.Context;
import android.graphics.Canvas;
import android.view.View;
import android.widget.EdgeEffect;

/** This class manages the edge glow effect when a WebView is flung or pulled beyond the edges. */
class OverScrollGlow {
    private View mHostView;

    private EdgeEffect mEdgeGlowTop;
    private EdgeEffect mEdgeGlowBottom;
    private EdgeEffect mEdgeGlowLeft;
    private EdgeEffect mEdgeGlowRight;

    private int mOverScrollDeltaX;
    private int mOverScrollDeltaY;

    private boolean mShouldPull;

    public OverScrollGlow(Context context, View host) {
        mHostView = host;
        mEdgeGlowTop = new EdgeEffect(context);
        mEdgeGlowBottom = new EdgeEffect(context);
        mEdgeGlowLeft = new EdgeEffect(context);
        mEdgeGlowRight = new EdgeEffect(context);
    }

    public void setShouldPull(boolean shouldPull) {
        mShouldPull = shouldPull;
    }

    /**
     * Pull leftover touch scroll distance into one of the edge glows as appropriate.
     *
     * @param x Current X scroll offset
     * @param y Current Y scroll offset
     * @param oldX Old X scroll offset
     * @param oldY Old Y scroll offset
     * @param maxX Maximum range for horizontal scrolling
     * @param maxY Maximum range for vertical scrolling
     */
    public void pullGlow(int x, int y, int oldX, int oldY, int maxX, int maxY) {
        if (!mShouldPull) return;
        // Only show overscroll bars if there was no movement in any direction
        // as a result of scrolling.
        if (oldX == mHostView.getScrollX() && oldY == mHostView.getScrollY()) {
            // Don't show left/right glows if we fit the whole content.
            // Also don't show if there was vertical movement.
            if (maxX > 0) {
                final int pulledToX = oldX + mOverScrollDeltaX;
                if (pulledToX < 0) {
                    // |mOverScrollDeltaX| will be negative when overscroll to the left.
                    mEdgeGlowLeft.onPull((float) -mOverScrollDeltaX / mHostView.getWidth());
                    if (!mEdgeGlowRight.isFinished()) {
                        mEdgeGlowRight.onRelease();
                    }
                } else if (pulledToX > maxX) {
                    mEdgeGlowRight.onPull((float) mOverScrollDeltaX / mHostView.getWidth());
                    if (!mEdgeGlowLeft.isFinished()) {
                        mEdgeGlowLeft.onRelease();
                    }
                }
                mOverScrollDeltaX = 0;
            }

            if (maxY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) {
                final int pulledToY = oldY + mOverScrollDeltaY;
                if (pulledToY < 0) {
                    // |mOverScrollDeltaY| will be negative when overscroll to the top.
                    mEdgeGlowTop.onPull((float) -mOverScrollDeltaY / mHostView.getHeight());
                    if (!mEdgeGlowBottom.isFinished()) {
                        mEdgeGlowBottom.onRelease();
                    }
                } else if (pulledToY > maxY) {
                    mEdgeGlowBottom.onPull((float) mOverScrollDeltaY / mHostView.getHeight());
                    if (!mEdgeGlowTop.isFinished()) {
                        mEdgeGlowTop.onRelease();
                    }
                }
                mOverScrollDeltaY = 0;
            }
        }
    }

    /**
     * Absorb leftover fling velocity into one of the edge glows as appropriate.
     *
     * @param x Current X scroll offset
     * @param y Current Y scroll offset
     * @param oldX Old X scroll offset
     * @param oldY Old Y scroll offset
     * @param rangeX Maximum range for horizontal scrolling
     * @param rangeY Maximum range for vertical scrolling
     * @param currentFlingVelocity Current fling velocity
     */
    public void absorbGlow(
            int x, int y, int oldX, int oldY, int rangeX, int rangeY, float currentFlingVelocity) {
        if (mShouldPull) {
            // Not absorb the glow because the user is pulling the glow now.
            // TODO(hush): crbug.com/501556. Do not use "mShouldPull" to switch
            // between absorbGlow and pullGlow. Use the velocity instead.
            return;
        }
        if (rangeY > 0 || mHostView.getOverScrollMode() == View.OVER_SCROLL_ALWAYS) {
            if (y < 0 && oldY >= 0) {
                mEdgeGlowTop.onAbsorb((int) currentFlingVelocity);
                if (!mEdgeGlowBottom.isFinished()) {
                    mEdgeGlowBottom.onRelease();
                }
            } else if (y > rangeY && oldY <= rangeY) {
                mEdgeGlowBottom.onAbsorb((int) currentFlingVelocity);
                if (!mEdgeGlowTop.isFinished()) {
                    mEdgeGlowTop.onRelease();
                }
            }
        }

        if (rangeX > 0) {
            if (x < 0 && oldX >= 0) {
                mEdgeGlowLeft.onAbsorb((int) currentFlingVelocity);
                if (!mEdgeGlowRight.isFinished()) {
                    mEdgeGlowRight.onRelease();
                }
            } else if (x > rangeX && oldX <= rangeX) {
                mEdgeGlowRight.onAbsorb((int) currentFlingVelocity);
                if (!mEdgeGlowLeft.isFinished()) {
                    mEdgeGlowLeft.onRelease();
                }
            }
        }
    }

    /**
     * Set touch delta values indicating the current amount of overscroll.
     *
     * @param deltaX
     * @param deltaY
     */
    public void setOverScrollDeltas(int deltaX, int deltaY) {
        mOverScrollDeltaX += deltaX;
        mOverScrollDeltaY += deltaY;
    }

    /**
     * Draw the glow effect along the sides of the widget.
     *
     * @param canvas Canvas to draw into, transformed into view coordinates.
     * @param maxScrollX maximum horizontal scroll offset
     * @param maxScrollY maximum vertical scroll offset
     * @return true if glow effects are still animating and the view should invalidate again.
     */
    public boolean drawEdgeGlows(Canvas canvas, int maxScrollX, int maxScrollY) {
        final int scrollX = mHostView.getScrollX();
        final int scrollY = mHostView.getScrollY();
        final int width = mHostView.getWidth();
        int height = mHostView.getHeight();

        boolean invalidateForGlow = false;
        if (!mEdgeGlowTop.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.translate(scrollX, Math.min(0, scrollY));
            mEdgeGlowTop.setSize(width, height);
            invalidateForGlow |= mEdgeGlowTop.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeGlowBottom.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.translate(-width + scrollX, Math.max(maxScrollY, scrollY) + height);
            canvas.rotate(180, width, 0);
            mEdgeGlowBottom.setSize(width, height);
            invalidateForGlow |= mEdgeGlowBottom.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeGlowLeft.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.rotate(270);
            canvas.translate(-height - scrollY, Math.min(0, scrollX));
            mEdgeGlowLeft.setSize(height, width);
            invalidateForGlow |= mEdgeGlowLeft.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }
        if (!mEdgeGlowRight.isFinished()) {
            final int restoreCount = canvas.save();

            canvas.rotate(90);
            canvas.translate(scrollY, -(Math.max(scrollX, maxScrollX) + width));
            mEdgeGlowRight.setSize(height, width);
            invalidateForGlow |= mEdgeGlowRight.draw(canvas);
            canvas.restoreToCount(restoreCount);
        }
        return invalidateForGlow;
    }

    /** @return True if any glow is still animating */
    public boolean isAnimating() {
        return (!mEdgeGlowTop.isFinished()
                || !mEdgeGlowBottom.isFinished()
                || !mEdgeGlowLeft.isFinished()
                || !mEdgeGlowRight.isFinished());
    }

    /** Release all glows from any touch pulls in progress. */
    public void releaseAll() {
        mEdgeGlowTop.onRelease();
        mEdgeGlowBottom.onRelease();
        mEdgeGlowLeft.onRelease();
        mEdgeGlowRight.onRelease();
    }
}