4 Android图像与动画
4.1高效显示Bitmap
在安卓应用中加载Bitmaps的操作是需要特别小心处理的,有下面几个方面的原因:
1、移动设备的系统资源 有限。安卓设备对于蛋哥程序至少需要16MB的内容。
Android Compatibility Definition Document (CDD)
, Section 3.7. Virtual Machine Compatibility 中给出了对于不同大小与密度的屏幕的最低内存需求。 应用应该在这个最低内存限制下去优化程序的效率。当然,大多数设备的都有更高的限制需求。
2、Bitmap会消耗很多内存,特别是对于类似照片等内容更加丰富的图片。
3、Android英勇的UI通常会一次操作中立即加载许多张bitmaps。例如在listview GridView 与View pager等控件中需要一次加载许多张bitmaps,而且需要预先加载一些没有在屏幕上显示的内容。
4.1.1高效加载大图
图片有不同的形状和大小。在大多数情况下,它们的实际大小都比需要呈现的尺寸大很多。例如,系统的图库应用会显示那些我们使用相机拍摄的照片,但是那些图片的分辨率通常都比设备屏幕的分比率要高很多。
考虑到应用在有限的内存下工作的,理想情况是我们只需要在内存中加载一个低分辨率照片即可。为了更便于显示,这个低分辨率的照片应该是与其对应的UI控件大小相匹配。加载一个超过屏幕分辨率的高分辨率照片不仅没有任何好处,还会占用宝贵的内存资源。
读取位图的尺寸与类型
BitmapFactoty提供了一些解码(decodeByteArray(),decodeFile(),decodeResource()等)的方法,用来从不同的资源中创建一个Bitmap。我们应该根据图片的数据源来选择合适的解决方法。这些方法在构造位图的时候会尝试分配内存,因此会容易导致outofmemory的异常,每一种解码方法都可以通过BitmapFactory.Options设置一些附加的标记,以此来指定解码选项。设置inJustDecodeBounds属性为true可以在解码的时候避免内存的分配,它会返回一个null的Bitmap, 但可以获取到outWidth,outHeight,与out Mime Type。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
加载一个按比例缩小的版本到内存中
通过上面的步骤,我们已经获取到图片的尺寸,这些数据可以用来帮主我们决定加载整个图片到内存中还是加载到一个缩小的版本。
1、评估加载完整图片所需要耗费的内存
2、程序在加载这张图片时可能涉及到的其他内存需求
3、呈现这张图片的控件的尺寸大小
4、屏幕大小与当前设备的屏幕密度。
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
为了使用该方法,首先需要设置
inJustDecodeBounds
为
true
, 把options的值传递过来,然后设置
inSampleSize
的值并设置 inJustDecodeBounds 为
false
,之后重新调用相关的解码方法。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
4.1.2非UI线程处理Bitmap
使用AsyncTask(Use a AsyncTask)
AsyncTask 类提供了一个在后台线程执行一些操作的简单方法,它还可以把后台的执行结果呈现到UI线程中。下面是一个加载大图的示例:
class BitmapWorkerTask extends AsyncTask {
private final WeakReference imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference(imageView);
}
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
为
ImageView
使用
WeakReference
确保了AsyncTask所引用的资源可以被垃圾回收器回收。由于当任务结束时不能确保
ImageView
仍然存在,因此我们必须在
onPostExecute()
里面对引用进行检查。该ImageView在有些情况下可能已经不存在了,例如,在任务结束之前用户使用了回退操作,或者是配置发生了改变(如旋转屏幕等)。
处理并发问题
通常类似listview与GridView等视图控件在使用上面岩石的AsyncTask方法时,会同时带来并发的问题。首先为了更高的效率,listview与GridView的子item视图会在用户滑动屏幕时,被循环使用。
如果每个子视图都触发一个AsyncTask,那么就无法确保关联的视图在结束任务时,分配的视图已经进入循环队列中,给另外一个子视图进行重用。而且无法确保所有的异步任务的完成顺序和他们本身的启动顺序保持一致。
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
(3)
在上面的代码示例中,
cancelPotentialWork
方法检查是否有另一个正在执行的任务与该ImageView关联了起来,如果的确是这样,它通过执行
cancel()
方法来取消另一个任务。在少数情况下, 新创建的任务数据可能会与已经存在的任务相吻合,这样的话就不需要进行下一步动作了。下面是
cancelPotentialWork
方法的实现 。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData == 0 || bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
(4)
在上面的代码中有一个辅助方法:
getBitmapWorkerTask()
,它被用作检索AsyncTask是否已经被分配到指定的ImageView:
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
(5)
最后一步是在BitmapWorkerTask的
onPostExecute()
方法里面做更新操作:
class BitmapWorkerTask extends AsyncTask {
...
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
4.1.3缓存Bitmap
使用内存缓存
内存缓存以花费宝贵的程序内存为前提来快速访问位图,LruCache类特别适合用来缓存Bitmaps。它使用一个强引用,并且在缓存超出设置大小的时候剔除最近最少使用到的对象。
为了给LruCache选择一个合适的大小,需要考虑下面一些因素:
1、应用剩下了多少可用的内存?
2、多少张图片会同时呈现到屏幕上?有多少图片需要准备好以便马上显示到屏幕上?
3、设备的屏幕大小与密度是多少?
4、Bitmap的尺寸与配置是多少,会花费多少内存?
5、图片被访问的频率如何?是其中一些比另外的访问更加频繁吗?如果是,那么我们可能希望在内存中保存那些最常访问的图片,或者根据访问聘礼给Bitmap风阻,为不同的Bitmap组设置多个LruCache对象。
6、是否可以在缓存图片的质量与数量之间寻找平衡点?某些事件保存大量低质量的Bitmap会非常有用,加载更高质量图片的任务可以交给另外一个后台线程。
private LruCache<String, Bitmap> mMemoryCache;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
当加载Bitmap显示到ImageView 之前,会先从LruCache 中检查是否存在这个Bitmap。如果确实存在,它会立即被用来显示到ImageView上,如果没有找到,会触发一个后台线程去处理显示该Bitmap任务。
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
上面的程序中
BitmapWorkerTask
需要把解析好的Bitmap添加到内存缓存中
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
使用磁盘缓存
内存缓存能够提高访问最近用过的Bitmap的速度,但是我们无法保证最近访问过的Bitmap都能够保存在缓存中。像类似GridView等需要大量数据填充的控件很容易就会用尽整个内存缓存。
我们的应用可能会被类似打电话等行为而暂停并退到后台,因为后台应用可能会被杀死,那么内存缓存就会被销毁,里面的Bitmap也就不存在了。一旦用户恢复应用的状态,那么应用就需要重新处理那些图片。
private DiskLruCache mDiskLruCache;
privatefinal Object mDiskCacheLock = new Object();
privateboolean mDiskCacheStarting = true;
privatestaticfinalint DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MBprivatestaticfinal String DISK_CACHE_SUBDIR = "thumbnails";
@OverrideprotectedvoidonCreate(Bundle savedInstanceState){
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
、初始化磁盘缓存
classInitDiskCacheTaskextendsAsyncTask<File, Void, Void> {
@Overrideprotected Void doInBackground(File... params){
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
returnnull;
}
}
初始化Bitmap加载线程
classBitmapWorkerTaskextendsAsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.@Overrideprotected Bitmap doInBackground(Integer... params){
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache// Process as normalfinal Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
/添加图片到缓存
publicvoidaddBitmapToCache(String key, Bitmap bitmap){
// Add to memory cache as beforeif (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cachesynchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
//获取图片从缓存
public Bitmap getBitmapFromDiskCache(String key){
synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background threadwhile (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
returnnull;
}
、、chuang造一个缓存文件夹
// Creates a unique subdirectory of the designated app cache directory. Tries to use external// but if not mounted, falls back on internal storage.publicstatic File getDiskCacheDir(Context context, String uniqueName){
// Check if media is mounted or storage is built-in, if so, try and use external cache dir// otherwise use internal cache dirfinal String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
returnnewFile(cachePath + File.separator + uniqueName);
}
处理配置改变
如果运行时设备配置信息发生改变,例如屏幕方向的改变会导致安卓当前的activity先被销毁然后重启。
幸运的是,在前面介绍使用内存缓存的部分,我们已经知道如何建立内存缓存。
这个缓存可以通过调用setRetainInstance(true)保留一个Fragment实例的方法把缓存传递给新的Activity。
在这个activity被重新创建后,这个保留的Fragment会重新附着上。
private LruCache<String, Bitmap> mMemoryCache;
@Overrideprotected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}
4.1.4管理Bitmap的内存使用
Android管理Bitmap内存使用的演变进程:
1、安卓2.2(API LEVEL 8)以及之前,当垃圾回收发生时,应用的线程是会被暂停的,这会导致一个延迟
滞后,并降低系统效率。从安卓2.3开始,添加了并发垃圾回收的机制,这意味着在一个Bitmap不再被
之后,它所占用的内存会被立即回收。
2、在安卓2.3.3以及之前,一个Bitmap的像素级数据是存放在Native内存空间中的,这些数据与Bitmap
本身是隔离的,Bitmap本身被存放在Dalvik堆中。我们无法预测在Native内存中的像素级数据何时会
被释放,这意味着程序容易超过它的内存限制并且崩溃。自从安卓3.0开始,像素级数据则是与Bitma
p
一起存放在Dalvik堆中。
管理安卓2.3.3及一下版本的内存使用
在安卓2.3.3以及更低版本上,推荐使用recycle()方法。
caution:只有当我们确定这个bitmap不再需要用到的时候才应该使用recycle()。在执行recycle()方法之后,
如果尝试绘制这个Bitmap,我们将得到“canvas:trying to use a recycled bitmap”
下面的代码片段演示了使用
recycle()
的例子。它使用了引用计数的方法(
mDisplayRefCount
与
mCacheRefCount
)来追踪一个Bitmap目前是否有被显示或者是在缓存中。并且在下面列举的条件同时满足时,回收Bitmap:
1、mDisplayRefCount
与 mCacheRefCount
的引用计数均为 0;
2、bitmap不为
null
, 并且它还没有被回收。
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.// Keep a count to determine when the drawable is no longer displayed.public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
// Notify the drawable that the cache state has changed.// Keep a count to determine when the drawable is no longer being cached.public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
管理Android3.0及其以上版本的内存
从安卓3.0开始,引进了Bitmapfactory.Options.inBitmap字段。如果使用了这个设置字段,decode方法会在
加载Bitmap数据的时候去重用已经存在的Bitmap。这意味着Bitmap的内存是被重新利用的。然而,使用
inBitmap是有一些限制,特别是安卓4.4之前,只有同等大小的位图才可以被重用。
实现图片加载图片到ViewPager
我们可以通过
PagerAdapter
与
ViewPager
控件来实现这个效果。 不过,一个更加合适的Adapter是PagerAdapter的一个子类,叫做
FragmentStatePagerAdapter
:它可以在某个ViewPager中的子视图切换出屏幕时自动销毁与保存Fragments的状态。这样能够保持更少的内存消耗。
Note:
如果只有为数不多的图片并且确保不会超出程序内存限制,那么使用PagerAdapter或 FragmentPagerAdapter会更加合适。
public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ViewPager mPager;
// A static dataset to back the ViewPager adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
@Override
public int getCount() {
return mSize;
}
@Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
}
}
Fragment里面包含了ImageView控件:
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
// Empty constructor, required as per Fragment docs
public ImageDetailFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// image_detail_fragment.xml contains just an ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final int resId = ImageDetailActivity.imageResIds[mImageNum];
mImageView.setImageResource(resId); // Load image into ImageView
}
}
希望你有发现上面示例存在的问题:在UI线程中读取图片可能会导致应用无响应。因此使用在第二课中学习的AsyncTask会更好。
public class ImageDetailActivity extends FragmentActivity {
...
public void loadBitmap(int resId, ImageView imageView) {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
... // include BitmapWorkerTask class
}
public class ImageDetailFragment extends Fragment {
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (ImageDetailActivity.class.isInstance(getActivity())) {
final int resId = ImageDetailActivity.imageResIds[mImageNum];
// Call out to ImageDetailActivity to load the bitmap in a background thread
((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
}
}
}
在BitmapWorkerTask中做一些例如重设图片大小,从网络拉取图片的任务,可以确保不会阻塞UI线程。如果后台线程不仅仅是一个简单的加载操作,增加一个内存缓存或者磁盘缓存会比较好(请参考第三课:缓存Bitmap),下面是一些为了内存缓存而附加的内容:
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache mMemoryCache;
@Override
public void onCreate(Bundle savedInstanceState) {
...
// initialize LruCache as per Use a Memory Cache section
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
... // include updated BitmapWorkerTask from Use a Memory Cache section
}
4.3添加动画
屏幕划动是在两个完整界面间的转换,它在一些UI中很常见,比如设置向导和幻灯放映。
用PageTransformer自定义动画
要展示不同于默认滑屏效果的动画,我们需要实现
ViewPager.PageTransformer
接口,
然后把它补充到ViewPager里就行了。这个接口只暴露了一个方法,
transformPage()
。
每次界面切换,这个方法都会为每个可见页面(通常只有一个页面可见)和刚消失的相邻页面调用一次。
例如,第三页可见而且用户向第四页拖动,
transformPage()
在操作的各个阶段为第二,三,四页分别调用。
决定哪些页面需要被动画转换,这样我们就能创建自己的动画。
position
参数表示特定页面相对于屏幕中的页面的位置。它的值在用户滑动页面过程中动态变化。当某一页面填充屏幕,它的值为0。
当页面刚向屏幕右侧方向被拖走,它的值为1。如果用户在页面1和页面2间滑动到一半,那么页面1的position为-0.5并且页面2的position为 0.5。
Zoom-out Page Transformer
当在相邻界面滑动时,这个Page Transformer使页面收缩并褪色。当页面越靠近中心,它将渐渐还原到正常大小并且图像渐入。
Depth Page Transformer
这个Page Transformer使用默认动画的屏幕左滑动画。但是为右滑使用一种“潜藏”效果的动画。潜藏动画将page淡出,并且线性缩小它。
展示Card翻转动画
Card的每一面都是一个独立的布局,比如两屏蚊子,两张图片,或者任何View的组合,然后我们将在应用
动画的Fragment里面用到这两个布局。
privatevoidflipCard(){
if (mShowingBack) {
getFragmentManager().popBackStack();
return;
}
// Flip to the back.
mShowingBack = true;
// Create and commit a new fragment transaction that adds the fragment for the back of// the card, uses custom animations, and is part of the fragment manager's back stack.
getFragmentManager()
.beginTransaction()
// Replace the default fragment animations with animator resources representing// rotations when switching to the back of the card, as well as animator// resources representing rotations when flipping back to the front (e.g. when// the system Back button is pressed).
.setCustomAnimations(
R.animator.card_flip_right_in, R.animator.card_flip_right_out,
R.animator.card_flip_left_in, R.animator.card_flip_left_out)
// Replace any fragments currently in the container view with a fragment// representing the next page (indicated by the just-incremented currentPage// variable).
.replace(R.id.container, new CardBackFragment())
// Add this transaction to the back stack, allowing users to press Back// to get to the front of the card.
.addToBackStack(null)
// Commit the transaction.
.commit();
}