虚化梦幻背景+自动来回移动动画效果
原文链接 http://www.rogerblog.cn/2015/06/25/e8-99-9a-e5-8c-96-e6-a2-a6-e5-b9-bb-e8-83-8c-e6-99-af-e8-87-aa-e5-8a-a8-e6-9d-a5-e5-9b-9e-e7-a7-bb-e5-8a-a8-e5-8a-a8-e7-94-bb-e6-95-88-e6-9e-9c/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
必须说写博客是一项非常需要毅力的事情,这两月稍微忙一点就完全忘了这茬了,罪过罪过。
今天解析一个 虚化梦幻背景+自动来回移动动画 的效果,这个动画也是从Muzei中提取出来了~感谢大神!!
首先上效果图:
MissView
背景中的动态模糊和自动移动 就是效果啦..第一次看到是不是有点酷炫呢? 让我弱弱的来分析一下源码。源码地址:https://github.com/Rogero0o/MissView
包结构如下:
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的使用和动画的优化工作!再次感谢大神!!
欢迎任何问题和拍砖提问 :)