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
源码上加了如下注释:
@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
方法不尽相同,这里我们看下AnimationDrawable
的inflate
方法:
@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
文件中的所有节点都遍历了一遍,获取所有的帧对应的Drawable
和Duration
,然后存到mAnimationState
中:
mAnimationState.addFrame(dr, duration);
供帧动画播放时调用。
去看看这个AnimationState
是何方神圣:
代码就不贴了,太多,看关键的地方:
AnimationState
有两个成员变量:
private int[] mDurations;
private boolean mOneShot = false;
很明显,mDurations
存的是每一帧持续的时间,而mOneShot
存的是是否只播放一次,true
表示只播放一次,false
表示播放多次。
AnimationState
从DrawableContainerState
继承了一个成员变量:
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
之前有诸多限制:
首先,图片的编码格式必须是jpeg
或png
格式。其次,必须是相同大小的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
表示只播放一次。