/*
* Copyright (C) 2010 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 android.widget;
import android.graphics.Rect;
import com.android.internal.R;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.view.animation.AnimationUtils;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
/**
* 该类用于绘制用户在两个方向上滚动超出边界时,可滚动小部件边界的图形效果.
*
* <p>EdgeEffect 是种状态.使用 EdgeEffect 的自定义小部件应该为显示该效果的每条边创建一个实例.
* 使用 {@link #onAbsorb(int)}、{@link #onPull(float)}和{@link #onRelease()} 传入数据,
* 在小部件重写的 {@link android.view.View#draw(Canvas)} 方法中使用 {@link #draw(Canvas)}
* 来绘制效果.如果绘制后 {@link #isFinished()} 返回假,说明边缘效果动画还没有完成,
* 为了使动画继续,小部件应该将其他的绘制内容稍后再处理.</p>
*
* <p>绘制时,小部件应该首先绘制自己及子视图的内容,通常是在重写的 <code>draw</code>
* 方法中执行 <code>super.draw(canvas)</code> 方法.(这就触发 onDraw 事件并绘制必要的子视图.)
* 边缘效果可能会使用 {@link #draw(Canvas)} 方法绘制在视图内容之上.</p>
*/
public class EdgeEffect {
@SuppressWarnings("UnusedDeclaration")
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 = 1.f;
private static final float HELD_EDGE_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 int mX;
private int mY;
private static 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;
private final Rect mBounds = new Rect();
private final int mEdgeHeight;
private final int mGlowHeight;
private final int mGlowWidth;
private final int mMaxEffectHeight;
/**
* 使用应用程序上下文中的主题构造一个新的 EdgeEffect 对象.
* @param context 用于为 EdgeEffect 提供主题及资源信息的应用程序上下文
*/
public EdgeEffect(Context context) {
final Resources res = context.getResources();
mEdge = res.getDrawable(R.drawable.overscroll_edge);
mGlow = res.getDrawable(R.drawable.overscroll_glow);
mEdgeHeight = mEdge.getIntrinsicHeight();
mGlowHeight = mGlow.getIntrinsicHeight();
mGlowWidth = mGlow.getIntrinsicWidth();
mMaxEffectHeight = (int) (Math.min(
mGlowHeight * MAX_GLOW_HEIGHT * mGlowHeight / mGlowWidth * 0.6f,
mGlowHeight * MAX_GLOW_HEIGHT) + 0.5f);
mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f);
mInterpolator = new DecelerateInterpolator();
}
/**
* 以像素为单位设置大小.
*
* @param width 以像素为单位设置效果影响范围的宽度.
* @param height 以像素为单位设置效果影响范围的高度.
*/
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
/**
* Set the position of this edge effect in pixels. This position is
* only used by {@link #getBounds(boolean)}.
*
* @param x The position of the edge effect on the X axis
* @param y The position of the edge effect on the Y axis
*/
void setPosition(int x, int y) {
mX = x;
mY = y;
}
/**
* 报告这个 EdgeEffect 动画是否已经完成.如果在调用 {@link #draw(Canvas)} 后,本方法返回假,
* 小部件应该稍后再绘制其他内容,让该动画继续.
*
* @return 如果动画已经完成返回真,否则如果应该继续着返回假.
*/
public boolean isFinished() {
return mState == STATE_IDLE;
}
/**
* 立即停止当前动画. 调用该函数后,{@link #isFinished()} 返回真.
*/
public void finish() {
mState = STATE_IDLE;
}
/**
* 当用户将内容从边界拉出视图时调用该函数. 该函数更新当前视觉状态,并关联动画。
* 宿主视图应该在调用该函数后执行 {@link android.view.View#invalidate()},
* 来根据设置绘制视图。
*
* @param deltaDistance 相对上次调用变更的距离. 值在 0(无变化)到1.0(整个视图)之间,
* 负值代表效果的起始边到边界的距离.
*/
public void onPull(float deltaDistance) {
final long now = AnimationUtils.currentAnimationTimeMillis();
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;
}
/**
* 释放拉动对象时调用. 该操作会启动“衰变”效果。调用该方法后,宿主视图应该调用
* {@link android.view.View#invalidate()} 以根据结果绘制视图。
*/
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 = AnimationUtils.currentAnimationTimeMillis();
mDuration = RECEDE_TIME;
}
/**
* 当效果抵消给定速度的影响时调用. 在快速滑动到达滚动边界时使用。
*
* <p>当使用 {@link android.widget.Scroller} 或 {@link android.widget.OverScroller} 时,
* 方法 <code>getCurrVelocity</code> 可以提供用在这里的合理的近似值。</p>
*
* @param velocity 速度的影响,每秒的像素数
*/
public void onAbsorb(int velocity) {
mState = STATE_ABSORB;
velocity = Math.max(MIN_VELOCITY, Math.abs(velocity));
mStartTime = AnimationUtils.currentAnimationTimeMillis();
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));
}
/**
* 在给定画布上绘图. 假设画布已经旋转完毕,并设置了大小。效果会全宽绘制,从X=0 到 X=width,
* 高度则从 Y=0 到 某个小于 1.0 的倍率。
*
* @param canvas 用于绘制的画布
* @return 如果动画在该帧结束后要继续绘制则返回真。
*/
public boolean draw(Canvas canvas) {
update();
mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255));
int glowBottom = (int) Math.min(
mGlowHeight * mGlowScaleY * mGlowHeight / mGlowWidth * 0.6f,
mGlowHeight * 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) (mEdgeHeight * 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);
if (mState == STATE_RECEDE && glowBottom == 0 && edgeBottom == 0) {
mState = STATE_IDLE;
}
return mState != STATE_IDLE;
}
/**
* Returns the bounds of the edge effect.
*
* @hide
*/
public Rect getBounds(boolean reverse) {
mBounds.set(0, 0, mWidth, mMaxEffectHeight);
mBounds.offset(mX, mY - (reverse ? mMaxEffectHeight : 0));
return mBounds;
}
private void update() {
final long time = AnimationUtils.currentAnimationTimeMillis();
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 = AnimationUtils.currentAnimationTimeMillis();
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 = AnimationUtils.currentAnimationTimeMillis();
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;
}
}
}
}