Android中展示逐帧动画的最有效方法

17
我正在尝试通过在ImageView中更改图像来逐帧显示动画。我尝试了XML中的animation drawable以及在Handler内更改ImageView的位图。我还尝试将仅三个位图存储在ArrayList中(以避免内存不足),作为缓存机制,但改善很小。我需要迭代36个图像以完成完整的动画。我面临的问题是,在我使用的所有方法中,我无法在给定的50毫秒时间范围内完成动画。图像的大小从最小的250 kb到最大的540 kb不等。动画的fps非常低。由于应用程序的iOS版本已准备就绪,因此我受到显示与iOS版本一致的动画的限制。我在renderscript和opengl方面是新手。是否有任何方法可以在50-60ms内显示大型图像的平滑动画。任何提示或建议都将不胜感激。以下是动画的快照: 这里是图片link链接,供任何感兴趣的人使用。 enter image description here

1
你有考虑过使用带有TextureView的画布吗? - Whitney
我从未使用过Canvas或TextureView。Canvas是我可能解决方案列表中的一项。你有其他的想法吗? - Illegal Argument
它有什么问题?速度不够快吗?绘制不正确吗? - Whitney
我添加了另一个示例活动,并添加了有关图像的一些注释。使用较小的图像似乎可以使事情变得更容易。 - Matt
8个回答

9

我写了一个简单的活动,只做最基本的事情:

在一个线程中加载所有位图,然后每40毫秒将更改发布到ImageView。

package mk.testanimation;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;
import java.util.ArrayList;

public class MainActivity extends Activity {
    ImageView mImageView;

    private int mImageRes[] = new int[]{
        R.drawable.s0,
        R.drawable.s1,
        R.drawable.s2,
        R.drawable.s3,
        R.drawable.s4,
        R.drawable.s5,
        R.drawable.s6,
        R.drawable.s7,
        R.drawable.s8,
        R.drawable.s9,
        R.drawable.s10,
        R.drawable.s11,
        R.drawable.s12,
        R.drawable.s13,
        R.drawable.s14,
        R.drawable.s15,
        R.drawable.s16,
        R.drawable.s17,
        R.drawable.s18,
        R.drawable.s19,
        R.drawable.s20,
        R.drawable.s21,
        R.drawable.s22,
        R.drawable.s23,
        R.drawable.s24,
        R.drawable.s25,
        R.drawable.s26,
        R.drawable.s27,
        R.drawable.s28,
        R.drawable.s29,
        R.drawable.s30,
        R.drawable.s31,
        R.drawable.s32,
        R.drawable.s33,
        R.drawable.s34,
        R.drawable.s35,
    };

    private ArrayList<Bitmap> mBitmaps = new ArrayList<Bitmap>(mImageRes.length);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final Handler handler = new Handler();
        mImageView = new ImageView(this);
        setContentView(mImageView);
        Thread important = new Thread() {
            @Override
            public void run() {
                long timestamp = System.currentTimeMillis();
                for (int i = 0; i < mImageRes.length; i++) {
                    mBitmaps.add(BitmapFactory.decodeResource(getResources(), mImageRes[i]));
                }
                Log.d("ANIM-TAG", "Loading all bitmaps took " + (System.currentTimeMillis() - timestamp) + "ms");
                for (int i = 0; i < mBitmaps.size(); i++) {
                    final int idx = i;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mImageView.setImageBitmap(mBitmaps.get(idx));
                        }
                    }, i * 40);
                }
            }
        };
        important.setPriority(Thread.MAX_PRIORITY);
        important.start();
    }
}

在我的Nexus 7上看起来还不错,但加载所有位图需要超过4秒的时间。

你能提前加载位图吗?

另外,虽然不能节省很多空间,但是你的png图片周围有很多填充透明空间。你可以裁剪它们并减少一些内存。否则压缩图像也会有帮助(例如限制使用的颜色数量)。

理想情况下,在上述解决方案中,当位图不再使用时,应立即回收它们。

此外,如果内存占用过高,您可以像您所提到的那样使用位图缓冲区,但我相信它需要比3个图像更大。

祝你好运。

编辑:尝试2。 首先,我将所有图像裁剪为590x590。这减少了约1mb的图像大小。然后我创建了一个新类,该类有点“忙碌”,没有固定的帧速率,但在图像准备就绪后立即呈现它们:

package mk.testanimation;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.widget.ImageView;

import java.util.ArrayList;

public class MainActivity extends Activity {
    ImageView mImageView;

    private int mImageRes[] = new int[]{R.drawable.s0, R.drawable.s1, R.drawable.s2, R.drawable.s3, R.drawable.s4, R.drawable.s5, R.drawable.s6, R.drawable.s7, R.drawable.s8, R.drawable.s9, R.drawable.s10, R.drawable.s11, R.drawable.s12, R.drawable.s13, R.drawable.s14, R.drawable.s15, R.drawable.s16, R.drawable.s17, R.drawable.s18, R.drawable.s19, R.drawable.s20, R.drawable.s21, R.drawable.s22, R.drawable.s23, R.drawable.s24, R.drawable.s25, R.drawable.s26, R.drawable.s27, R.drawable.s28, R.drawable.s29, R.drawable.s30, R.drawable.s31, R.drawable.s32, R.drawable.s33, R.drawable.s34, R.drawable.s35};

    private ArrayList<Bitmap> mBitmaps = new ArrayList<Bitmap>(mImageRes.length);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final long timestamp = System.currentTimeMillis();
        final Handler handler = new Handler();
        mImageView = new ImageView(this);
        setContentView(mImageView);
        Thread important = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < mImageRes.length; i++) {
                    mBitmaps.add(BitmapFactory.decodeResource(getResources(), mImageRes[i]));
                }
            }
        };
        important.setPriority(Thread.MAX_PRIORITY);
        important.start();
        Thread drawing = new Thread() {
            @Override
            public void run() {
                int i = 0;
                while (i < mImageRes.length) {
                    if (i >= mBitmaps.size()) {
                        Thread.yield();
                    } else {
                        final Bitmap bitmap = mBitmaps.get(i);
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                mImageView.setImageBitmap(bitmap);
                            }
                        });
                        i++;
                    }
                }
                Log.d("ANIM-TAG", "Time to render all frames:" + (System.currentTimeMillis() - timestamp) + "ms");
            }
        };
        drawing.setPriority(Thread.MAX_PRIORITY);
        drawing.start();
    }
}

上述方法使得渲染几乎立即开始,在我2012年的Nexus 7上只花费不到4秒钟。

最后,我将所有图片转换为8位PNG格式而非32位。这将渲染时间缩短至不到2秒钟!

我敢打赌,你最终使用的任何解决方案都会受益于尽可能地减小图片大小。

再次祝好运!


感谢您的回答。然而,这不是我正在寻找的解决方案。对此很抱歉。问题在于我需要在不到3秒的时间内消耗超过16 MB的数据。我之前尝试将类似的代码插入自定义ImageView中,结果效果更好,几乎只需3秒钟。但第一张图片显示的时间更长。在所有当前的答案中,您的答案提供了一些试验性的东西,我会记住的。谢谢。 - Illegal Argument
1
检查我的编辑,它有更多的建议,特别是减小图像的大小。对我来说少于2秒就可以工作! - Matt
请注意,阅读此帖子的每个人都应该知道它并没有解决我的问题。然而这是唯一一个有展示出一些解决问题的努力的答案。 - Illegal Argument
感谢您接受我的答案。您更新了图片并尝试了上述方法吗?它们应该在3秒内都能实现动画效果,我认为这是您的要求。 - Matt
不,我没有。对此感到很抱歉。在早期版本的动画试验中,我曾使用类似的代码。目前,我正在尝试使用NDK,但我还是个新手。由于管理层的决定,我不能缩小图像,而且该应用程序可在iPhone上使用。我为什么没有尝试你的代码是因为在展示这个动画之后,我还需要执行另一个更复杂的动画,类似于硬币翻转效果。无论如何,感谢您提供的试用品。我一定会回答这个问题并让您知道的。 - Illegal Argument

3

我遇到了同样的问题,通过重写AnimationDrawable来解决了它。所以,如果问题是由于数组中的图片太大而导致内存不足无法加载所有图片,那么可以在需要时再加载图片。 我的AnimationDrawable代码如下:

public abstract class MyAnimationDrawable extends AnimationDrawable {

    private Context context;
    private int current;
    private int reqWidth;
    private int reqHeight;
    private int totalTime;

    public MyAnimationDrawable(Context context, int reqWidth, int reqHeight) {
        this.context = context;
        this.current = 0;
        //In my case size of screen to scale Drawable
        this.reqWidth = reqWidth;
        this.reqHeight = reqHeight;
        this.totalTime = 0;
    }

    @Override
    public void addFrame(Drawable frame, int duration) {
        super.addFrame(frame, duration);
        totalTime += duration;
    }

    @Override
    public void start() {
        super.start();
        new Handler().postDelayed(new Runnable() {

            public void run() {
                onAnimationFinish();
            }
        }, totalTime);
    }

    public int getTotalTime() {
        return totalTime;
    }

    @Override
    public void draw(Canvas canvas) {
        try {
            //Loading image from assets, you could make it from resources 
            Bitmap bmp = BitmapFactory.decodeStream(context.getAssets().open("presentation/intro_000"+(current < 10 ? "0"+current : current)+".jpg"));
            //Scaling image to fitCenter
            Matrix m = new Matrix();
            m.setRectToRect(new RectF(0, 0, bmp.getWidth(), bmp.getHeight()), new RectF(0, 0, reqWidth, reqHeight), Matrix.ScaleToFit.CENTER);
            bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
            //Calculating the start 'x' and 'y' to paint the Bitmap 
            int x = (reqWidth - bmp.getWidth()) / 2;
            int y = (reqHeight - bmp.getHeight()) / 2;
            //Painting Bitmap in canvas
            canvas.drawBitmap(bmp, x, y, null);
            //Jump to next item
            current++;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    abstract void onAnimationFinish();

}

现在,若要播放动画,请按照以下步骤操作:
    //Get your ImageView
    View image = MainActivity.this.findViewById(R.id.presentation);

    //Create AnimationDrawable
    final AnimationDrawable animation = new MyAnimationDrawable(this, displayMetrics.widthPixels, displayMetrics.heightPixels) {

        @Override
        void onAnimationFinish() {
            //Do something when finish animation
        }

    };
    animation.setOneShot(true); //dont repeat animation
    //This is just to say that my AnimationDrawable has 72 frames with 50 milliseconds interval
    try {
        //Always load same bitmap, anyway you load the right one in draw() method in MyAnimationDrawable
        Bitmap bmp = BitmapFactory.decodeStream(MainActivity.this.getAssets().open("presentation/intro_00000.jpg"));
        for (int i = 0; i < 72; i++) {
            animation.addFrame(new BitmapDrawable(getResources(), bmp), 50);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }

    //Set AnimationDrawable to ImageView
    if (Build.VERSION.SDK_INT < 16) {
        image.setBackgroundDrawable(animation);
    } else {
        image.setBackground(animation);
    }

    //Start animation
    image.post(new Runnable() {

        @Override
        public void run() {
            animation.start();
        }

    });

这就是所有的内容,对我的工作来说都可以正常运行!

1

尝试为您的ImageView使用AnimationDrawable。 例如,请参见此处中的答案。


我已经使用了AnimationDrawable,但它需要近8秒钟的时间来加载,即使在像Nexus 5这样的高端设备上也会丢失帧。这是因为我正在使用大型图像文件。很抱歉,这并没有解决我的问题。 - Illegal Argument

1
在Android中展示逐帧动画最高效的方式是使用OpenGLNDK

我有点了解,但在NDK和openGl方面是个新手。就记录而言,我尝试过在opengl中实现,但没有成功,因为opengl只适用于精灵动画(小图像)。这是我认为的。 - Illegal Argument
这是在Android中最可能的解决方案 :) 尝试重新绘制(更新)仅更改的图片部分,不要尝试为每个帧重新绘制整个图片。 - Pavel
那可能是一个选项,但你可以看到图层是从中心向外辐射状地添加的。此外,这些图像还有阴影。我和我的设计师谈过,他听到这个想法后非常生气(哈哈)。很高兴知道我们的想法相似。 - Illegal Argument
@IllegalArgument 如果还没有这样的工具,我会编写一个计算OpenGL下一帧必须重新绘制的区域的工具。即使图层是从上到下辐射向外添加的,也可以考虑这个选项。 - Pavel
我正在尝试在ndk中完成它,但是任何公式都不适用于我的情况,因为帧与图像之间的相似性非常少,并且卡片之间的层添加和阴影会导致问题。此外,这是基本动画,之后我还要展示卡片翻转动画。我已经完成了那个,现在卡在这一个上了。 - Illegal Argument

0
使用编程创建一个imageView,然后旋转它并使另一个imageView旋转。根据您想要显示的imageView数量进行操作。您只需要为其添加一张图片即可。 您可以像这样旋转图像..
Matrix matrix=new Matrix();
imageView.setScaleType(ScaleType.MATRIX);   //required
matrix.postRotate((float) angle, pivX, pivY);
imageView.setImageMatrix(matrix);

0

从 assets 中加载位图而不是资源,为我节省了 40% 的解码时间。你可能想试试。

因此,在我的2012年版Nexus 7上,这个过程的时间从使用资源的5秒变成了3秒:

long time = System.currentTimeMillis();
try {
    for (int i = 0; i < 35; i++) {
        InputStream assetStream = getAssets().open(
                "_step" + (i + 1) + ".png");
        try {
            Bitmap bitmap = BitmapFactory.decodeStream(assetStream);
            if (bitmap == null) {
                throw new RuntimeException("Could not load bitmap");
            }
            mBitmaps.add(bitmap);
        } finally {
            assetStream.close();
        }
    }
} catch (Exception e) {
    throw new RuntimeException(e);
}
Log.d("ANIM", "Loading bitmaps elapsed "+(System.currentTimeMillis() - time)+"ms");

0
也许这个回答有点傻,但它可能会对你有所帮助。使用另一个工具制作动画并将其保存为高质量视频,然后只需播放该视频即可。

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接