关闭

pAdTy_1 构建图形和动画应用程序

标签: Android图形动画
761人阅读 评论(0) 收藏 举报
分类:

2015.11.12-11.18
个人英文阅读练习笔记。原文地址:http://developer.android.com/training/building-graphics.html

2015.11.12
此部分内容将展示如何用图形来完成任务以给应用程序带来竞争优势。如果您想超越基本的用户界面而想创造美丽的视觉体验,此部分内容将会帮助您完成此心愿。

1. 有效的显示位图

在保持用户界面的响应性时,如何加载和处理位图并避免超过内存限制。

学习保持用户界面组件的响应性并避免超过应用程序的内存限制的方法来处理和加载位图对象。如果不那么仔细,位图能够快速消耗掉可用的内存预算随之导致可怕的异常(java.lang.OutofMemoryError: bitmap size exceeds VM budget)而让应用程序崩溃。

以下是在安卓应用程序中载入位图时需要机警的几个原因:
- 移动设备的系统资源通常都比较受限制。安卓设备只能给每个应用程序16MB的可用内存空间。安卓兼容性定义文档(Android Compatibility Definition Document)第3.7节。虚拟机兼容会给各种不同尺寸和密度的屏幕下的应用程序最小的内存空间。应用程序应被优化到能够在最小内存空间运行的程度。然而,许多设备都会配置更高的内存限制。
- 位图会占用大量的内存,尤其是像照片这样的富图。例如,Galaxy Mexus设备上的相机拍照达2592x1936像素(500万像素)。如果位图配置使用ARGB_8888(安卓2.3版本以前默认),载入此照片消耗19MB内存(2592x1936x4字节),一下子就将某些设备上给应用程序预分配的可用空间给消耗了。
- 安卓应用程序用户界面在同一时刻需要载入几张位图。像ListView、GridView以及ViewPager这样的组件通常在同时包含多张位图(有的是跟随用户操作而即将展现的图片)。

1.1 有效地载入大型位图

在不超过每个应用程序内存限制的情况下解码大型位图。

不同的图片不同的形状和尺寸。在许多情况下应用程序的用户界面所需的图片都比实际的图片要小。例如,系统的画廊应用程序展示的用安卓设备相机拍的图片的分辨率通常就比设备屏幕的密度要高。

鉴于有限的内存,理想情况下只需加载一个低分辨率的版本到内存中。低版本分辨率应该要跟显示它的用户界面组件的尺寸匹配。一个拥有高分辨率的图片不会给显示带来好处,反而会更多的占用珍贵的内存并会引起额外的性能开销。

此节将通过载入图片的一小部分的方式解码图片以不超过应用程序有限的内存。

(1) 读取位图的尺寸和类型
BitmapFactory类提供了几种解码方法(decodeByteArray(),decodeFile(),decodeResource()等)来根据各种类型资源创建Bitmap。给予图片数据资源选择最合适的解码方法。这些方法尝试为所构建的位图分配内存,因此就能够很容易检测出outOfMemory异常。每种类型的解码方法都有额外的可以通过BitmapFactory.Options类制定编码选项的签名。解码时将inJustDecodeBounds特性设置为ture以避免内存分配,通过设置位图的outWidth、outHeight和outMimeType可返回null。此项技术允许在构建(以及内存分配)位图之前获取到图片的尺寸和类型。

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;

欲避免java.lang.outOfMemory异常,在解码位图时检查位图的尺寸,除非确定图片不会引来此异常。

(2) 载入图片的缩小版本到内存
在知道图片的尺寸后,此数据就可以用来判断是要将整张图片都载入内存还是将代替此图片的子样例载入内存。以下是需要考虑的因素:
- 估算整张图片会占用的内存。
- 被用来载入图片的内存是否会被应用程序的其它部分使用。
- 图片将要显示的目标ImageView或用户界面组件的尺寸。
- 现有设备屏幕尺寸和密度。

举例,如果一张1024x768像素的图片最终会被略缩显示在128x96像素的ImageView中,那么此图片就不值得全被载入到内存中。

欲告知解码器解码图片的子样本,载入一个更低像素版本的图片到内存中,需要将BitmapFactory.Options中的inSampleSize设置为ture。例如,一张像素为2048x1536的图片用inSampleSize值为4来解码会产生约512x384的位图。将解码后的图片载入内存只需花0.75MB,而将整张图片载入内存会消耗12MB内存(假设位图配置为ARGB_8888)。基于目标宽度和高度,有一种将样本尺寸计算出2的指数的方法。

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;
}

注:解码器最终将值舍到最接近2的指数的值。

欲用这种方法,首先要用被设置为true的inJusDecodeBounds解码一次,将选项传递再用值为false的inSampleSize和inJustDecodeBounds再解码一次。

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);
}

此方法让载入任意大小尺寸位图到ImageView变得简单。如在ImageView中显示一个100x100像素的缩略图时,用以下代码即可实现:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

可以用类似的步骤解码其它的资源来形成位图,通过替代合适的BitmapFactory.decode*方法即可。

1.2 在用户界面线程之外的线程处理位图

位图处理(重新设置尺寸,从远端下载等)不再主用户界面所在线程中处理。此部分笔记将带您学习用AsynTask创建后台线程来处理位图并解释如何处理并发问题。

在“有效地载入大型位图”一节中所讨论的BitmapFactory.decode*方法,如果图片资源数据在硬盘或网络( 或其它任何不在内存的位置)上,都不应该在用户界面主线程中使用这些方法。载入图片所花的时间是不可预测的,它基于各种各样的因素(从硬盘或网络读取数据的速度,图片尺寸,CPU的性能等)。如果因载入图片阻碍了用户界面线程,系统所运行的应用程序将不具有实时的响应性,用户也极有可能选择将此应用程序关闭(见设计具响应性的应用程序获取更多信息)。

(1) 使用异步任务(AsyncTask)
AsyncTask类提供了一种简单的方式在后台线程中执行一些任务并将结果返回到用户主线程中。欲使用此类,需要创建一个子类并重写所提供的方法。以下是使用AsyncTask和decodeSampledBitmapFromResource将一张大型图片载入到ImageView中的示例:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(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以及其引用的任何东西收集垃圾信息。不敢保证当任务执行完后ImageView仍旧还在,所以必须在onPostExecute()中检查其引用。就此例来说,在任务结束之前用户导航离开活动或者配置发生改变时,ImageView可能不再存在。

欲异步开始载入位图,简单的创造一个新的任务并执行与载入相关的代码即可:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

2015.11.13
(2) 处理并发
ListView、GridView等这些常见组件和AsyncTask结合使用会引来引来另外一个问题。为了有效地利用内存,随着用户滑动滚动条,这些组件会被重复利用为子视图显示。如果每个子视图都触发一个AsyncTask,不敢保证当AsyncTask完成时,对应的视图还未被重复利用来显示另外一个子视图。另外,也不能保证各异步线程是在其它线程利用完视图后再开始利用此视图。

博客“高性能的多线程(Multithreading for Performance)”深入的讨论了处理并发问题,并提供了当某任务完成后何AsyncTask将获得ImageView的引用何AsyncTask稍后再引用ImageView的解决方法。使用相似的方法,前一节提到的AsyncTask能够被扩展为一个成熟的模式。

创建一个微型的Drawable子类来存储一个返回到工作任务的引用。在这种情况下,当任务执行完时,BitmapDrawable将图片展示在ImageView中:

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference;

在执行bitmapWorkerTask以前,可以创建AsyncDrawable并将其绑定到目标ImageView上:

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);
    }
}

在以上代码样例中涉及到的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 is not yet set or it differs from the new 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;
}

以上代码所使用的getBitmapWorkerTask()方法用来检索所任务涉及的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;
}

最后一步是更新BitmapWorkerTask中的onPostExecte()以检查任务是否被取消,斌检查当前任务是否关联上了ImageView:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @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);
            }
        }
    }
}

这样的实现就适合用ListView、GridView以及其它的组件被重复用作子视图显示了。在将图片设置到ImageView的地方简单的调用loadBitmap。例如,在GridView的实现中,是调用getView()方法来实现的,此在后一节中描述。

1.3 缓存位图

此节教您在载入多张位图时如何使用内存和硬盘位图缓存来提升主用户界面的响应性和流动性。

载入一张位图到用户界面是比较简单的,然而当需要在同一时间就载入大量位图时就会变得复杂许多。在许多情况(如ListView、GridView或ViewPager组件)下,可能很快滚动到屏幕上显示的数量是无限的。

当向下移动屏幕时通过重复利用组件来表示子视图的方式来保持内存消耗量不上升。假如不保持长期的引用位图,垃圾回收器会释放载入的位图。这一点固然是好,但为了保持流畅和快速的加载用户界面,当图片每次重新回到屏幕上时也想避免次次都去处理它。一段内存或硬盘缓存 能够满足组件快速重载入之前经处理过的图片。

此节将展示当载入多张位图时,使用内存或硬盘位图缓存来提升用户界面的流动性和响应性。

(1) 使用内存缓存
占用应用程序可用内存空间的内存缓存用来保存位图可被快速访问。LruCache类(此类也存在于API level 4 对应的支持库中)特别适合于位图缓存、在强引用LinkedHashMap中保持最近的引用对象、在缓存越界之前驱逐最近引用最少的对象的任务。

注:在以前,流行的内存缓存的实现是SoftReference或WeakReference位图缓存,但现在不推荐此种缓存。从Android 2.3(API level 9)开始,垃圾回收器变得更加强大,它回收让对象几乎无效的软/弱引用。另外,在Android 3.0(API level 11)之前,位图的回收数据没有被提前释放而是被保存在本地内存中,这可能会引起应用程序超越其内存限制而崩溃。

欲给LruCache选择一个合适的尺寸,许多因素都应该被纳入考虑,如:
- 活动跟应用程序使用后所剩下的内存大小。
- 多少图片会被同一时间载入到屏幕上?需要准备多少图片到屏幕上?
- 设备的屏幕尺寸和密度是多少?对于相同数量的图片,像Galaxy Nexus这样屏幕密度格外高(xhdpi)的设备比Nexus S(hdpi)设备所要分配的内存缓存要大。
- 根据位图的尺寸和配置计算到图片所会占用的内存有多大?
- 图片被访问的频率有多大?是否其中有一部分图片的访问频率会高于其它图片?如果是这样,可能需要总是要在内存中保存特定的内容,设置为不同组的位图分配对应的LruCache对象。
- 需要平衡质量和质量么?有时选择存储大数量低质量的位图可能会更有用,而在后台进程中载入高质量的图片。

没有适合所有应用程序的特定的尺寸和规则,需要根据具体情况分析用量并作出相应的决策。如果缓存太小会引起附加开销,如果缓存太大就有可能会引起java.lang.OutOfMemory异常并会让应用程序智能使用很小的内存。

以下是为位图设置LruCache的示例代码:

private LruCache<String, Bitmap> mMemoryCache;

@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;

    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);
}

注:在此例中,将应用程序内存的八分之一分配作为了缓存。对于通常(hdpi)设备来说,这是缓存的最小值,约为4MB(32/8)。在一个800x480分辨的设备上,一个全屏的GridView填充的图片会占用约为1.5MB(800*480*4字节),所以此缓存约能存2.5张这样的图片。

当载入位图到ImageView中时,LruCache最先被检查。如果寻到入口,它会立马被用来更新ImageView,否则会催生一个后台线程来处理图片:

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也需要被更新以添加到内存缓存的入口:

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;
    }
    ...
}

(2) 使用硬盘缓存
内存缓冲区对最近常被查看的视图的访问速度的提升很有用,然而不能够让图片依赖此种缓存。像GridView这种拥有大量数据集的组件很容易就填满内存缓冲区。应用程序还可能会被诸如来电这样的任务打断,如此,在后台的线程就可能会被杀死即内存缓冲区会被销毁。一旦用户恢复应用程序后,应用程序不得不再次重新处理每张图片。

在以上描述的情况中可以使用硬盘缓存来保留经处理的位图并当内存缓冲区中的图片不可用时能减少载入次数。当然,从硬盘中取图片会比从内存载入慢且因为读硬盘次数不可预测,所以此举需要在后台线程中完成。

注:像画廊应用程序中访问频率较高的图片使用ContentProvider来提供图片缓存更合适。

Android 源码中的类样码使用DiskLruCache实现。以下代码在已有内存缓冲区后增加硬盘缓冲区:

private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@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 (mDiskCacheLock) {
            File cacheDir = params[0];
            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            mDiskCacheStarting = false; // Finished initialization
            mDiskCacheLock.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) {
        mMemoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (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 thread
        while (mDiskCacheStarting) {
            try {
                mDiskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (mDiskLruCache != null) {
            return mDiskLruCache.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);
}

注:即使是初始化硬盘缓冲区也需要硬盘操作且不应在主线程中完成这个过程。然而,在初始化之前确实也有访问缓存的机会。为了解决这个问题,在以上代码的实现中,使用锁住一个对象来确保在初始化缓冲区之前不能读硬盘缓冲区。

当内存缓冲区在用户界面线程被完成后,当硬盘缓冲区在后台线程中被创建后。与硬盘相关的操作不要在用户界面线程中操作。当图片处理完成后,最终的位图被同时增加到内存和硬盘缓冲区中,供后续使用。

(3) 处理配置更改
诸如屏幕方向改变这样的运行时配置改变时,会引起安卓销毁并用新配置重启运行的活动(关于此行为的更多信息见Handling Runtime Changes)。为让用户在配置改变时还能够感受到流利快速的用户体验需要避免再次处理所有的图片。

幸运的是,在Use a Memory Cache节为位图创建了好用的内存缓冲区。使用通过调用setRetainInstance(true)保存的碎片能够将缓冲区传递给新的活动实例。在活动被重建后,它能够重新获得附加的碎片且能够获取对存在缓冲区对象的访问权,允许快速的提取并重新填充到ImageView对象中。

以下代码处理配置改变后用碎片重新获取LruCache的过程:

private LruCache<String, Bitmap> mMemoryCache;

@Override
protected 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);
    }
}

旋转手机用重新获得/没获得碎片的情况来测试此段代码。您应该注意无滞后情况下,几乎是立即从重获的缓冲区中装载图片到活动中去的。如果内存缓冲区中无相应的图片就到硬盘缓冲区找寻找,如果硬盘缓冲区中亦无,那么就像平常一样处理。

1.4 管理位图内存

此节解释如何管理位图内存来最大化的提升应用程序性能。

除了在缓存位图中描述措施外,还有另外一些特殊的方法可以用来优化垃圾回收器和位图的重使用。具体的策略基于具体的安卓系统版本。BitmapFun应用程序示例包含展示设计跨不同安卓版本的应用程序的类。

在正式开始此节内容之前,展示下安卓管理位图内存的演化过程:
- 在安卓2.2(API level 8)及更低的版本中,当垃圾回收器工作时,应用程序中的线程将停止。这会给应用程序引起降低性能的滞后。Android 2.3增加了并发的垃圾回收器,这意味着在位图不再被引用后内存将被回收来供应用程序重新使用。
- 在安卓2.3.3(API level 10)及更低版本中,位图的像素数据(backing pixel data)被保存在本地内存中。它跟位图本身独立,位图被保存在Dalvik堆中。保存在本地内存中的像素数据不会以预测的方式释放,这可能会导致超出内存限制而使应用程序崩溃。从安卓3.0(API level 11)开始,像素数据跟相应的位图一起存储在Dalvik堆中。

以下几节将描述在不同安卓版本上如何优化位图内存管理。

(1) 在安卓2.3.3及更低版本中管理内存
在安卓2.3.3(API level 10)及更低版本中,推荐使用recycle()。如果在应用程序中显示大量的位图,很有可能出现outOfMemoryError()错误。recycle()方法能够尽快让应用程序重新获得位图所占用的内存。

注:只有在确定位图不再被使用时使用recycle()。若在调用recycle()后再尝试绘制位图,将会出现错误:“Canvas:尝试去用回收的位图”。

以下代码片段为调用recycle()的示例。此程序用引用计数(用mDisplayRefCount和mCacheRefCount)来跟踪当前是否有位图显示或在缓存中。当满足以下条件代码将回收位图:
- 引用计数mDisplayRefCount和mCacheRefCount都为0.
- 位图不为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();
}

2015.11.14
(2) 在安卓3.0及更高版本中管理内存
安卓3.0(API level 11)介绍了BitmapFactory.Options.inBitmap域。如果此选项被设置,当载入内容时解码方法将会用此选项去重新使用存在的位图。这就意味着位图的内存被重用,如此会导致性能的提升并省掉了内存的分配和释放。然而,用inBitmp也有几个限制。尤其是在安卓4.4之前(API level 19),只支持相等尺寸的位图。更多细节见inBitmap的文档。

[1] 保存位图供以后使用
以下代码片段演示如何保存位图来供以后使用。但应用程序运行在安卓3.0或更高版本中且位图从LruCache中被驱逐了出来,对位图的一个软应用被放置在HashSet中,供稍后可能用inBitmap来使用位图:

Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    mReusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                mReusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

[2] 使用存在的位图
应用程序在运行时,解码方法会检查是否有存在的位图可用。举例如下:

public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    ...
    BitmapFactory.decodeFile(filename, options);
    ...

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

addInBitmapOptions() inBitmap inBitmap

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found, set it as the value of
            // inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

// This method iterates through the reusable bitmaps, looking for one 
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

    if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
        synchronized (mReusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = mReusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();

                if (null != item && item.isMutable()) {
                    // Check to see it the item can be used for inBitmap.
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;

                        // Remove from reusable set so it can't be used again.
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

最后,此方法判断是否有候选的位图满足被inBitmap使用的尺寸标准:

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
static int getBytesPerPixel(Config config) {
    if (config == Config.ARGB_8888) {
        return 4;
    } else if (config == Config.RGB_565) {
        return 2;
    } else if (config == Config.ARGB_4444) {
        return 2;
    } else if (config == Config.ALPHA_8) {
        return 1;
    }
    return 1;
}

后一代码片段展示了上一代码片段所调用的方法。它寻找一个存在的位图并为之设值。注意此方法只在找到合适的位图之后才为其设值(不能假设总能匹配到合适的位图)。

1.5 将位图显示在用户界面中

此节将综合前几节内容,展示用后台线程和位图缓存来将位图载入到像ViewPager和GridView的组件中。

此节将结合前几节的内容,展示如何用后台线程和位图缓存将多张位图载入ViewPager和GridView组件中,并处理并发和配置改变的情况。

(1) 将位图载入ViewPager
用扫击视图模式(swipe view pattern)来导航图片画廊细节是一个不错的方法。可以用PagerAdapter支持的ViewPager来实现此模式。然而,更加适合的支持适配器是FragmentStatePagerAdapter的子类,此类能够根据视图从屏幕上消失与否的情况自动销毁和保存在ViewPager中的Fragments,并能够保持内存使用量不上升。

注:如果只有少量的图片并能够确保它们不会超过应用程序的内存限制,直接使用PagerAdapter或FragmentPagerAdapter可能会更加适合。

以下代码实现了ViewPager和其ImageView子视图。主活动持有此ViewPager和相应的适配器:

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
    }
}

希望您能够注意这个问题:读图片的操作在用户界面线程中实现,这可能会让引用程序挂起从而不得不强制关闭应用程序。使用不要在用户界面线程处理位图一节中提到的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中执行。如果后台线程的操作比从硬盘中载入图片的操作还多,那么像缓存位图一节描述的添加内存/硬盘缓冲区也是有好处的。以下代码是内存缓冲区的另外的一些修改:

public class ImageDetailActivity extends FragmentActivity {
    ...
    private LruCache<String, Bitmap> 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
}

将这些代码片段整合到一起就能够得到一个具响应性的、具图片载入时最小延迟的ViewPager的实现,并且因为后台线程处理图片,所以图片数量不会(明显)影响主用户界面的执行。

(2) 将位图载入GridView
网格列表构建模块(grid list building block )对显示图片数据集及其有用,用GridView组件可以实现网格列表构建模块,GridView组件可以在同一时间显示许多图片,如果用户滑动GridView的滚动条就需要做更多的准备来实现GridView中的图片的显示。在实现此种类型的控制时,必须确保用户界面的流畅性、内存余量充足、正确地处理并发(GridView会重复利用组件来显示子视图)。

作为开始,先贴出在Fragment中的拥有ImageView子视图的GridView的标准实现。同理,这看起来也已经比较完美了,但怎么做能将此做的更好?

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    private ImageAdapter mAdapter;

    // A static dataset to back the GridView 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};

    // Empty constructor as per Fragment docs
    public ImageGridFragment() {}

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new ImageAdapter(getActivity());
    }

    @Override
    public View onCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
        final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
        mGridView.setAdapter(mAdapter);
        mGridView.setOnItemClickListener(this);
        return v;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
        final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
        i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
        startActivity(i);
    }

    private class ImageAdapter extends BaseAdapter {
        private final Context mContext;

        public ImageAdapter(Context context) {
            super();
            mContext = context;
        }

        @Override
        public int getCount() {
            return imageResIds.length;
        }

        @Override
        public Object getItem(int position) {
            return imageResIds[position];
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView;
            if (convertView == null) { // if it's not recycled, initialize some attributes
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
                imageView.setLayoutParams(new GridView.LayoutParams(
                        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
            } else {
                imageView = (ImageView) convertView;
            }
            imageView.setImageResource(imageResIds[position]); // Load image into ImageView
            return imageView;
        }
    }
}

问题在于,将这个过程的实现放在了用户界面线程中。在图片量较小时,此代码能够正常工作。如果有更多的图片参与,那么用户界面可能会被挂起。

可以使用上一节使用的异步和缓存的方法来解决这个问题。然而,咱还需要为GridView考虑并发问题。为解决此问题,用“不要在用户界面处理位图”一节中介绍的技术:

public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
    ...

    private class ImageAdapter extends BaseAdapter {
        ...

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ...
            loadBitmap(imageResIds[position], imageView)
            return imageView;
        }
    }

    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);
        }
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                BitmapWorkerTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
        }

        public BitmapWorkerTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (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;
    }

    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;
    }

    ... // include updated BitmapWorkerTask class

注:此代码同样适用于ListView。

这种实现能够灵活的处理图片且不会影响用户界面的流畅性。在后台任务中可以从网络载入图片也可以调整大型的数字图片并且图片呈现的速度极快。

完整的样例代码和其它方面的讨论,请参看本节的样例应用程序。

2. 用OpenGL ES显示图形

在安卓应用程序框架中如何创建OpenGL图形,如何响应用户的点击输入。

安卓框架提供了许多标准的工具来创建具有吸引力、功能性的图形用户界面。然而,如果想要更多地控制应用程序去绘制屏幕,或者要往屏幕上绘制三维图形,就得使用不同的工具。由安卓框架提供的OpenGL ES APIs提供了显示高端动画图形的功能(只有您想不到,无做不到),并且还能够让您收益于安卓设备上的图形处理单元(GPUs)加速处理图形的好处。

此部分内容将带您使用OpenGL来开发一个基本的应用程序,包括组织、绘制对象、移动绘制的元素以及响应用户的触摸输入。

这里的代码样例使用的OpenGL ES 2.0 APIs,针对目前的安卓设备,推荐大家使用此版本的API。更多关于OpenGL ES版本的信息,见OpenGL开发手册。

注:不要将OpenGL ES 1.x API和OpenGL ES 2.0混淆!这两种APIs不能互换使用,一起使用它们会导致开发者累觉不爱。

2.1 构建一个OpenGL ES 环境

学习如何建立一个可以绘制OpenGL图形的应用程序。

为在应用程序中使用OpenGL ES绘制图形,必须实现它们的视图容器。一种实现视图容器更直接的方式是实现GLSurfaceView和FLSurfaceView.Render。GLSurfaceView是用OpenGL绘制图形的容器,FLSurfaceView.Render控制在视图中的绘制内容。更多关于两个类的信息见OpenGL ES开发手册。

GLSurfaceView只是将OpenGL ES图形结合到应用程序中的一种方法。对于全屏或接近全屏的图形显示,此方法是合适的选择。若开发者只是想将OpenGL ES图形作为布局中的一小部分,那么应该考虑下TextureView。其实,都可以使用GLSurfaceView来实现,只是此种方法需要更多的代码来实现。

此节将解释在简单的应用程序活动中如何完成的GLSurfaceView和FLSurfaceView.Render的最小实现。

(1) 在清单文件中声明OpenGL ES
欲在应用程序中使用OpenGL ES 2.0 API,必须在清单文件中作如下声明:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

如果应用程序使用纹理压缩,必须声明应用程序所支持的压缩格式,这样就只在兼容的设备上安装:

<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" />
<supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />

更多关于纹理压缩的格式,见OpenGL开发手册。

(2) 为OpenGL ES图形创建活动
使用OpenGL ES的安卓应用程序跟其它应用程序一样有用户界面所对应的活动。主要不同在于往活动的布局文件中所添加的东西。在其它的应用程序中的布局文件中往往可能包含TexView、Button或ListView,在使用OpenGL ES的应用程序中,还会往布局文件中添加GLSurfaceView。

以下代码样例是用GLSurfaceView作为原始视图的活动的一个最小实现:

public class OpenGLES20Activity extends Activity {

    private GLSurfaceView mGLView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a GLSurfaceView instance and set it
        // as the ContentView for this Activity.
        mGLView = new MyGLSurfaceView(this);
        setContentView(mGLView);
    }
}

注:OpenGL ES 2.0需要安卓2.2(API level 8)或更高的版本,所以要确保安卓工程的API目标。

(3) 构建GLSurfaceView对象
GLSurfaceView是一个可以绘制OpenGL ES图形的特殊视图。此视图本身不会为绘图做太多。实际控制绘制对象的是设置在此视图上的GLSurfaceView.Renderer。实际上,创建此对象的代码量很少,您可能想跳过扩展代码而只创建一个GLSurfaceView实例,但不要如此。需要扩展此类来获取触摸事件,此在“响应屏幕触摸”一节中讲述过。

实现GLSurfaceView的必要的代码很少,所以能够快速实现,它通常作为使用它的活动的内部类来实现:

class MyGLSurfaceView extends GLSurfaceView {

    private final MyGLRenderer mRenderer;

    public MyGLSurfaceView(Context context){
        super(context);

        // Create an OpenGL ES 2.0 context
        setEGLContextClientVersion(2);

        mRenderer = new MyGLRenderer();

        // Set the Renderer for drawing on the GLSurfaceView
        setRenderer(mRenderer);
    }
}

除了GLSurfaceView实现外,另外一种方法是当绘制内容有改变时用GLSurfaceView.RENDERMODE_WHEN_DIRTY将渲染模式设置只绘制视图。

// Render the view only when there is a change in the drawing data
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

此方法可以防止在确切调用requestRender()时GLSurfaceView的重复绘制,在本样例程序中这是一种更为高笑的方法。

(4) 构建渲染器(Renderer)类
在应用程序中实现GLSurfaceView.Renderer或renderer类使得使用OpenGL ES变得有趣。此类控制往与此类关联的GLSurfaceView中的绘制内容。渲染器类中有3个方法会被安卓系统调用以推测出怎么绘制GLSurfaceView以及往其中绘制的内容:
- onSurfaceCreate() - 被调用一次,用来设置视图的OpenGL ES环境。
- onDrawFrame() - 在每次绘制重新绘制视图时都会被调用。
- onSurfaceChanged() - 在视图形状改变时会被调用,如当设备屏幕方向改变时。

以下是一个OpenGL ES渲染器的一个非常基本的实现,它为GLSurfaceView绘制一个黑色的背景:

public class MyGLRenderer implements GLSurfaceView.Renderer {

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        // Set the background frame color
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    }

    public void onDrawFrame(GL10 unused) {
        // Redraw background color
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }

    public void onSurfaceChanged(GL10 unused, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }
}

以上的代码示例创建了一个用OpenGL来简单的显示一个黑色背景的安卓应用程序。并未做一些更有趣的事情,不过现在您已经具有了OpenGL的基础,那么您就可以开始用OpenGL来开始绘制图形元素了。

注:在使用OpenGL ES 2.0 APIs时,您可能想知道为什么这些方法需要GL10的参数。这些方法签名只是为能够在2.0 API中重用以保障安卓代码矿建的简单性。

如果您熟悉OpenGL ES APIs,就可以在应用程序中设置OpenGL ES环境并开始绘制图形。然而,如果您还需要更多的关于OpenGL的信息帮助,请继续往后看。

2.2 定义形状

学习如何定义形状,了解为什么需要知道图形轮廓(faces and winding)。

创建高端图形杰作的第一步是在OpenGL ES视图的上下文中定义被画的形状。若不知OpenGL ES定义图形对象的步骤,那么用OpenGL ES绘制图形就会有些困难。

此节解释“OpenGL ES在安卓设备屏幕上的坐标系”、“定义形状的基础”、“形状面”、“定义三角形或矩形”。

(1) 定义三角形
OpenGL ES运行在三维空间坐标定义欲绘制的对象。所以,在绘制三角形之前,必须先定义坐标。在OpenGL中,一般是通过定义以浮点数字组成的顶点数组来定义坐标。欲达最大效率,需要将这些坐标写进ByteBuffer,然后将其中的内容传递给OpenGL ES图形管道以作相应处理:

public class Triangle {

    private FloatBuffer vertexBuffer;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float triangleCoords[] = {   // in counterclockwise order:
             0.0f,  0.622008459f, 0.0f, // top
            -0.5f, -0.311004243f, 0.0f, // bottom left
             0.5f, -0.311004243f, 0.0f  // bottom right
    };

    // Set color with red, green, blue and alpha (opacity) values
    float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };

    public Triangle() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
                // (number of coordinate values * 4 bytes per float)
                triangleCoords.length * 4);
        // use the device hardware's native byte order
        bb.order(ByteOrder.nativeOrder());

        // create a floating point buffer from the ByteBuffer
        vertexBuffer = bb.asFloatBuffer();
        // add the coordinates to the FloatBuffer
        vertexBuffer.put(triangleCoords);
        // set the buffer to read the first coordinate
        vertexBuffer.position(0);
    }
}

默认情况下,OpenGL ES假设坐标系的0,0,0对应GLSurfaceView框架的中心,[1,1,0]为框架的右上角,[-1,-1,0]对应框架的左下角。欲看此坐标系的图解,见OpenGL ES开发手册。

注意形状的坐标系是以逆时针为顺序。绘制的顺序非常重要因为它定义那一边是形状的正面(正面会被绘制)以及哪一边是形状的背面(根据OpenGL ES剔除的特性,背面不会被绘制)。更多关于面(facing)和剔除(culling)见OpenGL ES 开发手册。

(2) 定义矩形
在OpenGL中定义三角形相当简单,但当定义图形变得稍加复杂时应该怎么定义?比如如,一个矩形。有几种方式可以定义矩形,在OpenGL定义矩形最为典型的方式是定义两个三角形来形成矩形。
这里写图片描述
图1. 用两个三角形绘制矩形

需要以逆时针的顺序来定义组成矩形的两个三角形,并将坐标 值都保存到ByteBuffer中。为避免重复定义三角形所共享顶点,需要用绘制清单来告知OpenGL ES图形管道怎么绘制这些顶点。以下是绘制矩形的代码:

public class Square {

    private FloatBuffer vertexBuffer;
    private ShortBuffer drawListBuffer;

    // number of coordinates per vertex in this array
    static final int COORDS_PER_VERTEX = 3;
    static float squareCoords[] = {
            -0.5f,  0.5f, 0.0f,   // top left
            -0.5f, -0.5f, 0.0f,   // bottom left
             0.5f, -0.5f, 0.0f,   // bottom right
             0.5f,  0.5f, 0.0f }; // top right

    private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices

    public Square() {
        // initialize vertex byte buffer for shape coordinates
        ByteBuffer bb = ByteBuffer.allocateDirect(
        // (# of coordinate values * 4 bytes per float)
                squareCoords.length * 4);
        bb.order(ByteOrder.nativeOrder());
        vertexBuffer = bb.asFloatBuffer();
        vertexBuffer.put(squareCoords);
        vertexBuffer.position(0);

        // initialize byte buffer for the draw list
        ByteBuffer dlb = ByteBuffer.allocateDirect(
        // (# of coordinate values * 2 bytes per short)
                drawOrder.length * 2);
        dlb.order(ByteOrder.nativeOrder());
        drawListBuffer = dlb.asShortBuffer();
        drawListBuffer.put(drawOrder);
        drawListBuffer.position(0);
    }
}

此例给了一个怎么用OpenGL来创建稍微复杂图形的小窥。通常来讲,都是使用三角形来绘制对象。在下一节中,将会介绍如何将这些形状绘制到屏幕上。

2015.11.15

2.3 绘制形状

学习在应用程序中如何绘制OpenGL 形状。

在用OpenGL定义形状后,就可以绘制它们了。用OpenGL ES 2.0绘制推行可能比您的想象还要多一些代码,因为这些API对图形渲染管道提供了极大的控制。

此节介绍怎么绘制前一节用OpenGL ES 2.0 API所定义的形状。

(1) 初始化形状
在作绘制之前,必须初始化并载入打算绘制的形状。除非程序中使用的形状的结构(坐标系)在执行过程中改变,否则都应该在渲染器的onSurfaceCreated()方法中为形状的内存和效率执行作初始化工作。

public class MyGLRenderer implements GLSurfaceView.Renderer {

    ...
    private Triangle mTriangle;
    private Square   mSquare;

    public void onSurfaceCreated(GL10 unused, EGLConfig config) {
        ...

        // initialize a triangle
        mTriangle = new Triangle();
        // initialize a square
        mSquare = new Square();
    }
    ...
}

(2) 绘制形状
绘制用OpenGL ES 2.0定义的形状需要大量代码,因为必须为图形渲染管道提供许多细节。尤其是需要定义以下介个方面内容:
- 顶点着色(Vertex Shader) - 渲染形状顶点的OpenGL ES 图形代码。
- 片段着色(Fragment Shader) - 用颜色或纹理来渲染形状各面的OpenGL ES代码。
- 程序(Program) - 用包含着色的OpenGL ES 对象来绘制一个或多个形状。

至少需要一个顶点着色来绘制形状一个片段着色来为形状着色。这些着色器必须被编译然后增添到OpenGL ES程序中,着色器会被用来绘制形状。以下代码描述在三角形类中如何定义基本的着色器来绘制形状:

public class Triangle {

    private final String vertexShaderCode =
        "attribute vec4 vPosition;" +
        "void main() {" +
        "  gl_Position = vPosition;" +
        "}";

    private final String fragmentShaderCode =
        "precision mediump float;" +
        "uniform vec4 vColor;" +
        "void main() {" +
        "  gl_FragColor = vColor;" +
        "}";

    ...
}

着色器使用的是OpenGL的着色语言(GLSL),着色器代码必须用OpenGL ES环境预编译。在渲染器类中创建一个方法来编译以上着色器的代码:

public static int loadShader(int type, String shaderCode){

    // create a vertex shader type (GLES20.GL_VERTEX_SHADER)
    // or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
    int shader = GLES20.glCreateShader(type);

    // add the source code to the shader and compile it
    GLES20.glShaderSource(shader, shaderCode);
    GLES20.glCompileShader(shader);

    return shader;
}

欲绘制形状,必须编译着色器代码,然后将编译后的代码添加到OpenGL ES程序对象中再连接程序。在绘制对象的构造函数中完成这个过程,这样此过程就只会被执行一次。

注:编译OpenGL ES着色器和连接程序对于CPU周期来和处理时间来说是比较耗时的操作,所以要避免此操作被执行多次。如果在运行时不知着色器的内容,应该构建(编译)代码这样代码只会被构建一次且可缓存供以后使用:

public class Triangle() {
    ...

    private final int mProgram;

    public Triangle() {
        ...

        int vertexShader = MyGLRenderer.loadShader(GLES20.GL_VERTEX_SHADER,
                                        vertexShaderCode);
        int fragmentShader = MyGLRenderer.loadShader(GLES20.GL_FRAGMENT_SHADER,
                                        fragmentShaderCode);

        // create empty OpenGL ES Program
        mProgram = GLES20.glCreateProgram();

        // add the vertex shader to program
        GLES20.glAttachShader(mProgram, vertexShader);

        // add the fragment shader to program
        GLES20.glAttachShader(mProgram, fragmentShader);

        // creates OpenGL ES program executables
        GLES20.glLinkProgram(mProgram);
    }
}

此时,到了可以调用方法来绘制形状的时候了。用OpenGL ES需要用几个参数来告知渲染管道将绘制的内容并如何绘制它们。因为绘制过程由形状决定,所以在图形类中包含图形的绘制逻辑是个不错的主意。

创建一个draw()方法来绘制图形。以下代码设置了位置和颜色值到形状的顶点着色器和片段着色器,并调用绘制方法来绘制形状。

private int mPositionHandle;
private int mColorHandle;

private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex

public void draw() {
    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // get handle to vertex shader's vPosition member
    mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");

    // Enable a handle to the triangle vertices
    GLES20.glEnableVertexAttribArray(mPositionHandle);

    // Prepare the triangle coordinate data
    GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,
                                 GLES20.GL_FLOAT, false,
                                 vertexStride, vertexBuffer);

    // get handle to fragment shader's vColor member
    mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");

    // Set color for drawing the triangle
    GLES20.glUniform4fv(mColorHandle, 1, color, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

只要以上代码全部就位,绘制此对象就只需要在渲染器的onDrawFrame()方法中调用draw()方法了:

public void onDrawFrame(GL10 unused) {
    ...

    mTriangle.draw();
}

运行应用程序,运行结果如下:
这里写图片描述
图1. 无投影或相机视图下绘制的三角形

在以上代码样例中存在几个问题。第一,此运行结果不会给人留下深刻印象。第二,当改变设备屏幕方向时三角形会有些变形。会变形的原因时对象的顶点没有随着屏幕变化而变化。可以通过下一节介绍的投影和相机视图来解决这个问题。

第三,图中的三角形是固定不动的,这显得有些无聊。在“增添运动”这一节中会使用OpenGL ES图形管道来让图像旋转以让所绘的图形看起来更有趣。

2.4 请求投影和相机视图

学习如何使用投影和相机视角获取所绘对象的新的视角。

在OpenGL ES环境中,投影和相机视图以一种更接近在现实中用眼睛看到物理对象那般呈现图片。这种模拟实际视图的方式是在绘制物体的坐标系中的通过数学变换实现的:
- 投影 - 此变换通过调整绘制对象坐标的宽度和高度来展现绘制图像。无此变换的计算,因视图窗口的比例的不等从而用OpenGL ES绘制的对象是倾斜的。当OpenGL视图比例确立或渲染器中onSurfaceChanged()方法中的视图比例改变时,投影变换将会重新计算。更多关于OpenGL ES的投影和坐标映射,见Mapping Coordinates for Drawn Objects.。
- 相机视图 - 此变换调整绘制对象坐标的虚拟相机位置。OpenGL ES并未定义实际的相机对象,它是通过变换绘制对象的显示而提供了工具方法来模拟相机。当确立GLSurfaceView或有基于用户或应用程序的动态改变时,相机视图可能只会被计算一次。

此节描述如何创建投影和相机以及如何在GLSurfaceView中应用它们。

(1) 定义投影
投影变换的数据在GLSurfaceView.Renderer类中的onSurfaceChanged()方法中计算。以下样例代码根据GLSurfaceView的高度和宽度用Matrix.frustumM()方法计算投影变换的Matrix:

// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
private final float[] mMVPMatrix = new float[16];
private final float[] mProjectionMatrix = new float[16];
private final float[] mViewMatrix = new float[16];

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    GLES20.glViewport(0, 0, width, height);

    float ratio = (float) width / height;

    // this projection matrix is applied to object coordinates
    // in the onDrawFrame() method
    Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
}

此段代码计算了投影矩阵mProjectionMatrix,相机视图将在onDrawFrame()方法中使用此矩阵,此将在下一节中介绍。

注:只对绘制对象应用投影一般会导致图形消失。通常,为了将图形重新显示在屏幕上还需要使用相机视图。

(2) 定义相机视图
为绘制对象增加相机视图变换方才算完成了图形的变换。在以下代码示例中,在Matrix.setLookATM()方法中完成相机试图变换并结合之前的投影变换矩阵。再将两种变换结合得到矩阵传递给绘制对象:

@Override
public void onDrawFrame(GL10 unused) {
    ...
    // Set the camera position (View matrix)
    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // Calculate the projection and view transformation
    Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);

    // Draw shape
    mTriangle.draw(mMVPMatrix);
}

(3) 请求投影和相机变换
欲结合投影和相机视图变换矩阵,首先需要在之前的三角形类中定义顶点着色器矩阵:

public class Triangle {

    private final String vertexShaderCode =
        // This matrix member variable provides a hook to manipulate
        // the coordinates of the objects that use this vertex shader
        "uniform mat4 uMVPMatrix;" +
        "attribute vec4 vPosition;" +
        "void main() {" +
        // the matrix must be included as a modifier of gl_Position
        // Note that the uMVPMatrix factor *must be first* in order
        // for the matrix multiplication product to be correct.
        "  gl_Position = uMVPMatrix * vPosition;" +
        "}";

    // Use to access and set the view transformation
    private int mMVPMatrixHandle;

    ...
}

然后,修改绘制对象的draw()方法以接收二者变换的矩阵并将此矩阵应用到图形:

public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix
    ...

    // get handle to shape's transformation matrix
    mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

    // Pass the projection and view transformation to the shader
    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);

    // Disable vertex array
    GLES20.glDisableVertexAttribArray(mPositionHandle);
}

一旦正确和应用了投影和相机视图变换,以正确比例被绘制的图形对象应该如下图所示:
这里写图片描述
图1. 用投影和相机视图变换的三角形绘制

至此,应用程序中已经用正确的比例绘制图形了,该往形状增添动画了。

2.5 增加运动

学习如何用OpenGL来实现所绘对象基本的移动和动画。

将对象绘制在屏幕之上只是OpenGL最基本的特定,安卓其它的图形框架类如Canvas及Drawable也能够完成此项工作。OpenGL ES还为图形提供了移动、变换图形到三维空间以及创造令人信服的用户体验的功能。

此节将继续学习OpenGL ES来通过旋转的方式移动图形。

(1) 旋转图形
用OpenGL ES 2.0来选中绘制对象比较简单。在渲染器中创建一个转换矩阵(旋转矩阵)并将其跟投影和相机视图变换矩阵结合到一块:

private float[] mRotationMatrix = new float[16];
public void onDrawFrame(GL10 gl) {
    float[] scratch = new float[16];

    ...

    // Create a rotation transformation for the triangle
    long time = SystemClock.uptimeMillis() % 4000L;
    float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);

    // Combine the rotation matrix with the projection and camera view
    // Note that the mMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}

如果做了这些改变后三角形仍旧还没有旋转,确保对GLSurfaceView.RENDERMODE_WHEN_DIRTY进行了注释,此内容在下一节讨论。

(2) 启用连续渲染
如果您已孜孜不倦地昨晚了样例代码中的所有内容,确保注释了设置渲染模式为只在脏(dirty)才绘制的一行代码,否则OpenGL只做一次旋转然后就等待调用GLSurfaceView容器中的requestRender()方法:

public MyGLSurfaceView(Context context) {
    ...
    // Render the view only when there is a change in the drawing data.
    // To allow the triangle to rotate automatically, this line is commented out:
    //setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

除非在无用户交互的情况下对象还有转动,否则应该将此语句的注释去掉。做好取消此语句注释的准备,因为下一节将会在程序中使用此语句。

2.6 响应触摸事件

学习怎么和OpenGL图形实现基本的互动。

移动预先设定程序中的对象是有用的,如此会得到用户的更多关注。但要是想让OpenGL ES图形能和用户交互又改怎么样做呢?让OpenGL ES应用程序能够和用户交互的关键是重写GLSurfaceView中的onTouchEvent()来监听触摸事件。

此节介绍如何监听用户的触摸事件以让用户旋转OpenGL ES对象。

(1) 设置触摸监听器
欲使OpenGL ES应用程序响应触摸事件,必须实现GLSurfaceView类中的onTouchEvent()方法。以下实现的代码展示了怎么监听MotionEvent.ACTION_MOVE事件并将它们转换为形状旋转的角度。

private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
private float mPreviousX;
private float mPreviousY;

@Override
public boolean onTouchEvent(MotionEvent e) {
    // MotionEvent reports input details from the touch screen
    // and other input controls. In this case, you are only
    // interested in events where the touch position changed.

    float x = e.getX();
    float y = e.getY();

    switch (e.getAction()) {
        case MotionEvent.ACTION_MOVE:

            float dx = x - mPreviousX;
            float dy = y - mPreviousY;

            // reverse direction of rotation above the mid-line
            if (y > getHeight() / 2) {
              dx = dx * -1 ;
            }

            // reverse direction of rotation to left of the mid-line
            if (x < getWidth() / 2) {
              dy = dy * -1 ;
            }

            mRenderer.setAngle(
                    mRenderer.getAngle() +
                    ((dx + dy) * TOUCH_SCALE_FACTOR));
            requestRender();
    }

    mPreviousX = x;
    mPreviousY = y;
    return true;
}

注意在计算旋转角度后,此方法调用了requestRender()来告知渲染器该渲染框架了。此方法在此样例中最为有效,因为框架不需要重画,除非旋转有变。然而,它不会影响效率除非用setRenderMode()方法来设置渲染器只进行重绘制操作,所以确保以下这行代码没有被注释:

public MyGLSurfaceView(Context context) {
    ...
    // Render the view only when there is a change in the drawing data
    setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}

(2) 获取旋转角度
以上代码样例需要通过增加一个公有的变量来揭露渲染器的旋转角度。由于渲染代码运行于独立于用户主线程的线程中,必须将此变量声明为volatile。以下代码声明了此变量且揭露了获取和设置方法:

public class MyGLRenderer implements GLSurfaceView.Renderer {
    ...

    public volatile float mAngle;

    public float getAngle() {
        return mAngle;
    }

    public void setAngle(float angle) {
        mAngle = angle;
    }
}

(3) 应用旋转
欲应用通过触摸输入产生的旋转,注释掉产生角度的代码并增加mAngle变量,此变量包含了触摸事件生成的旋转角度:

public void onDrawFrame(GL10 gl) {
    ...
    float[] scratch = new float[16];

    // Create a rotation for the triangle
    // long time = SystemClock.uptimeMillis() % 4000L;
    // float angle = 0.090f * ((int) time);
    Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

    // Combine the rotation matrix with the projection and camera view
    // Note that the mMVPMatrix factor *must be first* in order
    // for the matrix multiplication product to be correct.
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}

当完成以上描述的所有步骤后,运行程序并用手指再屏幕上滑动来选择三角形,运行结果会类似下图:
这里写图片描述
图1. 触摸输入选择三角形(圆圈表示触摸位置)

2015.11.16

3. 使用场景和变换来实现动画视图

在视图层次如何用转换来让动画状态改变。

活动对应的用户界面会常会因为响应用户输入或其它事件而变化。例如,包含供用户输入查询内容的查询条在用户提交后可以隐藏查询条而呈现查询结果。

欲在这些情形下提供视觉上的连续,可以在用户界面的不同视图层次中作动画般的改变。这些动画给以用户动作上的响应并帮助用户学习应用程序是怎么工作的。

安卓包含变换框架,此框架能够很易在两个视图层次间作动画改变。框架在运行时通过改变视图的某些特性来动画视图。框架中既包含针对于常见效果的内建动画也允许开发者自定义动画和变换生命周期回调方法。

此节教您使用变换框架内的内建动画来动画改变两个不同视图层次的视图。此节同样包含如何创建自定义动画。

注:对于在4.0(API level 14)和4.4.2(API level 19)的安卓版本,使用animateLayoutChanges属性来动画布局。欲获取更多信息,见Property Animation及Animating Layout Changes。

3.1 变换框架

学习变换框架主要的特性和组件。

动画应用程序用户界面不仅是视觉上的呼吁。动画强调改变且提供了应用程序是如何工作的视觉线索。

欲帮助开发者动画在两个视图层次的改变,安卓提供了变换框架。此框架能应用一个或多个动画到有改变的层次中的所有视图之间。

框架有以下特性:
组-级动画
应用一个或多个动画去影响视图层次中的所有视图。

变换-基础动画
    动画的运行基于视图特性值改变开始和结束之间。

内建动画
    包含具常见印象的预定义动画,诸如渐弱或移动。

资源文件支持
    从布局资源文件载入视图层和内建动画。

生命周期
    定义回调方法提供堆动画和层改变处理的更好控制。

(1) 概要
图1的图例展示动画是如何提供视觉线索来帮助用户的。当应用程序从搜索条屏幕改变到搜索结果屏幕时,屏幕渐弱不再使用的视图而渐现几个新的视图。

用户动画界面:http://developer.android.com/images/transitions/transition_sample_video.mp4
图1. 视觉线索使用用户界面动画。点击设备屏幕放映动画。

2015.11.16
此动画是使用变换框架的一个例子。框架动画改变两个视图层次中的所有视图。一个视图层次可以简单得只有一个视图也可以复杂到像ViewGroup包含复杂的视图树。框架在视图层的开始和结束期间通过改变视图的特性值来动画每个视图。

变换框架以并行的方式工作于视图层和动画。框架的目的是存储视图层的状态,在这些层之间作改变以修改屏幕的显示,通过存储和应用动画定义进行动画改变。

图2中所示的框图能够说明视图层、框架对象以及动画之间的关系:
这里写图片描述
图2. 变换框架各部分之间的关系

变换框架为场景、变换以及变换方式提供了抽象的理念。在后续节中将会详细描述三者。欲使用此框架,在应用程序中为计划改变的视图层创建场景。然后,为欲使用的各个动画创建变换。欲在两个视图层之间开始动画,用变换方法来制定欲使用的变换和结束场景。此过程在此节余留部分详细讲解。

(2) 场景
场景用来存储视图层的状态,包括所有视图以及它们的特性值。一个视图可能是简单的或是视图和其子视图的复杂的树视图。在场景中保存视图状态能够使得从另外的场景变换到此种状态。框架提供了Scene类来呈现场景。

变换框架能够根据布局资源文件或代码中的ViewGroup对象来创建场景。如果动态的创建或运行时修改视图层,那么在代码中创建场景会很有用。

在大多数情况下,不会精确的创建开始场景。如果已经应用了变换,框架用之前的结束场景作为后续变换的开始场景。如果并未应用变换,框架将会从屏幕当前状态收集 的信息。

也可以为场景定义场景自己的动作,当场景改变时将会运行此些动作。例如,在变换场景之后亲你管理视图设置。

除视图层和其属性值之外,场景还存储父视图层的引用。根视图被称为scene root。改变场景和动画会影响根场景下的场景。

更多关于创建场景的信息见“创建场景”。

(3) 变换
在变换框架中,动画创造了一系列描述各视图层在开始和结束场景之间变化的框架。关于动画的信息被保存在Transition对象中。用TransitionManager实例运行动画。框架能够在不用场景之间变换也能够在同一个场景的不同状态间变换。

框架包含了一套用于常见动画效果的内建变换,如渐变和调整视图尺寸。也可以用动画框架中的APIs来自定义变换以创建动画效果。变换框架同样允许联合包含内建或自定义变换组的变换集中的不同的动画。

变换的生命周期类似活动的生命周期,在动画开始和完成期间由框架监控生命周期对应的变换状态。一个重要的生命周期状态, 在变换阶段可以实现框架会调用的回调方法来调整用户界面。

更多关于变换的信息,见Applying a Transition及Creating Custom Transitions。

(4) 限制
以下理解了变换框架的一些知名的限制:
- 动画应用到SurfaceView可能不会正确的显示。SurfaceView实例在非用户界面线程中更新,所以更新可能超出其它视图的动画的异步范围。
- 当应用于TextureView时,一些特殊的变换类型可能不会产生应有的动画效果。
- 从AdapterView扩展类,如ListView,管理子视图的方法与变化框架不兼容。如果在AdapterView上实现动画视图,设备显示可能会被挂起。
- 如果通过动画来调整TextView的尺寸,在对象尺寸被调整完成之前文本将会突然跑到一个新的区域。欲避免此问题,不要用动画调整包含文本的视图。

3.2 创建场景

学习如何创建场景来存储视图层次的状态。

场景存储视图层的状态,包括所有视图和其特性值。变换框架在开始场景和结束场景之间运行动画。开始场景从用户当前界面的当前状态获得。对于结束场景,框架让开发者从布局资源文件或代码中的一组视图创建结束场景。

此节演示如何在应用程序中创建场景以及如何定义场景动作。下一节将演示如何在两个场景之间变换。

注:框架能够在一个无场景的视图层中动画改编,在Apply a Transition Without Scenes一节中已描述过。然而,理解此节内容对于变换来说是必要的。

(1) 从布局资源创建场景
可以根据布局资源文件直接创建场景。当文件中的视图层大多是静态时可以使用此技术。场景实例中的结果代表视图层的某时刻的状态。欲改变视图层,就需要重建场景。框架根据文件中的整个视图层来创建场景;不能只根据布局文件的某一部分创建场景。

欲根据布局资源文件创建场景,检索ViewGroup实例布局文件的场景根,然后用包含视图层的布局文件的场景根和资源ID调用Scene.getSceneForLayout()方法来创建场景。

[1] 为场景定义布局
此节后续部分代码将演示如何用相同的场景根元素来创建两个不同的场景。这些代码片段同时演示在不用声明场景彼此的相关性而载入多个不相关的场景对象。

样例有以下的布局定义组成:
- 活动的拥有一个文本标签和子布局的主布局文件。
- 第一个场景的有两个文本域的关系布局。
- 第二个场景的拥有与第一个布局相同内容但内容不同顺序的关系布局。

样例设计来让动画发生在活动的主布局的子布局中。在主布局中的文本标签仍然是静态的。

活动的主布局定义如下:
res/layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/master_layout">
    <TextView
        android:id="@+id/title"
        ...
        android:text="Title"/>
    <FrameLayout
        android:id="@+id/scene_root">
        <include layout="@layout/a_scene" />
    </FrameLayout>
</LinearLayout>

布局文件的定义包含了一个文本域和一个场景根的子布局。第一个场景的布局文件被包含在了主布局文件中。此允许应用程序将此作为用户界面的一部分来展示同时也能够将此载入到场景中,因为框架只能将整个布局文件载入场景中。

第一个场景的布局文件定义如下:
res/layout/a_scene.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view1
        android:text="Text Line 1" />
    <TextView
        android:id="@+id/text_view2
        android:text="Text Line 2" />
</RelativeLayout>

拥有与第一个场景布局文件相同的文本域(相同的ID)但放置顺序不同的第二个场景的布局文件内容如下:
res/layout/another_scene.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scene_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/text_view2
        android:text="Text Line 2" />
    <TextView
        android:id="@+id/text_view1
        android:text="Text Line 1" />
</RelativeLayout>

[2] 根据布局生成场景
为两个关系布局创建定义之后,就可以为每个布局文件各获取一个场景了。这能够使得稍后在两个用户界面配置之间作变换。欲获得场景,需要场景根的引用和布局资源ID。

以下代码片段展示了如何获取场景根的引用,并根据布局文件创建两个场景对象:

Scene mAScene;
Scene mAnotherScene;

// Create the scene root for the scenes in this app
mSceneRoot = (ViewGroup) findViewById(R.id.scene_root);

// Create the scenes
mAScene = Scene.getSceneForLayout(mSceneRoot, R.layout.a_scene, this);
mAnotherScene =

    Scene.getSceneForLayout(mSceneRoot, R.layout.another_scene, this);

此时,在应用程序中已有基于视图层的两个场景对象。两个场景都使用在res/layout/activity_main.xml中被FrameLayout元素定义的场景根。

(2) 在代码中创建场景
也可以根据ViewGroup对象用代码创建场景实例。当用代码直接修改视图层或动态创建视图时可使用此项技术。

欲根据视图层用代码创建场景,用Scene(sceneRoot, viewHierarchy)构造函数。当已经有相应的布局文件之后,调用此构造方法等效调用Scene.getSceneForLayout()方法。

以下代码片段演示如何根据场景根元素和视图层创建一个视图实例:

Scene mScene;

// Obtain the scene root element
mSceneRoot = (ViewGroup) mSomeLayoutElement;

// Obtain the view hierarchy to add as a child of
// the scene root when this scene is entered
mViewHierarchy = (ViewGroup) someOtherLayoutElement;

// Create a scene
mScene = new Scene(mSceneRoot, mViewHierarchy);

(3) 创建场景动作
框架允许定义在场景进入运行或场景退出运行时的自定义场景动作。在许多情况下,自定义场景动作都没必要,因为框架在场景之间自动的动画改变。

场景动作在处理以下情况中显得有用:
- 动画视图不在相同的视图层上。用进入或退出场景动作引起场景开始或结束以使用动画视图。
- 变换框架不能自动进行动画视图,如ListView对象。更多信息见Limitations。

欲提供自定义的场景动作,以Runnable对象定义动作并将动作传递给Scene.setExitAction()或Scene.setEnterAction()方法。框架在开始场景即运行变换动画之前调用setExitAction()方法,在结束场景即运行变换动画之后调用setEnterAction()方法。

注:不要用场景动作在开始视图和结束视图之间传递数据。更多信息见Defining Transition Lifecycle Callbacks。

2015.11.17

3.3 请求转换

学习如何变换视图层次的两个场景。

在变换框架中,动画创建了一系列描绘在开始和结束场景中的视图层的改变的帧。框架代表的动画作为变换对象,其中包含动画的信息。欲运行动画,需提供要使用的变换以及结束场景给变换方式。

此节向您展示用内建的变换在两个场景动画即移动、调整尺寸以及渐褪视图。下一节将向您演示如何自定义变换。

(1) 创建变换
在上一节中,您学会了如何创建代表不同视图层状态的场景。一旦定义了欲改变的开始场景和结束场景,就再需要创建一个定义动画的变换对象。框架能够在资源文件中指定内建的变换并且能将此关联到到代码中或直接在代码中定义一个内建变换的实例。
这里写图片描述

[1] 根据资源文件创建变换实例
此项技术能够在不修改活动代码的情况下就能够修改变换定义。此项技术也能够将复杂的变换定义跟应用程序代码独立开来,如 Specify Multiple Transitions中所述。

欲在资源文件中指定内建变换,跟随以下步骤:
- 增加/res/transition/目录到工程 中。
- 在刚所建的目录中新建一个XML资源文件。
- 将内建变换作为节点添加到XML文件中。

例如,以下资源文件指定了消退(Fade)变换:
res/transition/fade_transition.xml

<fade xmlns:android="http://schemas.android.com/apk/res/android" />

以下代码片段演示如何将资源文件中的变换实例关联到活动的代码中:

Transition mFadeTransition =
        TransitionInflater.from(this).
        inflateTransition(R.transition.fade_transition);

[2] 在代码中创建变换实例
此项技术对于在代码中修改用户界面动态创建变换对象非常有用,对于创建简单的内建变换实例不需要或只需要很少的参数。

欲创建内建变换实例,调用Transition类子类的其中一个构造函数即可。例如,以下代码片段创建了一个消退(fade)变换的实例:

Transition mFadeTransition = new Fade();

(2) 请求变换
一般来说,变换应用于改变不同的视图层以响应诸如用户动作这样的事件。例如,一个搜索应用程序:当用户在搜索条中输入内容并点击搜索按钮时,当应用消退(fade)变换时,应用程序改变到显示搜索结果的场景之上,在此场景中搜索条消失不见。

当应用变换来响应活动中的某些事件来实现场景变换,需要用结束场景和变换实例来调用TransitionManager.go()静态方法来实现动画,代码如下所示:

TransitionManager.go(mEndingScene, mFadeTransition);

在根据指定变换实例运行动画时,框架根据结束场景的视图层改变场景根下的视图层。开始场景为上一次变换的结束场景。如果之前无任何变换,系统根据用户界面当前状态作为开始场景。

如果没有指定变换实例,变换管理器能够将自动应用一个能够响应大多数情形的变换。更多信息见API参考TransitionManager类。

(3) 选择特定的目标视图
框架应用变换到默认的开始场景和结束场景中的所有视图。在某些清醒下,可能只想将变换应用到场景中的某部分子视图上。例如,框架不支持ListView对象的动画改变,所以在变换期间不会动画ListView对象。框架能够只选择欲动画的部分视图。

将每个会进行变换的视图称作目标。只能将场景视图层中的某些视图作为目标。

欲从目标列表中移除一个或多个视图,在开始变换前调用removeTarget()方法。欲增加视图到目标列表中,调用addTart()方法。更多信息见API 参考Transition类。

(4) 指定多个变换
欲获得动画的最大效果,应将动画跟场景间的变化匹配。例如,如果您正在场景间移除某些视图的同时又在增添其它的视图,消退(fade out)/消失(fade in)动画提供某些视图不在可用的显著提示。如果您正将视图移到屏幕的不同点处,一个更好的选择时动画移动以让用户注意视图的新位置。

不必只选择一个动画,因为变换框架能够在包含内建或自定义变换组的变换集中结合动画效果。

欲根据XML变换集定义变换集,在res/transitions/目录下创建资源文件并在transitionsSet元素下列出变换。以下代码片段定义了跟AutoTransition类相同行为的变换集:

<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
    android:transitionOrdering="sequential">
    <fade android:fadingMode="fade_out" />
    <changeBounds />
    <fade android:fadingMode="fade_in" />
</transitionSet>

欲在代码中将此变换集设置到TransitionSet对象中,在活动中调用 TransitionInflater.from()方法。TransitionSet类从Transition类继承,所以可以像其它变换实例一样用变换管理器来使用它。

(5) 请求无场景变换
改变视图层不是修改用户界面的唯一方式。亦可以在当前层次中通过增加、修改以及移除子视图的方式改变用户界面。例如,可以将搜索条实现在一个单独的布局文件中。开始显示搜索条。欲改变用户界面来显示搜索结果,当用户点击搜索条时调用ViewGroup.removeView()方法来将搜索条移除,并通过调用ViewGroup.addView()方法将搜索结果增加到用户界面中。

如果会相互替代的两层的内容几乎相同可用此方法来实现用户界面的改变。否则,还是创建动画来实现用户界面的改变,这样就可以只用包含可在代码中修改的视图层的一个布局文件。

如果以上述方法来改变当前视图层,就不再需要创建场景。可以创建并应用延迟变换到视图层的两个状态中。框架的这个特点开始于当前视图状态,记录对视图作的改变,当系统重绘用户界面是变换将动画改变。

欲在单个视图中创建延迟变换,需以下步骤:
[1]. 当触发变换的事件发生时,调用方法来提供欲用变换来改变的所有子视图的父视图。框架存储子视图当前的状态和特性值。
[2]. 欲改变子视图需要用户使用子视图。框架记录用户对子视图所作的改变及其特性。
[3]. 当系统根据改变重绘用户界面时,框架从原始状态动画改变到新状态。

以下代码样例展示如何使用延迟变换将一个文本视图动画的添加到视图层中。第一段代码片段展示的布局文件中的定义:
res/layout/activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <EditText
        android:id="@+id/inputText"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    ...
</RelativeLayout>

第二个代码片段展示动画增加文本视图的过程:
MainActivity.java

private TextView mLabelText;
private Fade mFade;
private ViewGroup mRootView;
...

// Load the layout
this.setContentView(R.layout.activity_main);
...

// Create a new TextView and set some View properties
mLabelText = new TextView();
mLabelText.setText("Label").setId("1");

// Get the root view and create a transition
mRootView = (ViewGroup) findViewById(R.id.mainLayout);
mFade = new Fade(IN);

// Start recording changes to the view hierarchy
TransitionManager.beginDelayedTransition(mRootView, mFade);

// Add the new TextView to the view hierarchy
mRootView.addView(mLabelText);

// When the system redraws the screen to show this update,
// the framework will animate the addition as a fade in

(6) 定义变换声明周期回调方法
变换的生命周期类似活动的生命周期。它代表框架在调用TransitionManager.go()方法和完成动画期间所监控的变换的状态。在重要的生命周期状态中,框架调用被TransitionListener接口定义的回调方法。

变换的生命周期回调函数很有用,例如,在场景改变时复制从开始场景到结束场景某个视图的特性值。不可简单的在视图层中复制开始视图和结束视图,因为结束视图层在变换完成前没有被关联。科学的做法是,将值保存在某变量中在框架完成变换后再将此变量拷贝到结束场景中。欲获得变换结束的通知,在活动中实现TransitionListener.onTransitionEnd()方法。

更多信息见API参考TransitionListener类。

3.4 创建自定义变换

学习如何创建不属于变换框架中的其它的动画效果。

自定义变换创建的动画对于任何内建变换类都不可用。例如,可以定义一个变换来返回文本的前景色并将输入域设置为灰色以按时此域在屏幕上已经失去了输入功能。这中效果将帮助用户理解此域失去了输入功能。

就像内建变换类型一样,自定义的变换可以应用动画到开始和结束场景中的子视图中。然而,也不像内建变换类型,需要提供来获取特性值和产生动画的代码。也可以围动画定义视图目标的子集。

此节教您获取特性值和产生动画来创建自定义变换。

(1) 扩展变换类
欲创建自定义变换,增加扩展Transition类的类到工程中并重写方法,如以下代码所示:

public class CustomTransition extends Transition {

    @Override
    public void captureStartValues(TransitionValues values) {}

    @Override
    public void captureEndValues(TransitionValues values) {}

    @Override
    public Animator createAnimator(ViewGroup sceneRoot,
                                   TransitionValues startValues,
                                   TransitionValues endValues) {}
}

后续内容解释如何重写这些方法。

(2) 获取视图属性值
变换动画用属性动画中的属性动画系统。属性动画在开始和结束值中的一段特殊时间改变视图属性,所以框架需要属性的开始和结束值来构建动画。

然而,属性动画通常只需要视图属性值的一个子集。例如,颜色动画需要颜色属性值,移动动画需要位置属性值。由于动画所需的属性值对于变化来说特殊,变化框架不会为变换提供每一个属性值。取而代之的是,框架将调用可以为变换获取变换所需的属性值的回调方法并将属性值存储在框架中。

[1] 获取开始值
欲传递开始视图值给框架,需实现captureStartValues(transitionValues)方法。框架将调用此方法来获取开始场景中的每一视图。此方法的参数是一个Transitionvalues对象,器包含一个视图引用和一个能够存储视图值的Map实例。在获取开始值的实现中,检索这些属性值将并将存储在Map中的属性值回传给框架。

欲确定属性值的键值不会和其它的TransitionValues键值冲突,用以下的命名方案:
package_name:transition_name:property_name

以下代码片段展示了captureStarValues()方法的实现:

public class CustomTransition extends Transition {

    // Define a key for storing a property value in
    // TransitionValues.values with the syntax
    // package_name:transition_class:property_name to avoid collisions
    private static final String PROPNAME_BACKGROUND =
            "com.example.android.customtransition:CustomTransition:background";

    @Override
    public void captureStartValues(TransitionValues transitionValues) {
        // Call the convenience method captureValues
        captureValues(transitionValues);
    }


    // For the view in transitionValues.view, get the values you
    // want and put them in transitionValues.values
    private void captureValues(TransitionValues transitionValues) {
        // Get a reference to the view
        View view = transitionValues.view;
        // Store its background property in the values map
        transitionValues.values.put(PROPNAME_BACKGROUND, view.getBackground());
    }
    ...
}

[2] 获取结束值
框架在结束场景中为每个目标视图调用一次captureEndValues(TransitionValues)方法。在其它方面,captureEndValues()跟captureStartValues()工作机制一致。

以下代码片段显示了captureEndValues()方法的实现:

@Override
public void captureEndValues(TransitionValues transitionValues) {
    captureValues(transitionValues);
}

在此例中,captureStartValues()和 captureEndValues()方法都调用了captureValues()方法来检索和保存值。captureValues()方法检索到的视图属性相同,但它在开始和结束场景中有着不同的值。框架分开映射开始和结束场景视图的值。

(3) 创建自定义动画
欲动画视图在其开始和结束场景中的改变,需要重写createAnimator()方法来提供动画器。当框架调用此方法时,它将传递动画器给场景根视图和所捕获的包含开始和结束场景给TransitionValues对象。

框架调用createAnimator()方法的次数基于开始和结束场景的改变。例如,自定义实现的消退/消失变换。如在开始场景中景中有5个目标但在结束场景中会被移除两个,那么在结束场景就只有三个目标,再在结束场景中添加一个新目标。那么框架将会调用createAnimator()方法六次。三次调用用于两个场景中的消退/消失;两次调用为移除的目标动画;一次调用为结束场景中的新目标。

对于存在于开始和结束场景中的视图目标,框架为startValues和endValues参数提供了Transitions对象。对于只存在于开始或结束场景中的目标视图,框架提供TransitionValues对象来联系参数和并用null联系其它。

当创建自定义变换实现createAnimator(ViewGroup, TransitionValues, TransitionValues)方法时,用捕获到的视图属性值来创建Animator对象并将此返回给框架。一个实现的样例,见自定义变换样例中的ChangeColor类。更多关于属性动画的值 Property Animation见。

(4) 应用自定义变换
自定义变换的工作机制跟内建变换相同。可以用变换管理应用于自定义变换,具体描述见 Applying a Transition。

4. 增加动画

如何将渐变的动画添加到用户界面中。

动画能够为通知用户关于应用程序发生了啥增加微妙的视觉线索并且会增加应用程序界面的心智模型(mental model)。当屏幕改变状态时动画尤其有用,如当内容载入或新动作变得有用时。同时动画也能够为应用程序增加光满的外观,这可以给应用程序一个更高质量的感觉。

但仍需记住,过度的使用动画或在错误的时间使用动画也会带来不利,如引起延迟。此节展示如何实现一些常见的能够带来实用并在不骚扰用户的情况下增加流动性的动画类型。

2015.11.18

4.1 两视图交叉淡入淡出

学习如何让两个重叠的视图淡入淡出。此节展示如何让进度条淡入包含文本内容的视图。

淡入淡出动画(亦称溶解)渐渐的消退某用户界面组件的同时渐入另一个组件。此动画对于转换内容或视图到应用程序的情形很有用。淡入淡出非常微妙同时也很短暂但提供了屏幕到文本的流利的变换。当不用淡入淡出动画时,这些变换都会显得有些突然。

此处有一个从进度条淡出文本内容的例子:http://developer.android.com/training/animation/anim_crossfade.mp4
淡入淡出动画
点击屏幕设备屏幕可放映动画

如果您想跳过后续内容并想看一个完整的代码示例,下载并运行样例,选择淡入淡出的例子。看一下几个文件中的代码实现:
· src/CrossfadeActivity.java
· layout/activity_crossfade.xml
· menu/activity_crossfade.xml

(1) 创建视图
创建欲淡入淡出的两个视图。以下代码片段创建了一个进度条和一个具滑动条的文本视图:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView style="?android:textAppearanceMedium"
            android:lineSpacingMultiplier="1.2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/lorem_ipsum"
            android:padding="16dp" />

    </ScrollView>

    <ProgressBar android:id="@+id/loading_spinner"
        style="?android:progressBarStyleLarge"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center" />

</FrameLayout>

(2) 设置动画
欲设置动画,遵循以下步骤:
[1] 为欲淡入淡出的视图创建成员变量。在动画期间需要这些视图的引用来修改视图。
[2] 对于淡入的视图,将其可见性设置为GONE。此值能够避免视图占据布局文件空间并忽略堆它们的计算以提高处理速度。
[3] 将config_shortAnimTime系统属性缓存在成员变量中。此属性为动画定义了一个标准的“短”的持续时间。此持续时间对微妙的动画或发生频率较高的动画比较理想。config_longAnimTime和config_mediumAnimTime也是可用的,如果您想用它们的话。

将前面代码片段所定义的内容作为以下代码描述的活动的布局文件:

public class CrossfadeActivity extends Activity {

    private View mContentView;
    private View mLoadingView;
    private int mShortAnimationDuration;

    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_crossfade);

        mContentView = findViewById(R.id.content);
        mLoadingView = findViewById(R.id.loading_spinner);

        // Initially hide the content view.
        mContentView.setVisibility(View.GONE);

        // Retrieve and cache the system's default "short" animation time.
        mShortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }

(3) 淡入淡出视图
至此,视图已被正确设置,欲对这些视图进行淡入淡出请遵循以下步骤:
[1] 对于淡入的视图,设置alpha值为0并将其可见值设置为VISIBLE。(记住其初始值为GONE)这样能够让这些视图可见但出于完全透明的状态。
[2] 对于淡入的视图,动画改变其alpha的值从0到1。同时,将淡出视图的alpha值从1动画改为0。
[3] 在Animator.AnimatorListener中使用onAnimationEnd()方法,将淡出视图的可见性设置为GONE。尽管这些视图的alpha值为0,将视图的可见性设置为GONE能够阻止视图占用布局文件空间且忽略布局计算,以提升处理速度。

以下方法展示如何做以上描述的步骤:

private View mContentView;
private View mLoadingView;
private int mShortAnimationDuration;

...

private void crossfade() {

    // Set the content view to 0% opacity but visible, so that it is visible
    // (but fully transparent) during the animation.
    mContentView.setAlpha(0f);
    mContentView.setVisibility(View.VISIBLE);

    // Animate the content view to 100% opacity, and clear any animation
    // listener set on the view.
    mContentView.animate()
            .alpha(1f)
            .setDuration(mShortAnimationDuration)
            .setListener(null);

    // Animate the loading view to 0% opacity. After the animation ends,
    // set its visibility to GONE as an optimization step (it won't
    // participate in layout passes, etc.)
    mLoadingView.animate()
            .alpha(0f)
            .setDuration(mShortAnimationDuration)
            .setListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mLoadingView.setVisibility(View.GONE);
                }
            });
}

4.2 使用ViewPager屏幕滑动

学习在滑动变换下动画变化屏幕。

屏幕滑动是从一个屏幕页面到另一个屏幕页面的变换。此节内容将展示如何用由support library提供的ViewPager来做到屏幕滑动变换。ViewPager自动扫描屏幕滑动。点击下图屏幕,由本页屏幕会自动滑动到下一页屏幕:

屏幕滑动动画:http://developer.android.com/training/animation/anim_screenslide.mp4
屏幕滑动动画
点击屏幕设备屏幕可放映动画

如果您想跳过后续内容且想看完整的工程代码,下载本节样例应用程序,选择屏幕滑动(Screen Slide)例子。看以下几个文件中的代码实现:
- src/ScreenSlidePageFragment.java
- src/ScreenSlideActivity.java
- layout/activity_screen_slide.xml
- layout/fragment_screen_slide_page.xml

(1) 创建视图
为稍后要使用的碎片的内容创建一个布局文件。以下实现的布局文件中包含一个显示文本的文本视图:

<!-- fragment_screen_slide_page.xml -->
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView style="?android:textAppearanceMedium"
        android:padding="16dp"
        android:lineSpacingMultiplier="1.2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/lorem_ipsum" />
</ScrollView>

同时也在碎片中定义了一个字符串。

(2) 创建碎片
创建一个返回在onCreateView()方法中创建的布局的Fragment(碎片)类。然后,在需要向用户展示新页的时候就可以在碎片的父活动中创建此片段实例:

import android.support.v4.app.Fragment;
...
public class ScreenSlidePageFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        ViewGroup rootView = (ViewGroup) inflater.inflate(
                R.layout.fragment_screen_slide_page, container, false);

        return rootView;
    }
}

(3) 增加ViewPager
ViewPager已经被内建在扫页面变换中,它的默认功能就是滑动屏幕动画,所以不必重新创建它。ViewPager展示PagerAdapter提供的页面,所以PagerAdapter也会用到之前所创建的碎片。

首先,在布局文件 中包含ViewPager:

<!-- activity_screen_slide.xml -->
<android.support.v4.view.ViewPager
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/pager"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

创建一个具以下功能的活动:
- 将活动的视图内容设置包含ViewPager的布局文件。
- 创建一个扩展于FragmentStatePagerAdapter类的类并实现getItem()方法来提供一个ScreenSlidePageFragment实例作为新页。页面适配器同样要求实现getCount()方法,此方法返回适配器所需创建的页面数。
- 将PagerAdapter挂到ViewPager上。
- 在碎片的虚拟栈中通过回移来响应设备的返回按钮。如果用户已经在第一页上,就返回到活动的栈底。

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
...
public class ScreenSlidePagerActivity extends FragmentActivity {
    /**
     * The number of pages (wizard steps) to show in this demo.
     */
    private static final int NUM_PAGES = 5;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next wizard steps.
     */
    private ViewPager mPager;

    /**
     * The pager adapter, which provides the pages to the view pager widget.
     */
    private PagerAdapter mPagerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_screen_slide);

        // Instantiate a ViewPager and a PagerAdapter.
        mPager = (ViewPager) findViewById(R.id.pager);
        mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
        mPager.setAdapter(mPagerAdapter);
    }

    @Override
    public void onBackPressed() {
        if (mPager.getCurrentItem() == 0) {
            // If the user is currently looking at the first step, allow the system to handle the
            // Back button. This calls finish() on this activity and pops the back stack.
            super.onBackPressed();
        } else {
            // Otherwise, select the previous step.
            mPager.setCurrentItem(mPager.getCurrentItem() - 1);
        }
    }

    /**
     * A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in
     * sequence.
     */
    private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
        public ScreenSlidePagerAdapter(FragmentManager fm) {
            super(fm);
        }

        @Override
        public Fragment getItem(int position) {
            return new ScreenSlidePageFragment();
        }

        @Override
        public int getCount() {
            return NUM_PAGES;
        }
    }
}

(4)用PageTransformer自定义动画
欲在默认的屏幕滑动动画中展示不同的动画页面,实现ViewPager.PagerTransformer接口并将此接口提供给视图页。此接口指暴露了transformPage()一个方法。每当屏幕变换时,每个可见视图(通常屏幕上只有一个可见页面)以及相邻的没有在屏幕上的页面就会调用此方法一次。例如,若当前屏幕为页面三,用户欲拖拽出页面四,在每一个手势发生时,transformPage()会被页面二、三、四调用。

在transformPage()的实现中,根据屏幕上页面的位置参数通过判断哪一个页面需要转换可以创建自定义的动画滑动,位置可从ransformPage()方法的position参数获得。

位置position参数会表明一个页面跟屏幕中心的位置关系。当用户滑动屏幕时此参数是一个动态值。当某页面填充到屏幕中时,其位置参数值为0。当一个页面刚好从屏幕右边消失时,其位置值为1。如果用户在页面1和页面2中滑动一半,页面1的位置值为-0.5,页面2的位置值为0.5。基于页面在屏幕上的具体位置,可以通过setAlpha()、setTranslationX()或setScaleY()方法设置页面属性值来自定义动画。

当实现PagerTransformer后,用此实现调用setPageTransformer()来应用自定义的动画。例如,假设有一个名为ZoomOutPageTransformer的PagerTransformer,可以像以下这样设置自定义动画:

ViewPager mPager = (ViewPager) findViewById(R.id.pager);
...
mPager.setPageTransformer(true, new ZoomOutPageTransformer());

见Zoom-out page transformer和Depth page transformer部分的例子及相应变换的视频。

[1] 页面缩小变换
当用户在相邻页面滑动时,页面将会缩小并消退出屏幕。当页面接近屏幕中心时,此页面将回到正常尺寸并渐入。

页面缩小变换动画:http://developer.android.com/training/animation/anim_page_transformer_zoomout.mp4
ZoomOutPageTransformer示例
点击设备屏幕放映动画

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.85f;
    private static final float MIN_ALPHA = 0.5f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();
        int pageHeight = view.getHeight();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 1) { // [-1,1]
            // Modify the default slide transition to shrink the page as well
            float scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));
            float vertMargin = pageHeight * (1 - scaleFactor) / 2;
            float horzMargin = pageWidth * (1 - scaleFactor) / 2;
            if (position < 0) {
                view.setTranslationX(horzMargin - vertMargin / 2);
            } else {
                view.setTranslationX(-horzMargin + vertMargin / 2);
            }

            // Scale the page down (between MIN_SCALE and 1)
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

            // Fade the page relative to its size.
            view.setAlpha(MIN_ALPHA +
                    (scaleFactor - MIN_SCALE) /
                    (1 - MIN_SCALE) * (1 - MIN_ALPHA));

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

[2] 页面深度变换
当用“depth”动画滑动页面到右边时,页面用默认的动画变换将滑动页面移到左边。深度变换将页面淡出并线性的减小其范围。

页面深度变换示例:http://developer.android.com/training/animation/anim_page_transformer_depth.mp4
DepthPageTransformer 示例
点击设备屏幕放映动画

注:在深度动画期间,默认的动画(屏幕滑动)仍旧发生了,所以必须构建一个X负方向的变换。例如:

view.setTranslationX(-1 * view.getWidth() * position);

以下代码演示如何在正移动的页面变换中抵消默认的屏幕动画滑动:

public class DepthPageTransformer implements ViewPager.PageTransformer {
    private static final float MIN_SCALE = 0.75f;

    public void transformPage(View view, float position) {
        int pageWidth = view.getWidth();

        if (position < -1) { // [-Infinity,-1)
            // This page is way off-screen to the left.
            view.setAlpha(0);

        } else if (position <= 0) { // [-1,0]
            // Use the default slide transition when moving to the left page
            view.setAlpha(1);
            view.setTranslationX(0);
            view.setScaleX(1);
            view.setScaleY(1);

        } else if (position <= 1) { // (0,1]
            // Fade the page out.
            view.setAlpha(1 - position);

            // Counteract the default slide transition
            view.setTranslationX(pageWidth * -position);

            // Scale the page down (between MIN_SCALE and 1)
            float scaleFactor = MIN_SCALE
                    + (1 - MIN_SCALE) * (1 - Math.abs(position));
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);

        } else { // (1,+Infinity]
            // This page is way off-screen to the right.
            view.setAlpha(0);
        }
    }
}

4.3 显示卡片翻转式动画

学习在翻转运动下如何实现两视图之间的动画。

此节将展示如何用自定义碎片动画来实现卡片翻转动画。视图内容间的卡片翻转通过模拟卡片翻转过来实现。

卡片翻转的过程如下所示:http://developer.android.com/training/animation/anim_card_flip.mp4
卡片翻转动画,点击设备屏幕放映动画

欲跳过后续内容而想看完整的样例,下载本节样例选择Card Flip样例打开看以下几个文件中的代码实现:
- src/CardFlipActivity.java
- animator/card_flip_right_in.xml
- animator/card_flip_right_out.xml
- animator/card_flip_left_in.xml
- animator/card_flip_left_out.xml
- layout/fragment_card_back.xml
- layout/fragment_card_front.xml

(1) 创建动画
欲创建卡片式动画翻转,需要两个动画场景,当前面的卡片动画从向左翻转出去时另外一个动画要从左方显示进来。同时,当卡片动画从右方向回来时另一动画需要从右方向消失。

card_flip_left_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="-180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_left_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

card_flip_right_in.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Before rotating, immediately set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:duration="0" />

    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="180"
        android:valueTo="0"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 1. -->
    <objectAnimator
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />

card_flip_right_out.xml

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Rotate. -->
    <objectAnimator
        android:valueFrom="0"
        android:valueTo="-180"
        android:propertyName="rotationY"
        android:interpolator="@android:interpolator/accelerate_decelerate"
        android:duration="@integer/card_flip_time_full" />

    <!-- Half-way through the rotation (see startOffset), set the alpha to 0. -->
    <objectAnimator
        android:valueFrom="1.0"
        android:valueTo="0.0"
        android:propertyName="alpha"
        android:startOffset="@integer/card_flip_time_half"
        android:duration="1" />
</set>

(2) 创建视图
“卡片”的每一面可以独立包含任何内容,如都包含文本、图片以及有关联的视图。动画变换时将会用存储在碎片中的视图布局。以下布局创建了卡片用用于显示文本的一面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#a6c"
    android:padding="16dp"
    android:gravity="bottom">

    <TextView android:id="@android:id/text1"
        style="?android:textAppearanceLarge"
        android:textStyle="bold"
        android:textColor="#fff"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/card_back_title" />

    <TextView style="?android:textAppearanceSmall"
        android:textAllCaps="true"
        android:textColor="#80ffffff"
        android:textStyle="bold"
        android:lineSpacingMultiplier="1.2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/card_back_description" />

</LinearLayout>

卡片的另一面用于展示ImageView:

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:src="@drawable/image1"
    android:scaleType="centerCrop"

    android:contentDescription="@string/description_image_1" />

(3) 创建碎片
为卡片的前面和背面创建碎片类。此类返回之前在每个片段中onCreateView()方法中所创建的布局。之后,可在欲显示卡片的碎片的父活动中声明此碎片实例。以下代码展示了在父活动中嵌套碎片类的实现:

public class CardFlipActivity extends Activity {
    ...
    /**
     * A fragment representing the front of the card.
     */
    public class CardFrontFragment extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_card_front, container, false);
        }
    }

    /**
     * A fragment representing the back of the card.
     */
    public class CardBackFragment extends Fragment {
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_card_back, container, false);
        }
    }
}

(4) 动画卡片翻转
至此,可以在父活动中展示片段了。欲此,首先为活动创建布局文件。以下代码创建了可以在运行时添加碎片的包含FrameLayout元素的布局文件:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

在活动类代码中,将刚创建的布局文件加载到活动类中。在活动被创建时显示默认的片段也是个不错的主意,所以以下活动类中的代码展示如何将卡片前面作为默认显示:

public class CardFlipActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_activity_card_flip);

        if (savedInstanceState == null) {
            getFragmentManager()
                    .beginTransaction()
                    .add(R.id.container, new CardFrontFragment())
                    .commit();
        }
    }
    ...
}

现在以后卡片的前面的显示,在适当的时候可以用翻转动画来显示卡片的背面。遵循以下步骤来显示卡片的另一面:
- 设置之前为碎片变换创建的自定义动画。
- 用新的碎片代替当前碎片的显示并触发自定义动画。
- 将之前的碎片置于栈顶的下一层,这样只要用户按下返回按钮,卡片就翻转过来了。

private void flipCard() {
    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();
}

4.4 缩放视图

学习在缩放触摸动画操作下如何放大视图。

此节演示如何实现触摸-缩放动画,此动画对于像相片画廊引用程序非常有用 - 从缩略图视图动画到全尺寸以填充屏幕。

这里有一个触摸-缩放动画:http://developer.android.com/training/animation/anim_zoom.mp4
缩放动画,点击屏幕放映动画

如果想直接看本节代码示例,下载并选择Zoom样例,见以下几个文件中的代码实现:
- src/TouchHighlightImageButton.java(一个帮助类,当按图片按钮时高亮点击的地方)
- src/ZoomActivity.java
- layout/activity_zoom.xml

(1) 创建视图
创建一个包含缩放所需的小和大尺寸视图的布局文件。以下代码创建了一个具点击响应事件的ImageButton按钮以及一个展示扩大图片的的ImageView视图:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <ImageButton
            android:id="@+id/thumb_button_1"
            android:layout_width="100dp"
            android:layout_height="75dp"
            android:layout_marginRight="1dp"
            android:src="@drawable/thumb1"
            android:scaleType="centerCrop"
            android:contentDescription="@string/description_image_1" />

    </LinearLayout>

    <!-- This initially-hidden ImageView will hold the expanded/zoomed version of
         the images above. Without transformations applied, it takes up the entire
         screen. To achieve the "zoom" animation, this view's bounds are animated
         from the bounds of the thumbnail button above, to its final laid-out
         bounds.
         -->

    <ImageView
        android:id="@+id/expanded_image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="invisible"
        android:contentDescription="@string/description_zoom_touch_close" />

</FrameLayout>

(2) 设置缩放动画
一旦应用布局文件,即可设置出发缩放动画的事件。以下代码增加View.onClickListener事件给ImageButton,如此,当用户点击此按钮时即实现缩放动画:

public class ZoomActivity extends FragmentActivity {
    // Hold a reference to the current animator,
    // so that it can be canceled mid-way.
    private Animator mCurrentAnimator;

    // The system "short" animation time duration, in milliseconds. This
    // duration is ideal for subtle animations or animations that occur
    // very frequently.
    private int mShortAnimationDuration;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_zoom);

        // Hook up clicks on the thumbnail views.

        final View thumb1View = findViewById(R.id.thumb_button_1);
        thumb1View.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                zoomImageFromThumb(thumb1View, R.drawable.image1);
            }
        });

        // Retrieve and cache the system's default "short" animation time.
        mShortAnimationDuration = getResources().getInteger(
                android.R.integer.config_shortAnimTime);
    }
    ...
}

(3) 缩放视图
在恰当的时机下需要从正常尺寸的视图缩放到某个尺寸的视图。通常来讲,需要根据视图的边界来动画。以下方法展示了怎么实现缩放动画(从缩略视图到更大尺寸):
[1]. 分配高分辨率图片到隐藏的“放大”(扩大)ImageView中。以下的样例代码将一张大型图片简单的载入到用户界面线程中。更科学的做法是在独立的线程中载入图片并在用户界面线程中设置位图以防止阻碍用户界面线程。理想情况下,位图不应该比屏幕尺寸大。
[2]. 计算ImageView开始和结束时的边界。
[3]. 根据开始边界和结束边界,同时动画四个位置和尺寸值X,Y(SCALE_X和SCALE_Y)。四个动画被增加到AnimatorSet中,这样他们可以在同时开始。
[4]. 当图片被放大用户再点击屏幕时运行类似的动画将视图缩小(还原)。可以为ImageView增加View.onClickListerer来监听用户的点击。当用户点击时,ImageView会还原缩略图大小并将其可见性设置为GONE来隐藏。

private void zoomImageFromThumb(final View thumbView, int imageResId) {
    // If there's an animation in progress, cancel it
    // immediately and proceed with this one.
    if (mCurrentAnimator != null) {
        mCurrentAnimator.cancel();
    }

    // Load the high-resolution "zoomed-in" image.
    final ImageView expandedImageView = (ImageView) findViewById(
            R.id.expanded_image);
    expandedImageView.setImageResource(imageResId);

    // Calculate the starting and ending bounds for the zoomed-in image.
    // This step involves lots of math. Yay, math.
    final Rect startBounds = new Rect();
    final Rect finalBounds = new Rect();
    final Point globalOffset = new Point();

    // The start bounds are the global visible rectangle of the thumbnail,
    // and the final bounds are the global visible rectangle of the container
    // view. Also set the container view's offset as the origin for the
    // bounds, since that's the origin for the positioning animation
    // properties (X, Y).
    thumbView.getGlobalVisibleRect(startBounds);
    findViewById(R.id.container)
            .getGlobalVisibleRect(finalBounds, globalOffset);
    startBounds.offset(-globalOffset.x, -globalOffset.y);
    finalBounds.offset(-globalOffset.x, -globalOffset.y);

    // Adjust the start bounds to be the same aspect ratio as the final
    // bounds using the "center crop" technique. This prevents undesirable
    // stretching during the animation. Also calculate the start scaling
    // factor (the end scaling factor is always 1.0).
    float startScale;
    if ((float) finalBounds.width() / finalBounds.height()
            > (float) startBounds.width() / startBounds.height()) {
        // Extend start bounds horizontally
        startScale = (float) startBounds.height() / finalBounds.height();
        float startWidth = startScale * finalBounds.width();
        float deltaWidth = (startWidth - startBounds.width()) / 2;
        startBounds.left -= deltaWidth;
        startBounds.right += deltaWidth;
    } else {
        // Extend start bounds vertically
        startScale = (float) startBounds.width() / finalBounds.width();
        float startHeight = startScale * finalBounds.height();
        float deltaHeight = (startHeight - startBounds.height()) / 2;
        startBounds.top -= deltaHeight;
        startBounds.bottom += deltaHeight;
    }

    // Hide the thumbnail and show the zoomed-in view. When the animation
    // begins, it will position the zoomed-in view in the place of the
    // thumbnail.
    thumbView.setAlpha(0f);
    expandedImageView.setVisibility(View.VISIBLE);

    // Set the pivot point for SCALE_X and SCALE_Y transformations
    // to the top-left corner of the zoomed-in view (the default
    // is the center of the view).
    expandedImageView.setPivotX(0f);
    expandedImageView.setPivotY(0f);

    // Construct and run the parallel animation of the four translation and
    // scale properties (X, Y, SCALE_X, and SCALE_Y).
    AnimatorSet set = new AnimatorSet();
    set
            .play(ObjectAnimator.ofFloat(expandedImageView, View.X,
                    startBounds.left, finalBounds.left))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.Y,
                    startBounds.top, finalBounds.top))
            .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,
            startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,
                    View.SCALE_Y, startScale, 1f));
    set.setDuration(mShortAnimationDuration);
    set.setInterpolator(new DecelerateInterpolator());
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            mCurrentAnimator = null;
        }

        @Override
        public void onAnimationCancel(Animator animation) {
            mCurrentAnimator = null;
        }
    });
    set.start();
    mCurrentAnimator = set;

    // Upon clicking the zoomed-in image, it should zoom back down
    // to the original bounds and show the thumbnail instead of
    // the expanded image.
    final float startScaleFinal = startScale;
    expandedImageView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (mCurrentAnimator != null) {
                mCurrentAnimator.cancel();
            }

            // Animate the four positioning/sizing properties in parallel,
            // back to their original values.
            AnimatorSet set = new AnimatorSet();
            set.play(ObjectAnimator
                        .ofFloat(expandedImageView, View.X, startBounds.left))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.Y,startBounds.top))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.SCALE_X, startScaleFinal))
                        .with(ObjectAnimator
                                .ofFloat(expandedImageView, 
                                        View.SCALE_Y, startScaleFinal));
            set.setDuration(mShortAnimationDuration);
            set.setInterpolator(new DecelerateInterpolator());
            set.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    thumbView.setAlpha(1f);
                    expandedImageView.setVisibility(View.GONE);
                    mCurrentAnimator = null;
                }
            });
            set.start();
            mCurrentAnimator = set;
        }
    });
}

4.5 动画布局文件的改变

学习当在布局文件中增加、移除以及更新子视图时如何开启内建动画。

布局动画是对布局文件配置更改之前预先载入动画。开发者需要做的就是在布局文件中设置属性来告知安卓系统动画改变布局文件的改变,系统用默认的动画效果动画显示它们。

提示:若欲实现自定义布局动画,需创建LayoutTransition对象并需通过setLayoutTransiton()方法将此对象应用到布局文件中。

此处有一个当增加列表中的条目时默认的布局动画:http://developer.android.com/training/animation/anim_layout_changes.mp4
布局动画,点击设备屏幕放映动画

若想直接看此部分的代码样例, 下载本节应用程序并选择Crossfade样例,看以下文件中的代码实现:
[1]. src/LayoutChangesActivity.java
[2]. layout/activity_layout_changes.xml
[3]. menu/activity_layout_changes.xml

(1) 创建布局
在活动的布局XML文件中,将布局文件中欲开启的动画的android:animateLayoutChanges属性设置为true。例:

<LinearLayout android:id="@+id/container"
    android:animateLayoutChanges="true"
    ...
/>

(2) 从布局文件中增加、 更新或移除内容
至此,所有需要做的操作就是增加、移除或更新布局文件中的内容,布局文件中的内容将会自动的以动画形式实现:

private ViewGroup mContainerView;
...
private void addItem() {
    View newView;
    ...
    mContainerView.addView(newView, 0);
}

[2015.11.18-16:35]

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:1632624次
    • 积分:18600
    • 等级:
    • 排名:第518名
    • 原创:385篇
    • 转载:0篇
    • 译文:42篇
    • 评论:403条
    文章分类