欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

AnimationDrawable

程序员文章站 2024-03-24 15:00:40
...

AnimationDrawable

Android的帧动画(frame-by-frame animation)。

AnimationDrawable的用法如下:

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);

  ImageView img = (ImageView)findViewById(R.id.rocket_image);
  img.setBackgroundResource(R.drawable.img_list);
  AnimationDrawable animationDrawable = (AnimationDrawable)img.getBackground();
  
  animationDrawable.start();
}

事实上,在onCreate()方法中调用AnimationDrawable.start()是无效的。看官方文档的解释:

It's important to note that the start() method called on the AnimationDrawable cannot be called during the onCreate() method of your Activity, because the AnimationDrawable is not yet fully attached to the window. If you want to play the animation immediately, without requiring interaction, then you might want to call it from the onWindowFocusChanged() method in your Activity, which will get called when Android brings your window into focus.

大意是,在onCreate方法中,AnimationDrawable没有完全关联到Window,这时调用start()方法是没有用的。需要在onWindowFocusChanged()方法中调用。

但令人费解的是,我尝试在onCreate()方法中调用了AnimationDrawable.start()方法,发现帧动画的确启动了。按照文档的说法应该是不会启动的。

AnimationDrawable的另一个问题

AnimationDrawable会一次性把所有图片加载到内存中,在某些内存吃紧的设备上会出现OutOfMemoryError

Bitmap是内存杀手,而AnimationDrawable则会一次性将所有用到的图片全部加载到内存中,很容易就会导致OutOfMemoryError。可以说是Bitmap的帮凶。

这个问题并不是必现的,只有在特定的机型上才会出现。所以这也增加了其隐蔽性。

从源码角度看AnimationDrawable如何加载bitmap

AnimationDrawable继承自Drawable。我们是通过xml文件保存帧动画信息的,所以从Drawable.createFromXml()方法看起:

public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme)
        throws XmlPullParserException, IOException {
    AttributeSet attrs = Xml.asAttributeSet(parser);

    int type;
    //noinspection StatementWithEmptyBody
    while ((type=parser.next()) != XmlPullParser.START_TAG
            && type != XmlPullParser.END_DOCUMENT) {
        // Empty loop.
    }

    if (type != XmlPullParser.START_TAG) {
        throw new XmlPullParserException("No start tag found");
    }

    Drawable drawable = createFromXmlInner(r, parser, attrs, theme);

    if (drawable == null) {
        throw new RuntimeException("Unknown initial tag: " + parser.getName());
    }

    return drawable;
}

XmlPullParser是一个Xml解析工具。

第一段while循环会一直执行直到找到xml的开始标志或到文件尾。如果直到文件尾也没有找到xml开始标志,则抛出XmlPullParserException异常。
找到xml开始标志后,会调用createFromXmlInner方法获取Drawable对象。

进入createFromXmlInner()方法:

public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs,
        Theme theme) throws XmlPullParserException, IOException {
    return r.getDrawableInflater().inflateFromXml(parser.getName(), parser, attrs, theme);
}

不同版本Android源码实现似乎不一样,在较低版本上createFromXmlInner()方法可以清楚看出如何加载AnimationDrawabl。而在level 25版本上,这个方法似乎并不能看出什么。关键在于r.getDrawableInflater()返回的DrawableInflater实例,但问题在于,我似乎怎么也找不到这个DrawableInflater类的具体实现。

最后在google git仓库中找到了DrawableInflater的实现。

DrawableInflater

这里插一句,DrawableInflater源码上加了如**释:

@hide Pending API finalization.

之所以加上@hide注释,是想阻止开发者使用SDK中那些未完成或不稳定的部分(接口或架构)。难怪我找不到DrawableInflater,原来是被Google隐藏了。

直接看DrawableInflater.inflatefromXml():

public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser,
        @NonNull AttributeSet attrs, @Nullable Theme theme)
        throws XmlPullParserException, IOException {
    if (name.equals("drawable")) {
        name = attrs.getAttributeValue(null, "class");
        if (name == null) {
            throw new InflateException("<drawable> tag must specify class attribute");
        }
    }
    Drawable drawable = inflateFromTag(name);
    if (drawable == null) {
        drawable = inflateFromClass(name);
    }
    drawable.inflate(mRes, parser, attrs, theme);
    return drawable;
}

着重看这句话:

Drawable drawable = inflateFromTag(name);

inflateFromTag(String name)会根据名字的不同加载不同的Drawable。看下是如何实现的:
PS:

private Drawable inflateFromTag(@NonNull String name) {
    switch (name) {
        case "selector":
            return new StateListDrawable();
        case "animated-selector":
            return new AnimatedStateListDrawable();
        case "level-list":
            return new LevelListDrawable();
        case "layer-list":
            return new LayerDrawable();
        case "transition":
            return new TransitionDrawable();
        case "ripple":
            return new RippleDrawable();
        case "color":
            return new ColorDrawable();
        case "shape":
            return new GradientDrawable();
        case "vector":
            return new VectorDrawable();
        case "animated-vector":
            return new AnimatedVectorDrawable();
        case "scale":
            return new ScaleDrawable();
        case "clip":
            return new ClipDrawable();
        case "rotate":
            return new RotateDrawable();
        case "animated-rotate":
            return new AnimatedRotateDrawable();
        case "animation-list":
            return new AnimationDrawable();
        case "inset":
            return new InsetDrawable();
        case "bitmap":
            return new BitmapDrawable();
        case "nine-patch":
            return new NinePatchDrawable();
        default:
            return null;
    }
}

很显然,当name为"AnimationDrawable"时,返回的是一个AnimationDrawable实例。

再回到inflateFromXml()方法。获取到Drawable实例后,就会调用该Drawable实例的inflate()方法。

PS:Drawable的每个子类的inflate方法不尽相同,这里我们看下AnimationDrawableinflate方法:

@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme)
        throws XmlPullParserException, IOException {
    final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawable);
    super.inflateWithAttributes(r, parser, a, R.styleable.AnimationDrawable_visible);
    updateStateFromTypedArray(a);
    updateDensity(r);
    a.recycle();

    inflateChildElements(r, parser, attrs, theme);

    setFrame(0, true, false);
}

嗯,看不出什么端倪,继续进到inflateChildElements方法中:

private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs,
        Theme theme) throws XmlPullParserException, IOException {
    int type;

    final int innerDepth = parser.getDepth()+1;
    int depth;
    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
            && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) {
        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        if (depth > innerDepth || !parser.getName().equals("item")) {
            continue;
        }

        final TypedArray a = obtainAttributes(r, theme, attrs,
                R.styleable.AnimationDrawableItem);

        final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1);
        if (duration < 0) {
            throw new XmlPullParserException(parser.getPositionDescription()
                    + ": <item> tag requires a 'duration' attribute");
        }

        Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable);

        a.recycle();

        if (dr == null) {
            while ((type=parser.next()) == XmlPullParser.TEXT) {
                // Empty
            }
            if (type != XmlPullParser.START_TAG) {
                throw new XmlPullParserException(parser.getPositionDescription()
                        + ": <item> tag requires a 'drawable' attribute or child tag"
                        + " defining a drawable");
            }
            dr = Drawable.createFromXmlInner(r, parser, attrs, theme);
        }

        mAnimationState.addFrame(dr, duration);
        if (dr != null) {
            dr.setCallback(this);
        }
    }
}

罪魁祸首就在这里,可以看到这里有一个while循环,将xml文件中的所有节点都遍历了一遍,获取所有的帧对应的DrawableDuration,然后存到mAnimationState中:

mAnimationState.addFrame(dr, duration);

供帧动画播放时调用。

去看看这个AnimationState是何方神圣:

代码就不贴了,太多,看关键的地方:
AnimationState有两个成员变量:

 private int[] mDurations;
 private boolean mOneShot = false;

很明显,mDurations存的是每一帧持续的时间,而mOneShot存的是是否只播放一次,true表示只播放一次,false表示播放多次。
AnimationStateDrawableContainerState继承了一个成员变量:

Drawable[] mDrawables;

很显然,存的是每一帧对应的Drawable对象。而mDrawables是在AnimationDrawable加载时一次性填满的。

可以想象,当设备内存不足,且一次性加载的位图过多,自然会触发OutOfMemoryError

既然已经找到了罪魁祸首,现在的当务之急就是如何解决这个问题。

问题的症结在于:AnimationDrawable非常心急的想一口吃个胖子,但手机表示伤不起。既然如此,我们何不矜持一点,分步加载需要的Bitmap,只有在需要某张Bitmap时才将其加载到内存中,并且让Bitmap对象可复用,不重复产生大量Bitmap对象。

说干就干。

关于复用Bitmap,可以使用BitmapFactory.Options实现:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inBitmap = bitmap;

如果设置了BitmapFactory.Options.inBitmap参数,当系统在加载Bitmap时,会尝试复用这个Bitmap,从而起到节省内存的作用。

这个参数在Android 3.0之后即可使用,但在Android 4.4之前有诸多限制:

首先,图片的编码格式必须是jpegpng格式。其次,必须是相同大小的Bitmap才被支持,且inSampleSize要设置为1。

但在Android 4.4之后就没有那么多限制了。只要保证

mBitmapOptions.inMutable = true;

即可。

直接上代码:

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;
            if(mIndex < totalImg - 1 || mRepeat) {
                mHandler.postDelayed(this, mDuration[mIndex % totalImg]);
            }

            if (imageView.isShown()) {
                int imageRes = getNext();
                if (mBitmap != null) {
                    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);
}

其实逻辑很简单,每次调用start()方法都会启动一个线程。根据下标index找到当前下一个要展示帧,设置为ImageView的背景,然后通过Handler post自身,不断循环,从而实现帧动画的效果。

这里不是在动画开始执行之前将所有Bitmap加载到内存中,而是每当要展示时,才去加载需要的Bitmap。而且由于在加载Bitmap时:

bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);

mBitmapOptions设置了如下属性:

mBitmapOptions.inBitmap = mBitmap;
mBitmapOptions.inMutable = true;
mBitmapOptions.inSampleSize = 1;

每次加载都会复用mBitmap,而不会在内存中产生大量的Bitmap对象。

用法

    private static final int[] mMeasureHeartRes = {
            R.drawable.img_measure_heart_1,
            R.drawable.img_measure_heart_2,
            R.drawable.img_measure_heart_3,
            R.drawable.img_measure_heart_4,
            R.drawable.img_measure_heart_5,
            R.drawable.img_measure_heart_6
    };
  mMeasureHeartAnimation = new FramesSequenceAnimation(mBandHRImg, mMeasureHeartRes, new int[]{
                    50,
                    50,
                    50,
                    50,
                    50,
                    50
            }, false);
  mMeasureHeartAnimation.start();

FramesSequenceAnimation构造函数的第一个参数是需要展示的ImageView,第二个参数是一个数组,里面存放了所有帧对应的图片资源Id,第三个参数是每个帧展示的时间,单位是毫秒。

第四个参数表示是否循环展示,true表示循环,false表示只播放一次。