精华内容
下载资源
问答
  • 属性动画: 这也是在android3.0之后引进的动画在手机的版本是android4.0就可以使用这个动 画,通过动态的改变对象的属性从而达到动画效果。 帧动画: 通过顺序播放一系列图像从而产生动画效果,。图片过多时...

    从源码分析 Android 动画如何展现到屏幕上:补间动画、属性动画、帧动画

    Android动画目前分为三种:

    1. 补间动画: 是通过对场景里的对象不断做图像变换(透明度、缩放、平移、旋转)从而产生动画效果,是一种渐进式动画,并且View动画支持自定义。
    2. 属性动画: 这也是在android3.0之后引进的动画,在手机的版本上是android4.0就可以使用这个动 画,通过动态的改变对象的属性从而达到动画效果。
    3. 帧动画: 通过顺序播放一系列图像从而产生动画效果,。图片过多时容易造成OOM(Out Of Memory内存用完)异常。

    补间动画和属性动画的区别

    1. 补间动画只是改变了View的显示效果而已,并不会真正的改变View的属性
    2. 属性动画可以改变View的显示效果和属性

    下面就从源码分析三种动画是如何生效,还有补间动画和属性动画的区别

    1. 补间动画

    找一种最简单的补间动画开始来分析补间动画是怎么生效的

    TranslateAnimation translateAnimation = new TranslateAnimation(0, 300.0f, 0, 0);
    translateAnimation.setDuration(2 * 1000);
    translateAnimation.setFillAfter(true);
    imageView.startAnimation(translateAnimation);
    

    看看 View 是如何开始动画的
    android.view.View

    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        // 记录下需要的动画
        setAnimation(animation);
        invalidateParentCaches();
        // 进行重绘
        invalidate(true);
    }
    
    public void setAnimation(Animation animation) {
        mCurrentAnimation = animation;
    
        if (animation != null) {
            // If the screen is off assume the animation start time is now instead of
            // the next frame we draw. Keeping the START_ON_FIRST_FRAME start time
            // would cause the animation to start when the screen turns back on
            if (mAttachInfo != null && mAttachInfo.mDisplayState == Display.STATE_OFF
                    && animation.getStartTime() == Animation.START_ON_FIRST_FRAME) {
                animation.setStartTime(AnimationUtils.currentAnimationTimeMillis());
            }
            animation.reset();
        }
    }
    

    View 进行重绘,必然要重新执行 draw 方法,这里也是动画最终执行的地方
    android.view.View

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
         * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
         * HW accelerated, it can't handle drawing RenderNodes.
         */
        ...
    
        // 第一部分:获取动画对 View 的偏移值,设置到 transformToApply 属性上
        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } 
    
        ...
    
        // 把动画对 View 的偏移值绘制到屏幕上
        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;
    
                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }
    
                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.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 && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }
        ...
    }
    

    先看第一部分,如何获取动画对 View 的偏移值,设置到 transformToApply 属性上
    android.view.View

    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        ...
    
        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
    
        ...
    }
    

    调用 Animation.getTransformation 方法来获取 Transformation
    根据 getTransformation 的方法名就可以知道,第二个参数是用来重新设置的,所以会把动画的属性设置到 parent.getChildTransformation() 上
    android.view.animation.Animation

    public boolean getTransformation(long currentTime, Transformation outTransformation,
            float scale) {
        mScaleFactor = scale;
        return getTransformation(currentTime, outTransformation);
    }
    
    
    public boolean getTransformation(long currentTime, Transformation outTransformation) {
        ...
            // 根据当前时间和补偿器来获取当前的 Transformation
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            applyTransformation(interpolatedTime, outTransformation);
        ...
    }
    

    applyTransformation 方法在 Animation 中是一个空实现,具体看移动动画的实现 TranslateAnimation
    android.view.animation.TranslateAnimation

    protected void applyTransformation(float interpolatedTime, Transformation t) {
        float dx = mFromXDelta;
        float dy = mFromYDelta;
        // 根据补偿器时间来计算当前需要偏移的 dx 和 dy,并只是到 Transformation 上
        if (mFromXDelta != mToXDelta) {
            dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
        }
        if (mFromYDelta != mToYDelta) {
            dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
        }
        t.getMatrix().setTranslate(dx, dy);
    }
    

    这里设置结束,也就相当于 parent.getChildTransformation() 设置了 dx 和 dy 的偏移
    后面 transformToApply = parent.getChildTransformation() ,transformToApply就相当于设置过偏移的 Transformation 实例了

    接下来,看下第二部分:把动画对 View 的偏移值绘制到屏幕上
    android.view.View

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
        /* If an attached view draws to a HW canvas, it may use its RenderNode + DisplayList.
         *
         * If a view is dettached, its DisplayList shouldn't exist. If the canvas isn't
         * HW accelerated, it can't handle drawing RenderNodes.
         */
        ...
    
        // 把动画对 View 的偏移值绘制到屏幕上
        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;
    
                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }
    
                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.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);
                            // 把 transformToApply 的偏移应用到 canvas 上
                            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 && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }
        ...
    }
    

    这也是补间动画,View 移动之后为什么不可以点击的原因。
    因为补间动画是通过 canvas 画布进行偏移绘制,而 View 的属性 left, right, top, bottom 都没改变,相当于对 View 进行触摸事件分发还在原来的地方。
    所以,点击原来位置可以响应,新的位置不响应。

    2. 属性动画

    同样找一种最简单的属性动画开始来分析属性动画是怎么生效的

    ObjectAnimator animator = ObjectAnimator.ofFloat(imageView, "translationX", 0, 300);
    animator.setDuration(2 * 1000);
    animator.start();
    

    首先是对 ObjectAnimator 进行动画参数的设置
    android.animation.ObjectAnimator

    public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
         // 设置动画参数
        anim.setFloatValues(values);
        return anim;
    }
    
    public void setFloatValues(float... values) {
        if (mValues == null || mValues.length == 0) {
            // No values yet - this animator is being constructed piecemeal. Init the values with
            // whatever the current propertyName is
            if (mProperty != null) {
                setValues(PropertyValuesHolder.ofFloat(mProperty, values));
            } else {
                // 设置动画参数
                setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
            }
        } else {
            super.setFloatValues(values);
        }
    }
    

    需要 ObjectAnimator 的父类 ValueAnimator 进行设置记录
    android.animation.ValueAnimator

    public void setValues(PropertyValuesHolder... values) {
        int numValues = values.length;
        // 记录下动画的参数,后面具体做动画时会使用到
        mValues = values;
        mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
        for (int i = 0; i < numValues; ++i) {
            PropertyValuesHolder valuesHolder = values[i];
            mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
        }
        // New property/values/target should cause re-initialization prior to starting
        mInitialized = false;
    }
    

    接下来真正开始动画
    android.animation.ObjectAnimator

    public void start() {
        ...
        super.start();
    }
    

    ObjectAnimator 的父类 ValueAnimator 真正开始动画
    android.animation.ValueAnimator

    public void start() {
        start(false);
    }
    
    private void start(boolean playBackwards) {
        ...
        // 增加动画的回调
        addAnimationCallback(0);
    
        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            // 开始动画
            startAnimation();
            if (mSeekFraction == -1) {
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                // 设置第一帧动画,进行开始
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }
    
    // 增加动画的回调
    private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        getAnimationHandler().addAnimationFrameCallback(this, delay);
    }
    

    在 AnimationHandler 加入 addAnimationFrameCallback 的回调
    android.animation.AnimationHandler

    public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
        // 第一次加入 callback 回调的时候,加入 mFrameCallback 回调
        if (mAnimationCallbacks.size() == 0) {
            getProvider().postFrameCallback(mFrameCallback);
        }
        // 只是把 callback 回调加入到 mAnimationCallbacks 的集合当中
        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);
        }
    
        if (delay > 0) {
            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
        }
    }
    

    addAnimationFrameCallback 方法只是把 callback 加入到了 mAnimationCallbacks 集合当中,我们来找下,哪里具体回调了 callback
    通过搜索可以快速找到在 doAnimationFrame 方法里回调了 callback 方法
    android.animation.AnimationHandler

    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i++) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }
            if (isCallbackDue(callback, currentTime)) {
                callback.doAnimationFrame(frameTime);
                if (mCommitCallbacks.contains(callback)) {
                    getProvider().postCommitCallback(new Runnable() {
                        @Override
                        public void run() {
                            commitAnimationFrame(callback, getProvider().getFrameTime());
                        }
                    });
                }
            }
        }
        cleanUpList();
    }
    

    继续往上回溯,找到调用 doAnimationFrame 的方法,是在 mFrameCallback 里面的
    android.animation.AnimationHandler

    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };
    

    看来就是前面 addAnimationFrameCallback 方法里面,第一次添加 callback 的时候加上去的,具体分析下 getProvider().postFrameCallback(mFrameCallback); 方法
    看到 getProvider 拿到的是 MyFrameCallbackProvider
    android.animation.AnimationHandler

    private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }
    

    接下来看下 MyFrameCallbackProvider 具体是什么。
    里面直接通过 Choreographer.getInstance() 方法拿到了当前线程的 mChoreographer对象。
    getProvider().postFrameCallback(mFrameCallback) 就是调用 mChoreographer.postFrameCallback(callback)
    Choreographer 会每 16ms 回调一次 doFrame 方法
    android.animation.AnimationHandler

    private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
    
        final Choreographer mChoreographer = Choreographer.getInstance();
    
        @Override
        public void postFrameCallback(Choreographer.FrameCallback callback) {
            mChoreographer.postFrameCallback(callback);
        }
    
        @Override
        public void postCommitCallback(Runnable runnable) {
            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
        }
    
        @Override
        public long getFrameTime() {
            return mChoreographer.getFrameTime();
        }
    
        @Override
        public long getFrameDelay() {
            return Choreographer.getFrameDelay();
        }
    
        @Override
        public void setFrameDelay(long delay) {
            Choreographer.setFrameDelay(delay);
        }
    }
    

    再回到 ValueAnimator.start 方法看 startAnimation 做了什么。
    其实就是做了一些初始化动画和通知动画开始的回调
    android.animation.ValueAnimator

    private void startAnimation() {
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
    
        mAnimationEndRequested = false;
        initAnimation();
        mRunning = true;
        if (mSeekFraction >= 0) {
            mOverallFraction = mSeekFraction;
        } else {
            mOverallFraction = 0f;
        }
        if (mListeners != null) {
            notifyStartListeners();
        }
    }
    
    void initAnimation() {
        if (!mInitialized) {
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].init();
            }
            mInitialized = true;
        }
    }
    

    通过前面的 AnimationHandler.addAnimationFrameCallback 方法已经知道,Choreographer 会每 16ms 回调一次 doFrame 方法,而 doFrame 方法会调用 callback.doAnimationFrame
    看下 ValueAnimator.doAnimationFrame 做了什么

    public final boolean doAnimationFrame(long frameTime) {
        ...
        // 计算当前的时间
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);
    
        if (finished) {
            endAnimation();
        }
        return finished;
    }
    
    
    boolean animateBasedOnTime(long currentTime) {
        boolean done = false;
        if (mRunning) {
            // 根据时间计算得到,当前的比例
            float currentIterationFraction = getCurrentIterationFraction(
                    mOverallFraction, mReversing);
            // 设置动画具体值,改变 View 位置关键所在
            animateValue(currentIterationFraction);
        }
        return done;
    }
    

    ObjectAnimator 对 animateValue 有自己的而实现
    android.animation.ObjectAnimator

    void animateValue(float fraction) {
        final Object target = getTarget();
        if (mTarget != null && target == null) {
            // We lost the target reference, cancel and clean up. Note: we allow null target if the
            /// target has never been set.
            cancel();
            return;
        }
    
        super.animateValue(fraction);
        int numValues = mValues.length;
        for (int i = 0; i < numValues; ++i) {
            mValues[i].setAnimatedValue(target);
        }
    }
    

    具体到 setAnimatedValue 看下怎么调用
    android.animation.PropertyValuesHolder$FloatPropertyValuesHolder

    void setAnimatedValue(Object target) {
        ...
        if (mJniSetter != 0) {
            nCallFloatMethod(target, mJniSetter, mFloatAnimatedValue);
            return;
        }
        ...
    }
    

    mJniSetter 是前面就设置好的,通过 setupSetter 提前已经设好了,而 setupSetter 的时机是在动画初始化的时候,ObjectAnimator.initAnimation 方法中调用的,详细可以自己去看
    android.animation.PropertyValuesHolder$FloatPropertyValuesHolder

    
    void setupSetter(Class targetClass) {
        ...
            if (!wasInMap) {
                String methodName = getMethodName("set", mPropertyName);
                try {
                    mJniSetter = nGetFloatMethod(targetClass, methodName);
                } catch (NoSuchMethodError e) {
                    // Couldn't find it via JNI - try reflection next. Probably means the method
                    // doesn't exist, or the type is wrong. An error will be logged later if
                    // reflection fails as well.
                }
                if (propertyMap == null) {
                    propertyMap = new HashMap<String, Long>();
                    sJNISetterPropertyMap.put(targetClass, propertyMap);
                }
                propertyMap.put(mPropertyName, mJniSetter);
            }
        ...
    }
    
    
    // 这里的 propertyName = translationX, 返回为函数名为 "setTranslationX"
    static String getMethodName(String prefix, String propertyName) {
        if (propertyName == null || propertyName.length() == 0) {
            // shouldn't get here
            return prefix;
        }
        char firstLetter = Character.toUpperCase(propertyName.charAt(0));
        String theRest = propertyName.substring(1);
        return prefix + firstLetter + theRest;
    }
    

    最终调用的 View 里面的 setTranslationX 方法
    android.view.View

    public void setTranslationX(float translationX) {
        if (translationX != getTranslationX()) {
            invalidateViewProperty(true, false);
            mRenderNode.setTranslationX(translationX);
            invalidateViewProperty(false, true);
    
            invalidateParentIfNeededAndWasQuickRejected();
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }
    

    View 的 setTranslationX,还是调用 RenderNode 里面的 native 方法,进行硬件加速动画绘制
    android.view.RenderNode

    public boolean setTranslationX(float translationX) {
        return nSetTranslationX(mNativeRenderNode, translationX);
    }
    

    下面是完整的调用链
    在这里插入图片描述

    总结一下属性动画更新的原理:通过 Choreographer 会每 16ms 回调一次 doFrame 方法,在 doFrame 方法里不断地更新 View 的属性以达到动画的效果。

    那么问题来了,属性动画并没有更新 View 的位置属性 left, right, top, botoom, 为什么和补间动画不一样,这个 View 还可以点击呢?

    其实,这个问题也很简单,在 dispatchTouchEvent 进行触摸事件分发的时候,对 View 的位置进行了一次转换
    android.view.ViewGroup

    public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
            }
    
            ...
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
            ...
    }
    
    // 对子 View 进行转黄
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            ...
    
            if (child == null) {
                handled = super.dispatchTouchEvent(transformedEvent);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                transformedEvent.offsetLocation(offsetX, offsetY);
                // 判断子 View 是否需要转换
                if (! child.hasIdentityMatrix()) {
                    transformedEvent.transform(child.getInverseMatrix());
                }
    
                handled = child.dispatchTouchEvent(transformedEvent);
            }
    
            // Done.
            transformedEvent.recycle();
            return handled;
        }
    

    获取当前 View 的转换矩阵
    android.view.View

    public final Matrix getInverseMatrix() {
        ensureTransformationInfo();
        if (mTransformationInfo.mInverseMatrix == null) {
            mTransformationInfo.mInverseMatrix = new Matrix();
        }
        final Matrix matrix = mTransformationInfo.mInverseMatrix;
        mRenderNode.getInverseMatrix(matrix);
        return matrix;
    }
    

    最终,还是要通过硬件加速的数据获取,setTranslationX 也最终会反应到这个 matrix 当中,有兴趣可以自己调试看下参数
    android.view.RenderNode

    public void getInverseMatrix(@NonNull Matrix outMatrix) {
        nGetInverseTransformMatrix(mNativeRenderNode, outMatrix.native_instance);
    }
    

    3. 逐帧动画

    同样找一种最简单的逐帧动画开始来分析逐帧动画是怎么生效的

    AnimationDrawable animationDrawable=(AnimationDrawable) getResources().getDrawable(R.drawable.frame_animation);
    view.setBackgroundDrawable(animationDrawable);
    animationDrawable.start()
    

    开始逐帧动画
    android.graphics.drawable.AnimationDrawable

    public void start() {
        mAnimating = true;
    
        if (!isRunning()) {
            // Start from 0th frame.
            // 设置为第一帧
            setFrame(0, false, mAnimationState.getChildCount() > 1
                    || !mAnimationState.mOneShot);
        }
    }
    

    具体怎么设置第一帧动画
    android.graphics.drawable.AnimationDrawable

    private void setFrame(int frame, boolean unschedule, boolean animate) {
        if (frame >= mAnimationState.getChildCount()) {
            return;
        }
        mAnimating = animate;
        mCurFrame = frame;
        // 设置为相应帧 Drawable
        selectDrawable(frame);
        if (unschedule || animate) {
            // 结束的时候停止循环更新
            unscheduleSelf(this);
        }
        if (animate) {
            // Unscheduling may have clobbered these values; restore them
            mCurFrame = frame;
            mRunning = true;
            // 把自己加入循环中,不断收到消息更新动画帧
            scheduleSelf(this, SystemClock.uptimeMillis() + mAnimationState.mDurations[frame]);
        }
    }
    

    scheduleSelf 是父类 Drawable 实现的
    android.graphics.drawable.Drawable

    public void scheduleSelf(@NonNull Runnable what, long when) {
        final Callback callback = getCallback();
        if (callback != null) {
            // 在 callback 中具体加入
            callback.scheduleDrawable(this, what, when);
        }
    }
    

    那这个 getCallback 拿到的是哪里设置的呢?只有一个 setCallback 方法
    android.graphics.drawable.Drawable

    public final void setCallback(@Nullable Callback cb) {
        mCallback = cb != null ? new WeakReference<>(cb) : null;
    }
    

    可以找到 setCallback 是 View.setBackgroundDrawable 方法的时候调用的,
    android.view.View

    public void setBackgroundDrawable(Drawable background) {
            computeOpaqueFlags();
    
        ...
            background.setCallback(this);
    }
    

    接下来看下 callback 是什么时候回调的。也是通过 mChoreographer 实例来回调,每 16ms 回调一次。
    android.view.View

    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
        if (verifyDrawable(who) && what != null) {
            final long delay = when - SystemClock.uptimeMillis();
            if (mAttachInfo != null) {
                mAttachInfo.mViewRootImpl.mChoreographer.postCallbackDelayed(
                        Choreographer.CALLBACK_ANIMATION, what, who,
                        Choreographer.subtractFrameDelay(delay));
            } else {
                // Postpone the runnable until we know
                // on which thread it needs to run.
                getRunQueue().postDelayed(what, delay);
            }
        }
    }
    

    看看 callback 回调中做了什么,找到 run 方法。

    public void run() {
        nextFrame(false);
    }
    
    
    private void nextFrame(boolean unschedule) {
        // 帧数加一
        int nextFrame = mCurFrame + 1;
        final int numFrames = mAnimationState.getChildCount();
        final boolean isLastFrame = mAnimationState.mOneShot && nextFrame >= (numFrames - 1);
    
        // Loop if necessary. One-shot animations should never hit this case.
        if (!mAnimationState.mOneShot && nextFrame >= numFrames) {
            nextFrame = 0;
        }
    
        // 不断循环调用 setFrame,知道结尾帧
        setFrame(nextFrame, unschedule, !isLastFrame);
    }
    
    展开全文
  • 我们常常在手机上左右滑动切换图片,要是PPT上可以展示这个效果,也是挺有意思的,我们来看下怎么吧。老规矩,先上效果图:那么,这个效果是如何实现的呢?简单来说就是以下几点:1)素材准备:需要手机轮廓框图和...

    我们常常在手机上左右滑动切换图片,要是PPT上可以展示这个效果,也是挺有意思的,我们来看下怎么做吧。

    老规矩,先上效果图:

    199a227fed07f3b2ed4c1eaca38c84a5.gif

    那么,这个效果是如何实现的呢?简单来说就是以下几点:

    1)素材准备:需要手机轮廓框图和相关图片,如果复杂点,还可以弄个手的图案

    2)将图片使用统一大小形状裁剪成一致大小

    3)将所有图片组合在一起,摆放切换效果

    4)利用PPT上下页面之间的切换效果,设置定时切换或鼠标点击后切换

    我们来看下具体操作步骤。

    一、素材准备

    准备一个手机轮廓图,或者直接利用形状绘制都可以。PP君是直接绘制的,请看下面的动态图。

    9cbc967541d97f5a415605d59a0e8408.gif

    准备好一组需要切换的图片。(最好大小尺寸一致,或者长宽比和所绘制的手机图案一致。这样不会失真)

    二、镂空手机屏幕部分

    之所以要镂空手机屏幕部分,是因为这样做了以后,可以得到三个图层:最上层是手机和当前展示图片,第二层是组合在一起的手机图片,第三层是背景。这样我们只要设置第二层的透明度,就可以遮住除了当前展示图片以外的其他图片。--理解这一点,非常重要!

    具体做法是:

    0)选中手机轮廓图的所有形状,组合在一起。

    1)在手机屏幕部分,插入一个矩形,正好覆盖掉屏幕部分。注意,插入矩形后,要设置边框为无实线,否则会有边框重叠。

    2)先选中手机轮廓形状,再选中这个插入矩形,选择「形状合并」中的「剪除」

    4b17ca8e9508faad70647c899b6a0142.gif

    三、设置背景,组合图片

    将图片插入和刚才一样形状大小的矩形中,然后连成一排,组合在一起。图片之间设置相等的间距。

    a73fe5ce92be4c0338176d7203b9e8f5.gif

    然后将刚才的手机轮廓图放到上面,刚好盖住其中一张图片,将手机轮廓图置于顶层。这样我们得到了一个最上层的手机轮廓图,一个最下层的图片组合层。

    接着,我们要做一个镂空矩形,放到中间层,再设置透明度,就可以刚好盖住下层,露出当前图片。(需要仔细理解这点哦)

    具体的做法是

    1)插入一个铺满页面的矩形,再插入一个手机屏幕一样大小的矩形

    2)先选中大矩形,再按Shift选中小矩形,点击「合并形状」--「剪除」

    3)然后将这个新的形状,在图层上上移一层,并设置这个形状深色的背景色,并设置透明度为10%左右,这样可以有效遮挡下层,有可以微微透出一些。

    f72dc48b4280cafd07ac2b8f40f945db.png
    b6f6e812dc40235d9c276f44d35281d5.png
    8fbe5eb017a4bb97d76241ef3f69d3e3.gif

    好了,到这里,基本已经完成90%了。

    四、复制页面,拖动每个页面的当前页面,然后设置页面切换

    有几张图片就复制几次,然后将每一页设置成不同图片为当前展示图片。

    7636d35d6b5ca75d4920ecc536389e23.gif

    重点来了,通过设置间隔页面之间切换的效果,和延时,就可以达到我要的效果了。

    点击菜单「切换」,选「平滑」,设置延时时间和切换是定时切换还是鼠标点击后切换。

    b84150c5c8475c57d7e8e2fb7a1fd2e0.png

    看动态图,了解具体设置:

    5bf92cce3baabf6a5bc9ac3e4622c657.gif

    O啦,大功告成

    (本文动态图在Mac+PPT2016下录制,Win系统下,对照使用)

    其实要实现这个效果,还有许多其他办法哦?朋友们也来思考下吧。

    视频讲解请移步这里-->PPT动画教程视频3-图片切换

    写在后面

    这里是PPo啦,O啦姐和PP君将会不定时发布关于PPT/PDF和Office使用小技巧哦。

    如果您已经看到这里了,麻烦给个关注和点赞吧。

    有任何问题需要咨询也欢迎发表评论向我们提出来。

    展开全文
  • PowerPoint中,制作动画的工具一应俱全,可谓“没有不到,只有想不到”。很多动画效果看起来很高端,实际实现的方法非常简单,不会只不过是缺少一些关键的小妙招。例如图,是我们经常能看到的一种动态图...

    edf52a0f1921044b34bf449854ef8237.png

    在PowerPoint中,制作动画的工具一应俱全,可谓“没有做不到,只有想不到”。很多动画效果看起来很高端,实际上实现的方法非常简单,不会做只不过是缺少一些关键的小妙招。

    07b81986d0131cb52b28e845a19608f6.gif

    例如上图,是我们经常能看到的一种动态图效果,它可以动态展示手机内容,相比静态的页面展示,可以承载更多信息,也更吸引人瞩目。手机已成为了当今人们不可或缺的工具,手机图片出现在PPT中的频率也越来越高,如果你能让你的手机图也动起来,那无疑是PPT效果的加分项!

    今天的我们就来介绍一个关于PPT动画的小妙招,它可以让你图片中的手机屏幕动起来。


    1、手机图扣屏

    首先我们需要一张手机图片作为PPT素材,要求是正面视角的图片,屏幕上有无内容都行。

    然后我们需要对手机图片进行抠屏处理——

    方式是,先用PPT形状覆盖住屏幕部分,然后利用合并形状的剪除功能,减去这部分形状。

    实例操作:

    利用PPT的圆角矩形绘图工具,绘制一个圆角矩形,尽量调整细节使它尽可能贴合屏幕边框。

    718e63e8c4327c6fa3924ea293a7f829.png

    我们可以看到这个屏幕有刘海屏,我们可以同样用圆角矩形绘制刘海屏的范围。

    94c2a58d52d9e4c8642a36e97e694292.png

    先选中大的屏幕矩形,再按住Ctrl键同时选中小的刘海屏矩形,在顶部菜单栏依次选择:绘图工具·格式>合并形状>剪除

    9dc4f476b939cd4e0d12b7a3fd0e2535.gif

    我们可以看到,这样完成后,就画出了一个基本贴合屏幕的形状。

    剪裁手机图片的方法跟上面类似,先选中手机图片,再按住Ctrl键同时选中屏幕范围形状,在顶部菜单栏依次选择:绘图工具·格式>合并形状>剪除

    8ac6ade380d02bfe7f70a3ccc1ee395f.png

    完成后,我们就获得了一个剪除了屏幕的手机图片。


    2、插入内容

    接下来我们需要准备手机播放内容的图片。可以用PS或直接在PPT中,将滚屏的内容组合成一张长图。

    然后将内容长图置于手机图片的底层,调整图片和手机图片的尺寸位置。

    af30bb464bd6710147ee144a1f5e82a9.png

    如果插入的内容较多,图片过长,建议多使用Ctrl+鼠标滚轮进行缩放操作,同时可以使用我们往期教学的“插入占位图形”配合使用,方便对长图内容进行编辑。

    将内容贴合手机屏幕到初始位置后,接下来就可以编辑动画了。


    3、编辑动画

    我们选中内容长图,为他增加直线向上的动画效果。

    4902b4df32eeb53cf991936517eca80b.png

    我们可以为其添加多次效果,如上图所示,我们让每一段动画轨迹都依次首尾相接,并设置他们都通过单机触发。这样可以在PPT播放时,控制屏幕动画多次滚动,分别展示内容。

    设置好后,我们来看实际播放效果:

    201b07ea91538499b25e06a4950c3dbe.png

    这样的妙招用在与手机有关的PPT里,一定会让你的PPT变得更优秀。


    求关注、点赞、分享,我们将在未来分享更多关于PPT的精彩内容。

    展开全文
  • 一个3.5寸手机上做恢复出厂设置时正在清除安卓机器人显示不出来,调查发现是因为默认png图片太大原因,于是想办法修改包含多帧的png图片。记录下了过程,如下: windows7 64位上安装python 把python 安装目录...

    转载
    恢复出厂设置动画的制作
    2016-11-11 20:29:06 cursem 阅读数 1129
    在一个3.5寸手机上做恢复出厂设置时正在清除安卓机器人显示不出来,调查发现是因为默认png图片太大原因,于是想办法修改包含多帧的png图片。记录下了过程,如下:

    1. 在windows7 64位上安装python
    2. 把python 安装目录加入环境变量 path 下 , 把D:\Python27\Scripts 目录也加入环境变量
    3. 安装easy_install
      下载64位ez_setup.py (https://bootstrap.pypa.io/ez_setup.py), 然后运行 python ez_setup.py install
    4. 安装pip
      easy_inatall pip
    5. 安装PIL 图形库
      从网站http://www.lfd.uci.edu/~gohlke/pythonlibs/ 下载Pillow-3.1.1-cp27-none-win_amd64.whl
      执行 pip install Pillow-3.1.1-cp27-none-win_amd64.whl

    6.把bootable\recovery\interlace-frames.py 作如下修改
    #import Image
    #import PngImagePlugin
    from PIL import Image
    from PIL import PngImagePlugin

    7.制作命令:
    1) 修改图片
    在KK的recovery\res\images 目下包含icon_installing.png icon_installing_overlay01.png 。。。。icon_installing_overlay07.png 8张图片
    把icon_installing_overlay01.png 。。。。icon_installing_overlay07.png 7张图片改成以icon_installing.png 为背景的新7张png 图片

    2)用上面做出来的新7张 icon_installing_overlay ,使用如下CMD命令,制作出LL上能使用的icon_installing.png 文件。
    python interlace-frames.py Y:\android\bootable\recovery\res\icon_installing_overlay01.png Y:\android\bootable\recovery\res\icon_installing_overlay02.png Y:\android\bootable\recovery\res\icon_installing_overlay03.png Y:\android\bootable\recovery\res\icon_installing_overlay04.png Y:\android\bootable\recovery\res\icon_installing_overlay05.png Y:\android\bootable\recovery\res\icon_installing_overlay06.png Y:\android\bootable\recovery\res\icon_installing_overlay07.png Y:\android\bootable\recovery\res\icon_installing.png

    原文链接:http://blog.sina.com.cn/s/blog_dfd0075c0102whto.html

    展开全文
  • 本文分类:PPT小妙招↓↓下方文字版讲解更详细↓↓也许你一些手机发布会中见到过这样的动画效果:产品图片被局部放大,就像是用放大镜移动过画面一样,效果平滑而酷炫。如果你也想要自己的PPT中展现这样的效果...
  •  这是一个非常简单又简单的布丁动画,我们可以很多手机APP里头都看见过这场动画,,印象最深的就是安卓手机发射火箭的时候清除缓存时所做的动画,首先我们来分析一下这个动画是怎么做的,我做动画前都有一个习惯,就是...
  • 工业领域,往往会出现这样的问题,有些设备能够采集到数据,希望能网页或者是手机上能看到这些数据,并且希望是以图形化的方式展示,但硬件的一般对前端的业务不是太熟悉。本文演示一个如何通过图也SVG编辑...
  • 我用js对css的一个样式进行了简单的移动效果实现,但在手机上会移出手机屏幕, 如何解决这个问题呢?谢谢!
  • 预览图: 这个小demo 会实时的把CSS实现过程输入在左边红色区域...没有成对称是为了适配ipone6s plus( github预览链接可以在手机上播放 || 只了一个适配 )。 不能翻墙的朋友可以直接复制以下代码 HTML部分 ...
  • 小伙伴们是不是经常在手机上见到“转场"的情况,手机上的页面转换已经不像pc上整体的页面跳转,很多都是利用动画平滑地在页面之间的切换。 那么如何页面之间的转换呢?首先要明确的是,所谓的页面之间的切换...
  • 首先看一下效果图:(原图效果图地址)压缩图1:iOS侧滑返回到一个界面-动画图使用过iOS系统的人都知道苹果手机可以向右滑动返回到一个界面,带给更好的交互和用户体验。ios是系统提供的,而android框架没有提供...
  • 首先看一下效果图:(原图效果图地址)压缩图1:iOS侧滑返回到一个界面-动画图使用过iOS系统的人都知道苹果手机可以向右滑动返回到一个界面,带给更好的交互和用户体验。ios是系统提供的,而android框架没有提供...
  • 60fps是人尽皆知的比较适合做动画的帧率。 地球人都知道,JavaScript中的定时器是不准确的。由于JavaScript运行时需要耗费时间,而JavaScript又是单线程的,所以如果一个定时器如果比较耗时的话&#...
  • Qt移动应用开发:使用动画框架  上一篇博客介绍了如何使用...有些平板硬件上做得和IPad一样是Retina屏(2048×1536),有些低端的手机分辨率只有320×480,这样宽高比又不一样了,所以设计App的过程一定要对内
  • 最近有人问我金山清理大师桌面上的一键加速的动画如何实现的,我下了个金山清理大师装在手机上,体现了一把,感觉还不错,所以就花了点时间研究了一下。 先看看效果: 点一下,   第一感觉就是在Window中增加...
  • 在手机上运行时,手机会有各种的权限提示dialog,而且我的录音按钮是响应的onTouch事件,所以就造成我的按钮在onkeydown时去初始化录音并显示dialog动画时,权限Dailog弹出,我的界面控件失去焦点,当我去选择权限时...
  • 开发对于切片动画,依然有着庞大的需求,尤其是需要丰富的游戏内容的时候。如果全部使用帧动画,内存绝对会直接爆掉。 由于国内游戏开发的XX现状,绝大部分公司是不会投入专人花几个月去专门开发一套切片动画...
  • 网络流行着这么一句话“世界最遥远的距离不是生与死,而是我你身边,你却低头玩手机”。就连3岁孩子都会说,爸爸妈妈又手机了,都不陪我玩。作为家长,你平时是如何陪伴孩子的呢?孩子玩玩具,你一...
  • 并未重启成功,手机中也确实找到了reboot文件,终端模拟器用reboot命令也是可以重启的。 那么,应该怎么才能实现重启呢?还有关机。 补充: 我用Runtime.getRuntime().exec("su -c \"reboot\"");并...
  • 前面,如果面对复杂的动画效果你一筹莫展,不烦看看这篇文章:LottieAndroid使用详解及源码解析—轻而易举实现各种复杂动画 该文章是结合我司产品手机迅雷的一个全面的性能分析及优化方案。本文篇幅较长,...
  • 学习目的: 1、掌握在Android中如何建立Gallery ...2、Gallery自带了滚动播放图片功能,此功能您可以通过模拟器拖曳鼠标或者在手机上拖拽验证 3、Gallery需要适配器来传输数据,如果您不熟悉“适配器设计模式”
  • JAVA百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、保存私钥到文件privateKey.dat、如何用Java对象序列化保存私钥...
  • Java生成密钥的实例 1个目标文件 摘要:Java源码,算法相关,密钥 Java生成密钥、保存密钥的实例源码,通过本源码可以了解到Java如何产生单钥加密的密钥(myKey)、产生双钥的密钥对(keyPair)、如何保存公钥的字节数组、...
  • 1.活动内容的数据传统网站上比较大,怎么一个或许只有3.5寸的手机上展示 2.照片如何分布,是自动生成动画还是怎么分布? 3.活动中如何跟踪? 4.如何结合LBS? 5.活动据点怎么?比如说发起K歌活动。KTV的...
  • 前面,如果面对复杂的动画效果你一筹莫展,不烦看看这篇文章:LottieAndroid使用详解及源码解析—轻而易举实现各种复杂动画 该文章是结合我司产品手机迅雷的一个全面的性能分析及优化方案。本文篇幅较长,...
  • 之前自定义view之写一个带...前边两篇都是入门文章,这篇算是一个基础文章,我们来制作一个模拟时钟,与手机上的时间保持同步运转。首先看一下我自己的的效果图(很low的一个界面): 可以看到53分钟结束到54...
  • 然后下载下来图片,在电脑上一张一张翻着看并不方便,所以便萌生了自己搭建一个小说网站的想法,真正的在手机上随时随地的看。 如何搭建一个漫画小说网站? PS:小说和漫画都有版权,本文主要交流技术,请勿用...

空空如也

空空如也

1 2 3 4 5
收藏数 85
精华内容 34
关键字:

如何在手机上做动画