pAdTy_1 构建图形和动画应用程序
2015.11.12-11.18
個(gè)人英文閱讀練習(xí)筆記。原文地址:http://developer.android.com/training/building-graphics.html。
2015.11.12
此部分內(nèi)容將展示如何用圖形來完成任務(wù)以給應(yīng)用程序帶來競爭優(yōu)勢。如果您想超越基本的用戶界面而想創(chuàng)造美麗的視覺體驗(yàn),此部分內(nèi)容將會幫助您完成此心愿。
1. 有效的顯示位圖
在保持用戶界面的響應(yīng)性時(shí),如何加載和處理位圖并避免超過內(nèi)存限制。
學(xué)習(xí)保持用戶界面組件的響應(yīng)性并避免超過應(yīng)用程序的內(nèi)存限制的方法來處理和加載位圖對象。如果不那么仔細(xì),位圖能夠快速消耗掉可用的內(nèi)存預(yù)算隨之導(dǎo)致可怕的異常(java.lang.OutofMemoryError: bitmap size exceeds VM budget)而讓應(yīng)用程序崩潰。
以下是在安卓應(yīng)用程序中載入位圖時(shí)需要機(jī)警的幾個(gè)原因:
- 移動(dòng)設(shè)備的系統(tǒng)資源通常都比較受限制。安卓設(shè)備只能給每個(gè)應(yīng)用程序16MB的可用內(nèi)存空間。安卓兼容性定義文檔(Android Compatibility Definition Document)第3.7節(jié)。虛擬機(jī)兼容會給各種不同尺寸和密度的屏幕下的應(yīng)用程序最小的內(nèi)存空間。應(yīng)用程序應(yīng)被優(yōu)化到能夠在最小內(nèi)存空間運(yùn)行的程度。然而,許多設(shè)備都會配置更高的內(nèi)存限制。
- 位圖會占用大量的內(nèi)存,尤其是像照片這樣的富圖。例如,Galaxy Mexus設(shè)備上的相機(jī)拍照達(dá)2592x1936像素(500萬像素)。如果位圖配置使用ARGB_8888(安卓2.3版本以前默認(rèn)),載入此照片消耗19MB內(nèi)存(2592x1936x4字節(jié)),一下子就將某些設(shè)備上給應(yīng)用程序預(yù)分配的可用空間給消耗了。
- 安卓應(yīng)用程序用戶界面在同一時(shí)刻需要載入幾張位圖。像ListView、GridView以及ViewPager這樣的組件通常在同時(shí)包含多張位圖(有的是跟隨用戶操作而即將展現(xiàn)的圖片)。
1.1 有效地載入大型位圖
在不超過每個(gè)應(yīng)用程序內(nèi)存限制的情況下解碼大型位圖。
不同的圖片不同的形狀和尺寸。在許多情況下應(yīng)用程序的用戶界面所需的圖片都比實(shí)際的圖片要小。例如,系統(tǒng)的畫廊應(yīng)用程序展示的用安卓設(shè)備相機(jī)拍的圖片的分辨率通常就比設(shè)備屏幕的密度要高。
鑒于有限的內(nèi)存,理想情況下只需加載一個(gè)低分辨率的版本到內(nèi)存中。低版本分辨率應(yīng)該要跟顯示它的用戶界面組件的尺寸匹配。一個(gè)擁有高分辨率的圖片不會給顯示帶來好處,反而會更多的占用珍貴的內(nèi)存并會引起額外的性能開銷。
此節(jié)將通過載入圖片的一小部分的方式解碼圖片以不超過應(yīng)用程序有限的內(nèi)存。
(1) 讀取位圖的尺寸和類型
BitmapFactory類提供了幾種解碼方法(decodeByteArray(),decodeFile(),decodeResource()等)來根據(jù)各種類型資源創(chuàng)建Bitmap。給予圖片數(shù)據(jù)資源選擇最合適的解碼方法。這些方法嘗試為所構(gòu)建的位圖分配內(nèi)存,因此就能夠很容易檢測出outOfMemory異常。每種類型的解碼方法都有額外的可以通過BitmapFactory.Options類制定編碼選項(xiàng)的簽名。解碼時(shí)將inJustDecodeBounds特性設(shè)置為ture以避免內(nèi)存分配,通過設(shè)置位圖的outWidth、outHeight和outMimeType可返回null。此項(xiàng)技術(shù)允許在構(gòu)建(以及內(nèi)存分配)位圖之前獲取到圖片的尺寸和類型。
欲避免java.lang.outOfMemory異常,在解碼位圖時(shí)檢查位圖的尺寸,除非確定圖片不會引來此異常。
(2) 載入圖片的縮小版本到內(nèi)存
在知道圖片的尺寸后,此數(shù)據(jù)就可以用來判斷是要將整張圖片都載入內(nèi)存還是將代替此圖片的子樣例載入內(nèi)存。以下是需要考慮的因素:
- 估算整張圖片會占用的內(nèi)存。
- 被用來載入圖片的內(nèi)存是否會被應(yīng)用程序的其它部分使用。
- 圖片將要顯示的目標(biāo)ImageView或用戶界面組件的尺寸。
- 現(xiàn)有設(shè)備屏幕尺寸和密度。
舉例,如果一張1024x768像素的圖片最終會被略縮顯示在128x96像素的ImageView中,那么此圖片就不值得全被載入到內(nèi)存中。
欲告知解碼器解碼圖片的子樣本,載入一個(gè)更低像素版本的圖片到內(nèi)存中,需要將BitmapFactory.Options中的inSampleSize設(shè)置為ture。例如,一張像素為2048x1536的圖片用inSampleSize值為4來解碼會產(chǎn)生約512x384的位圖。將解碼后的圖片載入內(nèi)存只需花0.75MB,而將整張圖片載入內(nèi)存會消耗12MB內(nèi)存(假設(shè)位圖配置為ARGB_8888)。基于目標(biāo)寬度和高度,有一種將樣本尺寸計(jì)算出2的指數(shù)的方法。
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的指數(shù)的值。
欲用這種方法,首先要用被設(shè)置為true的inJusDecodeBounds解碼一次,將選項(xiàng)傳遞再用值為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中顯示一個(gè)100x100像素的縮略圖時(shí),用以下代碼即可實(shí)現(xiàn):
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));可以用類似的步驟解碼其它的資源來形成位圖,通過替代合適的BitmapFactory.decode*方法即可。
1.2 在用戶界面線程之外的線程處理位圖
位圖處理(重新設(shè)置尺寸,從遠(yuǎn)端下載等)不再主用戶界面所在線程中處理。此部分筆記將帶您學(xué)習(xí)用AsynTask創(chuàng)建后臺線程來處理位圖并解釋如何處理并發(fā)問題。
在“有效地載入大型位圖”一節(jié)中所討論的BitmapFactory.decode*方法,如果圖片資源數(shù)據(jù)在硬盤或網(wǎng)絡(luò)( 或其它任何不在內(nèi)存的位置)上,都不應(yīng)該在用戶界面主線程中使用這些方法。載入圖片所花的時(shí)間是不可預(yù)測的,它基于各種各樣的因素(從硬盤或網(wǎng)絡(luò)讀取數(shù)據(jù)的速度,圖片尺寸,CPU的性能等)。如果因載入圖片阻礙了用戶界面線程,系統(tǒng)所運(yùn)行的應(yīng)用程序?qū)⒉痪哂袑?shí)時(shí)的響應(yīng)性,用戶也極有可能選擇將此應(yīng)用程序關(guān)閉(見設(shè)計(jì)具響應(yīng)性的應(yīng)用程序獲取更多信息)。
(1) 使用異步任務(wù)(AsyncTask)
AsyncTask類提供了一種簡單的方式在后臺線程中執(zhí)行一些任務(wù)并將結(jié)果返回到用戶主線程中。欲使用此類,需要?jiǎng)?chuàng)建一個(gè)子類并重寫所提供的方法。以下是使用AsyncTask和decodeSampledBitmapFromResource將一張大型圖片載入到ImageView中的示例:
為ImageView添加的WeakReference保證了AsyncTask不會阻止ImageView以及其引用的任何東西收集垃圾信息。不敢保證當(dāng)任務(wù)執(zhí)行完后ImageView仍舊還在,所以必須在onPostExecute()中檢查其引用。就此例來說,在任務(wù)結(jié)束之前用戶導(dǎo)航離開活動(dòng)或者配置發(fā)生改變時(shí),ImageView可能不再存在。
欲異步開始載入位圖,簡單的創(chuàng)造一個(gè)新的任務(wù)并執(zhí)行與載入相關(guān)的代碼即可:
public void loadBitmap(int resId, ImageView imageView) {BitmapWorkerTask task = new BitmapWorkerTask(imageView);task.execute(resId); }2015.11.13
(2) 處理并發(fā)
ListView、GridView等這些常見組件和AsyncTask結(jié)合使用會引來引來另外一個(gè)問題。為了有效地利用內(nèi)存,隨著用戶滑動(dòng)滾動(dòng)條,這些組件會被重復(fù)利用為子視圖顯示。如果每個(gè)子視圖都觸發(fā)一個(gè)AsyncTask,不敢保證當(dāng)AsyncTask完成時(shí),對應(yīng)的視圖還未被重復(fù)利用來顯示另外一個(gè)子視圖。另外,也不能保證各異步線程是在其它線程利用完視圖后再開始利用此視圖。
博客“高性能的多線程(Multithreading for Performance)”深入的討論了處理并發(fā)問題,并提供了當(dāng)某任務(wù)完成后何AsyncTask將獲得ImageView的引用何AsyncTask稍后再引用ImageView的解決方法。使用相似的方法,前一節(jié)提到的AsyncTask能夠被擴(kuò)展為一個(gè)成熟的模式。
創(chuàng)建一個(gè)微型的Drawable子類來存儲一個(gè)返回到工作任務(wù)的引用。在這種情況下,當(dāng)任務(wù)執(zhí)行完時(shí),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;在執(zhí)行bitmapWorkerTask以前,可以創(chuàng)建AsyncDrawable并將其綁定到目標(biāo)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方法是用來檢查是否有另外一個(gè)正在運(yùn)行的任務(wù)已經(jīng)在使用ImageView。如果有,此方法將調(diào)用cancel()方法來取消之前的任務(wù)。在少數(shù)情況下,新任務(wù)數(shù)據(jù)匹配已經(jīng)存在任務(wù)并且不需要其它的具體步驟。以下是cancelPotentialWork方法的一種實(shí)現(xiàn):
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()方法用來檢索所任務(wù)涉及的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()以檢查任務(wù)是否被取消,斌檢查當(dāng)前任務(wù)是否關(guān)聯(lián)上了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);}}} }這樣的實(shí)現(xiàn)就適合用ListView、GridView以及其它的組件被重復(fù)用作子視圖顯示了。在將圖片設(shè)置到ImageView的地方簡單的調(diào)用loadBitmap。例如,在GridView的實(shí)現(xiàn)中,是調(diào)用getView()方法來實(shí)現(xiàn)的,此在后一節(jié)中描述。
1.3 緩存位圖
此節(jié)教您在載入多張位圖時(shí)如何使用內(nèi)存和硬盤位圖緩存來提升主用戶界面的響應(yīng)性和流動(dòng)性。
載入一張位圖到用戶界面是比較簡單的,然而當(dāng)需要在同一時(shí)間就載入大量位圖時(shí)就會變得復(fù)雜許多。在許多情況(如ListView、GridView或ViewPager組件)下,可能很快滾動(dòng)到屏幕上顯示的數(shù)量是無限的。
當(dāng)向下移動(dòng)屏幕時(shí)通過重復(fù)利用組件來表示子視圖的方式來保持內(nèi)存消耗量不上升。假如不保持長期的引用位圖,垃圾回收器會釋放載入的位圖。這一點(diǎn)固然是好,但為了保持流暢和快速的加載用戶界面,當(dāng)圖片每次重新回到屏幕上時(shí)也想避免次次都去處理它。一段內(nèi)存或硬盤緩存 能夠滿足組件快速重載入之前經(jīng)處理過的圖片。
此節(jié)將展示當(dāng)載入多張位圖時(shí),使用內(nèi)存或硬盤位圖緩存來提升用戶界面的流動(dòng)性和響應(yīng)性。
(1) 使用內(nèi)存緩存
占用應(yīng)用程序可用內(nèi)存空間的內(nèi)存緩存用來保存位圖可被快速訪問。LruCache類(此類也存在于API level 4 對應(yīng)的支持庫中)特別適合于位圖緩存、在強(qiáng)引用LinkedHashMap中保持最近的引用對象、在緩存越界之前驅(qū)逐最近引用最少的對象的任務(wù)。
注:在以前,流行的內(nèi)存緩存的實(shí)現(xiàn)是SoftReference或WeakReference位圖緩存,但現(xiàn)在不推薦此種緩存。從Android 2.3(API level 9)開始,垃圾回收器變得更加強(qiáng)大,它回收讓對象幾乎無效的軟/弱引用。另外,在Android 3.0(API level 11)之前,位圖的回收數(shù)據(jù)沒有被提前釋放而是被保存在本地內(nèi)存中,這可能會引起應(yīng)用程序超越其內(nèi)存限制而崩潰。
欲給LruCache選擇一個(gè)合適的尺寸,許多因素都應(yīng)該被納入考慮,如:
- 活動(dòng)跟應(yīng)用程序使用后所剩下的內(nèi)存大小。
- 多少圖片會被同一時(shí)間載入到屏幕上?需要準(zhǔn)備多少圖片到屏幕上?
- 設(shè)備的屏幕尺寸和密度是多少?對于相同數(shù)量的圖片,像Galaxy Nexus這樣屏幕密度格外高(xhdpi)的設(shè)備比Nexus S(hdpi)設(shè)備所要分配的內(nèi)存緩存要大。
- 根據(jù)位圖的尺寸和配置計(jì)算到圖片所會占用的內(nèi)存有多大?
- 圖片被訪問的頻率有多大?是否其中有一部分圖片的訪問頻率會高于其它圖片?如果是這樣,可能需要總是要在內(nèi)存中保存特定的內(nèi)容,設(shè)置為不同組的位圖分配對應(yīng)的LruCache對象。
- 需要平衡質(zhì)量和質(zhì)量么?有時(shí)選擇存儲大數(shù)量低質(zhì)量的位圖可能會更有用,而在后臺進(jìn)程中載入高質(zhì)量的圖片。
沒有適合所有應(yīng)用程序的特定的尺寸和規(guī)則,需要根據(jù)具體情況分析用量并作出相應(yīng)的決策。如果緩存太小會引起附加開銷,如果緩存太大就有可能會引起java.lang.OutOfMemory異常并會讓應(yīng)用程序智能使用很小的內(nèi)存。
以下是為位圖設(shè)置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); }注:在此例中,將應(yīng)用程序內(nèi)存的八分之一分配作為了緩存。對于通常(hdpi)設(shè)備來說,這是緩存的最小值,約為4MB(32/8)。在一個(gè)800x480分辨的設(shè)備上,一個(gè)全屏的GridView填充的圖片會占用約為1.5MB(800*480*4字節(jié)),所以此緩存約能存2.5張這樣的圖片。
當(dāng)載入位圖到ImageView中時(shí),LruCache最先被檢查。如果尋到入口,它會立馬被用來更新ImageView,否則會催生一個(gè)后臺線程來處理圖片:
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也需要被更新以添加到內(nèi)存緩存的入口:
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) 使用硬盤緩存
內(nèi)存緩沖區(qū)對最近常被查看的視圖的訪問速度的提升很有用,然而不能夠讓圖片依賴此種緩存。像GridView這種擁有大量數(shù)據(jù)集的組件很容易就填滿內(nèi)存緩沖區(qū)。應(yīng)用程序還可能會被諸如來電這樣的任務(wù)打斷,如此,在后臺的線程就可能會被殺死即內(nèi)存緩沖區(qū)會被銷毀。一旦用戶恢復(fù)應(yīng)用程序后,應(yīng)用程序不得不再次重新處理每張圖片。
在以上描述的情況中可以使用硬盤緩存來保留經(jīng)處理的位圖并當(dāng)內(nèi)存緩沖區(qū)中的圖片不可用時(shí)能減少載入次數(shù)。當(dāng)然,從硬盤中取圖片會比從內(nèi)存載入慢且因?yàn)樽x硬盤次數(shù)不可預(yù)測,所以此舉需要在后臺線程中完成。
注:像畫廊應(yīng)用程序中訪問頻率較高的圖片使用ContentProvider來提供圖片緩存更合適。
Android 源碼中的類樣碼使用DiskLruCache實(shí)現(xiàn)。以下代碼在已有內(nèi)存緩沖區(qū)后增加硬盤緩沖區(qū):
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); }注:即使是初始化硬盤緩沖區(qū)也需要硬盤操作且不應(yīng)在主線程中完成這個(gè)過程。然而,在初始化之前確實(shí)也有訪問緩存的機(jī)會。為了解決這個(gè)問題,在以上代碼的實(shí)現(xiàn)中,使用鎖住一個(gè)對象來確保在初始化緩沖區(qū)之前不能讀硬盤緩沖區(qū)。
當(dāng)內(nèi)存緩沖區(qū)在用戶界面線程被完成后,當(dāng)硬盤緩沖區(qū)在后臺線程中被創(chuàng)建后。與硬盤相關(guān)的操作不要在用戶界面線程中操作。當(dāng)圖片處理完成后,最終的位圖被同時(shí)增加到內(nèi)存和硬盤緩沖區(qū)中,供后續(xù)使用。
(3) 處理配置更改
諸如屏幕方向改變這樣的運(yùn)行時(shí)配置改變時(shí),會引起安卓銷毀并用新配置重啟運(yùn)行的活動(dòng)(關(guān)于此行為的更多信息見Handling Runtime Changes)。為讓用戶在配置改變時(shí)還能夠感受到流利快速的用戶體驗(yàn)需要避免再次處理所有的圖片。
幸運(yùn)的是,在Use a Memory Cache節(jié)為位圖創(chuàng)建了好用的內(nèi)存緩沖區(qū)。使用通過調(diào)用setRetainInstance(true)保存的碎片能夠?qū)⒕彌_區(qū)傳遞給新的活動(dòng)實(shí)例。在活動(dòng)被重建后,它能夠重新獲得附加的碎片且能夠獲取對存在緩沖區(qū)對象的訪問權(quán),允許快速的提取并重新填充到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);} }旋轉(zhuǎn)手機(jī)用重新獲得/沒獲得碎片的情況來測試此段代碼。您應(yīng)該注意無滯后情況下,幾乎是立即從重獲的緩沖區(qū)中裝載圖片到活動(dòng)中去的。如果內(nèi)存緩沖區(qū)中無相應(yīng)的圖片就到硬盤緩沖區(qū)找尋找,如果硬盤緩沖區(qū)中亦無,那么就像平常一樣處理。
1.4 管理位圖內(nèi)存
此節(jié)解釋如何管理位圖內(nèi)存來最大化的提升應(yīng)用程序性能。
除了在緩存位圖中描述措施外,還有另外一些特殊的方法可以用來優(yōu)化垃圾回收器和位圖的重使用。具體的策略基于具體的安卓系統(tǒng)版本。BitmapFun應(yīng)用程序示例包含展示設(shè)計(jì)跨不同安卓版本的應(yīng)用程序的類。
在正式開始此節(jié)內(nèi)容之前,展示下安卓管理位圖內(nèi)存的演化過程:
- 在安卓2.2(API level 8)及更低的版本中,當(dāng)垃圾回收器工作時(shí),應(yīng)用程序中的線程將停止。這會給應(yīng)用程序引起降低性能的滯后。Android 2.3增加了并發(fā)的垃圾回收器,這意味著在位圖不再被引用后內(nèi)存將被回收來供應(yīng)用程序重新使用。
- 在安卓2.3.3(API level 10)及更低版本中,位圖的像素?cái)?shù)據(jù)(backing pixel data)被保存在本地內(nèi)存中。它跟位圖本身獨(dú)立,位圖被保存在Dalvik堆中。保存在本地內(nèi)存中的像素?cái)?shù)據(jù)不會以預(yù)測的方式釋放,這可能會導(dǎo)致超出內(nèi)存限制而使應(yīng)用程序崩潰。從安卓3.0(API level 11)開始,像素?cái)?shù)據(jù)跟相應(yīng)的位圖一起存儲在Dalvik堆中。
以下幾節(jié)將描述在不同安卓版本上如何優(yōu)化位圖內(nèi)存管理。
(1) 在安卓2.3.3及更低版本中管理內(nèi)存
在安卓2.3.3(API level 10)及更低版本中,推薦使用recycle()。如果在應(yīng)用程序中顯示大量的位圖,很有可能出現(xiàn)outOfMemoryError()錯(cuò)誤。recycle()方法能夠盡快讓應(yīng)用程序重新獲得位圖所占用的內(nèi)存。
注:只有在確定位圖不再被使用時(shí)使用recycle()。若在調(diào)用recycle()后再嘗試?yán)L制位圖,將會出現(xiàn)錯(cuò)誤:“Canvas:嘗試去用回收的位圖”。
以下代碼片段為調(diào)用recycle()的示例。此程序用引用計(jì)數(shù)(用mDisplayRefCount和mCacheRefCount)來跟蹤當(dāng)前是否有位圖顯示或在緩存中。當(dāng)滿足以下條件代碼將回收位圖:
- 引用計(jì)數(shù)mDisplayRefCount和mCacheRefCount都為0.
- 位圖不為null且位圖還未被回收。
2015.11.14
(2) 在安卓3.0及更高版本中管理內(nèi)存
安卓3.0(API level 11)介紹了BitmapFactory.Options.inBitmap域。如果此選項(xiàng)被設(shè)置,當(dāng)載入內(nèi)容時(shí)解碼方法將會用此選項(xiàng)去重新使用存在的位圖。這就意味著位圖的內(nèi)存被重用,如此會導(dǎo)致性能的提升并省掉了內(nèi)存的分配和釋放。然而,用inBitmp也有幾個(gè)限制。尤其是在安卓4.4之前(API level 19),只支持相等尺寸的位圖。更多細(xì)節(jié)見inBitmap的文檔。
[1] 保存位圖供以后使用
以下代碼片段演示如何保存位圖來供以后使用。但應(yīng)用程序運(yùn)行在安卓3.0或更高版本中且位圖從LruCache中被驅(qū)逐了出來,對位圖的一個(gè)軟應(yīng)用被放置在HashSet中,供稍后可能用inBitmap來使用位圖:
[2] 使用存在的位圖
應(yīng)用程序在運(yùn)行時(shí),解碼方法會檢查是否有存在的位圖可用。舉例如下:
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使用的尺寸標(biāo)準(zhǔn):
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; }后一代碼片段展示了上一代碼片段所調(diào)用的方法。它尋找一個(gè)存在的位圖并為之設(shè)值。注意此方法只在找到合適的位圖之后才為其設(shè)值(不能假設(shè)總能匹配到合適的位圖)。
1.5 將位圖顯示在用戶界面中
此節(jié)將綜合前幾節(jié)內(nèi)容,展示用后臺線程和位圖緩存來將位圖載入到像ViewPager和GridView的組件中。
此節(jié)將結(jié)合前幾節(jié)的內(nèi)容,展示如何用后臺線程和位圖緩存將多張位圖載入ViewPager和GridView組件中,并處理并發(fā)和配置改變的情況。
(1) 將位圖載入ViewPager
用掃擊視圖模式(swipe view pattern)來導(dǎo)航圖片畫廊細(xì)節(jié)是一個(gè)不錯(cuò)的方法。可以用PagerAdapter支持的ViewPager來實(shí)現(xiàn)此模式。然而,更加適合的支持適配器是FragmentStatePagerAdapter的子類,此類能夠根據(jù)視圖從屏幕上消失與否的情況自動(dòng)銷毀和保存在ViewPager中的Fragments,并能夠保持內(nèi)存使用量不上升。
注:如果只有少量的圖片并能夠確保它們不會超過應(yīng)用程序的內(nèi)存限制,直接使用PagerAdapter或FragmentPagerAdapter可能會更加適合。
以下代碼實(shí)現(xiàn)了ViewPager和其ImageView子視圖。主活動(dòng)持有此ViewPager和相應(yīng)的適配器:
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);}} }以下代碼實(shí)現(xiàn)Fragment持ImageView子視圖的細(xì)節(jié)。這似乎是一種完美的方法,您能看出此種方法的缺陷么?怎么提升?
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} }希望您能夠注意這個(gè)問題:讀圖片的操作在用戶界面線程中實(shí)現(xiàn),這可能會讓引用程序掛起從而不得不強(qiáng)制關(guān)閉應(yīng)用程序。使用不要在用戶界面線程處理位圖一節(jié)中提到的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);}} }任何其它的圖片處理(如改變圖片尺寸或從網(wǎng)絡(luò)提取圖片)可以在不影響主用戶界面的BitmapWorkerTask中執(zhí)行。如果后臺線程的操作比從硬盤中載入圖片的操作還多,那么像緩存位圖一節(jié)描述的添加內(nèi)存/硬盤緩沖區(qū)也是有好處的。以下代碼是內(nèi)存緩沖區(qū)的另外的一些修改:
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 }將這些代碼片段整合到一起就能夠得到一個(gè)具響應(yīng)性的、具圖片載入時(shí)最小延遲的ViewPager的實(shí)現(xiàn),并且因?yàn)楹笈_線程處理圖片,所以圖片數(shù)量不會(明顯)影響主用戶界面的執(zhí)行。
(2) 將位圖載入GridView
網(wǎng)格列表構(gòu)建模塊(grid list building block )對顯示圖片數(shù)據(jù)集及其有用,用GridView組件可以實(shí)現(xiàn)網(wǎng)格列表構(gòu)建模塊,GridView組件可以在同一時(shí)間顯示許多圖片,如果用戶滑動(dòng)GridView的滾動(dòng)條就需要做更多的準(zhǔn)備來實(shí)現(xiàn)GridView中的圖片的顯示。在實(shí)現(xiàn)此種類型的控制時(shí),必須確保用戶界面的流暢性、內(nèi)存余量充足、正確地處理并發(fā)(GridView會重復(fù)利用組件來顯示子視圖)。
作為開始,先貼出在Fragment中的擁有ImageView子視圖的GridView的標(biāo)準(zhǔn)實(shí)現(xiàn)。同理,這看起來也已經(jīng)比較完美了,但怎么做能將此做的更好?
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;}} }問題在于,將這個(gè)過程的實(shí)現(xiàn)放在了用戶界面線程中。在圖片量較小時(shí),此代碼能夠正常工作。如果有更多的圖片參與,那么用戶界面可能會被掛起。
可以使用上一節(jié)使用的異步和緩存的方法來解決這個(gè)問題。然而,咱還需要為GridView考慮并發(fā)問題。為解決此問題,用“不要在用戶界面處理位圖”一節(jié)中介紹的技術(shù):
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。
這種實(shí)現(xiàn)能夠靈活的處理圖片且不會影響用戶界面的流暢性。在后臺任務(wù)中可以從網(wǎng)絡(luò)載入圖片也可以調(diào)整大型的數(shù)字圖片并且圖片呈現(xiàn)的速度極快。
完整的樣例代碼和其它方面的討論,請參看本節(jié)的樣例應(yīng)用程序。
2. 用OpenGL ES顯示圖形
在安卓應(yīng)用程序框架中如何創(chuàng)建OpenGL圖形,如何響應(yīng)用戶的點(diǎn)擊輸入。
安卓框架提供了許多標(biāo)準(zhǔn)的工具來創(chuàng)建具有吸引力、功能性的圖形用戶界面。然而,如果想要更多地控制應(yīng)用程序去繪制屏幕,或者要往屏幕上繪制三維圖形,就得使用不同的工具。由安卓框架提供的OpenGL ES APIs提供了顯示高端動(dòng)畫圖形的功能(只有您想不到,無做不到),并且還能夠讓您收益于安卓設(shè)備上的圖形處理單元(GPUs)加速處理圖形的好處。
此部分內(nèi)容將帶您使用OpenGL來開發(fā)一個(gè)基本的應(yīng)用程序,包括組織、繪制對象、移動(dòng)繪制的元素以及響應(yīng)用戶的觸摸輸入。
這里的代碼樣例使用的OpenGL ES 2.0 APIs,針對目前的安卓設(shè)備,推薦大家使用此版本的API。更多關(guān)于OpenGL ES版本的信息,見OpenGL開發(fā)手冊。
注:不要將OpenGL ES 1.x API和OpenGL ES 2.0混淆!這兩種APIs不能互換使用,一起使用它們會導(dǎo)致開發(fā)者累覺不愛。
2.1 構(gòu)建一個(gè)OpenGL ES 環(huán)境
學(xué)習(xí)如何建立一個(gè)可以繪制OpenGL圖形的應(yīng)用程序。
為在應(yīng)用程序中使用OpenGL ES繪制圖形,必須實(shí)現(xiàn)它們的視圖容器。一種實(shí)現(xiàn)視圖容器更直接的方式是實(shí)現(xiàn)GLSurfaceView和FLSurfaceView.Render。GLSurfaceView是用OpenGL繪制圖形的容器,FLSurfaceView.Render控制在視圖中的繪制內(nèi)容。更多關(guān)于兩個(gè)類的信息見OpenGL ES開發(fā)手冊。
GLSurfaceView只是將OpenGL ES圖形結(jié)合到應(yīng)用程序中的一種方法。對于全屏或接近全屏的圖形顯示,此方法是合適的選擇。若開發(fā)者只是想將OpenGL ES圖形作為布局中的一小部分,那么應(yīng)該考慮下TextureView。其實(shí),都可以使用GLSurfaceView來實(shí)現(xiàn),只是此種方法需要更多的代碼來實(shí)現(xiàn)。
此節(jié)將解釋在簡單的應(yīng)用程序活動(dòng)中如何完成的GLSurfaceView和FLSurfaceView.Render的最小實(shí)現(xiàn)。
(1) 在清單文件中聲明OpenGL ES
欲在應(yīng)用程序中使用OpenGL ES 2.0 API,必須在清單文件中作如下聲明:
如果應(yīng)用程序使用紋理壓縮,必須聲明應(yīng)用程序所支持的壓縮格式,這樣就只在兼容的設(shè)備上安裝:
<supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture" /> <supports-gl-texture android:name="GL_OES_compressed_paletted_texture" />更多關(guān)于紋理壓縮的格式,見OpenGL開發(fā)手冊。
(2) 為OpenGL ES圖形創(chuàng)建活動(dòng)
使用OpenGL ES的安卓應(yīng)用程序跟其它應(yīng)用程序一樣有用戶界面所對應(yīng)的活動(dòng)。主要不同在于往活動(dòng)的布局文件中所添加的東西。在其它的應(yīng)用程序中的布局文件中往往可能包含TexView、Button或ListView,在使用OpenGL ES的應(yīng)用程序中,還會往布局文件中添加GLSurfaceView。
以下代碼樣例是用GLSurfaceView作為原始視圖的活動(dòng)的一個(gè)最小實(shí)現(xiàn):
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目標(biāo)。
(3) 構(gòu)建GLSurfaceView對象
GLSurfaceView是一個(gè)可以繪制OpenGL ES圖形的特殊視圖。此視圖本身不會為繪圖做太多。實(shí)際控制繪制對象的是設(shè)置在此視圖上的GLSurfaceView.Renderer。實(shí)際上,創(chuàng)建此對象的代碼量很少,您可能想跳過擴(kuò)展代碼而只創(chuàng)建一個(gè)GLSurfaceView實(shí)例,但不要如此。需要擴(kuò)展此類來獲取觸摸事件,此在“響應(yīng)屏幕觸摸”一節(jié)中講述過。
實(shí)現(xiàn)GLSurfaceView的必要的代碼很少,所以能夠快速實(shí)現(xiàn),它通常作為使用它的活動(dòng)的內(nèi)部類來實(shí)現(xiàn):
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實(shí)現(xiàn)外,另外一種方法是當(dāng)繪制內(nèi)容有改變時(shí)用GLSurfaceView.RENDERMODE_WHEN_DIRTY將渲染模式設(shè)置只繪制視圖。
// Render the view only when there is a change in the drawing data setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);此方法可以防止在確切調(diào)用requestRender()時(shí)GLSurfaceView的重復(fù)繪制,在本樣例程序中這是一種更為高笑的方法。
(4) 構(gòu)建渲染器(Renderer)類
在應(yīng)用程序中實(shí)現(xiàn)GLSurfaceView.Renderer或renderer類使得使用OpenGL ES變得有趣。此類控制往與此類關(guān)聯(lián)的GLSurfaceView中的繪制內(nèi)容。渲染器類中有3個(gè)方法會被安卓系統(tǒng)調(diào)用以推測出怎么繪制GLSurfaceView以及往其中繪制的內(nèi)容:
- onSurfaceCreate() - 被調(diào)用一次,用來設(shè)置視圖的OpenGL ES環(huán)境。
- onDrawFrame() - 在每次繪制重新繪制視圖時(shí)都會被調(diào)用。
- onSurfaceChanged() - 在視圖形狀改變時(shí)會被調(diào)用,如當(dāng)設(shè)備屏幕方向改變時(shí)。
以下是一個(gè)OpenGL ES渲染器的一個(gè)非常基本的實(shí)現(xiàn),它為GLSurfaceView繪制一個(gè)黑色的背景:
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);} }以上的代碼示例創(chuàng)建了一個(gè)用OpenGL來簡單的顯示一個(gè)黑色背景的安卓應(yīng)用程序。并未做一些更有趣的事情,不過現(xiàn)在您已經(jīng)具有了OpenGL的基礎(chǔ),那么您就可以開始用OpenGL來開始繪制圖形元素了。
注:在使用OpenGL ES 2.0 APIs時(shí),您可能想知道為什么這些方法需要GL10的參數(shù)。這些方法簽名只是為能夠在2.0 API中重用以保障安卓代碼礦建的簡單性。
如果您熟悉OpenGL ES APIs,就可以在應(yīng)用程序中設(shè)置OpenGL ES環(huán)境并開始繪制圖形。然而,如果您還需要更多的關(guān)于OpenGL的信息幫助,請繼續(xù)往后看。
2.2 定義形狀
學(xué)習(xí)如何定義形狀,了解為什么需要知道圖形輪廓(faces and winding)。
創(chuàng)建高端圖形杰作的第一步是在OpenGL ES視圖的上下文中定義被畫的形狀。若不知OpenGL ES定義圖形對象的步驟,那么用OpenGL ES繪制圖形就會有些困難。
此節(jié)解釋“OpenGL ES在安卓設(shè)備屏幕上的坐標(biāo)系”、“定義形狀的基礎(chǔ)”、“形狀面”、“定義三角形或矩形”。
(1) 定義三角形
OpenGL ES運(yùn)行在三維空間坐標(biāo)定義欲繪制的對象。所以,在繪制三角形之前,必須先定義坐標(biāo)。在OpenGL中,一般是通過定義以浮點(diǎn)數(shù)字組成的頂點(diǎn)數(shù)組來定義坐標(biāo)。欲達(dá)最大效率,需要將這些坐標(biāo)寫進(jìn)ByteBuffer,然后將其中的內(nèi)容傳遞給OpenGL ES圖形管道以作相應(yīng)處理:
默認(rèn)情況下,OpenGL ES假設(shè)坐標(biāo)系的0,0,0對應(yīng)GLSurfaceView框架的中心,[1,1,0]為框架的右上角,[-1,-1,0]對應(yīng)框架的左下角。欲看此坐標(biāo)系的圖解,見OpenGL ES開發(fā)手冊。
注意形狀的坐標(biāo)系是以逆時(shí)針為順序。繪制的順序非常重要因?yàn)樗x那一邊是形狀的正面(正面會被繪制)以及哪一邊是形狀的背面(根據(jù)OpenGL ES剔除的特性,背面不會被繪制)。更多關(guān)于面(facing)和剔除(culling)見OpenGL ES 開發(fā)手冊。
(2) 定義矩形
在OpenGL中定義三角形相當(dāng)簡單,但當(dāng)定義圖形變得稍加復(fù)雜時(shí)應(yīng)該怎么定義?比如如,一個(gè)矩形。有幾種方式可以定義矩形,在OpenGL定義矩形最為典型的方式是定義兩個(gè)三角形來形成矩形。
圖1. 用兩個(gè)三角形繪制矩形
需要以逆時(shí)針的順序來定義組成矩形的兩個(gè)三角形,并將坐標(biāo) 值都保存到ByteBuffer中。為避免重復(fù)定義三角形所共享頂點(diǎn),需要用繪制清單來告知OpenGL ES圖形管道怎么繪制這些頂點(diǎn)。以下是繪制矩形的代碼:
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);} }此例給了一個(gè)怎么用OpenGL來創(chuàng)建稍微復(fù)雜圖形的小窺。通常來講,都是使用三角形來繪制對象。在下一節(jié)中,將會介紹如何將這些形狀繪制到屏幕上。
2015.11.15
2.3 繪制形狀
學(xué)習(xí)在應(yīng)用程序中如何繪制OpenGL 形狀。
在用OpenGL定義形狀后,就可以繪制它們了。用OpenGL ES 2.0繪制推行可能比您的想象還要多一些代碼,因?yàn)檫@些API對圖形渲染管道提供了極大的控制。
此節(jié)介紹怎么繪制前一節(jié)用OpenGL ES 2.0 API所定義的形狀。
(1) 初始化形狀
在作繪制之前,必須初始化并載入打算繪制的形狀。除非程序中使用的形狀的結(jié)構(gòu)(坐標(biāo)系)在執(zhí)行過程中改變,否則都應(yīng)該在渲染器的onSurfaceCreated()方法中為形狀的內(nèi)存和效率執(zhí)行作初始化工作。
(2) 繪制形狀
繪制用OpenGL ES 2.0定義的形狀需要大量代碼,因?yàn)楸仨殲閳D形渲染管道提供許多細(xì)節(jié)。尤其是需要定義以下介個(gè)方面內(nèi)容:
- 頂點(diǎn)著色(Vertex Shader) - 渲染形狀頂點(diǎn)的OpenGL ES 圖形代碼。
- 片段著色(Fragment Shader) - 用顏色或紋理來渲染形狀各面的OpenGL ES代碼。
- 程序(Program) - 用包含著色的OpenGL ES 對象來繪制一個(gè)或多個(gè)形狀。
至少需要一個(gè)頂點(diǎn)著色來繪制形狀一個(gè)片段著色來為形狀著色。這些著色器必須被編譯然后增添到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環(huán)境預(yù)編譯。在渲染器類中創(chuàng)建一個(gè)方法來編譯以上著色器的代碼:
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程序?qū)ο笾性龠B接程序。在繪制對象的構(gòu)造函數(shù)中完成這個(gè)過程,這樣此過程就只會被執(zhí)行一次。
注:編譯OpenGL ES著色器和連接程序?qū)τ贑PU周期來和處理時(shí)間來說是比較耗時(shí)的操作,所以要避免此操作被執(zhí)行多次。如果在運(yùn)行時(shí)不知著色器的內(nèi)容,應(yīng)該構(gòu)建(編譯)代碼這樣代碼只會被構(gòu)建一次且可緩存供以后使用:
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);} }此時(shí),到了可以調(diào)用方法來繪制形狀的時(shí)候了。用OpenGL ES需要用幾個(gè)參數(shù)來告知渲染管道將繪制的內(nèi)容并如何繪制它們。因?yàn)槔L制過程由形狀決定,所以在圖形類中包含圖形的繪制邏輯是個(gè)不錯(cuò)的主意。
創(chuàng)建一個(gè)draw()方法來繪制圖形。以下代碼設(shè)置了位置和顏色值到形狀的頂點(diǎn)著色器和片段著色器,并調(diào)用繪制方法來繪制形狀。
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()方法中調(diào)用draw()方法了:
public void onDrawFrame(GL10 unused) {...mTriangle.draw(); }運(yùn)行應(yīng)用程序,運(yùn)行結(jié)果如下:
圖1. 無投影或相機(jī)視圖下繪制的三角形
在以上代碼樣例中存在幾個(gè)問題。第一,此運(yùn)行結(jié)果不會給人留下深刻印象。第二,當(dāng)改變設(shè)備屏幕方向時(shí)三角形會有些變形。會變形的原因時(shí)對象的頂點(diǎn)沒有隨著屏幕變化而變化。可以通過下一節(jié)介紹的投影和相機(jī)視圖來解決這個(gè)問題。
第三,圖中的三角形是固定不動(dòng)的,這顯得有些無聊。在“增添運(yùn)動(dòng)”這一節(jié)中會使用OpenGL ES圖形管道來讓圖像旋轉(zhuǎn)以讓所繪的圖形看起來更有趣。
2.4 請求投影和相機(jī)視圖
學(xué)習(xí)如何使用投影和相機(jī)視角獲取所繪對象的新的視角。
在OpenGL ES環(huán)境中,投影和相機(jī)視圖以一種更接近在現(xiàn)實(shí)中用眼睛看到物理對象那般呈現(xiàn)圖片。這種模擬實(shí)際視圖的方式是在繪制物體的坐標(biāo)系中的通過數(shù)學(xué)變換實(shí)現(xiàn)的:
- 投影 - 此變換通過調(diào)整繪制對象坐標(biāo)的寬度和高度來展現(xiàn)繪制圖像。無此變換的計(jì)算,因視圖窗口的比例的不等從而用OpenGL ES繪制的對象是傾斜的。當(dāng)OpenGL視圖比例確立或渲染器中onSurfaceChanged()方法中的視圖比例改變時(shí),投影變換將會重新計(jì)算。更多關(guān)于OpenGL ES的投影和坐標(biāo)映射,見Mapping Coordinates for Drawn Objects.。
- 相機(jī)視圖 - 此變換調(diào)整繪制對象坐標(biāo)的虛擬相機(jī)位置。OpenGL ES并未定義實(shí)際的相機(jī)對象,它是通過變換繪制對象的顯示而提供了工具方法來模擬相機(jī)。當(dāng)確立GLSurfaceView或有基于用戶或應(yīng)用程序的動(dòng)態(tài)改變時(shí),相機(jī)視圖可能只會被計(jì)算一次。
此節(jié)描述如何創(chuàng)建投影和相機(jī)以及如何在GLSurfaceView中應(yīng)用它們。
(1) 定義投影
投影變換的數(shù)據(jù)在GLSurfaceView.Renderer類中的onSurfaceChanged()方法中計(jì)算。以下樣例代碼根據(jù)GLSurfaceView的高度和寬度用Matrix.frustumM()方法計(jì)算投影變換的Matrix:
此段代碼計(jì)算了投影矩陣mProjectionMatrix,相機(jī)視圖將在onDrawFrame()方法中使用此矩陣,此將在下一節(jié)中介紹。
注:只對繪制對象應(yīng)用投影一般會導(dǎo)致圖形消失。通常,為了將圖形重新顯示在屏幕上還需要使用相機(jī)視圖。
(2) 定義相機(jī)視圖
為繪制對象增加相機(jī)視圖變換方才算完成了圖形的變換。在以下代碼示例中,在Matrix.setLookATM()方法中完成相機(jī)試圖變換并結(jié)合之前的投影變換矩陣。再將兩種變換結(jié)合得到矩陣傳遞給繪制對象:
(3) 請求投影和相機(jī)變換
欲結(jié)合投影和相機(jī)視圖變換矩陣,首先需要在之前的三角形類中定義頂點(diǎn)著色器矩陣:
然后,修改繪制對象的draw()方法以接收二者變換的矩陣并將此矩陣應(yīng)用到圖形:
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); }一旦正確和應(yīng)用了投影和相機(jī)視圖變換,以正確比例被繪制的圖形對象應(yīng)該如下圖所示:
圖1. 用投影和相機(jī)視圖變換的三角形繪制
至此,應(yīng)用程序中已經(jīng)用正確的比例繪制圖形了,該往形狀增添動(dòng)畫了。
2.5 增加運(yùn)動(dòng)
學(xué)習(xí)如何用OpenGL來實(shí)現(xiàn)所繪對象基本的移動(dòng)和動(dòng)畫。
將對象繪制在屏幕之上只是OpenGL最基本的特定,安卓其它的圖形框架類如Canvas及Drawable也能夠完成此項(xiàng)工作。OpenGL ES還為圖形提供了移動(dòng)、變換圖形到三維空間以及創(chuàng)造令人信服的用戶體驗(yàn)的功能。
此節(jié)將繼續(xù)學(xué)習(xí)OpenGL ES來通過旋轉(zhuǎn)的方式移動(dòng)圖形。
(1) 旋轉(zhuǎn)圖形
用OpenGL ES 2.0來選中繪制對象比較簡單。在渲染器中創(chuàng)建一個(gè)轉(zhuǎn)換矩陣(旋轉(zhuǎn)矩陣)并將其跟投影和相機(jī)視圖變換矩陣結(jié)合到一塊:
如果做了這些改變后三角形仍舊還沒有旋轉(zhuǎn),確保對GLSurfaceView.RENDERMODE_WHEN_DIRTY進(jìn)行了注釋,此內(nèi)容在下一節(jié)討論。
(2) 啟用連續(xù)渲染
如果您已孜孜不倦地昨晚了樣例代碼中的所有內(nèi)容,確保注釋了設(shè)置渲染模式為只在臟(dirty)才繪制的一行代碼,否則OpenGL只做一次旋轉(zhuǎn)然后就等待調(diào)用GLSurfaceView容器中的requestRender()方法:
除非在無用戶交互的情況下對象還有轉(zhuǎn)動(dòng),否則應(yīng)該將此語句的注釋去掉。做好取消此語句注釋的準(zhǔn)備,因?yàn)橄乱还?jié)將會在程序中使用此語句。
2.6 響應(yīng)觸摸事件
學(xué)習(xí)怎么和OpenGL圖形實(shí)現(xiàn)基本的互動(dòng)。
移動(dòng)預(yù)先設(shè)定程序中的對象是有用的,如此會得到用戶的更多關(guān)注。但要是想讓OpenGL ES圖形能和用戶交互又改怎么樣做呢?讓OpenGL ES應(yīng)用程序能夠和用戶交互的關(guān)鍵是重寫GLSurfaceView中的onTouchEvent()來監(jiān)聽觸摸事件。
此節(jié)介紹如何監(jiān)聽用戶的觸摸事件以讓用戶旋轉(zhuǎn)OpenGL ES對象。
(1) 設(shè)置觸摸監(jiān)聽器
欲使OpenGL ES應(yīng)用程序響應(yīng)觸摸事件,必須實(shí)現(xiàn)GLSurfaceView類中的onTouchEvent()方法。以下實(shí)現(xiàn)的代碼展示了怎么監(jiān)聽MotionEvent.ACTION_MOVE事件并將它們轉(zhuǎn)換為形狀旋轉(zhuǎn)的角度。
注意在計(jì)算旋轉(zhuǎn)角度后,此方法調(diào)用了requestRender()來告知渲染器該渲染框架了。此方法在此樣例中最為有效,因?yàn)榭蚣懿恍枰禺?#xff0c;除非旋轉(zhuǎn)有變。然而,它不會影響效率除非用setRenderMode()方法來設(shè)置渲染器只進(jìn)行重繪制操作,所以確保以下這行代碼沒有被注釋:
public MyGLSurfaceView(Context context) {...// Render the view only when there is a change in the drawing datasetRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); }(2) 獲取旋轉(zhuǎn)角度
以上代碼樣例需要通過增加一個(gè)公有的變量來揭露渲染器的旋轉(zhuǎn)角度。由于渲染代碼運(yùn)行于獨(dú)立于用戶主線程的線程中,必須將此變量聲明為volatile。以下代碼聲明了此變量且揭露了獲取和設(shè)置方法:
(3) 應(yīng)用旋轉(zhuǎn)
欲應(yīng)用通過觸摸輸入產(chǎn)生的旋轉(zhuǎn),注釋掉產(chǎn)生角度的代碼并增加mAngle變量,此變量包含了觸摸事件生成的旋轉(zhuǎn)角度:
當(dāng)完成以上描述的所有步驟后,運(yùn)行程序并用手指再屏幕上滑動(dòng)來選擇三角形,運(yùn)行結(jié)果會類似下圖:
圖1. 觸摸輸入選擇三角形(圓圈表示觸摸位置)
2015.11.16
3. 使用場景和變換來實(shí)現(xiàn)動(dòng)畫視圖
在視圖層次如何用轉(zhuǎn)換來讓動(dòng)畫狀態(tài)改變。
活動(dòng)對應(yīng)的用戶界面會常會因?yàn)轫憫?yīng)用戶輸入或其它事件而變化。例如,包含供用戶輸入查詢內(nèi)容的查詢條在用戶提交后可以隱藏查詢條而呈現(xiàn)查詢結(jié)果。
欲在這些情形下提供視覺上的連續(xù),可以在用戶界面的不同視圖層次中作動(dòng)畫般的改變。這些動(dòng)畫給以用戶動(dòng)作上的響應(yīng)并幫助用戶學(xué)習(xí)應(yīng)用程序是怎么工作的。
安卓包含變換框架,此框架能夠很易在兩個(gè)視圖層次間作動(dòng)畫改變。框架在運(yùn)行時(shí)通過改變視圖的某些特性來動(dòng)畫視圖。框架中既包含針對于常見效果的內(nèi)建動(dòng)畫也允許開發(fā)者自定義動(dòng)畫和變換生命周期回調(diào)方法。
此節(jié)教您使用變換框架內(nèi)的內(nèi)建動(dòng)畫來動(dòng)畫改變兩個(gè)不同視圖層次的視圖。此節(jié)同樣包含如何創(chuàng)建自定義動(dòng)畫。
注:對于在4.0(API level 14)和4.4.2(API level 19)的安卓版本,使用animateLayoutChanges屬性來動(dòng)畫布局。欲獲取更多信息,見Property Animation及Animating Layout Changes。
3.1 變換框架
學(xué)習(xí)變換框架主要的特性和組件。
動(dòng)畫應(yīng)用程序用戶界面不僅是視覺上的呼吁。動(dòng)畫強(qiáng)調(diào)改變且提供了應(yīng)用程序是如何工作的視覺線索。
欲幫助開發(fā)者動(dòng)畫在兩個(gè)視圖層次的改變,安卓提供了變換框架。此框架能應(yīng)用一個(gè)或多個(gè)動(dòng)畫到有改變的層次中的所有視圖之間。
框架有以下特性:
組-級動(dòng)畫
應(yīng)用一個(gè)或多個(gè)動(dòng)畫去影響視圖層次中的所有視圖。
(1) 概要
圖1的圖例展示動(dòng)畫是如何提供視覺線索來幫助用戶的。當(dāng)應(yīng)用程序從搜索條屏幕改變到搜索結(jié)果屏幕時(shí),屏幕漸弱不再使用的視圖而漸現(xiàn)幾個(gè)新的視圖。
用戶動(dòng)畫界面:http://developer.android.com/images/transitions/transition_sample_video.mp4
圖1. 視覺線索使用用戶界面動(dòng)畫。點(diǎn)擊設(shè)備屏幕放映動(dòng)畫。
2015.11.16
此動(dòng)畫是使用變換框架的一個(gè)例子。框架動(dòng)畫改變兩個(gè)視圖層次中的所有視圖。一個(gè)視圖層次可以簡單得只有一個(gè)視圖也可以復(fù)雜到像ViewGroup包含復(fù)雜的視圖樹。框架在視圖層的開始和結(jié)束期間通過改變視圖的特性值來動(dòng)畫每個(gè)視圖。
變換框架以并行的方式工作于視圖層和動(dòng)畫。框架的目的是存儲視圖層的狀態(tài),在這些層之間作改變以修改屏幕的顯示,通過存儲和應(yīng)用動(dòng)畫定義進(jìn)行動(dòng)畫改變。
圖2中所示的框圖能夠說明視圖層、框架對象以及動(dòng)畫之間的關(guān)系:
圖2. 變換框架各部分之間的關(guān)系
變換框架為場景、變換以及變換方式提供了抽象的理念。在后續(xù)節(jié)中將會詳細(xì)描述三者。欲使用此框架,在應(yīng)用程序中為計(jì)劃改變的視圖層創(chuàng)建場景。然后,為欲使用的各個(gè)動(dòng)畫創(chuàng)建變換。欲在兩個(gè)視圖層之間開始動(dòng)畫,用變換方法來制定欲使用的變換和結(jié)束場景。此過程在此節(jié)余留部分詳細(xì)講解。
(2) 場景
場景用來存儲視圖層的狀態(tài),包括所有視圖以及它們的特性值。一個(gè)視圖可能是簡單的或是視圖和其子視圖的復(fù)雜的樹視圖。在場景中保存視圖狀態(tài)能夠使得從另外的場景變換到此種狀態(tài)。框架提供了Scene類來呈現(xiàn)場景。
變換框架能夠根據(jù)布局資源文件或代碼中的ViewGroup對象來創(chuàng)建場景。如果動(dòng)態(tài)的創(chuàng)建或運(yùn)行時(shí)修改視圖層,那么在代碼中創(chuàng)建場景會很有用。
在大多數(shù)情況下,不會精確的創(chuàng)建開始場景。如果已經(jīng)應(yīng)用了變換,框架用之前的結(jié)束場景作為后續(xù)變換的開始場景。如果并未應(yīng)用變換,框架將會從屏幕當(dāng)前狀態(tài)收集 的信息。
也可以為場景定義場景自己的動(dòng)作,當(dāng)場景改變時(shí)將會運(yùn)行此些動(dòng)作。例如,在變換場景之后親你管理視圖設(shè)置。
除視圖層和其屬性值之外,場景還存儲父視圖層的引用。根視圖被稱為scene root。改變場景和動(dòng)畫會影響根場景下的場景。
更多關(guān)于創(chuàng)建場景的信息見“創(chuàng)建場景”。
(3) 變換
在變換框架中,動(dòng)畫創(chuàng)造了一系列描述各視圖層在開始和結(jié)束場景之間變化的框架。關(guān)于動(dòng)畫的信息被保存在Transition對象中。用TransitionManager實(shí)例運(yùn)行動(dòng)畫。框架能夠在不用場景之間變換也能夠在同一個(gè)場景的不同狀態(tài)間變換。
框架包含了一套用于常見動(dòng)畫效果的內(nèi)建變換,如漸變和調(diào)整視圖尺寸。也可以用動(dòng)畫框架中的APIs來自定義變換以創(chuàng)建動(dòng)畫效果。變換框架同樣允許聯(lián)合包含內(nèi)建或自定義變換組的變換集中的不同的動(dòng)畫。
變換的生命周期類似活動(dòng)的生命周期,在動(dòng)畫開始和完成期間由框架監(jiān)控生命周期對應(yīng)的變換狀態(tài)。一個(gè)重要的生命周期狀態(tài), 在變換階段可以實(shí)現(xiàn)框架會調(diào)用的回調(diào)方法來調(diào)整用戶界面。
更多關(guān)于變換的信息,見Applying a Transition及Creating Custom Transitions。
(4) 限制
以下理解了變換框架的一些知名的限制:
- 動(dòng)畫應(yīng)用到SurfaceView可能不會正確的顯示。SurfaceView實(shí)例在非用戶界面線程中更新,所以更新可能超出其它視圖的動(dòng)畫的異步范圍。
- 當(dāng)應(yīng)用于TextureView時(shí),一些特殊的變換類型可能不會產(chǎn)生應(yīng)有的動(dòng)畫效果。
- 從AdapterView擴(kuò)展類,如ListView,管理子視圖的方法與變化框架不兼容。如果在AdapterView上實(shí)現(xiàn)動(dòng)畫視圖,設(shè)備顯示可能會被掛起。
- 如果通過動(dòng)畫來調(diào)整TextView的尺寸,在對象尺寸被調(diào)整完成之前文本將會突然跑到一個(gè)新的區(qū)域。欲避免此問題,不要用動(dòng)畫調(diào)整包含文本的視圖。
3.2 創(chuàng)建場景
學(xué)習(xí)如何創(chuàng)建場景來存儲視圖層次的狀態(tài)。
場景存儲視圖層的狀態(tài),包括所有視圖和其特性值。變換框架在開始場景和結(jié)束場景之間運(yùn)行動(dòng)畫。開始場景從用戶當(dāng)前界面的當(dāng)前狀態(tài)獲得。對于結(jié)束場景,框架讓開發(fā)者從布局資源文件或代碼中的一組視圖創(chuàng)建結(jié)束場景。
此節(jié)演示如何在應(yīng)用程序中創(chuàng)建場景以及如何定義場景動(dòng)作。下一節(jié)將演示如何在兩個(gè)場景之間變換。
注:框架能夠在一個(gè)無場景的視圖層中動(dòng)畫改編,在Apply a Transition Without Scenes一節(jié)中已描述過。然而,理解此節(jié)內(nèi)容對于變換來說是必要的。
(1) 從布局資源創(chuàng)建場景
可以根據(jù)布局資源文件直接創(chuàng)建場景。當(dāng)文件中的視圖層大多是靜態(tài)時(shí)可以使用此技術(shù)。場景實(shí)例中的結(jié)果代表視圖層的某時(shí)刻的狀態(tài)。欲改變視圖層,就需要重建場景。框架根據(jù)文件中的整個(gè)視圖層來創(chuàng)建場景;不能只根據(jù)布局文件的某一部分創(chuàng)建場景。
欲根據(jù)布局資源文件創(chuàng)建場景,檢索ViewGroup實(shí)例布局文件的場景根,然后用包含視圖層的布局文件的場景根和資源ID調(diào)用Scene.getSceneForLayout()方法來創(chuàng)建場景。
[1] 為場景定義布局
此節(jié)后續(xù)部分代碼將演示如何用相同的場景根元素來創(chuàng)建兩個(gè)不同的場景。這些代碼片段同時(shí)演示在不用聲明場景彼此的相關(guān)性而載入多個(gè)不相關(guān)的場景對象。
樣例有以下的布局定義組成:
- 活動(dòng)的擁有一個(gè)文本標(biāo)簽和子布局的主布局文件。
- 第一個(gè)場景的有兩個(gè)文本域的關(guān)系布局。
- 第二個(gè)場景的擁有與第一個(gè)布局相同內(nèi)容但內(nèi)容不同順序的關(guān)系布局。
樣例設(shè)計(jì)來讓動(dòng)畫發(fā)生在活動(dòng)的主布局的子布局中。在主布局中的文本標(biāo)簽仍然是靜態(tài)的。
活動(dòng)的主布局定義如下:
res/layout/activity_main.xml
布局文件的定義包含了一個(gè)文本域和一個(gè)場景根的子布局。第一個(gè)場景的布局文件被包含在了主布局文件中。此允許應(yīng)用程序?qū)⒋俗鳛橛脩艚缑娴囊徊糠謥碚故就瑫r(shí)也能夠?qū)⒋溯d入到場景中,因?yàn)榭蚣苤荒軐⒄麄€(gè)布局文件載入場景中。
第一個(gè)場景的布局文件定義如下:
res/layout/a_scene.xml
擁有與第一個(gè)場景布局文件相同的文本域(相同的ID)但放置順序不同的第二個(gè)場景的布局文件內(nèi)容如下:
res/layout/another_scene.xml
[2] 根據(jù)布局生成場景
為兩個(gè)關(guān)系布局創(chuàng)建定義之后,就可以為每個(gè)布局文件各獲取一個(gè)場景了。這能夠使得稍后在兩個(gè)用戶界面配置之間作變換。欲獲得場景,需要場景根的引用和布局資源ID。
以下代碼片段展示了如何獲取場景根的引用,并根據(jù)布局文件創(chuàng)建兩個(gè)場景對象:
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);此時(shí),在應(yīng)用程序中已有基于視圖層的兩個(gè)場景對象。兩個(gè)場景都使用在res/layout/activity_main.xml中被FrameLayout元素定義的場景根。
(2) 在代碼中創(chuàng)建場景
也可以根據(jù)ViewGroup對象用代碼創(chuàng)建場景實(shí)例。當(dāng)用代碼直接修改視圖層或動(dòng)態(tài)創(chuàng)建視圖時(shí)可使用此項(xiàng)技術(shù)。
欲根據(jù)視圖層用代碼創(chuàng)建場景,用Scene(sceneRoot, viewHierarchy)構(gòu)造函數(shù)。當(dāng)已經(jīng)有相應(yīng)的布局文件之后,調(diào)用此構(gòu)造方法等效調(diào)用Scene.getSceneForLayout()方法。
以下代碼片段演示如何根據(jù)場景根元素和視圖層創(chuàng)建一個(gè)視圖實(shí)例:
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) 創(chuàng)建場景動(dòng)作
框架允許定義在場景進(jìn)入運(yùn)行或場景退出運(yùn)行時(shí)的自定義場景動(dòng)作。在許多情況下,自定義場景動(dòng)作都沒必要,因?yàn)榭蚣茉趫鼍爸g自動(dòng)的動(dòng)畫改變。
場景動(dòng)作在處理以下情況中顯得有用:
- 動(dòng)畫視圖不在相同的視圖層上。用進(jìn)入或退出場景動(dòng)作引起場景開始或結(jié)束以使用動(dòng)畫視圖。
- 變換框架不能自動(dòng)進(jìn)行動(dòng)畫視圖,如ListView對象。更多信息見Limitations。
欲提供自定義的場景動(dòng)作,以Runnable對象定義動(dòng)作并將動(dòng)作傳遞給Scene.setExitAction()或Scene.setEnterAction()方法。框架在開始場景即運(yùn)行變換動(dòng)畫之前調(diào)用setExitAction()方法,在結(jié)束場景即運(yùn)行變換動(dòng)畫之后調(diào)用setEnterAction()方法。
注:不要用場景動(dòng)作在開始視圖和結(jié)束視圖之間傳遞數(shù)據(jù)。更多信息見Defining Transition Lifecycle Callbacks。
2015.11.17
3.3 請求轉(zhuǎn)換
學(xué)習(xí)如何變換視圖層次的兩個(gè)場景。
在變換框架中,動(dòng)畫創(chuàng)建了一系列描繪在開始和結(jié)束場景中的視圖層的改變的幀。框架代表的動(dòng)畫作為變換對象,其中包含動(dòng)畫的信息。欲運(yùn)行動(dòng)畫,需提供要使用的變換以及結(jié)束場景給變換方式。
此節(jié)向您展示用內(nèi)建的變換在兩個(gè)場景動(dòng)畫即移動(dòng)、調(diào)整尺寸以及漸褪視圖。下一節(jié)將向您演示如何自定義變換。
(1) 創(chuàng)建變換
在上一節(jié)中,您學(xué)會了如何創(chuàng)建代表不同視圖層狀態(tài)的場景。一旦定義了欲改變的開始場景和結(jié)束場景,就再需要?jiǎng)?chuàng)建一個(gè)定義動(dòng)畫的變換對象。框架能夠在資源文件中指定內(nèi)建的變換并且能將此關(guān)聯(lián)到到代碼中或直接在代碼中定義一個(gè)內(nèi)建變換的實(shí)例。
[1] 根據(jù)資源文件創(chuàng)建變換實(shí)例
此項(xiàng)技術(shù)能夠在不修改活動(dòng)代碼的情況下就能夠修改變換定義。此項(xiàng)技術(shù)也能夠?qū)?fù)雜的變換定義跟應(yīng)用程序代碼獨(dú)立開來,如 Specify Multiple Transitions中所述。
欲在資源文件中指定內(nèi)建變換,跟隨以下步驟:
- 增加/res/transition/目錄到工程 中。
- 在剛所建的目錄中新建一個(gè)XML資源文件。
- 將內(nèi)建變換作為節(jié)點(diǎn)添加到XML文件中。
例如,以下資源文件指定了消退(Fade)變換:
res/transition/fade_transition.xml
以下代碼片段演示如何將資源文件中的變換實(shí)例關(guān)聯(lián)到活動(dòng)的代碼中:
Transition mFadeTransition =TransitionInflater.from(this).inflateTransition(R.transition.fade_transition);[2] 在代碼中創(chuàng)建變換實(shí)例
此項(xiàng)技術(shù)對于在代碼中修改用戶界面動(dòng)態(tài)創(chuàng)建變換對象非常有用,對于創(chuàng)建簡單的內(nèi)建變換實(shí)例不需要或只需要很少的參數(shù)。
欲創(chuàng)建內(nèi)建變換實(shí)例,調(diào)用Transition類子類的其中一個(gè)構(gòu)造函數(shù)即可。例如,以下代碼片段創(chuàng)建了一個(gè)消退(fade)變換的實(shí)例:
Transition mFadeTransition = new Fade();(2) 請求變換
一般來說,變換應(yīng)用于改變不同的視圖層以響應(yīng)諸如用戶動(dòng)作這樣的事件。例如,一個(gè)搜索應(yīng)用程序:當(dāng)用戶在搜索條中輸入內(nèi)容并點(diǎn)擊搜索按鈕時(shí),當(dāng)應(yīng)用消退(fade)變換時(shí),應(yīng)用程序改變到顯示搜索結(jié)果的場景之上,在此場景中搜索條消失不見。
當(dāng)應(yīng)用變換來響應(yīng)活動(dòng)中的某些事件來實(shí)現(xiàn)場景變換,需要用結(jié)束場景和變換實(shí)例來調(diào)用TransitionManager.go()靜態(tài)方法來實(shí)現(xiàn)動(dòng)畫,代碼如下所示:
TransitionManager.go(mEndingScene, mFadeTransition);在根據(jù)指定變換實(shí)例運(yùn)行動(dòng)畫時(shí),框架根據(jù)結(jié)束場景的視圖層改變場景根下的視圖層。開始場景為上一次變換的結(jié)束場景。如果之前無任何變換,系統(tǒng)根據(jù)用戶界面當(dāng)前狀態(tài)作為開始場景。
如果沒有指定變換實(shí)例,變換管理器能夠?qū)⒆詣?dòng)應(yīng)用一個(gè)能夠響應(yīng)大多數(shù)情形的變換。更多信息見API參考TransitionManager類。
(3) 選擇特定的目標(biāo)視圖
框架應(yīng)用變換到默認(rèn)的開始場景和結(jié)束場景中的所有視圖。在某些清醒下,可能只想將變換應(yīng)用到場景中的某部分子視圖上。例如,框架不支持ListView對象的動(dòng)畫改變,所以在變換期間不會動(dòng)畫ListView對象。框架能夠只選擇欲動(dòng)畫的部分視圖。
將每個(gè)會進(jìn)行變換的視圖稱作目標(biāo)。只能將場景視圖層中的某些視圖作為目標(biāo)。
欲從目標(biāo)列表中移除一個(gè)或多個(gè)視圖,在開始變換前調(diào)用removeTarget()方法。欲增加視圖到目標(biāo)列表中,調(diào)用addTart()方法。更多信息見API 參考Transition類。
(4) 指定多個(gè)變換
欲獲得動(dòng)畫的最大效果,應(yīng)將動(dòng)畫跟場景間的變化匹配。例如,如果您正在場景間移除某些視圖的同時(shí)又在增添其它的視圖,消退(fade out)/消失(fade in)動(dòng)畫提供某些視圖不在可用的顯著提示。如果您正將視圖移到屏幕的不同點(diǎn)處,一個(gè)更好的選擇時(shí)動(dòng)畫移動(dòng)以讓用戶注意視圖的新位置。
不必只選擇一個(gè)動(dòng)畫,因?yàn)樽儞Q框架能夠在包含內(nèi)建或自定義變換組的變換集中結(jié)合動(dòng)畫效果。
欲根據(jù)XML變換集定義變換集,在res/transitions/目錄下創(chuàng)建資源文件并在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>欲在代碼中將此變換集設(shè)置到TransitionSet對象中,在活動(dòng)中調(diào)用 TransitionInflater.from()方法。TransitionSet類從Transition類繼承,所以可以像其它變換實(shí)例一樣用變換管理器來使用它。
(5) 請求無場景變換
改變視圖層不是修改用戶界面的唯一方式。亦可以在當(dāng)前層次中通過增加、修改以及移除子視圖的方式改變用戶界面。例如,可以將搜索條實(shí)現(xiàn)在一個(gè)單獨(dú)的布局文件中。開始顯示搜索條。欲改變用戶界面來顯示搜索結(jié)果,當(dāng)用戶點(diǎn)擊搜索條時(shí)調(diào)用ViewGroup.removeView()方法來將搜索條移除,并通過調(diào)用ViewGroup.addView()方法將搜索結(jié)果增加到用戶界面中。
如果會相互替代的兩層的內(nèi)容幾乎相同可用此方法來實(shí)現(xiàn)用戶界面的改變。否則,還是創(chuàng)建動(dòng)畫來實(shí)現(xiàn)用戶界面的改變,這樣就可以只用包含可在代碼中修改的視圖層的一個(gè)布局文件。
如果以上述方法來改變當(dāng)前視圖層,就不再需要?jiǎng)?chuàng)建場景。可以創(chuàng)建并應(yīng)用延遲變換到視圖層的兩個(gè)狀態(tài)中。框架的這個(gè)特點(diǎn)開始于當(dāng)前視圖狀態(tài),記錄對視圖作的改變,當(dāng)系統(tǒng)重繪用戶界面是變換將動(dòng)畫改變。
欲在單個(gè)視圖中創(chuàng)建延遲變換,需以下步驟:
[1]. 當(dāng)觸發(fā)變換的事件發(fā)生時(shí),調(diào)用方法來提供欲用變換來改變的所有子視圖的父視圖。框架存儲子視圖當(dāng)前的狀態(tài)和特性值。
[2]. 欲改變子視圖需要用戶使用子視圖。框架記錄用戶對子視圖所作的改變及其特性。
[3]. 當(dāng)系統(tǒng)根據(jù)改變重繪用戶界面時(shí),框架從原始狀態(tài)動(dòng)畫改變到新狀態(tài)。
以下代碼樣例展示如何使用延遲變換將一個(gè)文本視圖動(dòng)畫的添加到視圖層中。第一段代碼片段展示的布局文件中的定義:
res/layout/activity_main.xml
第二個(gè)代碼片段展示動(dòng)畫增加文本視圖的過程:
MainActivity.java
(6) 定義變換聲明周期回調(diào)方法
變換的生命周期類似活動(dòng)的生命周期。它代表框架在調(diào)用TransitionManager.go()方法和完成動(dòng)畫期間所監(jiān)控的變換的狀態(tài)。在重要的生命周期狀態(tài)中,框架調(diào)用被TransitionListener接口定義的回調(diào)方法。
變換的生命周期回調(diào)函數(shù)很有用,例如,在場景改變時(shí)復(fù)制從開始場景到結(jié)束場景某個(gè)視圖的特性值。不可簡單的在視圖層中復(fù)制開始視圖和結(jié)束視圖,因?yàn)榻Y(jié)束視圖層在變換完成前沒有被關(guān)聯(lián)。科學(xué)的做法是,將值保存在某變量中在框架完成變換后再將此變量拷貝到結(jié)束場景中。欲獲得變換結(jié)束的通知,在活動(dòng)中實(shí)現(xiàn)TransitionListener.onTransitionEnd()方法。
更多信息見API參考TransitionListener類。
3.4 創(chuàng)建自定義變換
學(xué)習(xí)如何創(chuàng)建不屬于變換框架中的其它的動(dòng)畫效果。
自定義變換創(chuàng)建的動(dòng)畫對于任何內(nèi)建變換類都不可用。例如,可以定義一個(gè)變換來返回文本的前景色并將輸入域設(shè)置為灰色以按時(shí)此域在屏幕上已經(jīng)失去了輸入功能。這中效果將幫助用戶理解此域失去了輸入功能。
就像內(nèi)建變換類型一樣,自定義的變換可以應(yīng)用動(dòng)畫到開始和結(jié)束場景中的子視圖中。然而,也不像內(nèi)建變換類型,需要提供來獲取特性值和產(chǎn)生動(dòng)畫的代碼。也可以圍動(dòng)畫定義視圖目標(biāo)的子集。
此節(jié)教您獲取特性值和產(chǎn)生動(dòng)畫來創(chuàng)建自定義變換。
(1) 擴(kuò)展變換類
欲創(chuàng)建自定義變換,增加擴(kuò)展Transition類的類到工程中并重寫方法,如以下代碼所示:
后續(xù)內(nèi)容解釋如何重寫這些方法。
(2) 獲取視圖屬性值
變換動(dòng)畫用屬性動(dòng)畫中的屬性動(dòng)畫系統(tǒng)。屬性動(dòng)畫在開始和結(jié)束值中的一段特殊時(shí)間改變視圖屬性,所以框架需要屬性的開始和結(jié)束值來構(gòu)建動(dòng)畫。
然而,屬性動(dòng)畫通常只需要視圖屬性值的一個(gè)子集。例如,顏色動(dòng)畫需要顏色屬性值,移動(dòng)動(dòng)畫需要位置屬性值。由于動(dòng)畫所需的屬性值對于變化來說特殊,變化框架不會為變換提供每一個(gè)屬性值。取而代之的是,框架將調(diào)用可以為變換獲取變換所需的屬性值的回調(diào)方法并將屬性值存儲在框架中。
[1] 獲取開始值
欲傳遞開始視圖值給框架,需實(shí)現(xiàn)captureStartValues(transitionValues)方法。框架將調(diào)用此方法來獲取開始場景中的每一視圖。此方法的參數(shù)是一個(gè)Transitionvalues對象,器包含一個(gè)視圖引用和一個(gè)能夠存儲視圖值的Map實(shí)例。在獲取開始值的實(shí)現(xiàn)中,檢索這些屬性值將并將存儲在Map中的屬性值回傳給框架。
欲確定屬性值的鍵值不會和其它的TransitionValues鍵值沖突,用以下的命名方案:
package_name:transition_name:property_name
以下代碼片段展示了captureStarValues()方法的實(shí)現(xiàn):
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] 獲取結(jié)束值
框架在結(jié)束場景中為每個(gè)目標(biāo)視圖調(diào)用一次captureEndValues(TransitionValues)方法。在其它方面,captureEndValues()跟captureStartValues()工作機(jī)制一致。
以下代碼片段顯示了captureEndValues()方法的實(shí)現(xiàn):
@Override public void captureEndValues(TransitionValues transitionValues) {captureValues(transitionValues); }在此例中,captureStartValues()和 captureEndValues()方法都調(diào)用了captureValues()方法來檢索和保存值。captureValues()方法檢索到的視圖屬性相同,但它在開始和結(jié)束場景中有著不同的值。框架分開映射開始和結(jié)束場景視圖的值。
(3) 創(chuàng)建自定義動(dòng)畫
欲動(dòng)畫視圖在其開始和結(jié)束場景中的改變,需要重寫createAnimator()方法來提供動(dòng)畫器。當(dāng)框架調(diào)用此方法時(shí),它將傳遞動(dòng)畫器給場景根視圖和所捕獲的包含開始和結(jié)束場景給TransitionValues對象。
框架調(diào)用createAnimator()方法的次數(shù)基于開始和結(jié)束場景的改變。例如,自定義實(shí)現(xiàn)的消退/消失變換。如在開始場景中景中有5個(gè)目標(biāo)但在結(jié)束場景中會被移除兩個(gè),那么在結(jié)束場景就只有三個(gè)目標(biāo),再在結(jié)束場景中添加一個(gè)新目標(biāo)。那么框架將會調(diào)用createAnimator()方法六次。三次調(diào)用用于兩個(gè)場景中的消退/消失;兩次調(diào)用為移除的目標(biāo)動(dòng)畫;一次調(diào)用為結(jié)束場景中的新目標(biāo)。
對于存在于開始和結(jié)束場景中的視圖目標(biāo),框架為startValues和endValues參數(shù)提供了Transitions對象。對于只存在于開始或結(jié)束場景中的目標(biāo)視圖,框架提供TransitionValues對象來聯(lián)系參數(shù)和并用null聯(lián)系其它。
當(dāng)創(chuàng)建自定義變換實(shí)現(xiàn)createAnimator(ViewGroup, TransitionValues, TransitionValues)方法時(shí),用捕獲到的視圖屬性值來創(chuàng)建Animator對象并將此返回給框架。一個(gè)實(shí)現(xiàn)的樣例,見自定義變換樣例中的ChangeColor類。更多關(guān)于屬性動(dòng)畫的值 Property Animation見。
(4) 應(yīng)用自定義變換
自定義變換的工作機(jī)制跟內(nèi)建變換相同。可以用變換管理應(yīng)用于自定義變換,具體描述見 Applying a Transition。
4. 增加動(dòng)畫
如何將漸變的動(dòng)畫添加到用戶界面中。
動(dòng)畫能夠?yàn)橥ㄖ脩絷P(guān)于應(yīng)用程序發(fā)生了啥增加微妙的視覺線索并且會增加應(yīng)用程序界面的心智模型(mental model)。當(dāng)屏幕改變狀態(tài)時(shí)動(dòng)畫尤其有用,如當(dāng)內(nèi)容載入或新動(dòng)作變得有用時(shí)。同時(shí)動(dòng)畫也能夠?yàn)閼?yīng)用程序增加光滿的外觀,這可以給應(yīng)用程序一個(gè)更高質(zhì)量的感覺。
但仍需記住,過度的使用動(dòng)畫或在錯(cuò)誤的時(shí)間使用動(dòng)畫也會帶來不利,如引起延遲。此節(jié)展示如何實(shí)現(xiàn)一些常見的能夠帶來實(shí)用并在不騷擾用戶的情況下增加流動(dòng)性的動(dòng)畫類型。
2015.11.18
4.1 兩視圖交叉淡入淡出
學(xué)習(xí)如何讓兩個(gè)重疊的視圖淡入淡出。此節(jié)展示如何讓進(jìn)度條淡入包含文本內(nèi)容的視圖。
淡入淡出動(dòng)畫(亦稱溶解)漸漸的消退某用戶界面組件的同時(shí)漸入另一個(gè)組件。此動(dòng)畫對于轉(zhuǎn)換內(nèi)容或視圖到應(yīng)用程序的情形很有用。淡入淡出非常微妙同時(shí)也很短暫但提供了屏幕到文本的流利的變換。當(dāng)不用淡入淡出動(dòng)畫時(shí),這些變換都會顯得有些突然。
此處有一個(gè)從進(jìn)度條淡出文本內(nèi)容的例子:http://developer.android.com/training/animation/anim_crossfade.mp4
淡入淡出動(dòng)畫
點(diǎn)擊屏幕設(shè)備屏幕可放映動(dòng)畫
如果您想跳過后續(xù)內(nèi)容并想看一個(gè)完整的代碼示例,下載并運(yùn)行樣例,選擇淡入淡出的例子。看一下幾個(gè)文件中的代碼實(shí)現(xiàn):
· src/CrossfadeActivity.java
· layout/activity_crossfade.xml
· menu/activity_crossfade.xml
(1) 創(chuàng)建視圖
創(chuàng)建欲淡入淡出的兩個(gè)視圖。以下代碼片段創(chuàng)建了一個(gè)進(jìn)度條和一個(gè)具滑動(dòng)條的文本視圖:
(2) 設(shè)置動(dòng)畫
欲設(shè)置動(dòng)畫,遵循以下步驟:
[1] 為欲淡入淡出的視圖創(chuàng)建成員變量。在動(dòng)畫期間需要這些視圖的引用來修改視圖。
[2] 對于淡入的視圖,將其可見性設(shè)置為GONE。此值能夠避免視圖占據(jù)布局文件空間并忽略堆它們的計(jì)算以提高處理速度。
[3] 將config_shortAnimTime系統(tǒng)屬性緩存在成員變量中。此屬性為動(dòng)畫定義了一個(gè)標(biāo)準(zhǔn)的“短”的持續(xù)時(shí)間。此持續(xù)時(shí)間對微妙的動(dòng)畫或發(fā)生頻率較高的動(dòng)畫比較理想。config_longAnimTime和config_mediumAnimTime也是可用的,如果您想用它們的話。
將前面代碼片段所定義的內(nèi)容作為以下代碼描述的活動(dòng)的布局文件:
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) 淡入淡出視圖
至此,視圖已被正確設(shè)置,欲對這些視圖進(jìn)行淡入淡出請遵循以下步驟:
[1] 對于淡入的視圖,設(shè)置alpha值為0并將其可見值設(shè)置為VISIBLE。(記住其初始值為GONE)這樣能夠讓這些視圖可見但出于完全透明的狀態(tài)。
[2] 對于淡入的視圖,動(dòng)畫改變其alpha的值從0到1。同時(shí),將淡出視圖的alpha值從1動(dòng)畫改為0。
[3] 在Animator.AnimatorListener中使用onAnimationEnd()方法,將淡出視圖的可見性設(shè)置為GONE。盡管這些視圖的alpha值為0,將視圖的可見性設(shè)置為GONE能夠阻止視圖占用布局文件空間且忽略布局計(jì)算,以提升處理速度。
以下方法展示如何做以上描述的步驟:
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屏幕滑動(dòng)
學(xué)習(xí)在滑動(dòng)變換下動(dòng)畫變化屏幕。
屏幕滑動(dòng)是從一個(gè)屏幕頁面到另一個(gè)屏幕頁面的變換。此節(jié)內(nèi)容將展示如何用由support library提供的ViewPager來做到屏幕滑動(dòng)變換。ViewPager自動(dòng)掃描屏幕滑動(dòng)。點(diǎn)擊下圖屏幕,由本頁屏幕會自動(dòng)滑動(dòng)到下一頁屏幕:
屏幕滑動(dòng)動(dòng)畫:http://developer.android.com/training/animation/anim_screenslide.mp4
屏幕滑動(dòng)動(dòng)畫
點(diǎn)擊屏幕設(shè)備屏幕可放映動(dòng)畫
如果您想跳過后續(xù)內(nèi)容且想看完整的工程代碼,下載本節(jié)樣例應(yīng)用程序,選擇屏幕滑動(dòng)(Screen Slide)例子。看以下幾個(gè)文件中的代碼實(shí)現(xiàn):
- src/ScreenSlidePageFragment.java
- src/ScreenSlideActivity.java
- layout/activity_screen_slide.xml
- layout/fragment_screen_slide_page.xml
(1) 創(chuàng)建視圖
為稍后要使用的碎片的內(nèi)容創(chuàng)建一個(gè)布局文件。以下實(shí)現(xiàn)的布局文件中包含一個(gè)顯示文本的文本視圖:
同時(shí)也在碎片中定義了一個(gè)字符串。
(2) 創(chuàng)建碎片
創(chuàng)建一個(gè)返回在onCreateView()方法中創(chuàng)建的布局的Fragment(碎片)類。然后,在需要向用戶展示新頁的時(shí)候就可以在碎片的父活動(dòng)中創(chuàng)建此片段實(shí)例:
(3) 增加ViewPager
ViewPager已經(jīng)被內(nèi)建在掃頁面變換中,它的默認(rèn)功能就是滑動(dòng)屏幕動(dòng)畫,所以不必重新創(chuàng)建它。ViewPager展示PagerAdapter提供的頁面,所以PagerAdapter也會用到之前所創(chuàng)建的碎片。
首先,在布局文件 中包含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" />創(chuàng)建一個(gè)具以下功能的活動(dòng):
- 將活動(dòng)的視圖內(nèi)容設(shè)置包含ViewPager的布局文件。
- 創(chuàng)建一個(gè)擴(kuò)展于FragmentStatePagerAdapter類的類并實(shí)現(xiàn)getItem()方法來提供一個(gè)ScreenSlidePageFragment實(shí)例作為新頁。頁面適配器同樣要求實(shí)現(xiàn)getCount()方法,此方法返回適配器所需創(chuàng)建的頁面數(shù)。
- 將PagerAdapter掛到ViewPager上。
- 在碎片的虛擬棧中通過回移來響應(yīng)設(shè)備的返回按鈕。如果用戶已經(jīng)在第一頁上,就返回到活動(dòng)的棧底。
(4)用PageTransformer自定義動(dòng)畫
欲在默認(rèn)的屏幕滑動(dòng)動(dòng)畫中展示不同的動(dòng)畫頁面,實(shí)現(xiàn)ViewPager.PagerTransformer接口并將此接口提供給視圖頁。此接口指暴露了transformPage()一個(gè)方法。每當(dāng)屏幕變換時(shí),每個(gè)可見視圖(通常屏幕上只有一個(gè)可見頁面)以及相鄰的沒有在屏幕上的頁面就會調(diào)用此方法一次。例如,若當(dāng)前屏幕為頁面三,用戶欲拖拽出頁面四,在每一個(gè)手勢發(fā)生時(shí),transformPage()會被頁面二、三、四調(diào)用。
在transformPage()的實(shí)現(xiàn)中,根據(jù)屏幕上頁面的位置參數(shù)通過判斷哪一個(gè)頁面需要轉(zhuǎn)換可以創(chuàng)建自定義的動(dòng)畫滑動(dòng),位置可從ransformPage()方法的position參數(shù)獲得。
位置position參數(shù)會表明一個(gè)頁面跟屏幕中心的位置關(guān)系。當(dāng)用戶滑動(dòng)屏幕時(shí)此參數(shù)是一個(gè)動(dòng)態(tài)值。當(dāng)某頁面填充到屏幕中時(shí),其位置參數(shù)值為0。當(dāng)一個(gè)頁面剛好從屏幕右邊消失時(shí),其位置值為1。如果用戶在頁面1和頁面2中滑動(dòng)一半,頁面1的位置值為-0.5,頁面2的位置值為0.5。基于頁面在屏幕上的具體位置,可以通過setAlpha()、setTranslationX()或setScaleY()方法設(shè)置頁面屬性值來自定義動(dòng)畫。
當(dāng)實(shí)現(xiàn)PagerTransformer后,用此實(shí)現(xiàn)調(diào)用setPageTransformer()來應(yīng)用自定義的動(dòng)畫。例如,假設(shè)有一個(gè)名為ZoomOutPageTransformer的PagerTransformer,可以像以下這樣設(shè)置自定義動(dòng)畫:
ViewPager mPager = (ViewPager) findViewById(R.id.pager); ... mPager.setPageTransformer(true, new ZoomOutPageTransformer());見Zoom-out page transformer和Depth page transformer部分的例子及相應(yīng)變換的視頻。
[1] 頁面縮小變換
當(dāng)用戶在相鄰頁面滑動(dòng)時(shí),頁面將會縮小并消退出屏幕。當(dāng)頁面接近屏幕中心時(shí),此頁面將回到正常尺寸并漸入。
頁面縮小變換動(dòng)畫:http://developer.android.com/training/animation/anim_page_transformer_zoomout.mp4
ZoomOutPageTransformer示例
點(diǎn)擊設(shè)備屏幕放映動(dòng)畫
[2] 頁面深度變換
當(dāng)用“depth”動(dòng)畫滑動(dòng)頁面到右邊時(shí),頁面用默認(rèn)的動(dòng)畫變換將滑動(dòng)頁面移到左邊。深度變換將頁面淡出并線性的減小其范圍。
頁面深度變換示例:http://developer.android.com/training/animation/anim_page_transformer_depth.mp4
DepthPageTransformer 示例
點(diǎn)擊設(shè)備屏幕放映動(dòng)畫
注:在深度動(dòng)畫期間,默認(rèn)的動(dòng)畫(屏幕滑動(dòng))仍舊發(fā)生了,所以必須構(gòu)建一個(gè)X負(fù)方向的變換。例如:
view.setTranslationX(-1 * view.getWidth() * position);以下代碼演示如何在正移動(dòng)的頁面變換中抵消默認(rèn)的屏幕動(dòng)畫滑動(dòng):
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 顯示卡片翻轉(zhuǎn)式動(dòng)畫
學(xué)習(xí)在翻轉(zhuǎn)運(yùn)動(dòng)下如何實(shí)現(xiàn)兩視圖之間的動(dòng)畫。
此節(jié)將展示如何用自定義碎片動(dòng)畫來實(shí)現(xiàn)卡片翻轉(zhuǎn)動(dòng)畫。視圖內(nèi)容間的卡片翻轉(zhuǎn)通過模擬卡片翻轉(zhuǎn)過來實(shí)現(xiàn)。
卡片翻轉(zhuǎn)的過程如下所示:http://developer.android.com/training/animation/anim_card_flip.mp4
卡片翻轉(zhuǎn)動(dòng)畫,點(diǎn)擊設(shè)備屏幕放映動(dòng)畫
欲跳過后續(xù)內(nèi)容而想看完整的樣例,下載本節(jié)樣例選擇Card Flip樣例打開看以下幾個(gè)文件中的代碼實(shí)現(xiàn):
- 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) 創(chuàng)建動(dòng)畫
欲創(chuàng)建卡片式動(dòng)畫翻轉(zhuǎn),需要兩個(gè)動(dòng)畫場景,當(dāng)前面的卡片動(dòng)畫從向左翻轉(zhuǎn)出去時(shí)另外一個(gè)動(dòng)畫要從左方顯示進(jìn)來。同時(shí),當(dāng)卡片動(dòng)畫從右方向回來時(shí)另一動(dòng)畫需要從右方向消失。
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) 創(chuàng)建視圖
“卡片”的每一面可以獨(dú)立包含任何內(nèi)容,如都包含文本、圖片以及有關(guān)聯(lián)的視圖。動(dòng)畫變換時(shí)將會用存儲在碎片中的視圖布局。以下布局創(chuàng)建了卡片用用于顯示文本的一面:
卡片的另一面用于展示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) 創(chuàng)建碎片
為卡片的前面和背面創(chuàng)建碎片類。此類返回之前在每個(gè)片段中onCreateView()方法中所創(chuàng)建的布局。之后,可在欲顯示卡片的碎片的父活動(dòng)中聲明此碎片實(shí)例。以下代碼展示了在父活動(dòng)中嵌套碎片類的實(shí)現(xiàn):
(4) 動(dòng)畫卡片翻轉(zhuǎn)
至此,可以在父活動(dòng)中展示片段了。欲此,首先為活動(dòng)創(chuàng)建布局文件。以下代碼創(chuàng)建了可以在運(yùn)行時(shí)添加碎片的包含F(xiàn)rameLayout元素的布局文件:
在活動(dòng)類代碼中,將剛創(chuàng)建的布局文件加載到活動(dòng)類中。在活動(dòng)被創(chuàng)建時(shí)顯示默認(rèn)的片段也是個(gè)不錯(cuò)的主意,所以以下活動(dòng)類中的代碼展示如何將卡片前面作為默認(rèn)顯示:
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();}}... }現(xiàn)在以后卡片的前面的顯示,在適當(dāng)?shù)臅r(shí)候可以用翻轉(zhuǎn)動(dòng)畫來顯示卡片的背面。遵循以下步驟來顯示卡片的另一面:
- 設(shè)置之前為碎片變換創(chuàng)建的自定義動(dòng)畫。
- 用新的碎片代替當(dāng)前碎片的顯示并觸發(fā)自定義動(dòng)畫。
- 將之前的碎片置于棧頂?shù)南乱粚?#xff0c;這樣只要用戶按下返回按鈕,卡片就翻轉(zhuǎn)過來了。
4.4 縮放視圖
學(xué)習(xí)在縮放觸摸動(dòng)畫操作下如何放大視圖。
此節(jié)演示如何實(shí)現(xiàn)觸摸-縮放動(dòng)畫,此動(dòng)畫對于像相片畫廊引用程序非常有用 - 從縮略圖視圖動(dòng)畫到全尺寸以填充屏幕。
這里有一個(gè)觸摸-縮放動(dòng)畫:http://developer.android.com/training/animation/anim_zoom.mp4
縮放動(dòng)畫,點(diǎn)擊屏幕放映動(dòng)畫
如果想直接看本節(jié)代碼示例,下載并選擇Zoom樣例,見以下幾個(gè)文件中的代碼實(shí)現(xiàn):
- src/TouchHighlightImageButton.java(一個(gè)幫助類,當(dāng)按圖片按鈕時(shí)高亮點(diǎn)擊的地方)
- src/ZoomActivity.java
- layout/activity_zoom.xml
(1) 創(chuàng)建視圖
創(chuàng)建一個(gè)包含縮放所需的小和大尺寸視圖的布局文件。以下代碼創(chuàng)建了一個(gè)具點(diǎn)擊響應(yīng)事件的ImageButton按鈕以及一個(gè)展示擴(kuò)大圖片的的ImageView視圖:
(2) 設(shè)置縮放動(dòng)畫
一旦應(yīng)用布局文件,即可設(shè)置出發(fā)縮放動(dòng)畫的事件。以下代碼增加View.onClickListener事件給ImageButton,如此,當(dāng)用戶點(diǎn)擊此按鈕時(shí)即實(shí)現(xiàn)縮放動(dòng)畫:
(3) 縮放視圖
在恰當(dāng)?shù)臅r(shí)機(jī)下需要從正常尺寸的視圖縮放到某個(gè)尺寸的視圖。通常來講,需要根據(jù)視圖的邊界來動(dòng)畫。以下方法展示了怎么實(shí)現(xiàn)縮放動(dòng)畫(從縮略視圖到更大尺寸):
[1]. 分配高分辨率圖片到隱藏的“放大”(擴(kuò)大)ImageView中。以下的樣例代碼將一張大型圖片簡單的載入到用戶界面線程中。更科學(xué)的做法是在獨(dú)立的線程中載入圖片并在用戶界面線程中設(shè)置位圖以防止阻礙用戶界面線程。理想情況下,位圖不應(yīng)該比屏幕尺寸大。
[2]. 計(jì)算ImageView開始和結(jié)束時(shí)的邊界。
[3]. 根據(jù)開始邊界和結(jié)束邊界,同時(shí)動(dòng)畫四個(gè)位置和尺寸值X,Y(SCALE_X和SCALE_Y)。四個(gè)動(dòng)畫被增加到AnimatorSet中,這樣他們可以在同時(shí)開始。
[4]. 當(dāng)圖片被放大用戶再點(diǎn)擊屏幕時(shí)運(yùn)行類似的動(dòng)畫將視圖縮小(還原)。可以為ImageView增加View.onClickListerer來監(jiān)聽用戶的點(diǎn)擊。當(dāng)用戶點(diǎn)擊時(shí),ImageView會還原縮略圖大小并將其可見性設(shè)置為GONE來隱藏。
4.5 動(dòng)畫布局文件的改變
學(xué)習(xí)當(dāng)在布局文件中增加、移除以及更新子視圖時(shí)如何開啟內(nèi)建動(dòng)畫。
布局動(dòng)畫是對布局文件配置更改之前預(yù)先載入動(dòng)畫。開發(fā)者需要做的就是在布局文件中設(shè)置屬性來告知安卓系統(tǒng)動(dòng)畫改變布局文件的改變,系統(tǒng)用默認(rèn)的動(dòng)畫效果動(dòng)畫顯示它們。
提示:若欲實(shí)現(xiàn)自定義布局動(dòng)畫,需創(chuàng)建LayoutTransition對象并需通過setLayoutTransiton()方法將此對象應(yīng)用到布局文件中。
此處有一個(gè)當(dāng)增加列表中的條目時(shí)默認(rèn)的布局動(dòng)畫:http://developer.android.com/training/animation/anim_layout_changes.mp4
布局動(dòng)畫,點(diǎn)擊設(shè)備屏幕放映動(dòng)畫
若想直接看此部分的代碼樣例, 下載本節(jié)應(yīng)用程序并選擇Crossfade樣例,看以下文件中的代碼實(shí)現(xiàn):
[1]. src/LayoutChangesActivity.java
[2]. layout/activity_layout_changes.xml
[3]. menu/activity_layout_changes.xml
(1) 創(chuàng)建布局
在活動(dòng)的布局XML文件中,將布局文件中欲開啟的動(dòng)畫的android:animateLayoutChanges屬性設(shè)置為true。例:
(2) 從布局文件中增加、 更新或移除內(nèi)容
至此,所有需要做的操作就是增加、移除或更新布局文件中的內(nèi)容,布局文件中的內(nèi)容將會自動(dòng)的以動(dòng)畫形式實(shí)現(xiàn):
[2015.11.18-16:35]
總結(jié)
以上是生活随笔為你收集整理的pAdTy_1 构建图形和动画应用程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于ES2020语法2345加速浏览器不
- 下一篇: 另一个世界的人