今天周末,天气晴朗,太阳公公高照,哇!真是一个好天气啊!一直想对Android的动画机制、View原理深入学习一下,自上一课,我们学习了Choreographer的机制之后,有了一定的铺垫,从这一课开始,我们就进入Android动画的学习阶段了。想法就是从浅入深,先从一些简单的Demo入手,进行分析,到后边再展开,一步一步的搞清楚Android动画的整个流程。
好了,进入我们的主题,在网上找了个动画的Demo例子,是用Eclipse写的,这里顺带说一下,Google已经宣布停止对Eclipse开发Android的组件支持,大力推广AndroidStudio,所以,广大卓友们,也尽快换到AndroidStudio上来吧,AndroidStudio的功能确实非常强大,我也有一些博客是讲AndroidStudio使用相关的,供给大家参考。
超级简单的Android Studio jni 实现(无需命令行)
Android Studio使用gradle-experimental构建NDK工程(无需Android.mk、Application.mk文件)
有些是自己实践的,有的是在网上转载的,当然,英雄不分出处,只要能给我们解决实际问题就是好东西,也希望能给大家带来帮助!
好了,我的后续关于Android动画的讲解,都是以一个Demo为例,这里面实现了一些比较简单的动画,源码下载地址:
我们运行一下Demo,首页真漂亮,首页作的是一个滑动门效果的动画,这里没有实现向下滑动的效果,只实现了向上滑动,我们用手向上滑一下,然后松开,当滑动距离小于屏幕高度的一半时,页面掉下来,然后产生一个弹跳的效果,最后恢复初始状态;如果向上滑动的距离大于半屏,则整个就滑上去并且隐藏掉了。我们就从这个开始入手。实现的效果图如下:
这个类对应的Activity的就是Demo中的主界面MainActivity了,代码就不贴出来了,大家可以下载Demo源码查看,当滑上去的时候,我们可以看到,当前布局是两层,表面这个漂亮的View是第一层,下面还有一层,我们对照布局文件看一下,它是用一个FrameLayout来实现的,滑动的效果是添加在表面这一层上的。我们主要来研究一下表面这层,它是一个自定义的View,名字叫PullDoorView,定义在com.duguang.baseanimation.ui.custom.PullDoorView包下面,我们来看一下它的代码:
package com.duguang.baseanimation.ui.custom;
import com.duguang.baseanimation.R;
import com.duguang.baseanimation.ui.MainActivity;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.BounceInterpolator;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.Scroller;
/**
* zaker自定义效果页面
* @author Administrator
*
*/
public class PullDoorView extends RelativeLayout {
private static final String TAG = "PullDoorView";
private Context mContext;
private Scroller mScroller;
private int mScreenWidth = 0;
private int mScreenHeigh = 0;
private int mLastDownY = 0;
private int mCurryY;
private int mDelY;
private boolean mCloseFlag = false;
private ImageView mImgView;
public PullDoorView(Context context) {
super(context);
mContext = context;
setupView();
}
public PullDoorView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
setupView();
}
@SuppressLint("NewApi")
private void setupView() {
// 这个Interpolator你可以设置别的 我这里选择的是有弹跳效果的Interpolator
Interpolator polator = new BounceInterpolator();
mScroller = new Scroller(mContext, polator);
// 获取屏幕分辨率
WindowManager wm = (WindowManager) (mContext
.getSystemService(Context.WINDOW_SERVICE));
DisplayMetrics dm = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
mScreenHeigh = dm.heightPixels;
mScreenWidth = dm.widthPixels;
// 这里你一定要设置成透明背景,不然会影响你看到底层布局
setBackgroundColor(Color.argb(0, 0, 0, 0));
mImgView = new ImageView(mContext);
mImgView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));
mImgView.setScaleType(ImageView.ScaleType.FIT_XY);// 填充整个屏幕
mImgView.setImageResource(R.drawable.bg1); // 默认背景
addView(mImgView);
}
// 设置推动门背景
public void setBgImage(int id) {
mImgView.setImageResource(id);
}
// 设置推动门背景
public void setBgImage(Drawable drawable) {
mImgView.setImageDrawable(drawable);
}
// 推动门的动画
private void startBounceAnim(int startY, int dy, int duration) {
mScroller.startScroll(0, startY, 0, dy, duration);
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mLastDownY = (int) event.getY();
System.err.println("ACTION_DOWN=" + mLastDownY);
return true;
case MotionEvent.ACTION_MOVE:
mCurryY = (int) event.getY();
System.err.println("ACTION_MOVE=" + mCurryY);
mDelY = mCurryY - mLastDownY;
// 只准上滑有效
if (mDelY < 0) {
scrollTo(0, -mDelY);
}
System.err.println("------------- " + mDelY);
break;
case MotionEvent.ACTION_UP:
mCurryY = (int) event.getY();
mDelY = mCurryY - mLastDownY;
Log.i(TAG, "scroll y = " + getScrollY() + " at action up.");
if (mDelY < 0) {
if (Math.abs(mDelY) > mScreenHeigh / 2) {
// 向上滑动超过半个屏幕高的时候 开启向上消失动画
startBounceAnim(this.getScrollY(), mScreenHeigh, 450);
mCloseFlag = true;
} else {
// 向上滑动未超过半个屏幕高的时候 开启向下弹动动画
startBounceAnim(this.getScrollY(), -this.getScrollY(), 1000);
}
}
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
Log.i(TAG, "scroll y = " + getScrollY() + " at compute scroll.");
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
Log.i("scroller", "getCurrX()= " + mScroller.getCurrX()
+ " getCurrY()=" + mScroller.getCurrY()
+ " getFinalY() = " + mScroller.getFinalY());
// 不要忘记更新界面
invalidate();
} else {
if (mCloseFlag) {
this.setVisibility(View.GONE);
}
}
}
}
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* This method is called by ViewGroup.drawChild() to have each child view draw itself.
* This draw() method is an implementation detail and is not intended to be overridden or
* to be called from anywhere else other than ViewGroup.drawChild().
*/
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
boolean useDisplayListProperties = mAttachInfo != null && mAttachInfo.mHardwareAccelerated;
boolean more = false;
final boolean childHasIdentityMatrix = hasIdentityMatrix();
final int flags = parent.mGroupFlags;
if ((flags & ViewGroup.FLAG_CLEAR_TRANSFORMATION) == ViewGroup.FLAG_CLEAR_TRANSFORMATION) {
parent.getChildTransformation().clear();
parent.mGroupFlags &= ~ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
Transformation transformToApply = null;
boolean concatMatrix = false;
boolean scalingRequired = false;
boolean caching;
int layerType = getLayerType();
final boolean hardwareAccelerated = canvas.isHardwareAccelerated();
if ((flags & ViewGroup.FLAG_CHILDREN_DRAWN_WITH_CACHE) != 0 ||
(flags & ViewGroup.FLAG_ALWAYS_DRAWN_WITH_CACHE) != 0) {
caching = true;
// Auto-scaled apps are not hw-accelerated, no need to set scaling flag on DisplayList
if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired;
} else {
caching = (layerType != LAYER_TYPE_NONE) || hardwareAccelerated;
}
final Animation a = getAnimation();
if (a != null) {
more = drawAnimation(parent, drawingTime, a, scalingRequired);
concatMatrix = a.willChangeTransformationMatrix();
if (concatMatrix) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
transformToApply = parent.getChildTransformation();
} else {
if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) ==
PFLAG3_VIEW_IS_ANIMATING_TRANSFORM && mDisplayList != null) {
// No longer animating: clear out old animation matrix
mDisplayList.setAnimationMatrix(null);
mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
}
if (!useDisplayListProperties &&
(flags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
final Transformation t = parent.getChildTransformation();
final boolean hasTransform = parent.getChildStaticTransformation(this, t);
if (hasTransform) {
final int transformType = t.getTransformationType();
transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
}
}
}
concatMatrix |= !childHasIdentityMatrix;
// Sets the flag as early as possible to allow draw() implementations
// to call invalidate() successfully when doing animations
mPrivateFlags |= PFLAG_DRAWN;
if (!concatMatrix &&
(flags & (ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS |
ViewGroup.FLAG_CLIP_CHILDREN)) == ViewGroup.FLAG_CLIP_CHILDREN &&
canvas.quickReject(mLeft, mTop, mRight, mBottom, Canvas.EdgeType.BW) &&
(mPrivateFlags & PFLAG_DRAW_ANIMATION) == 0) {
mPrivateFlags2 |= PFLAG2_VIEW_QUICK_REJECTED;
return more;
}
mPrivateFlags2 &= ~PFLAG2_VIEW_QUICK_REJECTED;
if (hardwareAccelerated) {
// Clear INVALIDATED flag to allow invalidation to occur during rendering, but
// retain the flag's value temporarily in the mRecreateDisplayList flag
mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) == PFLAG_INVALIDATED;
mPrivateFlags &= ~PFLAG_INVALIDATED;
}
DisplayList displayList = null;
Bitmap cache = null;
boolean hasDisplayList = false;
if (caching) {
if (!hardwareAccelerated) {
if (layerType != LAYER_TYPE_NONE) {
layerType = LAYER_TYPE_SOFTWARE;
buildDrawingCache(true);
}
cache = getDrawingCache(true);
} else {
switch (layerType) {
case LAYER_TYPE_SOFTWARE:
if (useDisplayListProperties) {
hasDisplayList = canHaveDisplayList();
} else {
buildDrawingCache(true);
cache = getDrawingCache(true);
}
break;
case LAYER_TYPE_HARDWARE:
if (useDisplayListProperties) {
hasDisplayList = canHaveDisplayList();
}
break;
case LAYER_TYPE_NONE:
// Delay getting the display list until animation-driven alpha values are
// set up and possibly passed on to the view
hasDisplayList = canHaveDisplayList();
break;
}
}
}
useDisplayListProperties &= hasDisplayList;
if (useDisplayListProperties) {
displayList = getDisplayList();
if (!displayList.isValid()) {
// Uncommon, but possible. If a view is removed from the hierarchy during the call
// to getDisplayList(), the display list will be marked invalid and we should not
// try to use it again.
displayList = null;
hasDisplayList = false;
useDisplayListProperties = false;
}
}
int sx = 0;
int sy = 0;
if (!hasDisplayList) {
computeScroll();
sx = mScrollX;
sy = mScrollY;
}
final boolean hasNoCache = cache == null || hasDisplayList;
final boolean offsetForScroll = cache == null && !hasDisplayList &&
layerType != LAYER_TYPE_HARDWARE;
int restoreTo = -1;
if (!useDisplayListProperties || transformToApply != null) {
restoreTo = canvas.save();
}
if (offsetForScroll) {
canvas.translate(mLeft - sx, mTop - sy);
} else {
if (!useDisplayListProperties) {
canvas.translate(mLeft, mTop);
}
if (scalingRequired) {
if (useDisplayListProperties) {
// TODO: Might not need this if we put everything inside the DL
restoreTo = canvas.save();
}
// mAttachInfo cannot be null, otherwise scalingRequired == false
final float scale = 1.0f / mAttachInfo.mApplicationScale;
canvas.scale(scale, scale);
}
}
float alpha = useDisplayListProperties ? 1 : (getAlpha() * getTransitionAlpha());
if (transformToApply != null || alpha < 1 || !hasIdentityMatrix() ||
(mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PFLAG3_VIEW_IS_ANIMATING_ALPHA) {
if (transformToApply != null || !childHasIdentityMatrix) {
int transX = 0;
int transY = 0;
if (offsetForScroll) {
transX = -sx;
transY = -sy;
}
if (transformToApply != null) {
if (concatMatrix) {
if (useDisplayListProperties) {
displayList.setAnimationMatrix(transformToApply.getMatrix());
} else {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}
if (!childHasIdentityMatrix && !useDisplayListProperties) {
canvas.translate(-transX, -transY);
canvas.concat(getMatrix());
canvas.translate(transX, transY);
}
}
// Deal with alpha if it is or used to be <1
if (alpha < 1 ||
(mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) == PFLAG3_VIEW_IS_ANIMATING_ALPHA) {
if (alpha < 1) {
mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_ALPHA;
} else {
mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_ALPHA;
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
if (hasNoCache) {
final int multipliedAlpha = (int) (255 * alpha);
if (!onSetAlpha(multipliedAlpha)) {
int layerFlags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 ||
layerType != LAYER_TYPE_NONE) {
layerFlags |= Canvas.CLIP_TO_LAYER_SAVE_FLAG;
}
if (useDisplayListProperties) {
displayList.setAlpha(alpha * getAlpha() * getTransitionAlpha());
} else if (layerType == LAYER_TYPE_NONE) {
final int scrollX = hasDisplayList ? 0 : sx;
final int scrollY = hasDisplayList ? 0 : sy;
canvas.saveLayerAlpha(scrollX, scrollY, scrollX + mRight - mLeft,
scrollY + mBottom - mTop, multipliedAlpha, layerFlags);
}
} else {
// Alpha is handled by the child directly, clobber the layer's alpha
mPrivateFlags |= PFLAG_ALPHA_SET;
}
}
}
} else if ((mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
onSetAlpha(255);
mPrivateFlags &= ~PFLAG_ALPHA_SET;
}
if ((flags & ViewGroup.FLAG_CLIP_CHILDREN) == ViewGroup.FLAG_CLIP_CHILDREN &&
!useDisplayListProperties && cache == null) {
if (offsetForScroll) {
canvas.clipRect(sx, sy, sx + (mRight - mLeft), sy + (mBottom - mTop));
} else {
if (!scalingRequired || cache == null) {
canvas.clipRect(0, 0, mRight - mLeft, mBottom - mTop);
} else {
canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
}
}
}
if (!useDisplayListProperties && hasDisplayList) {
displayList = getDisplayList();
if (!displayList.isValid()) {
// Uncommon, but possible. If a view is removed from the hierarchy during the call
// to getDisplayList(), the display list will be marked invalid and we should not
// try to use it again.
displayList = null;
hasDisplayList = false;
}
}
if (hasNoCache) {
boolean layerRendered = false;
if (layerType == LAYER_TYPE_HARDWARE && !useDisplayListProperties) {
final HardwareLayer layer = getHardwareLayer();
if (layer != null && layer.isValid()) {
mLayerPaint.setAlpha((int) (alpha * 255));
((HardwareCanvas) canvas).drawHardwareLayer(layer, 0, 0, mLayerPaint);
layerRendered = true;
} else {
final int scrollX = hasDisplayList ? 0 : sx;
final int scrollY = hasDisplayList ? 0 : sy;
canvas.saveLayer(scrollX, scrollY,
scrollX + mRight - mLeft, scrollY + mBottom - mTop, mLayerPaint,
Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
}
}
if (!layerRendered) {
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
} else {
draw(canvas);
}
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawDisplayList(displayList, null, flags);
}
}
} else if (cache != null) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
Paint cachePaint;
if (layerType == LAYER_TYPE_NONE) {
cachePaint = parent.mCachePaint;
if (cachePaint == null) {
cachePaint = new Paint();
cachePaint.setDither(false);
parent.mCachePaint = cachePaint;
}
if (alpha < 1) {
cachePaint.setAlpha((int) (alpha * 255));
parent.mGroupFlags |= ViewGroup.FLAG_ALPHA_LOWER_THAN_ONE;
} else if ((flags & ViewGroup.FLAG_ALPHA_LOWER_THAN_ONE) != 0) {
cachePaint.setAlpha(255);
parent.mGroupFlags &= ~ViewGroup.FLAG_ALPHA_LOWER_THAN_ONE;
}
} else {
cachePaint = mLayerPaint;
cachePaint.setAlpha((int) (alpha * 255));
}
canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
}
if (restoreTo >= 0) {
canvas.restoreToCount(restoreTo);
}
if (a != null && !more) {
if (!hardwareAccelerated && !a.getFillAfter()) {
onSetAlpha(255);
}
parent.finishAnimatingView(this, a);
}
if (more && hardwareAccelerated) {
if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
// alpha animations should cause the child to recreate its display list
invalidate(true);
}
}
mRecreateDisplayList = false;
return more;
}
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
static float viscousFluid(float x)
{
x *= sViscousFluidScale;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
x *= sViscousFluidNormalize;
return x;
}
/**
* An interpolator where the change bounces at the end.
*/
public class BounceInterpolator implements Interpolator {
public BounceInterpolator() {
}
@SuppressWarnings({"UnusedDeclaration"})
public BounceInterpolator(Context context, AttributeSet attrs) {
}
private static float bounce(float t) {
return t * t * 8.0f;
}
public float getInterpolation(float t) {
// _b(t) = t * t * 8
// bs(t) = _b(t) for t < 0.3535
// bs(t) = _b(t - 0.54719) + 0.7 for t < 0.7408
// bs(t) = _b(t - 0.8526) + 0.9 for t < 0.9644
// bs(t) = _b(t - 1.0435) + 0.95 for t <= 1.0
// b(t) = bs(t * 1.1226)
t *= 1.1226f;
if (t < 0.3535f) return bounce(t);
else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
else return bounce(t - 1.0435f) + 0.95f;
}
}