Я потратил много времени на это и у меня есть два разных решения, оба хороших ..
Во-первых, проблема:
1) Android загружает все изображения в оперативную память в несжатом растровом формате.
2) Android использует масштабирование ресурсов, поэтому на телефоне с дисплеем xxxhdpi (например, LG G3) каждый кадр занимает ТОНН пространства, поэтому у вас быстро заканчивается ОЗУ.
Решение № 1
1) Обходит масштабирование ресурсов Android. 2) Хранит байтовые массивы всех файлов в памяти (они маленькие, особенно для JPEG). 3) Создает растровые изображения покадрово, поэтому почти невозможно исчерпать ОЗУ.
Недостатки: он спамит ваши логи, поскольку Android выделяет память для новых растровых изображений и перерабатывает старые. Он также плохо работает на старых устройствах (Galaxy S1), но хорошо работает на современных бюджетных телефонах (читай: Alcatel C1 за 10 долларов, который я купил в BestBuy). Второе решение, приведенное ниже, работает лучше на старых устройствах, но при некоторых обстоятельствах может не хватить ОЗУ.
public class MyAnimationDrawable {
public static class MyFrame {
byte[] bytes;
int duration;
Drawable drawable;
boolean isReady = false;
}
public interface OnDrawableLoadedListener {
public void onDrawableLoaded(List<MyFrame> myFrames);
}
public static void loadRaw(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) {
loadFromXml(resourceId, context, onDrawableLoadedListener);
}
private static void loadFromXml(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) {
new Thread(new Runnable() {
@Override
public void run() {
final ArrayList<MyFrame> myFrames = new ArrayList<>();
XmlResourceParser parser = context.getResources().getXml(resourceId);
try {
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_DOCUMENT) {
} else if (eventType == XmlPullParser.START_TAG) {
if (parser.getName().equals("item")) {
byte[] bytes = null;
int duration = 1000;
for (int i=0; i<parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals("drawable")) {
int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
bytes = IOUtils.toByteArray(context.getResources().openRawResource(resId));
}
else if (parser.getAttributeName(i).equals("duration")) {
duration = parser.getAttributeIntValue(i, 1000);
}
}
MyFrame myFrame = new MyFrame();
myFrame.bytes = bytes;
myFrame.duration = duration;
myFrames.add(myFrame);
}
} else if (eventType == XmlPullParser.END_TAG) {
} else if (eventType == XmlPullParser.TEXT) {
}
eventType = parser.next();
}
}
catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
// Run on UI Thread
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
if (onDrawableLoadedListener != null) {
onDrawableLoadedListener.onDrawableLoaded(myFrames);
}
}
});
}
}).run();
}
public static void animateRawManually(int resourceId, final ImageView imageView, final Runnable onStart, final Runnable onComplete) {
loadRaw(resourceId, imageView.getContext(), new OnDrawableLoadedListener() {
@Override
public void onDrawableLoaded(List<MyFrame> myFrames) {
if (onStart != null) {
onStart.run();
}
animateRawManually(myFrames, imageView, onComplete);
}
});
}
public static void animateRawManually(List<MyFrame> myFrames, ImageView imageView, Runnable onComplete) {
animateRawManually(myFrames, imageView, onComplete, 0);
}
private static void animateRawManually(final List<MyFrame> myFrames, final ImageView imageView, final Runnable onComplete, final int frameNumber) {
final MyFrame thisFrame = myFrames.get(frameNumber);
if (frameNumber == 0) {
thisFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(thisFrame.bytes, 0, thisFrame.bytes.length));
}
else {
MyFrame previousFrame = myFrames.get(frameNumber - 1);
((BitmapDrawable) previousFrame.drawable).getBitmap().recycle();
previousFrame.drawable = null;
previousFrame.isReady = false;
}
imageView.setImageDrawable(thisFrame.drawable);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Make sure ImageView hasn't been changed to a different Image in this time
if (imageView.getDrawable() == thisFrame.drawable) {
if (frameNumber + 1 < myFrames.size()) {
MyFrame nextFrame = myFrames.get(frameNumber+1);
if (nextFrame.isReady) {
// Animate next frame
animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
}
else {
nextFrame.isReady = true;
}
}
else {
if (onComplete != null) {
onComplete.run();
}
}
}
}
}, thisFrame.duration);
// Load next frame
if (frameNumber + 1 < myFrames.size()) {
new Thread(new Runnable() {
@Override
public void run() {
MyFrame nextFrame = myFrames.get(frameNumber+1);
nextFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(nextFrame.bytes, 0, nextFrame.bytes.length));
if (nextFrame.isReady) {
// Animate next frame
animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
}
else {
nextFrame.isReady = true;
}
}
}).run();
}
}
}
** Решение № 2 **
Он загружает ресурс XML, анализирует его и загружает сырые ресурсы - тем самым обходя масштабирование ресурсов Android (которое отвечает за большинство исключений OutOfMemoryException), и создает AnimationDrawable.
Преимущества: лучше работает на старых устройствах (например, Galaxy S1)
Недостатки: может все еще не хватать ОЗУ, поскольку в нем хранятся все несжатые растровые изображения (но они меньше, потому что они не масштабируются так, как Android обычно масштабирует изображения)
public static void animateManuallyFromRawResource(int animationDrawableResourceId, ImageView imageView, Runnable onStart, Runnable onComplete) {
AnimationDrawable animationDrawable = new AnimationDrawable();
XmlResourceParser parser = imageView.getContext().getResources().getXml(animationDrawableResourceId);
try {
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_DOCUMENT) {
} else if (eventType == XmlPullParser.START_TAG) {
if (parser.getName().equals("item")) {
Drawable drawable = null;
int duration = 1000;
for (int i=0; i<parser.getAttributeCount(); i++) {
if (parser.getAttributeName(i).equals("drawable")) {
int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
byte[] bytes = IoUtils.readBytes(imageView.getContext().getResources().openRawResource(resId));
drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
}
else if (parser.getAttributeName(i).equals("duration")) {
duration = parser.getAttributeIntValue(i, 66);
}
}
animationDrawable.addFrame(drawable, duration);
}
} else if (eventType == XmlPullParser.END_TAG) {
} else if (eventType == XmlPullParser.TEXT) {
}
eventType = parser.next();
}
}
catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
if (onStart != null) {
onStart.run();
}
animateDrawableManually(animationDrawable, imageView, onComplete, 0);
}
private static void animateDrawableManually(final AnimationDrawable animationDrawable, final ImageView imageView, final Runnable onComplete, final int frameNumber) {
final Drawable frame = animationDrawable.getFrame(frameNumber);
imageView.setImageDrawable(frame);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Make sure ImageView hasn't been changed to a different Image in this time
if (imageView.getDrawable() == frame) {
if (frameNumber + 1 < animationDrawable.getNumberOfFrames()) {
// Animate next frame
animateDrawableManually(animationDrawable, imageView, onComplete, frameNumber + 1);
}
else {
// Animation complete
if (onComplete != null) {
onComplete.run();
}
}
}
}
}, animationDrawable.getDuration(frameNumber));
}
Если у вас все еще есть проблемы с памятью, используйте меньшие изображения ... или сохраните имя ресурса + длительность и сгенерируйте байтовый массив + Drawable в каждом кадре. Это почти наверняка приведет к слишком большому переключению между кадрами, но использует почти нулевую оперативную память.