Android动画全解析(一)

本文详细解析了一个Android滑动门动画的实现过程,通过分析PullDoorView自定义View类,介绍了如何通过Scroller和Vsync机制实现平滑动画效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     今天周末,天气晴朗,太阳公公高照,哇!真是一个好天气啊!一直想对Android的动画机制、View原理深入学习一下,自上一课,我们学习了Choreographer的机制之后,有了一定的铺垫,从这一课开始,我们就进入Android动画的学习阶段了。想法就是从浅入深,先从一些简单的Demo入手,进行分析,到后边再展开,一步一步的搞清楚Android动画的整个流程。

     好了,进入我们的主题,在网上找了个动画的Demo例子,是用Eclipse写的,这里顺带说一下,Google已经宣布停止对Eclipse开发Android的组件支持,大力推广AndroidStudio,所以,广大卓友们,也尽快换到AndroidStudio上来吧,AndroidStudio的功能确实非常强大,我也有一些博客是讲AndroidStudio使用相关的,供给大家参考。

     使用Android Studio阅读整个Android源码

      超级简单的Android Studio jni 实现(无需命令行)

      Android Studio使用gradle-experimental构建NDK工程(无需Android.mk、Application.mk文件)

     有些是自己实践的,有的是在网上转载的,当然,英雄不分出处,只要能给我们解决实际问题就是好东西,也希望能给大家带来帮助!

     好了,我的后续关于Android动画的讲解,都是以一个Demo为例,这里面实现了一些比较简单的动画,源码下载地址:

     Android动画集合

     我们运行一下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);
			}
		}
	}

}
     从它的定义中我们可以看出来,它是一个容器类,继承了RelativeLayout,那么就可以往它里边添加子View,我们下面沿着我们的操作来讲一下它的实现,当我们手指开始解析屏幕、滑动、离开屏幕时,系统会监测到事件,并通过onTouchEvent方法回调给我们,就是说现在有触摸事件了,你要不要用,分别监测到的事件类型是MotionEvent.ACTION_DOWN、MotionEvent.ACTION_MOVE、MotionEvent.ACTION_UP,这里说一下,我们项目组之前也有自定义View的,也重写了系统的方法,是哪个方法我给忘了,我们要注意的是什么呢?就是在重写系统的方法时,一定要注意,系统都会预留回调给我们,要重写一定要按系统的意图来,不能乱重写,系统实现每个框架都是有组织的,我们如果随便乱重写,就会导致我们的意图和系统相违背,那么系统执行过程中,无法按照它的意思执行,而我们想要的效果也就实现不了了。所以,大家如果要重写,一般都要搞清楚,到底要重写哪个方法,这个方法到底是干什么用的,我们应该在这个方法里边执行什么逻辑,不能执行什么逻辑,一般可以重写的方法都是以on开关的,比如我们Activity启动过程中,有非常多的回调,onCreate、onStart、onResume、onPause、onStop、onDestroy等等。
     好了,扯远了,回到我们的主题,我们先来看一下这个PullDoorView它是个容器类,那么添加了什么子类,先看一下布局文件,里边定义了一个TextView,这个TextView也就是我们当前滑动门最下边的“上滑可以进入首页”的View,作者还给它加了一个动画,有闪动的效果,在它的构造方法中,构建了一个mImgView,并且给它设置了源图片,就是我们当前看到的这个漂亮的效果了,然后把它添加到PullDoorView当中。也就是说PullDoorView里边一共就两个子View。
     好,我们接下来看一下滑动事件的整个过程。
     从onTouchEvent事件开始,首先接收到的是MotionEvent.ACTION_DOWN,在这个case中,只是记录下了当前触摸按下的Y坐标,没有进行其它操作,关于触摸事件的逻辑我们这里就不展开了,我们的重点是分析动画的实现。紧接着是MotionEvent.ACTION_MOVE事件,在这个case当中,我们通过mDelY = mCurryY - mLastDownY可以得到当前的偏移量,mCurryY是通过onTouchEvent(MotionEvent event)中的event参数获取的,因为系统在回调我们时,会把当前的触摸点的所有属性封装在这个event对象里,我们才可以通过它获取到。然后当mDelY小于0,也就是上滑时,就调用scrollTo(0, -mDelY)滑动当前的View,这也就是为什么我们一开始下滑时没有效果的原因。scrollTo方法是由framework实现的,它的方法代码如下:
    /**
     * 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();
            }
        }
    }
     我们看一下它的逻辑,整个方法的执行要判断(mScrollX != x || mScrollY != y),如果当前的X轴偏移量或者Y轴偏移量与传进来的数据不同,则说位置要发生变化,就执行,否则说明X和Y的偏移量都是相同的,那就不需要滑动了,剩下的逻辑也比较简单,就是把传进来的坐标位置赋值给当前的偏移量,然后调用invalidateParentCaches()方法使PFLAG_INVALIDATED状态位改变,那么在下一次的Vsync信号到来时,就会执行位置变动,把当前的View移到目标位置,那么如果有滚动条的话,还需要加上滚动条mInvalidateOnAnimationRunnable,这个对象也是我们上一课分析Vsync信号回调的三个目标中的一个。所以,大家如果对Vsync信号在Choreographer中的分发还是理解的话,请回头学习一下上一课的知识:Android Choreographer源码分析,这样MotionEvent.ACTION_MOVE事件就处理完了。接下来就是我们的最重点目标MotionEvent.ACTION_UP了,在这呢,还需要说明一下,我们手指按在屏幕上上下移动时,View位置的变化标准的说法应该叫拖动,而滑动是我们的手指离开屏幕之后,由于惯性View产生的没有任何用户控制下的一种状态,从原理上来说,这也是两者最根本的区别。
     MotionEvent.ACTION_UP事件当中首先也是先记录下当前的Y轴偏移量,当Y轴偏移量大于半屏的话,则让PullDoorView一直上滑,直到最顶然后隐藏,而小于半屏的话,就是我们看到的滑动门的效果。两种情况都是调用startBounceAnim方法处理的,只是产生的动画效果不一样,startBounceAnim方法的实现也比较简单,调用类变量mScroller.startScroll(0, startY, 0, dy, duration)开始执行滑动效果,然后调用invalidate()使当前的画面失效。
     invalidate()方法我们就不跟进去了,它最终产生的效果就是调用ViewRootImpl的scheduleTraversals()方法,往Choreographer的CALLBACK_TRAVERSAL队列中放一个mTraversalRunnable对象,这样就可以在下一次的Vsync信号到来时,将我们更新好的View视图效果显示到界面上。我们主要来看一下mScroller.startScroll()方法的执行逻辑,动画的实现也都是要这里的。
     mScroller是一个Scroller对象,它是在当前PullDoorView的构造方法中赋值进去的,mScroller = new Scroller(mContext, polator),构造方法中传了一个BounceInterpolator对象作为第二个参数,我们整个看一上PullDoorView的代码,事件执行的逻辑到MotionEvent.ACTION_UP事件结束就完了,没有其他代码了,那滑动门的效果是哪里产生的呢?这就是追溯到View的整个绘制过程了。当每一次执行View绘制时,三步曲measure、layout、draw,都会从我们当前视图的根布局DecorView开始,一层层传递,把每个需要重绘的View都传达到,在执行draw方法时,会调用View的draw方法,我们来看一下定的实现:
    /**
     * 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;
    }
     我们可以看到draw方法的实现非常复杂,里边各种计算,这也是View绘制机制的核心,到目前为止,我对这个方法当中的逻辑也只停留在自己相关的有了解,其他的调用都没有弄清楚是什么意思。我们主要想研究一下动画实现,和动画不相关的逻辑我们就不看了。调用过程中,我们当前的场景下,hasDisplayList都为false,也就会执行computeScroll()逻辑,从方法的名字上就可以理解出,它就是计算滑动量的,它在系统中是一个空实现,从这里的代码我们就非常清楚系统的意思了,它就是留给我们的一个口子,如果我们需要,就可以重写这个方法,在里边实现我们的逻辑,这样系统就会调用,来完成我们想要的效果。本例中PullDoorView就是重写了此方法,里边的逻辑也非常简单,通过mScroller.computeScrollOffset()来判断当前动画是否结束,如果结束了并且mCloseFlag标志位为true,就把当前的View隐藏掉,如果没有结束,就调用View的scrollTo方法继续把当前的PullDoorView滑动的指定位置。
     看到这里大家应该已经对Android的动画原理有了一个框架性的认识了吧,它就是在每次系统回调我们时,在computeScroll()方法,把我们的目标位置传给系统,配合Vsync机制,每次把我们的View移动到指定位置,这样每16ms刷新一次,连起来整个动画就出来了!!哈哈哈哈,高兴吧,这样的理解不仅仅是代码上的理解,而是对系统框架性的认识!!
     哈哈哈哈哈,写到这里,我自己都感觉好兴奋,框架性的认识应该是一个档次的提升,以后如果我们要搭建一个项目框架,也可以借鉴这样的思想!
     好了,我们继续看一下computeScrollOffset()方法是如何实现的:
    /**
     * 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;
    }
     到这呢,相信大家应该都已经产生了一个疑问了,这个mScroller到底是干啥的,它只是在View的构造方法中赋值了一下,为什么在computeScroll()系统回调时会得知当前View的状态呢,它们是如何联系起来的呢?呵呵,先不急,我们继续往下看,看完了,大家就会清楚了。首先判断当前动画是否已经结束,如果动画已经结束了,那Scroller的使命也就结束了,直接返回。如果动画没有结束,就通过int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime)计算出现在的时间距离动画开始过去多久了,mStartTime就是在我们调用mScroller.startScroll(0, startY, 0, dy, duration)开始执行动画时赋值的,它表示这个动画的起始时间点。下面的逻辑非常明确,通过比对(timePassed < mDuration)的大小来判定动画是否已经结束,如果已执行的时间大于动画的时长,那也说明动画结束了,相反就说明动画还在执行。else分支的逻辑非常简单,如果动画结束了,就修改类变量mCurrX、mCurrY、mFinished的值为终止位置的值。我们主要看一下if分支。mMode就是指定前滑动的模式,分为两种类型:SCROLL_MODE、FLING_MODE,SCROLL_MODE就是平稳的滑动,就像我们当前的Demo中的那样,FLING_MODE就是非常快速的滑动,我们的Demo中是SCROLL_MODE,所以我们就来分析一下它的分支,这里的代码也比较简单,最终的目的就是计算出当前的偏移量mCurrX、mCurrY,这两个值就是我们要回传给系统的,也就是让系统把我们的View滑动到这个位置。
     mDurationReciprocal是当前动画时长的倒数,也是在我们调用startScroll方法时计算出来的,这也就是Android系统动画中归一化的原理,不管我们的动画时长是多长,在这里计算时,都会进行归一化,把整个动画的持续时间作为单位一计算,相当于就是百分比的意思,然后根据已经执行了多久,计算出目前到百分之多少了,float x = timePassed * mDurationReciprocal,这里的x就是表示当前执行了整个动画过程的百分之多少了,mInterpolator表示当前动画的插入器,相当于物理学中的加速度计一样,如果用户没有给当前动画指定加速度,则调用viscousFluid(x)方法产生一个默认的加速度,如果用户有指定,那么就使用用户指定的加速度,我们先来看一下系统默认的加速度,再回头来分析我们当前的滑动门的加速度。
     viscousFluid(x)方法的代码如下:
    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;
    }
     从方法命名上也比较容易理解,当前的加速度就是想产生一种粘性的效果,也就是我们平时经常使用的ViewPager、ListView在上下滑动,手松开之后,还会平滑的滚动一段时间,它的原理就是在这里计算出来的,当然,这就涉及到数学模型了,如果用加速度表示出一个View滑动的惯性,这个我没有研究过,这里就不展开分析了,如果有哪位读者弄清楚了,请指点我一下,好了,我们回到我们PullDoorView的滑动门效果当中。
     我们创建mScroller的时候,指定了插入器为BounceInterpolator,这是Android系统提供给我们的,我们来看一下它的实现:
/**
 * 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;
    }
}
     它是继承Interpolator的,而Interpolator又继承TimeInterpolator,TimeInterpolator接口定义中只有一个方法float getInterpolation(float input),输入参数input也就是我们当前动画的百分比,根据当前的百分比,系统计算返回给我们一个当前的加速度,而我们看到的滑动门的效果就是BounceInterpolator类的getInterpolation方法中计算完成的,这里也涉及到具体的数学模型,我不会,只好略过。好了,再回到我们Scroller类的case SCROLL_MODE当中,我们得到了当前的加速度后,然后,mCurrX = mStartX + Math.round(x * mDeltaX),mCurrY = mStartY + Math.round(x * mDeltaY)就可以计算出我们要偏移的目标位置了。大家可以想一下,它就是控制我们位置变化的一权重,一会这里,一会那里,所以在界面上就产生了上下跳动的效果了。计算完mCurrX、mCurrY,执行到这个if分支中,都说明动画没结束,就返回true,再往上就回到我们PullDoorView重写的computeScroll()方法中的第一个if分支了,返回true,那么就继续调用scrollTo滑动我们的View,而目标位置已经在Scroller中计算好了,这里只需要调用就行。
     这样,每次系统调用我们的computeScroll()方法,我们就传目标位置给系统,收到Vsync信号,系统就把我们的View移动到指定位置,一帧一帧的,动画也就出来了,我们的第一节课也就讲到这里了。
     课本内容讲完了,我们预留的作业问题还没讲解,就是那个Scroller是干啥用的,相信大家已经非常清楚了,它其实只是一个辅助类,它的作用主要就产生在case SCROLL_MODE分支中,因为我们调用startScroll方法时,Scroller知道我们的起始坐标,后边的每个坐标点,它其实是根据插入器模拟出来的,模拟出来的坐标也用来指导我们对View进行滑动,如果我们去掉它的话,那么View位置的变动就会非常生硬,当前还在这,下一刻忽的一下跳到那去了,没有一点连续的感觉。哈哈哈哈,忽这个字感觉挺形象的,我们中文真是强大啊,记得我们高中的物理老师就非常喜欢我们陕西话,什么叫绳绳,什么叫带带,什么叫线线,非常容易理解,哈哈哈哈哈…………
     好了,这节课就到这里了,后面我们会继续分析一些复杂的动画实现,请大家持续关注。同学们,下课!!!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红-旺永福

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值