![](https://img.haomeiwen.com/i788577/319a8cf9c034528c.jpg)
结合 官方文档 阅读《Android 开发艺术探索》时所做的学习笔记。本篇记录第 6、7、12 章:Drawable、动画、Bitmap 相关。
Drawable
Drawable 是一种抽象的概念,表示一种可以在屏幕上进行绘制 (Draw) 的图像,常见的颜色和图片都可以是 Drawable。
Drawable 一般没有大小的概念,当作为 View 的 background 的时候,它会被拉伸至 View 同等大小。不过它有两个参数,getIntrinsicWidth
和 getIntrinsicHeight
表示内在的宽高,可以理解为默认宽高,比如显示一张图片时,这个值所对应的宽高就是当前图片的宽高,当作为 ColorDrawable 的时候该值则为 -1。
Drawable 的分类
Drawable 类型比较多,而且也没必要记住所有属性,需要时查文档就可以了:
- BitmapDrawable
- NinePatchDrawable
- ShapeDrawable
- LayerDrawable
- StateListDrawable
- LevelListDrawable
- TransitionDrawable
- InsetDrawable
- ClipDrawable
- ScaleDrawable
自定义 Drawable
只要重写 draw() 方法就可以了,例子见:android-art-res / Chapter_6
扩展阅读
Animation
动画可以分为属性动画 (Property Animation) 和 View 动画。View 动画通过对图像做各种变换(平移、旋转、缩放、透明度)从而产生动画效果,而属性动画则通过动态修改对象的属性来达到动画效果。推荐优先使用属性动画。
View 动画
Tween animation
补间动画分为平移动画、缩放动画、旋转动画和透明度动画,它们既可以通过 XML 定义也可以通过代码动态创建。xml 文件一般放在 res/anim 目录下。
![](https://img.haomeiwen.com/i788577/ff11352e23847c19.png)
Frame Animation
帧动画其实就是一组预先定义好的图片,然后逐帧显示。使用帧动画如果图片过大,可能会导致 OOM,所以需要注意图片尺寸的问题。xml 文件一般放在 res/drawable 目录下。
特殊的 Tween Animation
LayoutAnimation
主要用于 ViewGroup,使用后子元素出场时会显示该动画,比如用于 ListView 或者 RecyclerView,为 item 设置进入时的从左往右进入,或者先变大然后恢复的动效。
扩展阅读:Auto animate layout updates
Activity 切换效果
我们可以通过在 activity 启动以及结束前使用 overridePendingTransition(enterAnimId, exitAnimId)
或者 ActivityOptions.makeCustomAnimation(context, enterAnimId, exitAnimId)
来指定 Activity 的切换效果。
另外,Android 5.0 之后还支持共享元素的过渡效果,具体请阅读:Start an activity using an animation
属性动画
属性动画与 View 动画不同,它对作用对象进行了扩展,因此可以对任何对象做动画,甚至可以没有对象,而且实现的效果也更加丰富多样。
属性动画的使用
常用的类有 ValueAnimator
,ObjectAnimator
和 AnimatorSet
。其中 ObjectAnimator
继承自 ValueAnimator
,AnimatorSet
是一组动画集合。对应的 xml 标签分别是 <objectAnimator> <animator> <set>,xml 文件一般放在 res/animator 目录下。
不过属性动画使用代码进行操作更为简便,比如垂直平移一个 View 只要这样:
ObjectAnimator.ofFloat(view, "translationY", 100f).start()
插值器和估值器
插值器我理解为动画的变化方式(速率等),而估值器表示某一个时间节点下的变化的值。系统自带的插值器有:TimeInterpolator
/ LinearInterpolator
/ AccelerateDecelerateInterpolator
等,如果我们想要自定义动画效果,一般需要实现 Interpolator 和 TypeEvaluator。
属性动画的监听器
我们可以实现对动画播放过程的监听,主要通过 Animator.AnimatorListener
,可以监听动画的开始、结束、取消、重新播放。 ValueAnimator.AnimatorUpdateListener
,可以监听动画的更新,通过 ValueAnimator.getAnimatedValue() 获取变化值,我们还可以利用这个方法自定义出一些特殊效果。
属性动画的工作原理
通过获取 View 上的该属性的初始值(如果没有提供初始值则调用属性的 get 方法)和最终值(即我们 set 进去的值,xml 中的 toValue
),然后以动画的效果,多次调用其 set 方法,直到达到最终值。源码解析就不贴了。
对于某些属性,即使提供了 set get 方法,但是对其做属性动画依旧没效果,原因是该属性不会带来 UI 显示上的变化,自然看上去就像没有效果了,比如 TextView 的 setWidth 和 getWidth,这个方法指定的是最大宽度,而不是实际显示的宽度,所以对它做属性动画不会有效果。
我们可以通过两种方式来改变这种状况,一种是通过包装原始对象,并为其提供 set 和 get 方法。另一种是通过上面提到的 ValueAnimator.AnimtorUpdateListener
加上估值器,手动修改对应的属性来实现效果。例子见:android-art-res / Chapter_7。推荐使用第一种,因为更容易复用。
扩展阅读
Bitmap 的加载和缓存
Bitmap 的高效加载
我们通过 Bitmap 加载图片的时候,一般都需要考虑图片大小的问题,图片越大占用的内存也就越多,一不小心还有可能造成 OOM,所以对于大图来说一般会做缩放后再显示。
核心思想是,首先通过为 BitmapFactory.Options
的 inJustDecodeBounds
设置 true,对图片进行采样获取到宽高(不会为图片像素点分配内存,因此不会消耗太大),然后再根据实际需要显示的宽高,计算出合理的缩放倍数,然后再对图片进行真正的加载。
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;
}
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);
}
BitmapFactory
除了 decodeResource()
这一从资源中读取 Bitmap 的方式外,还可以使用 decodeFile()
、decodeStream()
、decodeByteArray()
这几种方式,分别表示从文件、字节流、Byte 数组中读取并加载 Bitmap,这些方法最终调用的都是 native 方法,由 Android 底层实现。
扩展阅读
内存缓存和磁盘缓存
在 RecyclerView 或者 ViewPager 中加载大量图片时,如果不做特殊处理,由于 View 的复用以及垃圾回收机制的存在,屏幕之外的图片很快会被回收掉,所以为了让页面保持流畅(不出现白屏也不会因为重复加载图片导致卡顿),我们就需要使用缓存来加速图片的恢复加载。
内存缓存
通过内存缓存(LruCache),我们可以将 Bitmap 缓存在应用内存中来提升加载速度。LruCache
中使用一个 LinkedHashMap
保存最近引用过的对象,当引用数量超出容量限制的时候就会将最近最少使用的对象移除。
使用 LruCache
缓存图片的时候需要考虑以下问题:
- 图片大小多少,占用多少内存?
- 一屏加载多少图片?有多少是需要预加载的?
- 你的 activity 除了图片之外,其他部分耗内存吗?
- 有哪些图片是频繁访问的?如果有特定图片是频繁访问的,可以选择常驻到内存缓存中。另外,如果有确定的访问频率不一致的图片组,可以考虑使用多个
LruCache
。 - 如果是特别大的图片,可以考虑使用多种清晰度的图片,先加载低清晰度图片,然后再使用后台任务加载高清度的图片。
- 如果是本地图片,需要考虑在不同屏幕大小和屏幕密度上设备是否表现一致。
使用 LruCache 加载 Bitmap 的例子:
private LruCache<String, Bitmap> memoryCache;
@Override
protected 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;
memoryCache = 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) {
memoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return memoryCache.get(key);
}
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 前先判断是否有缓存:
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);
}
}
磁盘缓存
内存缓存虽然速度快,但是我们也不能完全只依赖它,因为用户手机可能内存很小,应用随时可能被杀死(用户离开一段时间或者有更高优先级的任务占用了内存等等),当用户重新打开你的页面的时候,又要重新加载图片。
DiskLruCache
可以帮我们把加载过的 Bitmap 持久化到存储空间中从而减少用户重新加载的等待时间。
private DiskLruCache diskLruCache;
private final Object diskCacheLock = new Object();
private boolean diskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
private static final int APP_VERSION = 1;
private static final int VALUE_COUNT = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
}
class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void doInBackground(File... params) {
synchronized (diskCacheLock) {
File cacheDir = params[0];
diskLruCache = DiskLruCache.open(
cacheDir, APP_VERSION, VALUE_COUNT, DISK_CACHE_SIZE);
diskCacheStarting = false; // Finished initialization
diskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected 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 normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
memoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (diskCacheLock) {
if (diskLruCache != null && diskLruCache.get(key) == null) {
diskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (diskCacheLock) {
// Wait while disk cache is started from background thread
while (diskCacheStarting) {
try {
diskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (diskLruCache != null) {
return diskLruCache.get(key);
}
}
return null;
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static 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 dir
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
因为涉及到文件读写,所以速度肯定要慢一点。而且可以看到,上面 DiskLruCache
是在子线程中创建的,所以当添加或者读取的时候,我们需要使用同步锁。
扩展阅读
ImageLoader
的实现与使用
一个图片加载框架一般需要考虑:
- 图片的压缩
- 内存缓存与磁盘缓存
- 同步加载、异步加载
- 通过网络加载
具体实现见:android-art-res / Chapter_12
照片墙效果
优化列表卡顿
-
不要在
getView()
方法中做耗时操作。比如加载图片是耗时操作,如果在getView()
中进行加载,那么一定会导致卡顿,所以一般需要异步加载。 -
控制异步任务的执行频率。当用户快速滑动列表时会产生大量异步任务,随后通知主线程进行大量的 UI 更新操作,此时很容易造成卡顿。所以我们可以为列表设置
OnScrollListener.onScrollStateChanged
,并在其中判断是否滑动,禁止在滑动时加载图片。 -
开启硬件加速。为 activity 设置
android:hardwareAccelerated="true"
,很多莫名的卡顿问题可能都是因为硬件加速没开。
网友评论