pAdTy_1 构建图形和动画应用程序
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。此項技術允許在構建(以及內存分配)位圖之前獲取到圖片的尺寸和類型。
欲避免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 imagefinal 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 dimensionsfinal BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(res, resId, options);// Calculate inSampleSizeoptions.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);// Decode bitmap with inSampleSize setoptions.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中的示例:
為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 dataif (bitmapData == 0 || bitmapData != data) {// Cancel previous taskbitmapWorkerTask.cancel(true);} else {// The same work is already in progressreturn false;}}// No task associated with the ImageView, or an existing task was cancelledreturn 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> {...@Overrideprotected 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) {@Overrideprotected 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.@Overrideprotected 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 threadFile cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);new InitDiskCacheTask().execute(cacheDir);... }class InitDiskCacheTask extends AsyncTask<File, Void, Void> {@Overrideprotected Void doInBackground(File... params) {synchronized (mDiskCacheLock) {File cacheDir = params[0];mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);mDiskCacheStarting = false; // Finished initializationmDiskCacheLock.notifyAll(); // Wake any waiting threads}return null;} }class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {...// Decode image in background.@Overrideprotected Bitmap doInBackground(Integer... params) {final String imageKey = String.valueOf(params[0]);// Check disk cache in background threadBitmap bitmap = getBitmapFromDiskCache(imageKey);if (bitmap == null) { // Not found in disk cache// Process as normalfinal Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100));}// Add final bitmap to cachesaddBitmapToCache(imageKey, bitmap);return bitmap;}... }public void addBitmapToCache(String key, Bitmap bitmap) {// Add to memory cache as beforeif (getBitmapFromMemCache(key) == null) {mMemoryCache.put(key, bitmap);}// Also add to disk cachesynchronized (mDiskCacheLock) {if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {mDiskLruCache.put(key, bitmap);}} }public Bitmap getBitmapFromDiskCache(String key) {synchronized (mDiskCacheLock) {// Wait while disk cache is started from background threadwhile (mDiskCacheStarting) {try {mDiskCacheLock.wait();} catch (InterruptedException e) {}}if (mDiskLruCache != null) {return mDiskLruCache.get(key);}}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 dirfinal 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;}@Overridepublic 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且位圖還未被回收。
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來使用位圖:
[2] 使用存在的位圖
應用程序在運行時,解碼方法會檢查是否有存在的位圖可用。舉例如下:
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 1return 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 adapterpublic 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};@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.image_detail_pager); // Contains just a ViewPagermAdapter = 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;}@Overridepublic int getCount() {return mSize;}@Overridepublic 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 docspublic ImageDetailFragment() {}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {// image_detail_fragment.xml contains just an ImageViewfinal View v = inflater.inflate(R.layout.image_detail_fragment, container, false);mImageView = (ImageView) v.findViewById(R.id.imageView);return v;}@Overridepublic 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 {...@Overridepublic 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;@Overridepublic 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 adapterpublic 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 docspublic ImageGridFragment() {}@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mAdapter = new ImageAdapter(getActivity());}@Overridepublic 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;}@Overridepublic 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;}@Overridepublic int getCount() {return imageResIds.length;}@Overridepublic Object getItem(int position) {return imageResIds[position];}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup container) {ImageView imageView;if (convertView == null) { // if it's not recycled, initialize some attributesimageView = 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 ImageViewreturn imageView;}} }問題在于,將這個過程的實現放在了用戶界面線程中。在圖片量較小時,此代碼能夠正常工作。如果有更多的圖片參與,那么用戶界面可能會被掛起。
可以使用上一節使用的異步和緩存的方法來解決這個問題。然而,咱還需要為GridView考慮并發問題。為解決此問題,用“不要在用戶界面處理位圖”一節中介紹的技術:
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {...private class ImageAdapter extends BaseAdapter {...@Overridepublic 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 taskbitmapWorkerTask.cancel(true);} else {// The same work is already in progressreturn false;}}// No task associated with the ImageView, or an existing task was cancelledreturn 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,必須在清單文件中作如下聲明:
如果應用程序使用紋理壓縮,必須聲明應用程序所支持的壓縮格式,這樣就只在兼容的設備上安裝:
<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;@Overridepublic 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 contextsetEGLContextClientVersion(2);mRenderer = new MyGLRenderer();// Set the Renderer for drawing on the GLSurfaceViewsetRenderer(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 colorGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);}public void onDrawFrame(GL10 unused) {// Redraw background colorGLES20.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圖形管道以作相應處理:
默認情況下,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 arraystatic final int COORDS_PER_VERTEX = 3;static float squareCoords[] = {-0.5f, 0.5f, 0.0f, // top left-0.5f, -0.5f, 0.0f, // bottom left0.5f, -0.5f, 0.0f, // bottom right0.5f, 0.5f, 0.0f }; // top rightprivate short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw verticespublic Square() {// initialize vertex byte buffer for shape coordinatesByteBuffer 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 listByteBuffer 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()方法中為形狀的內存和效率執行作初始化工作。
(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 itGLES20.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 ProgrammProgram = GLES20.glCreateProgram();// add the vertex shader to programGLES20.glAttachShader(mProgram, vertexShader);// add the fragment shader to programGLES20.glAttachShader(mProgram, fragmentShader);// creates OpenGL ES program executablesGLES20.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 vertexpublic void draw() {// Add program to OpenGL ES environmentGLES20.glUseProgram(mProgram);// get handle to vertex shader's vPosition membermPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");// Enable a handle to the triangle verticesGLES20.glEnableVertexAttribArray(mPositionHandle);// Prepare the triangle coordinate dataGLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX,GLES20.GL_FLOAT, false,vertexStride, vertexBuffer);// get handle to fragment shader's vColor membermColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");// Set color for drawing the triangleGLES20.glUniform4fv(mColorHandle, 1, color, 0);// Draw the triangleGLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);// Disable vertex arrayGLES20.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:
此段代碼計算了投影矩陣mProjectionMatrix,相機視圖將在onDrawFrame()方法中使用此矩陣,此將在下一節中介紹。
注:只對繪制對象應用投影一般會導致圖形消失。通常,為了將圖形重新顯示在屏幕上還需要使用相機視圖。
(2) 定義相機視圖
為繪制對象增加相機視圖變換方才算完成了圖形的變換。在以下代碼示例中,在Matrix.setLookATM()方法中完成相機試圖變換并結合之前的投影變換矩陣。再將兩種變換結合得到矩陣傳遞給繪制對象:
(3) 請求投影和相機變換
欲結合投影和相機視圖變換矩陣,首先需要在之前的三角形類中定義頂點著色器矩陣:
然后,修改繪制對象的draw()方法以接收二者變換的矩陣并將此矩陣應用到圖形:
public void draw(float[] mvpMatrix) { // pass in the calculated transformation matrix...// get handle to shape's transformation matrixmMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");// Pass the projection and view transformation to the shaderGLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);// Draw the triangleGLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);// Disable vertex arrayGLES20.glDisableVertexAttribArray(mPositionHandle); }一旦正確和應用了投影和相機視圖變換,以正確比例被繪制的圖形對象應該如下圖所示:
圖1. 用投影和相機視圖變換的三角形繪制
至此,應用程序中已經用正確的比例繪制圖形了,該往形狀增添動畫了。
2.5 增加運動
學習如何用OpenGL來實現所繪對象基本的移動和動畫。
將對象繪制在屏幕之上只是OpenGL最基本的特定,安卓其它的圖形框架類如Canvas及Drawable也能夠完成此項工作。OpenGL ES還為圖形提供了移動、變換圖形到三維空間以及創造令人信服的用戶體驗的功能。
此節將繼續學習OpenGL ES來通過旋轉的方式移動圖形。
(1) 旋轉圖形
用OpenGL ES 2.0來選中繪制對象比較簡單。在渲染器中創建一個轉換矩陣(旋轉矩陣)并將其跟投影和相機視圖變換矩陣結合到一塊:
如果做了這些改變后三角形仍舊還沒有旋轉,確保對GLSurfaceView.RENDERMODE_WHEN_DIRTY進行了注釋,此內容在下一節討論。
(2) 啟用連續渲染
如果您已孜孜不倦地昨晚了樣例代碼中的所有內容,確保注釋了設置渲染模式為只在臟(dirty)才繪制的一行代碼,否則OpenGL只做一次旋轉然后就等待調用GLSurfaceView容器中的requestRender()方法:
除非在無用戶交互的情況下對象還有轉動,否則應該將此語句的注釋去掉。做好取消此語句注釋的準備,因為下一節將會在程序中使用此語句。
2.6 響應觸摸事件
學習怎么和OpenGL圖形實現基本的互動。
移動預先設定程序中的對象是有用的,如此會得到用戶的更多關注。但要是想讓OpenGL ES圖形能和用戶交互又改怎么樣做呢?讓OpenGL ES應用程序能夠和用戶交互的關鍵是重寫GLSurfaceView中的onTouchEvent()來監聽觸摸事件。
此節介紹如何監聽用戶的觸摸事件以讓用戶旋轉OpenGL ES對象。
(1) 設置觸摸監聽器
欲使OpenGL ES應用程序響應觸摸事件,必須實現GLSurfaceView類中的onTouchEvent()方法。以下實現的代碼展示了怎么監聽MotionEvent.ACTION_MOVE事件并將它們轉換為形狀旋轉的角度。
注意在計算旋轉角度后,此方法調用了requestRender()來告知渲染器該渲染框架了。此方法在此樣例中最為有效,因為框架不需要重畫,除非旋轉有變。然而,它不會影響效率除非用setRenderMode()方法來設置渲染器只進行重繪制操作,所以確保以下這行代碼沒有被注釋:
public MyGLSurfaceView(Context context) {...// Render the view only when there is a change in the drawing datasetRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); }(2) 獲取旋轉角度
以上代碼樣例需要通過增加一個公有的變量來揭露渲染器的旋轉角度。由于渲染代碼運行于獨立于用戶主線程的線程中,必須將此變量聲明為volatile。以下代碼聲明了此變量且揭露了獲取和設置方法:
(3) 應用旋轉
欲應用通過觸摸輸入產生的旋轉,注釋掉產生角度的代碼并增加mAngle變量,此變量包含了觸摸事件生成的旋轉角度:
當完成以上描述的所有步驟后,運行程序并用手指再屏幕上滑動來選擇三角形,運行結果會類似下圖:
圖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
布局文件的定義包含了一個文本域和一個場景根的子布局。第一個場景的布局文件被包含在了主布局文件中。此允許應用程序將此作為用戶界面的一部分來展示同時也能夠將此載入到場景中,因為框架只能將整個布局文件載入場景中。
第一個場景的布局文件定義如下:
res/layout/a_scene.xml
擁有與第一個場景布局文件相同的文本域(相同的ID)但放置順序不同的第二個場景的布局文件內容如下:
res/layout/another_scene.xml
[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
以下代碼片段演示如何將資源文件中的變換實例關聯到活動的代碼中:
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
第二個代碼片段展示動畫增加文本視圖的過程:
MainActivity.java
(6) 定義變換聲明周期回調方法
變換的生命周期類似活動的生命周期。它代表框架在調用TransitionManager.go()方法和完成動畫期間所監控的變換的狀態。在重要的生命周期狀態中,框架調用被TransitionListener接口定義的回調方法。
變換的生命周期回調函數很有用,例如,在場景改變時復制從開始場景到結束場景某個視圖的特性值。不可簡單的在視圖層中復制開始視圖和結束視圖,因為結束視圖層在變換完成前沒有被關聯。科學的做法是,將值保存在某變量中在框架完成變換后再將此變量拷貝到結束場景中。欲獲得變換結束的通知,在活動中實現TransitionListener.onTransitionEnd()方法。
更多信息見API參考TransitionListener類。
3.4 創建自定義變換
學習如何創建不屬于變換框架中的其它的動畫效果。
自定義變換創建的動畫對于任何內建變換類都不可用。例如,可以定義一個變換來返回文本的前景色并將輸入域設置為灰色以按時此域在屏幕上已經失去了輸入功能。這中效果將幫助用戶理解此域失去了輸入功能。
就像內建變換類型一樣,自定義的變換可以應用動畫到開始和結束場景中的子視圖中。然而,也不像內建變換類型,需要提供來獲取特性值和產生動畫的代碼。也可以圍動畫定義視圖目標的子集。
此節教您獲取特性值和產生動畫來創建自定義變換。
(1) 擴展變換類
欲創建自定義變換,增加擴展Transition類的類到工程中并重寫方法,如以下代碼所示:
后續內容解釋如何重寫這些方法。
(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 collisionsprivate static final String PROPNAME_BACKGROUND ="com.example.android.customtransition:CustomTransition:background";@Overridepublic void captureStartValues(TransitionValues transitionValues) {// Call the convenience method captureValuescaptureValues(transitionValues);}// For the view in transitionValues.view, get the values you// want and put them in transitionValues.valuesprivate void captureValues(TransitionValues transitionValues) {// Get a reference to the viewView view = transitionValues.view;// Store its background property in the values maptransitionValues.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) 創建視圖
創建欲淡入淡出的兩個視圖。以下代碼片段創建了一個進度條和一個具滑動條的文本視圖:
(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;...@Overrideprotected 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() {@Overridepublic 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) 創建視圖
為稍后要使用的碎片的內容創建一個布局文件。以下實現的布局文件中包含一個顯示文本的文本視圖:
同時也在碎片中定義了一個字符串。
(2) 創建碎片
創建一個返回在onCreateView()方法中創建的布局的Fragment(碎片)類。然后,在需要向用戶展示新頁的時候就可以在碎片的父活動中創建此片段實例:
(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上。
- 在碎片的虛擬棧中通過回移來響應設備的返回按鈕。如果用戶已經在第一頁上,就返回到活動的棧底。
(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示例
點擊設備屏幕放映動畫
[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 pageview.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 transitionview.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) 創建視圖
“卡片”的每一面可以獨立包含任何內容,如都包含文本、圖片以及有關聯的視圖。動畫變換時將會用存儲在碎片中的視圖布局。以下布局創建了卡片用用于顯示文本的一面:
卡片的另一面用于展示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()方法中所創建的布局。之后,可在欲顯示卡片的碎片的父活動中聲明此碎片實例。以下代碼展示了在父活動中嵌套碎片類的實現:
(4) 動畫卡片翻轉
至此,可以在父活動中展示片段了。欲此,首先為活動創建布局文件。以下代碼創建了可以在運行時添加碎片的包含FrameLayout元素的布局文件:
在活動類代碼中,將剛創建的布局文件加載到活動類中。在活動被創建時顯示默認的片段也是個不錯的主意,所以以下活動類中的代碼展示如何將卡片前面作為默認顯示:
public class CardFlipActivity extends Activity {@Overrideprotected 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();}}... }現在以后卡片的前面的顯示,在適當的時候可以用翻轉動畫來顯示卡片的背面。遵循以下步驟來顯示卡片的另一面:
- 設置之前為碎片變換創建的自定義動畫。
- 用新的碎片代替當前碎片的顯示并觸發自定義動畫。
- 將之前的碎片置于棧頂的下一層,這樣只要用戶按下返回按鈕,卡片就翻轉過來了。
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視圖:
(2) 設置縮放動畫
一旦應用布局文件,即可設置出發縮放動畫的事件。以下代碼增加View.onClickListener事件給ImageButton,如此,當用戶點擊此按鈕時即實現縮放動畫:
(3) 縮放視圖
在恰當的時機下需要從正常尺寸的視圖縮放到某個尺寸的視圖。通常來講,需要根據視圖的邊界來動畫。以下方法展示了怎么實現縮放動畫(從縮略視圖到更大尺寸):
[1]. 分配高分辨率圖片到隱藏的“放大”(擴大)ImageView中。以下的樣例代碼將一張大型圖片簡單的載入到用戶界面線程中。更科學的做法是在獨立的線程中載入圖片并在用戶界面線程中設置位圖以防止阻礙用戶界面線程。理想情況下,位圖不應該比屏幕尺寸大。
[2]. 計算ImageView開始和結束時的邊界。
[3]. 根據開始邊界和結束邊界,同時動畫四個位置和尺寸值X,Y(SCALE_X和SCALE_Y)。四個動畫被增加到AnimatorSet中,這樣他們可以在同時開始。
[4]. 當圖片被放大用戶再點擊屏幕時運行類似的動畫將視圖縮小(還原)。可以為ImageView增加View.onClickListerer來監聽用戶的點擊。當用戶點擊時,ImageView會還原縮略圖大小并將其可見性設置為GONE來隱藏。
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。例:
(2) 從布局文件中增加、 更新或移除內容
至此,所有需要做的操作就是增加、移除或更新布局文件中的內容,布局文件中的內容將會自動的以動畫形式實現:
[2015.11.18-16:35]
總結
以上是生活随笔為你收集整理的pAdTy_1 构建图形和动画应用程序的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于ES2020语法2345加速浏览器不
- 下一篇: 另一个世界的人