/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.fastergallery.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.android.fastergallery.glrenderer.GLCanvas;
import com.android.fastergallery.glrenderer.ResourceTexture;
import com.android.fastergallery.R;
// This is copied from android.widget.EdgeEffect with some small modifications:
// (1) Copy the images (overscroll_{edge|glow}.png) to local resources.
// (2) Use "GLCanvas" instead of "Canvas" for draw()'s parameter.
// (3) Use a private Drawable class (which inherits from ResourceTexture)
// instead of android.graphics.drawable.Drawable to hold the images.
// The private Drawable class is used to translate original Canvas calls to
// corresponding GLCanvas calls.
/**
* This class performs the graphical effect used at the edges of scrollable
* widgets when the user scrolls beyond the content bounds in 2D space.
*
* <p>
* EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
* instance for each edge that should show the effect, feed it input data using
* the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and
* {@link #onRelease()}, and draw the effect using {@link #draw(Canvas)} in the
* widget's overridden {@link android.view.View#draw(Canvas)} method. If
* {@link #isFinished()} returns false after drawing, the edge effect's
* animation is not yet complete and the widget should schedule another drawing
* pass to continue the animation.
* </p>
*
* <p>
* When drawing, widgets should draw their main content and child views first,
* usually by invoking <code>super.draw(canvas)</code> from an overridden
* <code>draw</code> method. (This will invoke onDraw and dispatch drawing to
* child views as needed.) The edge effect may then be drawn on top of the
* view's content using the {@link #draw(Canvas)} method.
* </p>
*/
public class EdgeEffect {
@SuppressWarnings("unused")
private static final String TAG = "EdgeEffect";
// Time it will take the effect to fully recede in ms
private static final int RECEDE_TIME = 1000;
// Time it will take before a pulled glow begins receding in ms
private static final int PULL_TIME = 167;
// Time it will take in ms for a pulled glow to decay to partial strength
// before release
private static final int PULL_DECAY_TIME = 1000;
private static final float MAX_ALPHA = 0.8f;
private static final float HELD_EDGE_ALPHA = 0.7f;
private static final float HELD_EDGE_SCALE_Y = 0.5f;
private static final float HELD_GLOW_ALPHA = 0.5f;
private static final float HELD_GLOW_SCALE_Y = 0.5f;
private static final float MAX_GLOW_HEIGHT = 4.f;
private static final float PULL_GLOW_BEGIN = 1.f;
private static final float PULL_EDGE_BEGIN = 0.6f;
// Minimum velocity that will be absorbed
private static final int MIN_VELOCITY = 100;
private static final float EPSILON = 0.001f;
private final Drawable mEdge;
private final Drawable mGlow;
private int mWidth;
private int mHeight;
private final int MIN_WIDTH = 300;
private final int mMinWidth;
private float mEdgeAlpha;
private float mEdgeScaleY;
private float mGlowAlpha;
private float mGlowScaleY;
private float mEdgeAlphaStart;
private float mEdgeAlphaFinish;
private float mEdgeScaleYStart;
private float mEdgeScaleYFinish;
private float mGlowAlphaStart;
private float mGlowAlphaFinish;
private float mGlowScaleYStart;
private float mGlowScaleYFinish;
private long mStartTime;
private float mDuration;
private final Interpolator mInterpolator;
private static final int STATE_IDLE = 0;
private static final int STATE_PULL = 1;
private static final int STATE_ABSORB = 2;
private static final int STATE_RECEDE = 3;
private static final int STATE_PULL_DECAY = 4;
// How much dragging should effect the height of the edge image.
// Number determined by user testing.
private static final int PULL_DISTANCE_EDGE_FACTOR = 7;
// How much dragging should effect the height of the glow image.
// Number determined by user testing.
private static final int PULL_DISTANCE_GLOW_FACTOR = 7;
private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f;
private static final int VELOCITY_EDGE_FACTOR = 8;
private static final int VELOCITY_GLOW_FACTOR = 16;
private int mState = STATE_IDLE;
private float mPullDistance;
/**
* Construct a new EdgeEffect with a theme appropriate for the provided
* context.
*
* @param context
* Context used to provide theming and resource information for
* the EdgeEffect
*/
public EdgeEffect(Context context) {
mEdge = new Drawable(context, R.drawable.overscroll_edge);
mGlow = new Drawable(context, R.drawable.overscroll_glow);
mMinWidth = (int) (context.getResources().getDisplayMetrics().density
* MIN_WIDTH + 0.5f);
mInterpolator = new DecelerateInterpolator();
}
/**
* Set the size of this edge effect in pixels.
*
* @param width
* Effect width in pixels
* @param height
* Effect height in pixels
*/
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
/**
* Reports if this EdgeEffect's animation is finished. If this method
* returns false after a call to {@link #draw(Canvas)} the host widget
* should schedule another drawing pass to continue the animation.
*
* @return true if animation is finished, false if drawing should continue
* on the next frame.
*/
public boolean isFinished() {
return mState == STATE_IDLE;
}
/**
* Immediately finish the current animation. After this call
* {@link #isFinished()} will return true.
*/
public void finish() {
mState = STATE_IDLE;
}
/**
* A view should call this when content is pulled away from an edge by the
* user. This will update the state of the current visual effect and its
* associated animation. The host view should always
* {@link android.view.View#invalidate()} after this and draw the results
* accordingly.
*
* @param deltaDistance
* Change in distance since the last call. Values may be 0 (no
* change) to 1.f (full length of the view) or negative values to
* express change back toward the edge reached to initiate the
* effect.
*/
public void onPull(float deltaDistance) {
final long now = AnimationTime.get();
if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
return;
}
if (mState != STATE_PULL) {
mGlowScaleY = PULL_GLOW_BEGIN;
}
mState = STATE_PULL;
mStartTime = now;
mDuration = PULL_TIME;
mPullDistance += deltaDistance;
float distance = Math.abs(mPullDistance);
mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN,
Math.min(distance, MAX_ALPHA));
mEdgeScaleY = mEdgeScaleYStart = Math.max(HELD_EDGE_SCALE_Y,
Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f));
mGlowAlpha = mGlowAlphaStart = Math
.min(MAX_ALPHA,
mGlowAlpha
+ (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
float glowChange = Math.abs(deltaDistance);
if (deltaDistance > 0 && mPullDistance < 0) {
glowChange = -glowChange;
}
if (mPullDistance == 0) {
mGlowScaleY = 0;
}
// Do not allow glow to get larger than MAX_GLOW_HEIGHT.
mGlowScaleY = mGlowScaleYStart = Math.min(
MAX_GLOW_HEIGHT,
Math.max(0, mGlowScaleY + glowChange
* PULL_DISTANCE_GLOW_FACTOR));
mEdgeAlphaFinish = mEdgeAlpha;
mEdgeScaleYFinish = mEdgeScaleY;
mGlowAlphaFinish = mGlowAlpha;
mGlowScaleYFinish = mGlowScaleY;
}
/**
* Call when the object is released after being pulled. This will begin the
* "decay" phase of the effect. After calling this method the host view
* should {@link android.view.View#invalidate()} and thereby draw the
* results accordingly.
*/
public void onRelease() {
mPullDistance = 0;
if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
return;
}
mState = STATE_RECEDE;
mEdgeAlphaStart = mEdgeAlpha;
mEdgeScaleYStart = mEdgeScaleY;
mGlowAlphaStart = mGlowAlpha;
mGlowScaleYStart = mGlowScaleY;
mEdgeAlphaFinish = 0.f;
mEdgeScaleYFinish = 0.f;
mGlowAlphaFinish = 0.f;
mGlowScaleYFinish = 0.f;
mStartTime = AnimationTime.get();
mDuration = RECEDE_TIME;
}
/**
* Call when the effect absorbs an impact at the given velocity. Used when a
* fling reaches the scroll boundary.
*
* <p>
* When using a {@link android.widget.Scroller} or
* {@link android.widget.OverScroller}, the method
* <code>getCurrVelocity</code> will provide a reasonable approximation to
* use here.
* </p>
*
* @param velocity
* Velocity at impact in pixels per second.
*/
public void onAbsorb(int velocity) {
mState = STATE_ABSORB;
velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
mStartTime = AnimationTime.get();
mDuration = 0.1f + (velocity * 0.03f);
// The edge should always be at least partially visible, regardless
// of velocity.
mEdgeAlphaStart = 0.f;
mEdgeScaleY = mEdgeScaleYStart = 0.f;
// The glow depends more on the velocity, and therefore starts out
// nearly invisible.
mGlowAlphaStart = 0.5f;
mGlowScaleYStart = 0.f;
// Factor the velocity by 8. Testing on device shows this works best to
// reflect the strength of the user's scrolling.
mEdgeAlphaFinish = Math.max(0,
Math.min(velocity * VELOCITY_EDGE_FACTOR, 1));
// Edge should never get larger than the size of its asset.
mEdgeScaleYFinish = Math.max(HELD_EDGE_SCALE_Y,
Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f));
// Growth for the size of the glow should be quadratic to properly
// respond
// to a user's scrolling speed. The faster the scrolling speed, the more
// intense the effect should be for both the size and the saturation.
mGlowScaleYFinish = Math.min(
0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f);
// Alpha should change for the glow as well as size.
mGlowAlphaFinish = Math.max(mGlowAlphaStart,
Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
}
/**
* Draw into the provided canvas. Assumes that the canvas has been rotated
* accordingly and the size has been set. The effect will be drawn the full
* width of X=0 to X=width, beginning from Y=0 and extending to some factor
* < 1.f of height.
*
* @param canvas
* Canvas to draw into
* @return true if drawing should continue beyond this frame to continue the
* animation
*/
public boolean draw(GLCanvas canvas) {
update();
final int edgeHeight = mEdge.getIntrinsicHeight();
final int edgeWidth = mEdge.getIntrinsicWidth();
final int glowHeight = mGlow.getIntrinsicHeight();
final int glowWidth = mGlow.getIntrinsicWidth();
mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
int glowBottom = (int) Math.min(glowHeight * mGlowScaleY * glowHeight
/ glowWidth * 0.6f, glowHeight * MAX_GLOW_HEIGHT);
if (mWidth < mMinWidth) {
// Center the glow and clip it.
int glowLeft = (mWidth - mMinWidth) / 2;
mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom);
} else {
// Stretch the glow to fit.
mGlow.setBounds(0, 0, mWidth, glowBottom);
}
mGlow.draw(canvas);
mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255));
int edgeBottom = (int) (edgeHeight * mEdgeScaleY);
if (mWidth < mMinWidth) {
// Center the edge and clip it.
int edgeLeft = (mWidth - mMinWidth) / 2;
mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom);
} else {
// Stretch the edge to fit.
mEdge.setBounds(0, 0, mWidth, edgeBottom);
}
mEdge.draw(canvas);
return mState != STATE_IDLE;
}
private void update() {
final long time = AnimationTime.get();
final float t = Math.min((time - mStartTime) / mDuration, 1.f);
final float interp = mInterpolator.getInterpolation(t);
mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart)
* interp;
mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart)
* interp;
mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart)
* interp;
mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart)
* interp;
if (t >= 1.f - EPSILON) {
switch (mState) {
case STATE_ABSORB:
mState = STATE_RECEDE;
mStartTime = AnimationTime.get();
mDuration = RECEDE_TIME;
mEdgeAlphaStart = mEdgeAlpha;
mEdgeScaleYStart = mEdgeScaleY;
mGlowAlphaStart = mGlowAlpha;
mGlowScaleYStart = mGlowScaleY;
// After absorb, the glow and edge should fade to nothing.
mEdgeAlphaFinish = 0.f;
mEdgeScaleYFinish = 0.f;
mGlowAlphaFinish = 0.f;
mGlowScaleYFinish = 0.f;
break;
case STATE_PULL:
mState = STATE_PULL_DECAY;
mStartTime = AnimationTime.get();
mDuration = PULL_DECAY_TIME;
mEdgeAlphaStart = mEdgeAlpha;
mEdgeScaleYStart = mEdgeScaleY;
mGlowAlphaStart = mGlowAlpha;
mGlowScaleYStart = mGlowScaleY;
// After pull, the glow and edge should fade to nothing.
mEdgeAlphaFinish = 0.f;
mEdgeScaleYFinish = 0.f;
mGlowAlphaFinish = 0.f;
mGlowScaleYFinish = 0.f;
break;
case STATE_PULL_DECAY:
// When receding, we want edge to decrease more slowly
// than the glow.
float factor = mGlowScaleYFinish != 0 ? 1 / (mGlowScaleYFinish * mGlowScaleYFinish)
: Float.MAX_VALUE;
mEdgeScaleY = mEdgeScaleYStart
+ (mEdgeScaleYFinish - mEdgeScaleYStart) * interp
* factor;
mState = STATE_RECEDE;
break;
case STATE_RECEDE:
mState = STATE_IDLE;
break;
}
}
}
private static class Drawable extends ResourceTexture {
private Rect mBounds = new Rect();
private int mAlpha = 255;
public Drawable(Context context, int resId) {
super(context, resId);
}
public int getIntrinsicWidth() {
return getWidth();
}
public int getIntrinsicHeight() {
return getHeight();
}
public void setBounds(int left, int top, int right, int bottom) {
mBounds.set(left, top, right, bottom);
}
public void setAlpha(int alpha) {
mAlpha = alpha;
}
public void draw(GLCanvas canvas) {
canvas.save(GLCanvas.SAVE_FLAG_ALPHA);
canvas.multiplyAlpha(mAlpha / 255.0f);
Rect b = mBounds;
draw(canvas, b.left, b.top, b.width(), b.height());
canvas.restore();
}
}
}