网上有很多关于怎么实现android播放GIF的帖子。但是本人发现,其中多多少少都有些不如人意的地方。因此,花了几天时间,重写了ImageView以实现GIF图片的播放。在此小结一下,也希望可以给后来者一点参考。
大致我们会在网上搜到下面四种解决方法:
【方案一】用外部工具拆分GIF
【方案二】用Android开源项目GifView包
【方案三】手动解码GIF
【方案四】用系统自带的类Movie
本文采用方案四,继承ImageView实现GIF动画播放,支持ImageView的命名空间属性设置,支持ImageView通用接口。
项目源码下载地址:
http://download.csdn.net/detail/yarkey09/6499717
【什么是GIF】
GIF,就我的理解,就是很多张位图图片的集合,然后使用了某种编码方式,使得它可以体积很小但是又够清晰。由于体积小,不依赖特别的平台,所以GIF很流行。
好吧,知道的就这么多,各位看官想了解清楚的话还是请自行百度吧。不过了解了大概概念,我们就可以知道,其实让GIF播放,实际就是显示多张图片而已。
【方案一】用外部工具拆分GIF
大概情况是这样:
1,首先我们得有一张GIF (提示:选择赏心悦目的动画,可以提高学习兴趣哦^_^)
2,然后使用工具,千刀万剐将GIF分成多张图片 => pic0.png,pic1.png,pic2.png,pic3.png,pic4.png,pic5.png
3,接着编写android xml资源文件放在drawable目录下,说明各个帧图片以及时间duration
4,然后代码里面使用AnimationDrawable类即可实现
四张图片按照xml定义的时间,一张张切换,看起来就是动画了!
动画资源文件格式是这样:(drawable/anim_gif.xml)
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:duration="150" android:drawable="@drawable/pic0" /> <item android:duration="150" android:drawable="@drawable/pic1" /> <item android:duration="150" android:drawable="@drawable/pic2" /> <item android:duration="150" android:drawable="@drawable/pic3" /> <item android:duration="150" android:drawable="@drawable/pic4" /> <item android:duration="150" android:drawable="@drawable/pic5" /> </animation-list>
布局文件可以是这样:
<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/anim_gif" android:id="@+id/imgGif"></ImageView>
代码是这样:
ImageView imageView = (ImageView) findViewById(R.id.imgGif); Object ob = imageView.getBackground(); AnimationDrawable anim = (AnimationDrawable) ob; anim.start();
如果我们需要在界面上显示一个简单且固定的动画,单纯用于点缀画面,增强动感,这种方法比较方便。当然,这种方法在某些场合下显得很不灵活,不能满足要求效果,那么请继续参考后面的方法吧。
参考帖子:Android开发:教您如何让Gif动画动起来
【方案二】用Android开源项目GifView包
我们同样可以在网上搜到这个开源项目的相关应用。有了这个包,我们要让GIF播放这个事情就变得非常轻松。看看它那强大的接口就知道了!使用GifView几乎就跟ImageView是一样的。方便!开源项目确实有很多代码都有非常好的学习价值,表示有空应该好好拜读一番!
// 从xml中得到GifView的句柄 gif1 = (GifView) findViewById(R.id.gif1); // 设置Gif图片源 gif1.setGifImage(R.drawable.gif1); // 添加监听器 gif1.setOnClickListener(this); // 设置显示的大小,拉伸或者压缩 gif1.setShowDimension(300, 300); // 设置加载方式:先加载后显示、边加载边显示、只显示第一帧再显示 gif1.setGifImageType(GifImageType.COVER);
参考帖子:介绍一个Android开源项目:GifView——Android显示GIF动画
【方案三】手动解码GIF
用java来解码,很多人会觉得效率比较低。但是我们目的是学习,完全可以尝试一下!当然,也可以用native代码完成解码,在java用JNI调用。
将GIF文件解码后,我们可以得到所有想要的信息。比如Gif版本GIF87a, GIF89a等等,关键是我们可以得到几张Bitmap图片,还有各张图片的显示延续时间。其实,这里解码工作也就大致等同于上面的方案一。不同的是,我们的app可以直接播放GIF,而不需要外部的工具!
有了各帧图片以及显示延续时间,我们便可以开始了!新建一个线程用于计时,时间一到就刷新View切换图片。这就是GIF了!
注意一下在非主线程让View刷新,应该调用postInvalidate() 而不是invalidate()。
下面参考帖子附有解码源程序,然后按照参考文档来阅读,很快可以看明白^_^
参考帖子: Android 解码播放GIF图像
参考文档: GIF文件格式
【方案四】用系统自带的类Movie
接下来说说具体要讲的基于Movie的实现方法吧!
使用Movie类播放GIF很简单。但是我们的目的是,继承ImageView,保留它显示图片的基本功能,尽量使得接口函数能够通用简便。这样,原先使用ImageView的项目代码只要经过少量的修改,即可支持GIF动画。根据要求,我们至少要重载下面四个通用接口以支持GIF动画:
public void setImageResource( int resID ) public void setImageURI( Uri uri ) public void setScaleType( ScaleType scaleType ) public void setPadding( int left, int top, int right, int bottom )
说明一下:
// 我们设置了图片,那么跟ImageView一样显示出图片 setImageResource( R.drawable.pngtest ); // 我们这次设置了GIF动画,那么应该显示动画 setImageResource( R.drawable.giftest ); // 支持SD卡中的GIF动画 setImageURI( Uri.parse( "file://" + Environment.getExternalStorageDirectory().getPath() + "/sdcard_giftest.gif" );
为了实现以上要求,其中遇到很多问题,我们慢慢说吧。
-1- Movie 是啥东西
android.graphics.Movie 在SDK文档中没有说明,翻看源代码,发现它只是一个java壳,实际上直接调用native代码。这样导致我们没能快速学习掌握它的用法。
不过幸亏有APIDemo!这真的是一个好东西!打开其中BitmapDecode我们可以发现代码中就用了Movie类!
直接安装APIDemo到手机中,运行... 发现旗子飘动起来了!
它的源代码简单清晰,大概是这样。Movie对象管理着时间轴上对应的GIF各帧图片,我们通过传入时间,便可以取出对应的帧,然后再用draw()方法,将当前的帧画到画布canvas上面。如果我们的View不停的刷新,时间不停地跑,Movie的帧就不停的切换,那么画出来的View就动起来了!
-2- Copy APIDemo 源码
那么好了,按照它的代码,我们可以很快copy一份出来,然后编译安装到手机,我们想GIF似乎就这样完成了。关键代码如下。
private static class MovieGifView extends View { private Movie mMovie; private long mMovieStart; public MovieGifView(Context context) { super(context); java.io.InputStream is; is = context.getResources().openRawResource(R.drawable.animated_gif); mMovie = Movie.decodeStream(is); } @Override protected void onDraw(Canvas canvas) { long now = android.os.SystemClock.uptimeMillis(); if (mMovieStart == 0) { // first time mMovieStart = now; } if (mMovie != null) { int dur = mMovie.duration(); if (dur == 0) { dur = 1000; } int relTime = (int) ((now - mMovieStart) % dur); mMovie.setTime(relTime); mMovie.draw(canvas, getWidth() - mMovie.width(), getHeight() - mMovie.height()); invalidate(); } } }
但是结果却是那么不如人意,自己写的app在一部平板(android 4.3)上运行时,GIF没有动起来,美女并没有向我眨眼!
我第一反应便是拿去我的屌丝神机I589(I5830电信版 android 2.3) 上试试。结果反而动起来了!这么神马回事?!
难道是android 4.3 版本太新,Movie方法不支持?后来我又找到了一部android 4.1 的手机,安装发现,GIF同样没有动!
奇怪!头疼!
-3- hardwareAccelerated 惹的祸
为什么APIDemo的代码可以,我的代码直接copy,却不行了?我翻看了很久代码,最后找到了唯一不同点,在这里 -> AndroidManifest.xml
<activity android:hardwareAccelerated="false" android:name=".graphics.BitmapDecode" android:label="Graphics/BitmapDecode"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.SAMPLE_CODE" /> </intent-filter> </activity>
BitmapDecode Activity 属性设置中,有个东东不曾相识→ → android:hardwareAccelerated="false",不使能硬件加速?什么概念?
于是便开始查看各种说明,大概意思我是这么理解的:硬件加速并不是什么新鲜的东西,已经运用于windows composition 或 OpenGL games等等。而android 在 3.0之后的版本开始支持。但是它现在暂时只支持standard widgets and drawables。一旦使能硬件加速的特性,所有的画图工作都交给GPU来做。
但是,我们现在是自定义View类,使用Movie类的draw()方法画图,这个方法并没有在硬件加速支持列表(如下)中找到踪影。
The following table describes the support level of various operations across API levels:
API level | ||||
< 16 | 16 | 17 | 18 | |
Canvas | ||||
drawBitmapMesh() (colors array) | ✗ | ✗ | ✗ | ✓ |
drawPicture() | ✗ | ✗ | ✗ | ✗ |
drawPosText() | ✗ | ✓ | ✓ | ✓ |
drawTextOnPath() | ✗ | ✓ | ✓ | ✓ |
drawVertices() | ✗ | ✗ | ✗ | ✗ |
setDrawFilter() | ✗ | ✓ | ✓ | ✓ |
clipPath() | ✗ | ✗ | ✗ | ✓ |
clipRegion() | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.XOR) | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.Difference) | ✗ | ✗ | ✗ | ✓ |
clipRect(Region.Op.ReverseDifference) | ✗ | ✗ | ✗ | ✓ |
clipRect() with rotation/perspective | ✗ | ✗ | ✗ | ✓ |
详见官方说明: Android 3.0 (API level 11) Hardware Acceleration
所以,我们认为硬件加速不支持Movie draw()方法。而I589(android 2.3)本身没有这个特性,所以不出现问题。而android 4.3平板具备硬件加速并默认开启,而我们没有关掉,所以出了问题。我准备尝试关了这个特性再试试,再不行就死给你看!
关闭Hardware Acceleeration可以有几种方法,针对不同的级别(Application, Activity, Window, View )。具体请详见官方说明。
为了影响最小,可以使用View级别的 setLayerType(View.LAYER_TYPE_SOFTWARE, null);
关掉了硬件加速,我的GIF终于动起来了!美女开始眨眼,多么好看的GIF动画!欢呼吧!\(^o^)/
-4- 继承ImageView
GIF已经动起来了,感觉事情已经搞定了一大半。但是后面发现,实际上不是这样的!
再次说说我们的设计目标:设计一个类可以通过设置 Resource ID或者URI 播放图片和GIF动画。
我们自然想到继承ImageView,然后加入GIF功能代码。这样可以节省很多代码。但是查看ImageView源码,很多private成员变量, private成员函数,真是让人望而却步呀。但是,耐心的啃一啃ImageView的源码,大概还是可以看出思路的。
-5- 解读ImageView源码
源码链接:Android ImageView.java 源码在线阅读
个人认为ImageView源码最关键的部分,也是我们继承它最需要考虑的问题有两点:
- 控件的大小 (onMeasure()回调方法)
- 图片的大小与位置 (configureBounds()私有方法)
a) 图片的大小与位置
先谈谈图片的大小与位置吧,因为待会它在onMeasure方法中会用到。
两个关键的成员变量 mDrawable, mDrawMatrix。一个是"图片",一个是矩阵。mDrawable调节自身大小颜色透明度等等。mDrawMatrix则定义了画布的缩放平移旋转等等。在onDraw()方法调用之前,我们必须设置好这两个变量,才能画出正确的图形。ImageView对这两个变量的配置,大部分工作在configureBounds()方法中完成。
configureBounds()私有方法,根据当前View的大小(除去Padding部分)、Drawable实际大小、以及ScaleType参数,设置了图片最终要显示的大小以及对齐等属性。而padding参数的平移效果,在onDraw()中通过平移画布实现。
// yarkey@20131029 : // configureBounds()方法的产物:mDrawable,mDrawMatrix // onDraw()方法将使用这里的mDrawable,mDrawMatrix作画 private void configureBounds() { if (mDrawable == null || !mHaveFrame) { return; } int dwidth = mDrawableWidth; int dheight = mDrawableHeight; int vwidth = getWidth() - mPaddingLeft - mPaddingRight; int vheight = getHeight() - mPaddingTop - mPaddingBottom; boolean fits = (dwidth < 0 || vwidth == dwidth) && (dheight < 0 || vheight == dheight); // yarkey@20131029 : 以下根据ScaleType设置mDrawMatrix if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) { /* If the drawable has no intrinsic size, or we're told to scaletofit, then we just fill our entire view. */ // yarkey@20131029 : fitXY的情况较简单,直接将Drawable缩放至View的大小即可(除去padding) mDrawable.setBounds(0, 0, vwidth, vheight); mDrawMatrix = null; } else { // We need to do the scaling ourself, so have the drawable // use its native size. // yarkey@20131029 : 获取图片固有的大小 // dwidth = mDrawable.getIntrinsicWidth() // dheight = mDrawable.getIntrinsicHeight() // 涉及设备像素密度(density),图片存放目录(drawable-mdpi/drawable-hdpi) mDrawable.setBounds(0, 0, dwidth, dheight); if (ScaleType.MATRIX == mScaleType) { // Use the specified matrix as-is. if (mMatrix.isIdentity()) { mDrawMatrix = null; } else { mDrawMatrix = mMatrix; } } else if (fits) { // The bitmap fits exactly, no transform needed. mDrawMatrix = null; } else if (ScaleType.CENTER == mScaleType) { // Center bitmap in view, no scaling. // yarkey@20131029 : 按原图大小居中显示,超过View长宽则截取中间部分 mDrawMatrix = mMatrix; mDrawMatrix.setTranslate((int) ((vwidth - dwidth) * 0.5f + 0.5f), (int) ((vheight - dheight) * 0.5f + 0.5f)); } else if (ScaleType.CENTER_CROP == mScaleType) { mDrawMatrix = mMatrix; float scale; float dx = 0, dy = 0; // yarkey@20131029 : centerCrop,长宽算出比例,然后取比例“大”的 if (dwidth * vheight > vwidth * dheight) { scale = (float) vheight / (float) dheight; dx = (vwidth - dwidth * scale) * 0.5f; } else { scale = (float) vwidth / (float) dwidth; dy = (vheight - dheight * scale) * 0.5f; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); } else if (ScaleType.CENTER_INSIDE == mScaleType) { mDrawMatrix = mMatrix; float scale; float dx; float dy; // yarkey@20131029 : centerCrop,长宽算出比例,然后取比例“小”的 if (dwidth <= vwidth && dheight <= vheight) { scale = 1.0f; } else { scale = Math.min((float) vwidth / (float) dwidth, (float) vheight / (float) dheight); } dx = (int) ((vwidth - dwidth * scale) * 0.5f + 0.5f); dy = (int) ((vheight - dheight * scale) * 0.5f + 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); } else { // yarkey@20131029 : 剩下fitCenter,fitStart,fitEnd三种 // Generate the required transform. mTempSrc.set(0, 0, dwidth, dheight); mTempDst.set(0, 0, vwidth, vheight); mDrawMatrix = mMatrix; mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType)); } } }
一旦mDrawable或者mDrawMatrix需要改变的时候,configureBounds() 方法就会被调用。大概是这样子的:
只要调用invalidate()方法,onDraw()方法就会被调用,从而刷新界面。
我们现在关心的是,播放GIF的时候,我们手上是Movie对象,而不是Drawable对象。因此,用于Drawable的位置计算,不能适用于Movie的场合。ImageView可以通过Drawable setBounds()方法设置大小,Movie却没有这种方法,因此我们只能通过缩放画布(canvas)来实现相同的效果。
至于怎么用Matrix矩阵来变换画图,请移步:
参考帖子:Android Matrix理论与应用详解
b) 控件的大小
控件的大小需要在onMeasure()方法中设置,使用setMeasuredDimension()方法。根据输入参数int widthMeasureSpec, int heightMeasureSpec 可以得到父容器(Layout)提供的Measure模式Mode以及参考大小Size。MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST, MeasureSpec.EXACTLY。
对应这三种模式的不同设置方法,ImageView源码注释得很清楚(resolveAdjustedSize()方法中)。我们到时copy它的代码就可以(因为是private方法,子类不可以调用)。
重写onMeasure()方法,我们归为一点:把里面的mDrawable替换为mMovie即可。
但是,执行完onMeasure()后,如果立即去getWidth(), getHeight(),我们只会得到旧值!如果想要onMeasure()后,立即计算图片的缩放移动旋转参数,那么需要用getMeasuredWidth()和getMeasuredHeight()代替。
调用requestLayout()方法,可以触发onMeasure()方法。
-6- MovieImageView
代码比较长,这里直接贴上一个测试没有问题的版本。
这里只实现新增方法setMovie(),如果想实现setImageResource(), setImageUri(),只要将以下的代码稍作修改,将Movie对象的初始化部分放到MovieImageView类里头完成,即可实现!
package com.yarkey.giftest2; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Movie; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.ImageView; public class MovieImageView extends ImageView { private static final boolean DB = true; private static final boolean DB_DETAIL = false; private static final String DB_TAG = "MovieImageView"; /** Feature Support GIF */ private static final boolean FEATURE_IS_GIF_SUPPORTED = true; /** @see #syncParentParameter() */ private int mSuperPaddingTop; private int mSuperPaddingLeft; private int mSuperPaddingRight; private int mSuperPaddingBottom; private ScaleType mSuperScaleType; private Matrix mSuperDrawMatrix; /** mMovie==null means we work the same as parent(ImageView) */ private Movie mMovie = null; private Matrix mMatrix; private Matrix mDrawMatrix; private long mMovieStartTime = 0; private long mMovieDuration = 0; private int mDefLayerType; // AdjustViewBounds behavior will be in compatibility mode for older apps. private boolean mAdjustViewBoundsCompat = false; public MovieImageView(Context context) { super(context); initGifAndImageView(); } public MovieImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MovieImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); initGifAndImageView(); } private void prepareForMovie(boolean isToDo) { if (FEATURE_IS_GIF_SUPPORTED && isToDo) { if (getLayerType() != View.LAYER_TYPE_SOFTWARE) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } setWillNotCacheDrawing(false); mMovieStartTime = 0; } else if (mDefLayerType != 0 && mDefLayerType != getLayerType()) { setLayerType(mDefLayerType, null); mMovie = null; } } /** * You may open an inputstream of certain GIF file, and then decode by * Movie.decodeStream. * * @param movie */ public void setMovie(Movie movie) { Log("setMovie"); if (FEATURE_IS_GIF_SUPPORTED && mMovie != movie) { mMovie = movie; if (mMovie != null) { prepareForMovie(true); mMovieDuration = mMovie.duration(); requestLayout(); // configureDrawMatrix();//will get called after onMeasures } else { prepareForMovie(false); } invalidate(); } } private void syncParentParameter() { Log("syncParentParameter"); mSuperPaddingTop = getPaddingTop(); mSuperPaddingLeft = getPaddingLeft(); mSuperPaddingRight = getPaddingRight(); mSuperPaddingBottom = getPaddingBottom(); mSuperScaleType = getScaleType(); mSuperDrawMatrix = getImageMatrix(); } @Override public void setImageBitmap(Bitmap bm) { prepareForMovie(false); super.setImageBitmap(bm); } @Override public void setImageDrawable(Drawable drawable) { prepareForMovie(false); super.setImageDrawable(drawable); } @Override public void setImageResource(int resId) { prepareForMovie(false); super.setImageResource(resId); } @Override public void setImageURI(Uri uri) { prepareForMovie(false); super.setImageURI(uri); } @Override public void setScaleType(ScaleType scaleType) { super.setScaleType(scaleType); configureDrawMatrix(); } @Override public void setImageMatrix(Matrix matrix) { super.setImageMatrix(matrix); // We should do the following whether we are in "MovieMode" or not, // because we can not get the matrix from // parent later. if (matrix != null && matrix.isIdentity()) { matrix = null; } if (matrix == null && !mMatrix.isIdentity() || matrix != null && !mMatrix.equals(matrix)) { mMatrix.set(matrix); configureDrawMatrix(); invalidate(); } } @Override public void setPadding(int left, int top, int right, int bottom) { super.setPadding(left, top, right, bottom); configureDrawMatrix(); } private void initGifAndImageView() { Log("initGifAndImageView"); if (FEATURE_IS_GIF_SUPPORTED) { mMatrix = new Matrix(); mDefLayerType = getLayerType(); } mAdjustViewBoundsCompat = this.getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1; } private void configureDrawMatrix() { Log("configureDrawMatrix"); if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) { return; } // getWidth/Height() aren't valid until after a layout if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) return; syncParentParameter(); int movieWidth = mMovie.width();// 实际像素 int movieHeight = mMovie.height(); Log("movieWidth = " + movieWidth + ", movieHeight = " + movieHeight); // in pixels // int vWidth = getWidth() - mSuperPaddingLeft - mSuperPaddingRight; // int vHeight = getHeight() - mSuperPaddingTop - mSuperPaddingBottom; int vWidth = getMeasuredWidth() - mSuperPaddingLeft - mSuperPaddingRight; int vHeight = getMeasuredHeight() - mSuperPaddingTop - mSuperPaddingBottom; Log("vWidth = " + vWidth + ", vHeight = " + vHeight); if (ScaleType.CENTER == mSuperScaleType) { mDrawMatrix = mMatrix; mDrawMatrix.setTranslate((int) ((vWidth - movieWidth) * 0.5f + 0.5f), (int) ((vHeight - movieHeight) * 0.5f + 0.5f)); } else if (ScaleType.CENTER_CROP == mSuperScaleType) { mDrawMatrix = mMatrix; float scale = Math.max((float) vHeight / (float) movieHeight, (float) vWidth / (float) movieWidth); float dx = (vWidth - movieWidth * scale) * 0.5f; float dy = (vHeight - movieHeight * scale) * 0.5f; mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); } else if (ScaleType.CENTER_INSIDE == mSuperScaleType) { mDrawMatrix = mMatrix; float scale; if (movieWidth <= vWidth && movieHeight <= vHeight) { scale = 1.0f; } else { scale = Math.min((float) vWidth / (float) movieWidth, (float) vHeight / (float) movieHeight); } float dx = (int) ((vWidth - movieWidth * scale) * 0.5f + 0.5f); float dy = (int) ((vHeight - movieHeight * scale) * 0.5f + 0.5f); mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate(dx, dy); } else if (ScaleType.FIT_XY == mSuperScaleType) { mDrawMatrix = mMatrix; float scaleX = (float) vWidth / (float) movieWidth; float scaleY = (float) vHeight / (float) movieHeight; Log("ScaleType.FIT_XY, scaleX = " + scaleX + ", scaleY = " + scaleY); mDrawMatrix.setScale(scaleX, scaleY); // mDrawMatrix.postTranslate(mSuperPaddingLeft, mSuperPaddingTop); } else if (ScaleType.MATRIX == mSuperScaleType) { mDrawMatrix = mSuperDrawMatrix; } else { /* fit */ mDrawMatrix = mMatrix; float scale = Math.min((float) vHeight / (float) movieHeight, (float) vWidth / (float) movieWidth); float dx = 0.0f; float dy = 0.0f; if (ScaleType.FIT_START == mSuperScaleType) { // dx = 0.0f; // dy = 0.0f; } else if (ScaleType.FIT_CENTER == mSuperScaleType) { dx = (vWidth - movieWidth * scale) * 0.5f + 0.5f; dy = (vHeight - movieHeight * scale) * 0.5f + 0.5f; } else {/* ScaleType.FIT_END == mSuperScaleType */ dx = vWidth - movieWidth * scale; dy = vHeight - movieHeight * scale; } mDrawMatrix.setScale(scale, scale); mDrawMatrix.postTranslate((int) dx, (int) dy); } } private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) { Log("resolveAdjustedSize, desiredSize=" + desiredSize + ",maxSize=" + maxSize + ",measureSpec=" + measureSpec); int result = desiredSize; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: /* * Parent says we can be as big as we want. Just don't be larger * than max size imposed on ourselves. */ result = Math.min(desiredSize, maxSize); break; case MeasureSpec.AT_MOST: // Parent says we can be as big as we want, up to specSize. // Don't be larger than specSize, and don't be larger than // the max size imposed on ourselves. result = Math.min(Math.min(desiredSize, specSize), maxSize); break; case MeasureSpec.EXACTLY: // "60dp" // No choice. Do what we are told. result = specSize; break; } return result; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log("onMeasure"); if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } syncParentParameter(); int w; int h; // Desired aspect ratio of the view's contents (not including padding) float desiredAspect = 0.0f; // We are allowed to change the view's width boolean resizeWidth = false; // We are allowed to change the view's height boolean resizeHeight = false; final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); if (mMovie == null) { w = h = 0; } else { w = mMovie.width(); h = mMovie.height(); Log("onMeasure, w = " + w + ", h = " + h); if (w <= 0) w = 1; if (h <= 0) h = 1; // We are supposed to adjust view bounds to match the aspect // ratio of our drawable. See if that is possible. if (getAdjustViewBounds()) { resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; desiredAspect = (float) w / (float) h; } } int widthSize, heightSize; Log("onMeasure, resizeWidth=" + resizeWidth + ", resizeHeight=" + resizeHeight); if (resizeWidth || resizeHeight) { int maxWidth = getMaxWidth(); int maxHeight = getMaxHeight(); /* * If we get here, it means we want to resize to match the drawables * aspect ratio, and we have the freedom to change at least one * dimension. */ // Get the max possible width given our constraints widthSize = resolveAdjustedSize(w + mSuperPaddingLeft + mSuperPaddingRight, maxWidth, widthMeasureSpec); // Get the max possible height given our constraints heightSize = resolveAdjustedSize(h + mSuperPaddingTop + mSuperPaddingBottom, maxHeight, heightMeasureSpec); if (desiredAspect != 0.0f) { // See what our actual aspect ratio is float actualAspect = (float) (widthSize - mSuperPaddingLeft - mSuperPaddingRight) / (heightSize - mSuperPaddingTop - mSuperPaddingBottom); if (Math.abs(actualAspect - desiredAspect) > 0.0000001) { boolean done = false; // Try adjusting width to be proportional to height if (resizeWidth) { int newWidth = (int) (desiredAspect * (heightSize - mSuperPaddingTop - mSuperPaddingBottom)) + mSuperPaddingLeft + mSuperPaddingRight; // Allow the width to outgrow its original estimate if // height is fixed. if (!resizeHeight && !mAdjustViewBoundsCompat) { widthSize = resolveAdjustedSize(newWidth, maxWidth, widthMeasureSpec); } if (newWidth <= widthSize) { widthSize = newWidth; done = true; } } // Try adjusting height to be proportional to width if (!done && resizeHeight) { int newHeight = (int) ((widthSize - mSuperPaddingLeft - mSuperPaddingRight) / desiredAspect) + mSuperPaddingTop + mSuperPaddingBottom; // Allow the height to outgrow its original estimate if // width is fixed. if (!resizeWidth && !mAdjustViewBoundsCompat) { heightSize = resolveAdjustedSize(newHeight, maxHeight, heightMeasureSpec); } if (newHeight <= heightSize) { heightSize = newHeight; } } } } } else { /* * We are either don't want to preserve the drawables aspect ratio, * or we are not allowed to change view dimensions. Just measure in * the normal way. */ w += mSuperPaddingLeft + mSuperPaddingRight; h += mSuperPaddingTop + mSuperPaddingBottom; w = Math.max(w, getSuggestedMinimumWidth()); h = Math.max(h, getSuggestedMinimumHeight()); widthSize = resolveSizeAndState(w, widthMeasureSpec, 0); heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); } Log("onMeasure, widthSize=" + widthSize + ", heightSize=" + heightSize); setMeasuredDimension(widthSize, heightSize); configureDrawMatrix(); } @Override protected void onDraw(Canvas canvas) { if (!FEATURE_IS_GIF_SUPPORTED || mMovie == null) { super.onDraw(canvas); return; } // Movie set time if (mMovieDuration == 0) { mMovie.setTime(0); } else { long now = android.os.SystemClock.uptimeMillis(); if (mMovieStartTime == 0) { mMovieStartTime = now;// first time } mMovie.setTime((int) ((now - mMovieStartTime) % mMovieDuration)); } // save the current matrix and clip of canvas int saveCount = canvas.getSaveCount(); canvas.save(); boolean superCropToPadding = getCropToPadding(); Log("superCropToPadding = " + superCropToPadding, DB_DETAIL); if (superCropToPadding) { int superScrollX = getScrollX(); int superScrollY = getScrollY(); int superRight = getRight(); int superLeft = getLeft(); int superBottom = getBottom(); int superTop = getTop(); canvas.clipRect(superScrollX + mSuperPaddingLeft, superScrollY + mSuperPaddingTop, superScrollX + superRight - superLeft - mSuperPaddingRight, superScrollY + superBottom - superTop - mSuperPaddingBottom); } if (mDrawMatrix != null && !mDrawMatrix.isIdentity()) { canvas.concat(mDrawMatrix); } mMovie.draw(canvas, mSuperPaddingLeft, mSuperPaddingTop); canvas.restoreToCount(saveCount); invalidate(); } @Override public void setWillNotCacheDrawing(boolean willNotCacheDrawing) { if (FEATURE_IS_GIF_SUPPORTED && mMovie != null) { super.setWillNotCacheDrawing(false); } else { super.setWillNotCacheDrawing(willNotCacheDrawing); } } private static void Log(String log) { if (DB) { Log.i(DB_TAG, log); } } private static void Log(String log, boolean enable) { if (enable) { Log.i(DB_TAG, log); } } }
初始化Movie对象以及测试用代码:
package com.yarkey.giftest2; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import android.app.Activity; import android.graphics.Movie; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.ImageView; import android.widget.ImageView.ScaleType; public class MainActivity extends Activity implements OnClickListener { private static final String TAG = "MainActivity"; ImageView image2; MovieImageView mView; Button mBtnA, mBtnB, mBtnC, mBtnD; static final String ExternalPath = Environment.getExternalStorageDirectory().getPath(); // setVisibility test private static int sIndexA = 0; private static int[] sVisibles = new int[] { View.INVISIBLE, View.GONE, View.VISIBLE }; private static String[] sDescrA = new String[] { "INVISIBLE", "GONE", "VISIBLE" }; // setScaleType test private static int sIndexB = 0; private static ScaleType[] sScaleTypes = new ScaleType[] { ScaleType.CENTER, ScaleType.CENTER_CROP, ScaleType.CENTER_INSIDE, ScaleType.FIT_CENTER, ScaleType.FIT_END, ScaleType.FIT_START, ScaleType.FIT_XY }; private static String[] sDescrB = new String[] { "CENTER", "CENTER_CROP", "CENTER_INSIDE", "FIT_CENTER", "FIT_END", "FIT_START", "FIT_XY" }; // setImageResource test private static int sIndexC = 0; private static int[] sResources = new int[] { R.drawable.pngtest1, R.drawable.giftest1, R.drawable.giftest2, R.drawable.giftest3 }; // setImageUri test private static int sIndexD = 0; private static Uri[] sImageUris = new Uri[] { Uri.parse("file://" + ExternalPath + "/giftest1.gif"), Uri.parse("file://" + ExternalPath + "/giftest2.gif"), Uri.parse("file://" + ExternalPath + "/giftest3.gif") }; // setMovie test private static int sIndexE = 0; private static Movie[] sMovies = new Movie[4]; private static String[] sDescrE = new String[] { "Movie1", "Movie2", "Movie3", "Movie4" }; private static int sIndexF = 0; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); setContentView(R.layout.activity_main); image2 = (ImageView) this.findViewById(R.id.image2); mView = (MovieImageView) this.findViewById(R.id.gifView); mBtnA = (Button) this.findViewById(R.id.btnA); mBtnB = (Button) this.findViewById(R.id.btnB); mBtnC = (Button) this.findViewById(R.id.btnC); mBtnD = (Button) this.findViewById(R.id.btnD); mBtnA.setOnClickListener(this); mBtnB.setOnClickListener(this); mBtnC.setOnClickListener(this); mBtnD.setOnClickListener(this); image2 = (ImageView) this.findViewById(R.id.image2); InputStream uriInputStream = null; try { uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[0])); uriInputStream.mark(uriInputStream.available()); sMovies[0] = Movie.decodeStream(uriInputStream); uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[1])); uriInputStream.mark(uriInputStream.available()); sMovies[1] = Movie.decodeStream(uriInputStream); uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream(sImageUris[2])); uriInputStream.mark(uriInputStream.available()); sMovies[2] = Movie.decodeStream(uriInputStream); uriInputStream = new BufferedInputStream(this.getContentResolver().openInputStream( Uri.parse("android.resource://com.yarkey.giftest2/" + R.raw.largegif))); uriInputStream.mark(uriInputStream.available()); sMovies[3] = Movie.decodeStream(uriInputStream); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } public void onClick(View arg0) { Log.i("MainActivity", "onClick"); switch (arg0.getId()) { case R.id.btnA: image2.setVisibility(sVisibles[sIndexA]); mView.setVisibility(sVisibles[sIndexA]); mBtnA.setText("Visible: " + sDescrA[sIndexA]); sIndexA++; if (sIndexA >= sVisibles.length) { sIndexA = 0; } break; case R.id.btnB: image2.setScaleType(sScaleTypes[sIndexB]); mView.setScaleType(sScaleTypes[sIndexB]); mBtnB.setText("ScaleType: " + sDescrB[sIndexB]); sIndexB++; if (sIndexB >= sScaleTypes.length) { sIndexB = 0; } break; case R.id.btnC: image2.setImageResource(R.drawable.ic_launcher); mView.setMovie(sMovies[sIndexE]); mBtnC.setText("setMovie: " + sDescrE[sIndexE]); sIndexE++; if (sIndexE >= sMovies.length) { sIndexE = 0; } break; case R.id.btnD: switch (sIndexF) { case 0: image2.setPadding(100, 0, 0, 0); mView.setPadding(100, 0, 0, 0); mBtnD.setText("setPadding: 100,0,0,0"); break; case 1: image2.setPadding(0, 100, 0, 0); mView.setPadding(0, 100, 0, 0); mBtnD.setText("setPadding: 0,100,0,0"); break; case 2: image2.setPadding(0, 0, 100, 0); mView.setPadding(0, 0, 100, 0); mBtnD.setText("setPadding: 0,0,100,0"); break; case 3: image2.setPadding(0, 0, 0, 100); mView.setPadding(0, 0, 0, 100); mBtnD.setText("setPadding: 0,0,0,100"); break; case 4: image2.setPadding(100, 100, 100, 100); mView.setPadding(100, 100, 100, 100); mBtnD.setText("setPadding: 100,100,100,100"); break; case 5: image2.setPadding(0, 0, 0, 0); mView.setPadding(0, 0, 0, 0); mBtnD.setText("setPadding: no"); break; } sIndexF++; if (sIndexF == 6) { sIndexF = 0; } break; } } }
布局文件:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="wrap_content" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <TextView android:id="@+id/textView1" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#FFBBBBBB" android:gravity="center" android:padding="10dp" android:text="InCallScreen" android:textAppearance="?android:attr/textAppearanceLarge" /> <ImageView android:id="@+id/image2" android:layout_width="match_parent" android:layout_height="100dp" android:background="#AACCFF" android:scaleType="center" /> <com.yarkey.giftest2.MovieImageView android:id="@+id/gifView" android:layout_width="match_parent" android:layout_height="100dp" android:background="#88AADD" android:scaleType="center" /> <Button android:id="@+id/btnA" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="Visible:VISIBLE" /> <Button android:id="@+id/btnB" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="ScaleType:default" /> <Button android:id="@+id/btnC" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="setMovie:null" /> <Button android:id="@+id/btnD" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" android:text="setPadding:no" /> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > </LinearLayout> </LinearLayout> </ScrollView>
好吧,整个帖子写得我自己都觉得非常乱了。
不过,记住一下ImageView流程也好: setImageDrawable -> configureBounds -> requestLayout -> onMeasure -> setFrame -> onDraw
我们计算Movie在View中显示的位置大小平移等效果,必须在setFrame函数调用后,才能执行(此时getWidth(), getHeight() 才会生效)。
写得太乱了,我的天!就此断了吧。有空再整理。
Best regards !