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

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

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

首先上效果图:

blob.png

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

包结构如下:
blob.png

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, 0, 0);
        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, 0, 0);//设置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 = 0;
    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(0, 0, 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\[0\] = new GLPicture(bitmapRegionLoader, mHeight);
            if (mMaxPrescaledBlurPixels == 0) {
                for (int f = 1; f & lt; = mBlurKeyframes; f++) {
                    mPictures\[f\] = mPictures\[0\];
                }
            } 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(0, 0, 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 & lt; = 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), 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 & amp; & amp; focusAmount & gt; 0) {
            RectF artDetailViewport = ArtDetailViewport.getInstance().getViewport(mId);
            if (artDetailViewport.width() == 0 || artDetailViewport.height() == 0) {
                if (!mDemoMode & amp; & amp; ! 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, 0, mCurrentViewport.left, mCurrentViewport.right, mCurrentViewport.bottom, mCurrentViewport.top, 1, 10);
    }
    public void drawFrame(float globalAlpha) {
        if (!mHasBitmap) {
            return;
        }
        Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mPMatrix, 0, mMVPMatrix, 0);
        float blurFrame = mBlurAnimator.currentValue();
        int lo = (int) Math.floor(blurFrame);
        int hi = (int) Math.ceil(blurFrame);
        float localHiAlpha = (blurFrame - lo);
        if (globalAlpha & lt; = 0) {
            // 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 = 0; i & lt; mPictures.length; i++) {
            if (mPictures\[i\] == null) {
                continue;
            }
            mPictures\[i\].destroy();
            mPictures\[i\] = null;
        }
    }
}

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

往后看,重点来了:

// And finally, create a blurred copy for each keyframe.
for (int f = 1; f & lt; = 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的使用和动画的优化工作!再次感谢大神!!

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