虚化梦幻背景+自动来回移动动画效果

blue and moving background

Posted by Roger on June 25, 2015

必须说写博客是一项非常需要毅力的事情,这两月稍微忙一点就完全忘了这茬了,罪过罪过。

今天解析一个 虚化梦幻背景+自动来回移动动画 的效果,这个动画也是从Muzei中提取出来了~感谢大神!!

首先上效果图:

MissView

背景中的动态模糊和自动移动 就是效果啦..第一次看到是不是有点酷炫呢? 让我弱弱的来分析一下源码。源码地址:https://github.com/Rogero0o/MissView

包结构如下:

package

MissView.java 是主要的显示类

BlurRenderer.java 主要负责模糊像素处理

DemoRenderController.java 主要负责移动动画的处理

类不多而且一点都不复杂,若有心半小时内就能搞懂。

我们从MissView.java开始

public class MissView extends GLTextureView implements RenderController.Callbacks, BlurRenderer.Callbacks {

    private BlurRenderer mRenderer;
    private RenderController mRenderController;

    public MissView(Context context) {
        super(context);
        init();
    }

    public MissView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        mRenderer = new BlurRenderer(getContext(), this);
        setEGLContextClientVersion(2);
        setEGLConfigChooser(8, 8, 8, 8, , );
        setRenderer(mRenderer);
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    public void initPicture(String mPictureName) {
        mRenderController = new DemoRenderController(getContext(), mRenderer,
                this, true, mPictureName);
        mRenderer.setDemoMode(true);
        mRenderController.setVisible(true);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mRenderer.hintViewportSize(w, h);
        mRenderController.reloadCurrentArtwork(true);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (null != mRenderController) {
            mRenderController.destroy();
            queueEventOnGlThread(new Runnable() {
                @Override
                public void run() {
                    mRenderer.destroy();
                }
            });
        }
    }

    @Override
    public void queueEventOnGlThread(Runnable runnable) {
        if (this == null) {
            return;
        }
        super.queueEvent(runnable);
    }

    @Override
    public void requestRender() {
        if (this == null) {
            return;
        }
        super.requestRender();
    }

}

首先MissView继承的是GLTextureView,GLTextureView的详情可参考 http://stackoverflow.com/questions/12061419/converting-from-glsurfaceview-to-textureview-via-gltextureview
,总的来说就是4.0后一个可以显示OpenGL效果的View。关于OPENGL初级使用方法:http://android.tgbus.com/Android/tutorial/201103/343847.shtml

初始化MissView后执行init方法:

mRenderer = new BlurRenderer(getContext(), this);//初始化模糊控制器,使GLTextureView绘制图像。
setEGLContextClientVersion(2);//设置OPENGL版本
setEGLConfigChooser(8, 8, 8, 8, , );//设置OPENGL参数等
setRenderer(mRenderer);//设置Renderer
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);//设置模式

初始化完成后,需要调用 initPicture 初始化图片

mRenderController = new DemoRenderController(getContext(), mRenderer,
this, true, mPictureName);//初始化RenderController
mRenderer.setDemoMode(true);//设置Demo模式为true(带动画效果模式)
mRenderController.setVisible(true);//设置照片可见

这里提一下为什么要调用 mRenderController.setVisible(true);

跟进去,发现 setVisible 方法是这样写的:

public void setVisible(boolean visible) {
    mVisible = visible;
    if (visible) {
        mCallbacks.queueEventOnGlThread(new Runnable() {
            @Override
            public void run() {
                if (mQueuedBitmapRegionLoader != null) {
                    mRenderer.setAndConsumeBitmapRegionLoader(mQueuedBitmapRegionLoader);
                    mQueuedBitmapRegionLoader = null;
                }
            }
        });
        mCallbacks.requestRender();
    }
}

除了将View设置为可见外,在OPENGL线程中将 mQueuedBitmapRegionLoader 初始化好的图片设置进 View中,所以可以多次 initPicture 。

接下来一些回收内存的方法和操作,如onDetachedFromWindow ,在销毁View时将mRenderController和mRenderer回收。

接下来看看BlurRenderer中是怎么实现模糊的效果的,我们知道android4.4以后提供了模糊效果的方法,那在这里是如何实现模糊动画的呢?

在上面我们看到 setVisible 方法里有一个 mRenderer.setAndConsumeBitmapRegionLoader(mQueuedBitmapRegionLoader);

mRenderer其实就是BlurRenderer的一个实例。在方法 setAndConsumeBitmapRegionLoader 中看到这样一行:mNextGLPictureSet.load(bitmapRegionLoader);

继续跟进,看到内部类GLPictureSet,这是个重要的类

private class GLPictureSet {
        private int mId;
        private volatile float[] mPMatrix = new float[16];
        private final float[] mMVPMatrix = new float[16];
        private GLPicture[] mPictures = new GLPicture[mBlurKeyframes + 1];
        private boolean mHasBitmap = false;
        private float mBitmapAspectRatio = 1f;
        private int mDimAmount = ;

        public GLPictureSet(int id) {
            mId = id;
        }

        public void load(BitmapRegionLoader bitmapRegionLoader) {
            mHasBitmap = (bitmapRegionLoader != null);
            mBitmapAspectRatio = mHasBitmap
                    ? bitmapRegionLoader.getWidth() * 1f / bitmapRegionLoader.getHeight()
                    : 1f;

            mDimAmount = DEFAULT_MAX_DIM;

            destroyPictures();

            if (bitmapRegionLoader != null) {
                BitmapFactory.Options options = new BitmapFactory.Options();
                Rect rect = new Rect();
                int originalWidth = bitmapRegionLoader.getWidth();
                int originalHeight = bitmapRegionLoader.getHeight();

                // Calculate image darkness to determine dim amount
                rect.set(, , originalWidth, originalHeight);
                options.inSampleSize = ImageUtil.calculateSampleSize(originalHeight, 64);
                Bitmap tempBitmap = bitmapRegionLoader.decodeRegion(rect, options);
                float darkness = ImageUtil.calculateDarkness(tempBitmap);
                mDimAmount = mDemoMode
                        ? DEMO_DIM
                        : (int) (mMaxDim * ((1 - DIM_RANGE) + DIM_RANGE * Math.sqrt(darkness)));
                tempBitmap.recycle();

                // Create the GLPicture objects
                mPictures[] = new GLPicture(bitmapRegionLoader, mHeight);
                if (mMaxPrescaledBlurPixels == ) {
                    for (int f = 1; f <= mBlurKeyframes; f++) {
                        mPictures[f] = mPictures[];
                    }
                } else {
                    ImageBlurrer blurrer = new ImageBlurrer(mContext);
                    // To blur, first load the entire bitmap region, but at a very large
                    // sample size that's appropriate for the final blurred image
                    options.inSampleSize = ImageUtil.calculateSampleSize(
                            originalHeight, mHeight / mBlurredSampleSize);
                    rect.set(, , originalWidth, originalHeight);
                    tempBitmap = bitmapRegionLoader.decodeRegion(rect, options);

                    // Next, create a scaled down version of the bitmap so that the blur radius
                    // looks appropriate (tempBitmap will likely be bigger than the final blurred
                    // bitmap, and thus the blur may look smaller if we just used tempBitmap as
                    // the final blurred bitmap).

                    // Note that image width should be a multiple of 4 to avoid
                    // issues with RenderScript allocations.
                    int scaledHeight = Math.max(2, MathUtil.floorEven(
                            mHeight / mBlurredSampleSize));
                    int scaledWidth = Math.max(4, MathUtil.roundMult4(
                            (int) (scaledHeight * mBitmapAspectRatio)));
                    Bitmap scaledBitmap = Bitmap.createScaledBitmap(
                            tempBitmap, scaledWidth, scaledHeight, true);

                    tempBitmap.recycle();

                    // And finally, create a blurred copy for each keyframe.
                    for (int f = 1; f <= mBlurKeyframes; f++) {
                        float desaturateAmount = mMaxGrey / 500f * f / mBlurKeyframes;
                        Bitmap blurredBitmap = blurrer.blurBitmap(
                                scaledBitmap, blurRadiusAtFrame(f), desaturateAmount);
                        mPictures[f] = new GLPicture(blurredBitmap);
                        blurredBitmap.recycle();
                    }

                    scaledBitmap.recycle();
                    blurrer.destroy();
                }
            }

            recomputeTransformMatrices();
            mCallbacks.requestRender();
        }

        private void recomputeTransformMatrices() {
            float screenToBitmapAspectRatio = mAspectRatio / mBitmapAspectRatio;

            // Ensure the bitmap is wider than the screen relatively by applying zoom
            // if necessary. Vary width but keep height the same.
            float zoom = Math.max(1f, 1.15f * screenToBitmapAspectRatio);

            // Total scale factors in both zoom and scale due to aspect ratio.
            float totalScale = zoom / screenToBitmapAspectRatio;

            mCurrentViewport.left = MathUtil.interpolate(
                    -1f * Math.min(1f, screenToBitmapAspectRatio), // remove screenToBitmapAspectRatio to unconstrain panning amount
                    1f * Math.min(1f, screenToBitmapAspectRatio),
                    mNormalOffsetX * (totalScale - 1) / totalScale);
            mCurrentViewport.right = mCurrentViewport.left + 2f / totalScale;
            mCurrentViewport.bottom = -1f / zoom;
            mCurrentViewport.top = 1f / zoom;

            float focusAmount = (mBlurKeyframes - mBlurAnimator.currentValue()) / mBlurKeyframes;
            if (mBlurRelatedToArtDetailMode && focusAmount > ) {
                RectF artDetailViewport = ArtDetailViewport.getInstance().getViewport(mId);
                if (artDetailViewport.width() == || artDetailViewport.height() == ) {
                    if (!mDemoMode && !mPreview) {
                        // reset art detail viewport
                        ArtDetailViewport.getInstance().setViewport(mId,
                                MathUtil.uninterpolate(-1, 1, mCurrentViewport.left),
                                MathUtil.uninterpolate(1, -1, mCurrentViewport.top),
                                MathUtil.uninterpolate(-1, 1, mCurrentViewport.right),
                                MathUtil.uninterpolate(1, -1, mCurrentViewport.bottom),
                                false);
                    }
                } else {
                    // interpolate
                    mCurrentViewport.left = MathUtil.interpolate(
                            mCurrentViewport.left,
                            MathUtil.interpolate(-1, 1, artDetailViewport.left),
                            focusAmount);
                    mCurrentViewport.top = MathUtil.interpolate(
                            mCurrentViewport.top,
                            MathUtil.interpolate(1, -1, artDetailViewport.top),
                            focusAmount);
                    mCurrentViewport.right = MathUtil.interpolate(
                            mCurrentViewport.right,
                            MathUtil.interpolate(-1, 1, artDetailViewport.right),
                            focusAmount);
                    mCurrentViewport.bottom = MathUtil.interpolate(
                            mCurrentViewport.bottom,
                            MathUtil.interpolate(1, -1, artDetailViewport.bottom),
                            focusAmount);
                }
            }

            Matrix.orthoM(mPMatrix, ,
                    mCurrentViewport.left, mCurrentViewport.right,
                    mCurrentViewport.bottom, mCurrentViewport.top,
                    1, 10);
        }

        public void drawFrame(float globalAlpha) {
            if (!mHasBitmap) {
                return;
            }

            Matrix.multiplyMM(mMVPMatrix, , mVMatrix, , mMMatrix, );
            Matrix.multiplyMM(mMVPMatrix, , mPMatrix, , mMVPMatrix, );

            float blurFrame = mBlurAnimator.currentValue();
            int lo = (int) Math.floor(blurFrame);
            int hi = (int) Math.ceil(blurFrame);

            float localHiAlpha = (blurFrame - lo);
            if (globalAlpha <= ) {
                // Nothing to draw
            } else if (lo == hi) {
                // Just draw one
                mPictures[lo].draw(mMVPMatrix, globalAlpha);
            } else if (globalAlpha == 1) {
                // Simple drawing
                mPictures[lo].draw(mMVPMatrix, 1);
                mPictures[hi].draw(mMVPMatrix, localHiAlpha);
            } else {
                // If there's both a global and local alpha, re-compose alphas, to
                // effectively compose hi and lo before composing the result
                // with the background.
                //
                // The math, where a1,a2 are previous alphas and b1,b2 are new alphas:
                //   b1 = a1 * (a2 - 1) / (a1 * a2 - 1)
                //   b2 = a1 * a2
                float newLocalLoAlpha = globalAlpha * (localHiAlpha - 1)
                        / (globalAlpha * localHiAlpha - 1);
                float newLocalHiAlpha = globalAlpha * localHiAlpha;
                mPictures[lo].draw(mMVPMatrix, newLocalLoAlpha);
                mPictures[hi].draw(mMVPMatrix, newLocalHiAlpha);
            }
        }

        public void destroyPictures() {
            for (int i = ; i < mPictures.length; i++) {
                if (mPictures[i] == null) {
                    continue;
                }

                mPictures[i].destroy();
                mPictures[i] = null;
            }
        }
    }

    public void destroy() {
        mCurrentGLPictureSet.destroyPictures();
        mNextGLPictureSet.destroyPictures();
    }

    public boolean isBlurred() {
        return mIsBlurred;
    }

    public void setIsBlurred(final boolean isBlurred, final boolean artDetailMode) {
        if (artDetailMode && !isBlurred && !mDemoMode && !mPreview) {
            // Reset art detail viewport
            ArtDetailViewport.getInstance().setViewport(, , , , , false);
            ArtDetailViewport.getInstance().setViewport(1, , , , , false);
        }

        mBlurRelatedToArtDetailMode = artDetailMode;
        mIsBlurred = isBlurred;
        mBlurAnimator.cancel();
        mBlurAnimator
                .to(isBlurred ? mBlurKeyframes : )
                .withDuration(BLUR_ANIMATION_DURATION * (mDemoMode ? 5 : 1))
                .withEndListener(new Runnable() {
                    @Override
                    public void run() {
                        if (isBlurred && artDetailMode) {
                            System.gc();
                        }
                    }
                })
                .start();
        mCallbacks.requestRender();
    }

    public static interface Callbacks {
        void requestRender();
    }

首先看到load方法,在一些读取文件和设置属性的操作后我们看到 ImageBlurrer,嘿嘿,主角终于出现了。

往后看,重点来了:

// And finally, create a blurred copy for each keyframe.
for (int f = 1; f <= mBlurKeyframes; f++) {
    float desaturateAmount = mMaxGrey / 500f * f / mBlurKeyframes;
    Bitmap blurredBitmap = blurrer.blurBitmap(
    scaledBitmap, blurRadiusAtFrame(f), desaturateAmount);
    mPictures[f] = new GLPicture(blurredBitmap);
    blurredBitmap.recycle();
}

And finally, create a blurred copy for each keyframe. 最终,把每一帧的模糊图像拷贝到数组mPictures中!大功告成!!在 drawFrame 方法中,你会看到如何将数组中的图像展示的,就不再赘述了。

那么剩下的横向来回移动呢?

请看到 DemoRenderController 类的 runAnimation 方法:

mCurrentScrollAnimator = ObjectAnimator.ofFloat(mRenderer, "normalOffsetX",mReverseDirection ? 1f : 0f, mReverseDirection ? 0f : 1f).setDuration(ANIMATION_CYCLE_TIME_MILLIS);
mCurrentScrollAnimator.start();
mCurrentScrollAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    super.onAnimationEnd(animation);
    mReverseDirection = !mReverseDirection;
    runAnimation();
    }
});

是不是比想象中的简单?一个移动动画再加一个监听就完成了~~..

至此功能解析基本结束,其实这个效果的实现最关键的是要掌握OPENGL的使用和动画的优化工作!再次感谢大神!!

欢迎任何问题和拍砖提问 :)