AnimationDrawable

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表示只播放一次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,263评论 25 707
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,335评论 0 17
  • 画出他的草稿 紧接着我们要上色了。 好,完成。 总结——不足之处在于没有表现出云的渐变。 阴影黑色过浓。 画面不干净。
    SinNOSG阅读 808评论 5 3
  • 赤笛 自画戟盔甲中,吹响 夜色加重,微弱的星光渐远 篷帐更加透明 火光,纤细的十指捧亮迷惑的眼睛 斗篷遮不住修长的...
    凉爽清风阅读 267评论 0 5
  • http://www.lfd.uci.edu/~gohlke/pythonlibs/ 在这里可以下载所有常见的库的...
    rogerwu1228阅读 185评论 0 0