android - para - stop motion shop app
Causar OutOfMemoryError en la animación fotograma a fotograma en Android (10)
Estoy teniendo muchas imágenes como marcos en mi carpeta resources / drawable (digamos aproximadamente 200). Y usando estas imágenes quiero ejecutar una animación. La animación más larga es de 80 marcos. Con éxito puedo ejecutar la animación al hacer clic en los botones para algunos, pero para algunas animaciones me está dando OutOfMemoryError diciendo que VM no puede proporcionar dicha memoria. Está fuera de VM Budget. Cuento que el tamaño de todas las imágenes es de aproximadamente 10MB. El tamaño de cada imagen es 320x480 en píxeles.
Intento buscar en Google y descubrí que necesito llamar explícitamente al recolector de basura utilizando el método System.gc (). Lo he hecho, pero todavía estoy recibiendo un error de memoria de tiempo. ¿Puede alguien por favor amablemente ayudarme en esto?
Algún Código: -
ImageView img = (ImageView)findViewById(R.id.xxx);
img.setBackgroundResource(R.anim.angry_tail_animation);
AnimationDrawable mailAnimation = (AnimationDrawable) img.getBackground();
MediaPlayer player = MediaPlayer.create(this.getApplicationContext(), R.raw.angry);
if(mailAnimation.isRunning()) {
mailAnimation.stop();
mailAnimation.start();
if (player.isPlaying()) {
player.stop();
player.start();
}
else {
player.start();
}
}
else {
mailAnimation.start();
if (player.isPlaying()) {
player.stop();
player.start();
}
else {
player.start();
}
}
Este es el código que he escrito al hacer clic en un Botón .....
Archivo de recursos dentro de res / dibujable / anim
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" >
<item android:drawable="@drawable/cat_angry0000" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0001" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0002" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0003" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0004" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0005" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0006" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0007" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0008" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0009" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0010" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0011" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0012" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0013" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0014" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0015" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0016" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0017" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0018" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0019" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0020" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0021" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0022" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0023" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0024" android:duration="50"/>
<item android:drawable="@drawable/cat_angry0025" android:duration="50"/>
</animation-list>
** Lo anterior es el archivo de recursos utilizado en setBackgroundResource, del mismo modo que tengo 10 archivos más para otra animación diferente. **
Registro de errores
01-16 22:23:41.594: E/AndroidRuntime(399): FATAL EXCEPTION: main
01-16 22:23:41.594: E/AndroidRuntime(399): java.lang.IllegalStateException: Could not execute method of the activity
01-16 22:23:41.594: E/AndroidRuntime(399): at android.view.View$1.onClick(View.java:2144)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.view.View.performClick(View.java:2485)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.view.View$PerformClick.run(View.java:9080)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.os.Handler.handleCallback(Handler.java:587)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.os.Handler.dispatchMessage(Handler.java:92)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.os.Looper.loop(Looper.java:123)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.app.ActivityThread.main(ActivityThread.java:3683)
01-16 22:23:41.594: E/AndroidRuntime(399): at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
01-16 22:23:41.594: E/AndroidRuntime(399): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
01-16 22:23:41.594: E/AndroidRuntime(399): at dalvik.system.NativeStart.main(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.reflect.InvocationTargetException
01-16 22:23:41.594: E/AndroidRuntime(399): at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.view.View$1.onClick(View.java:2139)
01-16 22:23:41.594: E/AndroidRuntime(399): ... 11 more
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:460)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:697)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.content.res.Resources.loadDrawable(Resources.java:1709)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.drawable.AnimationDrawable.inflate(AnimationDrawable.java:267)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.drawable.Drawable.createFromXmlInner(Drawable.java:787)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.graphics.drawable.Drawable.createFromXml(Drawable.java:728)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.content.res.Resources.loadDrawable(Resources.java:1694)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399): at android.view.View.setBackgroundResource(View.java:7533)
01-16 22:23:41.594: E/AndroidRuntime(399): at talking.cat.CatActivity.middleButtonClicked(CatActivity.java:83)
De la misma manera tengo diferentes botones para diferentes animaciones ... Gracias
Creé una clase de animación que muestra marcos basados en los recursos transferibles transferibles y las duraciones de los marcos.
protected class SceneAnimation{
private ImageView mImageView;
private int[] mFrameRess;
private int[] mDurations;
private int mDuration;
private int mLastFrameNo;
private long mBreakDelay;
public SceneAnimation(ImageView pImageView, int[] pFrameRess, int[] pDurations){
mImageView = pImageView;
mFrameRess = pFrameRess;
mDurations = pDurations;
mLastFrameNo = pFrameRess.length - 1;
mImageView.setImageResource(mFrameRess[0]);
play(1);
}
public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration){
mImageView = pImageView;
mFrameRess = pFrameRess;
mDuration = pDuration;
mLastFrameNo = pFrameRess.length - 1;
mImageView.setImageResource(mFrameRess[0]);
playConstant(1);
}
public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration, long pBreakDelay){
mImageView = pImageView;
mFrameRess = pFrameRess;
mDuration = pDuration;
mLastFrameNo = pFrameRess.length - 1;
mBreakDelay = pBreakDelay;
mImageView.setImageResource(mFrameRess[0]);
playConstant(1);
}
private void play(final int pFrameNo){
mImageView.postDelayed(new Runnable(){
public void run() {
mImageView.setImageResource(mFrameRess[pFrameNo]);
if(pFrameNo == mLastFrameNo)
play(0);
else
play(pFrameNo + 1);
}
}, mDurations[pFrameNo]);
}
private void playConstant(final int pFrameNo){
mImageView.postDelayed(new Runnable(){
public void run() {
mImageView.setImageResource(mFrameRess[pFrameNo]);
if(pFrameNo == mLastFrameNo)
playConstant(0);
else
playConstant(pFrameNo + 1);
}
}, pFrameNo==mLastFrameNo && mBreakDelay>0 ? mBreakDelay : mDuration);
}
};
Se usa así:
private ImageView mTapScreenTextAnimImgView;
private final int[] mTapScreenTextAnimRes = {R.drawable.tap0001_b, R.drawable.tap0002_b, R.drawable.tap0003_b,
R.drawable.tap0004_b, R.drawable.tap0005_b, R.drawable.tap0006_b, R.drawable.tap0005_b, R.drawable.tap0004_b,
R.drawable.tap0003_b, R.drawable.tap0002_b, R.drawable.tap0001_b};
private final int mTapScreenTextAnimDuration = 100;
private final int mTapScreenTextAnimBreak = 500;
y en onCreate:
mTapScreenTextAnimImgView = (ImageView) findViewById(R.id.scene1AnimBottom);
new SceneAnimation(mTapScreenTextAnimImgView, mTapScreenTextAnimRes, mTapScreenTextAnimDuration, mTapScreenTextAnimBreak);
Pasé mucho tiempo con esto y tengo dos soluciones diferentes, ambas buenas ...
Primero, el problema: 1) Android carga todas las imágenes en la RAM, en formato de mapa de bits sin comprimir. 2) Android usa escalado de recursos, por lo que en un teléfono con una pantalla xxxhdpi (como LG G3), cada cuadro ocupa una TONELADA de espacio, por lo que rápidamente se queda sin memoria RAM.
Solución n. ° 1
1) Pasa por alto el escalado de recursos de Android. 2) Almacena bytearrays de todos los archivos en la memoria (estos son pequeños, especialmente para archivos JPEG). 3) Genera Bitmaps cuadro por cuadro, por lo que es casi imposible quedarse sin RAM.
Desventajas: espacia sus registros ya que Android asigna memoria para nuevos mapas de bits y recicla los viejos. También funciona mal en dispositivos más antiguos (Galaxy S1), pero tiene un buen rendimiento en los teléfonos económicos actuales (léase: Alcatel C1 de $ 10 recogido en BestBuy). La segunda solución a continuación funciona mejor en dispositivos más antiguos, pero podría agotarse en algunas circunstancias.
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();
}
}
}
** Solución # 2 **
Carga el recurso XML, lo analiza y carga los recursos sin procesar, lo que evita el escalado de recursos de Android (que es responsable de la mayoría de OutOfMemoryExceptions) y crea un AnimationDrawable.
Ventajas: se desempeña mejor en dispositivos más antiguos (por ejemplo, Galaxy S1)
Desventajas: Aún se puede quedar sin RAM, ya que mantiene todos los mapas de bits descomprimidos en la memoria (pero son más pequeños porque no tienen la escala de la forma en que normalmente escalan las imágenes)
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));
}
Si todavía tiene problemas de memoria, use imágenes más pequeñas ... o almacene el nombre del recurso + duración, y genere la matriz de bytes + Drawable en cada cuadro. Eso casi con certeza causará demasiados cortes entre fotogramas, pero usa RAM casi nula.
Supongo que las imágenes de tu marco de animación están comprimidas (PNG o JPG). El tamaño comprimido no es útil para calcular cuánta memoria se necesita para visualizarlos. Para eso, debes pensar en el tamaño sin comprimir. Este será el número de píxeles (320x480) multiplicado por el número de bytes por píxel, que generalmente es de 4 (32 bits). Para sus imágenes, entonces, cada una tendrá 614,400 bytes. Para el ejemplo de animación de 26 cuadros que proporcionó, requerirá un total de 15,974,400 bytes para contener los datos brutos del mapa de bits para todos los cuadros, sin contar la sobrecarga del objeto.
Al observar el código fuente de AnimationDrawable
, parece cargar todos los cuadros en la memoria a la vez, lo que básicamente tendría que hacer para un buen rendimiento.
Si puede asignar tanta memoria o no depende mucho del sistema. Al menos recomendaría probar esto en un dispositivo real en lugar del emulador. También puede intentar ajustar el tamaño de RAM disponible del emulador, pero esto es solo una adivinanza.
Hay formas de utilizar BitmapFactory.inPreferredConfig
para cargar mapas de bits en un formato más eficiente en memoria como RGB 565 (en lugar de ARGB 8888). Esto ahorraría algo de espacio, pero aún así podría no ser suficiente.
Si no puede asignar esa cantidad de memoria a la vez, debe considerar otras opciones. La mayoría de las aplicaciones de gráficos de alto rendimiento (por ejemplo, juegos) dibujan sus gráficos a partir de combinaciones de gráficos más pequeños (sprites) o primitivas 2D o 3D (rectángulos, triángulos). Dibujar un mapa de bits de pantalla completa para cada cuadro es efectivamente lo mismo que representar un video; no necesariamente el más eficiente.
¿El contenido completo de tu animación cambia con cada fotograma? Otra optimización podría ser animar solo la parte que realmente cambia, y cortar sus bitmaps para dar cuenta de eso.
Para resumir, necesitas encontrar una manera de dibujar tu animación usando menos memoria. Hay muchas opciones, pero depende mucho de cómo debe verse su animación.
Transmití una solución a Xamarin Android e hice algunas mejoras.
Funciona bien con los cambios de orientación y especialmente con imágenes de aproximadamente 300 de ancho y alto (cuanto más grande es la imagen, más tiempo se tarda en cargar la imagen, mayor es el parpadeo).
using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Widget;
using System;
namespace ...Droid.Util
{
public class FramesSequenceAnimation
{
private int[] animationFrames;
private int currentFrame;
private bool shouldRun; // true if the animation should continue running. Used to stop the animation
private bool isRunning; // true if the animation currently running. prevents starting the animation twice
private ImageView imageview;
private Handler handler;
private int delayMillis;
private bool oneShot = false;
private FramesSequenceAnimationListener onAnimationStoppedListener;
private Bitmap bitmap = null;
private BitmapFactory.Options bitmapOptions;
private Action action;
private static object Lock = new object();
public interface FramesSequenceAnimationListener
{
void AnimationStopped();
}
public void SetFramesSequenceAnimationListener(FramesSequenceAnimationListener onAnimationStoppedListener)
{
this.onAnimationStoppedListener = onAnimationStoppedListener;
}
public int GetCurrentFrame()
{
return currentFrame;
}
public void SetCurrentFrame(int currentFrame)
{
this.currentFrame = currentFrame;
}
public FramesSequenceAnimation(FramesSequenceAnimationListener onAnimationStoppedListener, ImageView imageview, int[] animationFrames, int fps)
{
this.onAnimationStoppedListener = onAnimationStoppedListener;
this.imageview = imageview;
this.animationFrames = animationFrames;
delayMillis = 1000 / fps;
currentFrame = -1;
shouldRun = false;
isRunning = false;
handler = new Handler();
imageview.SetImageResource(this.animationFrames[0]);
//// use in place bitmap to save GC work (when animation images are the same size & type)
//if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
//{
// Bitmap bmp = ((BitmapDrawable)imageview.Drawable).Bitmap;
// int width = bmp.Width;
// int height = bmp.Height;
// Bitmap.Config config = bmp.GetConfig();
// bitmap = Bitmap.CreateBitmap(width, height, config);
// bitmapOptions = new BitmapFactory.Options(); // setup bitmap reuse options
// bitmapOptions.InBitmap = bitmap; // reuse this bitmap when loading content
// bitmapOptions.InMutable = true;
// bitmapOptions.InSampleSize = 1;
//}
bitmapOptions = newOptions();
bitmap = decode(bitmapOptions, getNext());
bitmapOptions.InBitmap = bitmap;
}
private BitmapFactory.Options newOptions()
{
BitmapFactory.Options options = new BitmapFactory.Options();
options.InSampleSize = 1;
options.InMutable = true;
options.InJustDecodeBounds = true;
options.InPurgeable = true;
options.InInputShareable = true;
options.InPreferredConfig = Bitmap.Config.Rgb565;
return options;
}
private Bitmap decode(BitmapFactory.Options options, int imageRes)
{
return BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
}
public void SetOneShot(bool oneShot)
{
this.oneShot = oneShot;
}
private int getNext()
{
currentFrame++;
if (currentFrame >= animationFrames.Length)
{
if (oneShot)
{
shouldRun = false;
currentFrame = animationFrames.Length - 1;
}
else
{
currentFrame = 0;
}
}
return animationFrames[currentFrame];
}
public void stop()
{
lock (Lock)
{
shouldRun = false;
}
}
public void start()
{
lock (Lock)
{
shouldRun = true;
if (isRunning)
{
return;
}
Action tempAction = new Action(delegate
{
if (!shouldRun || imageview == null)
{
isRunning = false;
if (onAnimationStoppedListener != null)
{
onAnimationStoppedListener.AnimationStopped();
onAnimationStoppedListener = null;
handler.RemoveCallbacks(action);
}
return;
}
isRunning = true;
handler.PostDelayed(action, delayMillis);
if (imageview.IsShown)
{
int imageRes = getNext();
if (bitmap != null)
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
{
if (bitmap != null && !bitmap.IsRecycled)
{
bitmap.Recycle();
bitmap = null;
}
}
try
{
bitmap = BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
}
catch (Exception e)
{
bitmap.Recycle();
bitmap = null;
Console.WriteLine("Exception: " + e.StackTrace);
}
if (bitmap != null)
{
imageview.SetImageBitmap(bitmap);
}
else
{
imageview.SetImageResource(imageRes);
bitmap.Recycle();
bitmap = null;
}
}
else
{
imageview.SetImageResource(imageRes);
}
}
});
action = tempAction;
handler.Post(action);
}
}
}
}
This is my splash screen class: (this class reads the images from the drawable folder that are named "splash_0001, splash_0002 ...". So no need to name your image resources on an array. Increase the number of frames per second (FPS) to speed up the animation).
using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using ...Droid.Base;
using ...Droid.Util;
using System;
using System.Collections.Generic;
using static ...Util.FramesSequenceAnimation;
namespace ...Droid.Activities
{
[Activity(MainLauncher = true)]
public class SplashActivity : BaseActivity, FramesSequenceAnimationListener
{
private FramesSequenceAnimation framesSequenceAnimation;
private const string
IMAGE_NAME_PREFIX = "splash_",
KEY_CURRENT_FRAME = "key_current_frame";
private int FPS = 50;
private int numberOfImages;
protected override OrientationEnum GetOrientation()
{
return OrientationEnum.ORIENTATION_CHECK_DEVICE_SIZE;
}
protected override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_splash);
RelativeLayout background = FindViewById<RelativeLayout>(Resource.Id.splash_background);
background.Click += Click;
ImageView imageView = FindViewById<ImageView>(Resource.Id.splash_imageview);
imageView.Click += Click;
numberOfImages = GetSplashImagesCount();
framesSequenceAnimation = new FramesSequenceAnimation(this, imageView, GetImageResourcesIDs(), FPS);
framesSequenceAnimation.SetOneShot(true);
if (savedInstanceState != null)
{
int currentFrame = savedInstanceState.GetInt(KEY_CURRENT_FRAME) + 1;
if (currentFrame < numberOfImages)
{
framesSequenceAnimation.SetCurrentFrame(currentFrame);
}
}
framesSequenceAnimation.start();
}
private int[] GetImageResourcesIDs()
{
List<int> list = new List<int>();
for (int i = 1; i <= numberOfImages; i++)
{
var image_name = IMAGE_NAME_PREFIX + i.ToString().PadLeft(4, ''0'');
int resID = Resources.GetIdentifier(image_name, "drawable", PackageName);
list.Add(resID);
}
return list.ToArray();
}
private int GetSplashImagesCount()
{
// Count number of images in drawable folder
int count = 0;
var fields = typeof(Resource.Drawable).GetFields();
foreach (var field in fields)
{
if (field.Name.StartsWith(IMAGE_NAME_PREFIX))
{
count++;
}
}
return count;
}
private void Click(object sender, EventArgs e)
{
framesSequenceAnimation.SetFramesSequenceAnimationListener(null);
GoToLoginScreen();
}
private void GoToLoginScreen()
{
Finish();
StartActivity(new Intent(this, typeof(LoginActivity)));
OverridePendingTransition(0, Resource.Animation.abc_fade_out);
}
void FramesSequenceAnimationListener.AnimationStopped()
{
GoToLoginScreen();
}
protected override void OnSaveInstanceState(Bundle outState)
{
base.OnSaveInstanceState(outState);
outState.PutInt(KEY_CURRENT_FRAME, framesSequenceAnimation.GetCurrentFrame());
}
}
}
Tuve este problema y lo resolví haciendo las dos cosas siguientes:
- Corta la resolución de las imágenes de animación por la mitad ... 1/4 del tamaño en bytes sin comprimir.
- Coloque las imágenes en la carpeta drawable-nodpi para que Android no las amplíe.
Mi animación todavía no se cargaba en algunos teléfonos después de hacer el paso 1. El paso 2 lo hizo funcionar en esos teléfonos.
Espero que esto le ahorre a alguien más en algún momento.
EDITAR: Todavía estaba experimentando bloqueos después de ir a la actividad que reproduce el disco de animación, pero lo tengo trabajando ahora. Estas son las cosas adicionales que hice:
- No use una lista de animación en xml. En su lugar, crea AnimationDrawable cada vez que necesites usarlo. De lo contrario, la próxima vez que cargue la animación dibujable del recurso, todavía intentará usar los mapas de bits que termine reciclando.
- Recicle los mapas de bits en AnimationDrawable cuando haya terminado de usarlo. Esta es la magia que libera la memoria.
- Utilice el Monitor del dispositivo Android para supervisar los bytes asignados en su pila.
Aquí está el código que estoy usando para crear el AnimationDrawable:
protected AnimationDrawable CreateLoadingAnimationDrawable()
{
AnimationDrawable animation = new AnimationDrawable ();
animation.OneShot = false;
for (int i = 0; i < kNumberOfFrames; ++i) {
int index = (i * 2) + 1;
string stringIndex = index.ToString ("00");
string bitmapStringId = kBaseAnimationName + stringIndex;
int resID = this.Resources.GetIdentifier (bitmapStringId, "drawable", this.PackageName);
Bitmap bitmap = BitmapFactory.DecodeResource (this.Resources, resID);
BitmapDrawable frame = new BitmapDrawable (bitmap);
//Drawable frame = Resources.GetDrawable (resID);
animation.AddFrame (frame, 111);
}
return animation;
}
Y codifique para liberar los bitmaps cuando termine de usarlos. Puedes hacer esto en OnPause o OnDestroy. _loadingAnimation es mi AnimationDrawable creado anteriormente. Me encantaría saber qué hace SetCallback () por usted en este caso. Acabo de copiar eso de otro lado en SO.
if (_loadingAnimation != null) {
_loadingAnimation.Stop ();
_loadingImageView.SetBackgroundResource (Resource.Drawable.loading_anim_full7001);
for (int i = 0; i < _loadingAnimation.NumberOfFrames; ++i) {
BitmapDrawable frame = _loadingAnimation.GetFrame (i) as BitmapDrawable;
if (frame != null) {
Android.Graphics.Bitmap bitmap = frame.Bitmap;
bitmap.Recycle ();
frame.SetCallback(null);
}
}
_loadingAnimation.SetCallback(null);
_loadingAnimation = null;
}
Ted
Yo tuve el mismo problema. Android carga todos los dibujables a la vez, por lo que la animación con muchos fotogramas causa este error.
Terminé creando mi propia animación de secuencia simple:
public class AnimationsContainer {
public int FPS = 30; // animation FPS
// single instance procedures
private static AnimationsContainer mInstance;
private AnimationsContainer() {
};
public static AnimationsContainer getInstance() {
if (mInstance == null)
mInstance = new AnimationsContainer();
return mInstance;
}
// animation progress dialog frames
private int[] mProgressAnimFrames = { R.drawable.logo_30001, R.drawable.logo_30002, R.drawable.logo_30003 };
// animation splash screen frames
private int[] mSplashAnimFrames = { R.drawable.logo_ding200480001, R.drawable.logo_ding200480002 };
/**
* @param imageView
* @return progress dialog animation
*/
public FramesSequenceAnimation createProgressDialogAnim(ImageView imageView) {
return new FramesSequenceAnimation(imageView, mProgressAnimFrames);
}
/**
* @param imageView
* @return splash screen animation
*/
public FramesSequenceAnimation createSplashAnim(ImageView imageView) {
return new FramesSequenceAnimation(imageView, mSplashAnimFrames);
}
/**
* AnimationPlayer. Plays animation frames sequence in loop
*/
public class FramesSequenceAnimation {
private int[] mFrames; // animation frames
private int mIndex; // current frame
private boolean mShouldRun; // true if the animation should continue running. Used to stop the animation
private boolean mIsRunning; // true if the animation currently running. prevents starting the animation twice
private SoftReference<ImageView> mSoftReferenceImageView; // Used to prevent holding ImageView when it should be dead.
private Handler mHandler;
private int mDelayMillis;
private OnAnimationStoppedListener mOnAnimationStoppedListener;
private Bitmap mBitmap = null;
private BitmapFactory.Options mBitmapOptions;
public FramesSequenceAnimation(ImageView imageView, int[] frames, int fps) {
mHandler = new Handler();
mFrames = frames;
mIndex = -1;
mSoftReferenceImageView = new SoftReference<ImageView>(imageView);
mShouldRun = false;
mIsRunning = false;
mDelayMillis = 1000 / fps;
imageView.setImageResource(mFrames[0]);
// use in place bitmap to save GC work (when animation images are the same size & type)
if (Build.VERSION.SDK_INT >= 11) {
Bitmap bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
int width = bmp.getWidth();
int height = bmp.getHeight();
Bitmap.Config config = bmp.getConfig();
mBitmap = Bitmap.createBitmap(width, height, config);
mBitmapOptions = new BitmapFactory.Options();
// setup bitmap reuse options.
mBitmapOptions.inBitmap = mBitmap;
mBitmapOptions.inMutable = true;
mBitmapOptions.inSampleSize = 1;
}
}
private int getNext() {
mIndex++;
if (mIndex >= mFrames.length)
mIndex = 0;
return mFrames[mIndex];
}
/**
* Starts the animation
*/
public synchronized void start() {
mShouldRun = true;
if (mIsRunning)
return;
Runnable runnable = new Runnable() {
@Override
public void run() {
ImageView imageView = mSoftReferenceImageView.get();
if (!mShouldRun || imageView == null) {
mIsRunning = false;
if (mOnAnimationStoppedListener != null) {
mOnAnimationStoppedListener.AnimationStopped();
}
return;
}
mIsRunning = true;
mHandler.postDelayed(this, mDelayMillis);
if (imageView.isShown()) {
int imageRes = getNext();
if (mBitmap != null) { // so Build.VERSION.SDK_INT >= 11
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);
} catch (Exception e) {
e.printStackTrace();
}
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(imageRes);
mBitmap.recycle();
mBitmap = null;
}
} else {
imageView.setImageResource(imageRes);
}
}
}
};
mHandler.post(runnable);
}
/**
* Stops the animation
*/
public synchronized void stop() {
mShouldRun = false;
}
}
}
Uso:
FramesSequenceAnimation anim = AnimationsContainer.getInstance().createSplashAnim(mSplashImageView);
anim.start();
- no te olvides de detenerlo ...
I solved my outOfMemoryError problem by cutting down the framerate brutally and scaling down the images in gimp. Depending on what you are doing you can probably get away with a lot less fps than you''d expect.
It''s big problem with the sdk but it can be solved by using threads for concurrently loading the bitmap images instead of loading the entire image at the same time.
Similar a otras respuestas, usando rxjava:
public final class RxSequenceAnimation {
private static final int[] PNG_RESOURCES = new int[]{
R.drawable.sequence_frame_00,
R.drawable.sequence_frame_01,
R.drawable.sequence_frame_02
};
private static final String TAG = "rx-seq-anim";
private final Resources mResource;
private final ImageView mImageView;
private final byte[][] RAW_PNG_DATA = new byte[PNG_RESOURCES.length][];
private final byte[] buff = new byte[1024];
private Subscription sub;
public RxSequenceAnimation(Resources resources, ImageView imageView) {
mResource = resources;
mImageView = imageView;
}
public void start() {
sub = Observable
.interval(16, TimeUnit.MILLISECONDS)
.map(new Func1<Long, Bitmap>() {
@Override
public Bitmap call(Long l) {
int i = (int) (l % PNG_RESOURCES.length);
if (RAW_PNG_DATA[i] == null) {
// read raw png data (compressed) if not read already into RAM
try {
RAW_PNG_DATA[i] = read(PNG_RESOURCES[i]);
} catch (IOException e) {
Log.e(TAG, "IOException " + String.valueOf(e));
}
Log.d(TAG, "decoded " + i + " size " + RAW_PNG_DATA[i].length);
}
// decode directly from RAM - only one full blown bitmap is in RAM at a time
return BitmapFactory.decodeByteArray(RAW_PNG_DATA[i], 0, RAW_PNG_DATA[i].length);
}
})
.subscribeOn(Schedulers.newThread())
.onBackpressureDrop()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(new Action1<Bitmap>() {
@Override
public void call(Bitmap b) {
mImageView.setImageBitmap(b);
}
})
.subscribe(LogErrorSubscriber.newInstance(TAG));
}
public void stop() {
if (sub != null) {
sub.unsubscribe();
}
}
private byte[] read(int resId) throws IOException {
return streamToByteArray(inputStream(resId));
}
private InputStream inputStream(int id) {
return mResource.openRawResource(id);
}
private byte[] streamToByteArray(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while ((i = is.read(buff, 0, buff.length)) > 0) {
baos.write(buff, 0, i);
}
byte[] bytes = baos.toByteArray();
is.close();
return bytes;
}
}
He resuelto este problema colocando todas las imágenes en una matriz y uso la demora después de mostrar cada una de ellas. El conjunto de imágenes fuente en res / cadena <!-- Array table for the pictures to show on the spinner--> <array name="spinner_list"> <item>@drawable/arrows_loop__00000_org</item> <item>@drawable/arrows_loop__00005_org</item> <item >@drawable/arrows_loop__00010_org</item> <item>@drawable/arrows_loop__00015_org</item> <item >@drawable/arrows_loop__00020_org</item> <item >@drawable/arrows_loop__00025_org</item> . . . </array>
Declaro sobre el spinner imageView private static ImageView imagespinner;
Luego en mi clase lo llamo aquí:
final TypedArray imgs = getResources().obtainTypedArray(R.array.spinner_list);
runimage(imgs, imgs.length());
y luego en runimage hago el ciclo con un retraso como este:
/* handle the spinner frame by frame */
runimage public void (final TypedArray array, int index) {
int size = array.length();
if(index<size) {// show in sequence the images
final int localindex= index;
handler.postDelayed(new Runnable() {
public void run() {
imagespinner.setImageResource(array.getResourceId(localindex, -1));// find the picture to show
runimage(array,(localindex+1));// because use final arg need to do the increase inside
}
}, 55);
}
else // after show all images go ahead
{
textview2.setVisibility(View.VISIBLE);
handler.postDelayed(myRunnablewait, 2000); // make some time to see text before go to ather fragment
}
}
así que ejecuto todas las imágenes con un retraso de 55 milisegundos en el Imagepinner. Después de terminar haz el siguiente trabajo.