日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時間:2024/3/13 编程问答 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 pAdTy_1 构建图形和动画应用程序 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

2015.11.12-11.18
個人英文閱讀練習(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)性時,如何加載和處理位圖并避免超過內(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)用程序中載入位圖時需要機(jī)警的幾個原因:
- 移動設(shè)備的系統(tǒng)資源通常都比較受限制。安卓設(shè)備只能給每個應(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)用程序用戶界面在同一時刻需要載入幾張位圖。像ListView、GridView以及ViewPager這樣的組件通常在同時包含多張位圖(有的是跟隨用戶操作而即將展現(xiàn)的圖片)。

1.1 有效地載入大型位圖

在不超過每個應(yīng)用程序內(nèi)存限制的情況下解碼大型位圖。

不同的圖片不同的形狀和尺寸。在許多情況下應(yīng)用程序的用戶界面所需的圖片都比實(shí)際的圖片要小。例如,系統(tǒng)的畫廊應(yīng)用程序展示的用安卓設(shè)備相機(jī)拍的圖片的分辨率通常就比設(shè)備屏幕的密度要高。

鑒于有限的內(nèi)存,理想情況下只需加載一個低分辨率的版本到內(nèi)存中。低版本分辨率應(yīng)該要跟顯示它的用戶界面組件的尺寸匹配。一個擁有高分辨率的圖片不會給顯示帶來好處,反而會更多的占用珍貴的內(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)的簽名。解碼時將inJustDecodeBounds特性設(shè)置為ture以避免內(nèi)存分配,通過設(shè)置位圖的outWidth、outHeight和outMimeType可返回null。此項(xiàng)技術(shù)允許在構(gòu)建(以及內(nèi)存分配)位圖之前獲取到圖片的尺寸和類型。

BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(getResources(), R.id.myimage, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;

欲避免java.lang.outOfMemory異常,在解碼位圖時檢查位圖的尺寸,除非確定圖片不會引來此異常。

(2) 載入圖片的縮小版本到內(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)存中。

欲告知解碼器解碼圖片的子樣本,載入一個更低像素版本的圖片到內(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中顯示一個100x100像素的縮略圖時,用以下代碼即可實(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)該在用戶界面主線程中使用這些方法。載入圖片所花的時間是不可預(yù)測的,它基于各種各樣的因素(從硬盤或網(wǎng)絡(luò)讀取數(shù)據(jù)的速度,圖片尺寸,CPU的性能等)。如果因載入圖片阻礙了用戶界面線程,系統(tǒng)所運(yùn)行的應(yīng)用程序?qū)⒉痪哂袑?shí)時的響應(yīng)性,用戶也極有可能選擇將此應(yīng)用程序關(guān)閉(見設(shè)計(jì)具響應(yīng)性的應(yīng)用程序獲取更多信息)。

(1) 使用異步任務(wù)(AsyncTask)
AsyncTask類提供了一種簡單的方式在后臺線程中執(zhí)行一些任務(wù)并將結(jié)果返回到用戶主線程中。欲使用此類,需要創(chuàng)建一個子類并重寫所提供的方法。以下是使用AsyncTask和decodeSampledBitmapFromResource將一張大型圖片載入到ImageView中的示例:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {private final WeakReference<ImageView> imageViewReference;private int data = 0;public BitmapWorkerTask(ImageView imageView) {// Use a WeakReference to ensure the ImageView can be garbage collectedimageViewReference = new WeakReference<ImageView>(imageView);}// Decode image in background.@Overrideprotected Bitmap doInBackground(Integer... params) {data = params[0];return decodeSampledBitmapFromResource(getResources(), data, 100, 100));}// Once complete, see if ImageView is still around and set bitmap.@Overrideprotected void onPostExecute(Bitmap bitmap) {if (imageViewReference != null && bitmap != null) {final ImageView imageView = imageViewReference.get();if (imageView != null) {imageView.setImageBitmap(bitmap);}}} }

為ImageView添加的WeakReference保證了AsyncTask不會阻止ImageView以及其引用的任何東西收集垃圾信息。不敢保證當(dāng)任務(wù)執(zhí)行完后ImageView仍舊還在,所以必須在onPostExecute()中檢查其引用。就此例來說,在任務(wù)結(jié)束之前用戶導(dǎo)航離開活動或者配置發(fā)生改變時,ImageView可能不再存在。

欲異步開始載入位圖,簡單的創(chuàng)造一個新的任務(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é)合使用會引來引來另外一個問題。為了有效地利用內(nèi)存,隨著用戶滑動滾動條,這些組件會被重復(fù)利用為子視圖顯示。如果每個子視圖都觸發(fā)一個AsyncTask,不敢保證當(dāng)AsyncTask完成時,對應(yīng)的視圖還未被重復(fù)利用來顯示另外一個子視圖。另外,也不能保證各異步線程是在其它線程利用完視圖后再開始利用此視圖。

博客“高性能的多線程(Multithreading for Performance)”深入的討論了處理并發(fā)問題,并提供了當(dāng)某任務(wù)完成后何AsyncTask將獲得ImageView的引用何AsyncTask稍后再引用ImageView的解決方法。使用相似的方法,前一節(jié)提到的AsyncTask能夠被擴(kuò)展為一個成熟的模式。

創(chuàng)建一個微型的Drawable子類來存儲一個返回到工作任務(wù)的引用。在這種情況下,當(dāng)任務(wù)執(zhí)行完時,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方法是用來檢查是否有另外一個正在運(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é)教您在載入多張位圖時如何使用內(nèi)存和硬盤位圖緩存來提升主用戶界面的響應(yīng)性和流動性。

載入一張位圖到用戶界面是比較簡單的,然而當(dāng)需要在同一時間就載入大量位圖時就會變得復(fù)雜許多。在許多情況(如ListView、GridView或ViewPager組件)下,可能很快滾動到屏幕上顯示的數(shù)量是無限的。

當(dāng)向下移動屏幕時通過重復(fù)利用組件來表示子視圖的方式來保持內(nèi)存消耗量不上升。假如不保持長期的引用位圖,垃圾回收器會釋放載入的位圖。這一點(diǎn)固然是好,但為了保持流暢和快速的加載用戶界面,當(dāng)圖片每次重新回到屏幕上時也想避免次次都去處理它。一段內(nèi)存或硬盤緩存 能夠滿足組件快速重載入之前經(jīng)處理過的圖片。

此節(jié)將展示當(dāng)載入多張位圖時,使用內(nèi)存或硬盤位圖緩存來提升用戶界面的流動性和響應(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選擇一個合適的尺寸,許多因素都應(yīng)該被納入考慮,如:
- 活動跟應(yīng)用程序使用后所剩下的內(nèi)存大小。
- 多少圖片會被同一時間載入到屏幕上?需要準(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ù)量低質(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)。在一個800x480分辨的設(shè)備上,一個全屏的GridView填充的圖片會占用約為1.5MB(800*480*4字節(jié)),所以此緩存約能存2.5張這樣的圖片。

當(dāng)載入位圖到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也需要被更新以添加到內(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ù)。當(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)在主線程中完成這個過程。然而,在初始化之前確實(shí)也有訪問緩存的機(jī)會。為了解決這個問題,在以上代碼的實(shí)現(xiàn)中,使用鎖住一個對象來確保在初始化緩沖區(qū)之前不能讀硬盤緩沖區(qū)。

當(dāng)內(nèi)存緩沖區(qū)在用戶界面線程被完成后,當(dāng)硬盤緩沖區(qū)在后臺線程中被創(chuàng)建后。與硬盤相關(guān)的操作不要在用戶界面線程中操作。當(dāng)圖片處理完成后,最終的位圖被同時增加到內(nèi)存和硬盤緩沖區(qū)中,供后續(xù)使用。

(3) 處理配置更改
諸如屏幕方向改變這樣的運(yùn)行時配置改變時,會引起安卓銷毀并用新配置重啟運(yùn)行的活動(關(guān)于此行為的更多信息見Handling Runtime Changes)。為讓用戶在配置改變時還能夠感受到流利快速的用戶體驗(yàn)需要避免再次處理所有的圖片。

幸運(yùn)的是,在Use a Memory Cache節(jié)為位圖創(chuàng)建了好用的內(nèi)存緩沖區(qū)。使用通過調(diào)用setRetainInstance(true)保存的碎片能夠?qū)⒕彌_區(qū)傳遞給新的活動實(shí)例。在活動被重建后,它能夠重新獲得附加的碎片且能夠獲取對存在緩沖區(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ū)中裝載圖片到活動中去的。如果內(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)垃圾回收器工作時,應(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()錯誤。recycle()方法能夠盡快讓應(yīng)用程序重新獲得位圖所占用的內(nèi)存。

注:只有在確定位圖不再被使用時使用recycle()。若在調(diào)用recycle()后再嘗試?yán)L制位圖,將會出現(xiàn)錯誤:“Canvas:嘗試去用回收的位圖”。

以下代碼片段為調(diào)用recycle()的示例。此程序用引用計(jì)數(shù)(用mDisplayRefCount和mCacheRefCount)來跟蹤當(dāng)前是否有位圖顯示或在緩存中。當(dāng)滿足以下條件代碼將回收位圖:
- 引用計(jì)數(shù)mDisplayRefCount和mCacheRefCount都為0.
- 位圖不為null且位圖還未被回收。

private int mCacheRefCount = 0; private int mDisplayRefCount = 0; ... // Notify the drawable that the displayed state has changed. // Keep a count to determine when the drawable is no longer displayed. public void setIsDisplayed(boolean isDisplayed) {synchronized (this) {if (isDisplayed) {mDisplayRefCount++;mHasBeenDisplayed = true;} else {mDisplayRefCount--;}}// Check to see if recycle() can be called.checkState(); }// Notify the drawable that the cache state has changed. // Keep a count to determine when the drawable is no longer being cached. public void setIsCached(boolean isCached) {synchronized (this) {if (isCached) {mCacheRefCount++;} else {mCacheRefCount--;}}// Check to see if recycle() can be called.checkState(); }private synchronized void checkState() {// If the drawable cache and display ref counts = 0, and this drawable// has been displayed, then recycle.if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed&& hasValidBitmap()) {getBitmap().recycle();} }private synchronized boolean hasValidBitmap() {Bitmap bitmap = getBitmap();return bitmap != null && !bitmap.isRecycled(); }

2015.11.14
(2) 在安卓3.0及更高版本中管理內(nèi)存
安卓3.0(API level 11)介紹了BitmapFactory.Options.inBitmap域。如果此選項(xiàng)被設(shè)置,當(dāng)載入內(nèi)容時解碼方法將會用此選項(xiàng)去重新使用存在的位圖。這就意味著位圖的內(nèi)存被重用,如此會導(dǎo)致性能的提升并省掉了內(nèi)存的分配和釋放。然而,用inBitmp也有幾個限制。尤其是在安卓4.4之前(API level 19),只支持相等尺寸的位圖。更多細(xì)節(jié)見inBitmap的文檔。

[1] 保存位圖供以后使用
以下代碼片段演示如何保存位圖來供以后使用。但應(yīng)用程序運(yùn)行在安卓3.0或更高版本中且位圖從LruCache中被驅(qū)逐了出來,對位圖的一個軟應(yīng)用被放置在HashSet中,供稍后可能用inBitmap來使用位圖:

Set<SoftReference<Bitmap>> mReusableBitmaps; private LruCache<String, BitmapDrawable> mMemoryCache;// If you're running on Honeycomb or newer, create a // synchronized HashSet of references to reusable bitmaps. if (Utils.hasHoneycomb()) {mReusableBitmaps =Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>()); }mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {// Notify the removed entry that is no longer being cached.@Overrideprotected void entryRemoved(boolean evicted, String key,BitmapDrawable oldValue, BitmapDrawable newValue) {if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {// The removed entry is a recycling drawable, so notify it// that it has been removed from the memory cache.((RecyclingBitmapDrawable) oldValue).setIsCached(false);} else {// The removed entry is a standard BitmapDrawable.if (Utils.hasHoneycomb()) {// We're running on Honeycomb or later, so add the bitmap// to a SoftReference set for possible use with inBitmap later.mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));}}} .... }

[2] 使用存在的位圖
應(yīng)用程序在運(yùn)行時,解碼方法會檢查是否有存在的位圖可用。舉例如下:

public static Bitmap decodeSampledBitmapFromFile(String filename,int reqWidth, int reqHeight, ImageCache cache) {final BitmapFactory.Options options = new BitmapFactory.Options();...BitmapFactory.decodeFile(filename, options);...// If we're running on Honeycomb or newer, try to use inBitmap.if (Utils.hasHoneycomb()) {addInBitmapOptions(options, cache);}...return BitmapFactory.decodeFile(filename, options); }

addInBitmapOptions() inBitmap inBitmap

private static void addInBitmapOptions(BitmapFactory.Options options,ImageCache cache) {// inBitmap only works with mutable bitmaps, so force the decoder to// return mutable bitmaps.options.inMutable = true;if (cache != null) {// Try to find a bitmap to use for inBitmap.Bitmap inBitmap = cache.getBitmapFromReusableSet(options);if (inBitmap != null) {// If a suitable bitmap has been found, set it as the value of// inBitmap.options.inBitmap = inBitmap;}} }// This method iterates through the reusable bitmaps, looking for one // to use for inBitmap: protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {Bitmap bitmap = null;if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {synchronized (mReusableBitmaps) {final Iterator<SoftReference<Bitmap>> iterator= mReusableBitmaps.iterator();Bitmap item;while (iterator.hasNext()) {item = iterator.next().get();if (null != item && item.isMutable()) {// Check to see it the item can be used for inBitmap.if (canUseForInBitmap(item, options)) {bitmap = item;// Remove from reusable set so it can't be used again.iterator.remove();break;}} else {// Remove from the set if the reference has been cleared.iterator.remove();}}}}return bitmap; }

最后,此方法判斷是否有候選的位圖滿足被inBitmap使用的尺寸標(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)用的方法。它尋找一個存在的位圖并為之設(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é)是一個不錯的方法。可以用PagerAdapter支持的ViewPager來實(shí)現(xiàn)此模式。然而,更加適合的支持適配器是FragmentStatePagerAdapter的子類,此類能夠根據(jù)視圖從屏幕上消失與否的情況自動銷毀和保存在ViewPager中的Fragments,并能夠保持內(nèi)存使用量不上升。

注:如果只有少量的圖片并能夠確保它們不會超過應(yīng)用程序的內(nèi)存限制,直接使用PagerAdapter或FragmentPagerAdapter可能會更加適合。

以下代碼實(shí)現(xiàn)了ViewPager和其ImageView子視圖。主活動持有此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} }

希望您能夠注意這個問題:讀圖片的操作在用戶界面線程中實(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 }

將這些代碼片段整合到一起就能夠得到一個具響應(yīng)性的、具圖片載入時最小延遲的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組件可以在同一時間顯示許多圖片,如果用戶滑動GridView的滾動條就需要做更多的準(zhǔn)備來實(shí)現(xiàn)GridView中的圖片的顯示。在實(shí)現(xiàn)此種類型的控制時,必須確保用戶界面的流暢性、內(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;}} }

問題在于,將這個過程的實(shí)現(xiàn)放在了用戶界面線程中。在圖片量較小時,此代碼能夠正常工作。如果有更多的圖片參與,那么用戶界面可能會被掛起。

可以使用上一節(jié)使用的異步和緩存的方法來解決這個問題。然而,咱還需要為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提供了顯示高端動畫圖形的功能(只有您想不到,無做不到),并且還能夠讓您收益于安卓設(shè)備上的圖形處理單元(GPUs)加速處理圖形的好處。

此部分內(nèi)容將帶您使用OpenGL來開發(fā)一個基本的應(yī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)建一個OpenGL ES 環(huán)境

學(xué)習(xí)如何建立一個可以繪制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)于兩個類的信息見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)用程序活動中如何完成的GLSurfaceView和FLSurfaceView.Render的最小實(shí)現(xiàn)。

(1) 在清單文件中聲明OpenGL ES
欲在應(yīng)用程序中使用OpenGL ES 2.0 API,必須在清單文件中作如下聲明:

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

如果應(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)建活動
使用OpenGL ES的安卓應(yīng)用程序跟其它應(yīng)用程序一樣有用戶界面所對應(yīng)的活動。主要不同在于往活動的布局文件中所添加的東西。在其它的應(yīng)用程序中的布局文件中往往可能包含TexView、Button或ListView,在使用OpenGL ES的應(yīng)用程序中,還會往布局文件中添加GLSurfaceView。

以下代碼樣例是用GLSurfaceView作為原始視圖的活動的一個最小實(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是一個可以繪制OpenGL ES圖形的特殊視圖。此視圖本身不會為繪圖做太多。實(shí)際控制繪制對象的是設(shè)置在此視圖上的GLSurfaceView.Renderer。實(shí)際上,創(chuàng)建此對象的代碼量很少,您可能想跳過擴(kuò)展代碼而只創(chuàng)建一個GLSurfaceView實(shí)例,但不要如此。需要擴(kuò)展此類來獲取觸摸事件,此在“響應(yīng)屏幕觸摸”一節(jié)中講述過。

實(shí)現(xiàn)GLSurfaceView的必要的代碼很少,所以能夠快速實(shí)現(xiàn),它通常作為使用它的活動的內(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)容有改變時用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()時GLSurfaceView的重復(fù)繪制,在本樣例程序中這是一種更為高笑的方法。

(4) 構(gòu)建渲染器(Renderer)類
在應(yīng)用程序中實(shí)現(xiàn)GLSurfaceView.Renderer或renderer類使得使用OpenGL ES變得有趣。此類控制往與此類關(guān)聯(lián)的GLSurfaceView中的繪制內(nèi)容。渲染器類中有3個方法會被安卓系統(tǒng)調(diào)用以推測出怎么繪制GLSurfaceView以及往其中繪制的內(nèi)容:
- onSurfaceCreate() - 被調(diào)用一次,用來設(shè)置視圖的OpenGL ES環(huán)境。
- onDrawFrame() - 在每次繪制重新繪制視圖時都會被調(diào)用。
- onSurfaceChanged() - 在視圖形狀改變時會被調(diào)用,如當(dāng)設(shè)備屏幕方向改變時。

以下是一個OpenGL ES渲染器的一個非常基本的實(shí)現(xiàn),它為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);} }

以上的代碼示例創(chuàng)建了一個用OpenGL來簡單的顯示一個黑色背景的安卓應(yīng)用程序。并未做一些更有趣的事情,不過現(xiàn)在您已經(jīng)具有了OpenGL的基礎(chǔ),那么您就可以開始用OpenGL來開始繪制圖形元素了。

注:在使用OpenGL ES 2.0 APIs時,您可能想知道為什么這些方法需要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)處理:

public class Triangle {private FloatBuffer vertexBuffer;// number of coordinates per vertex in this arraystatic final int COORDS_PER_VERTEX = 3;static float triangleCoords[] = { // in counterclockwise order:0.0f, 0.622008459f, 0.0f, // top-0.5f, -0.311004243f, 0.0f, // bottom left0.5f, -0.311004243f, 0.0f // bottom right};// Set color with red, green, blue and alpha (opacity) valuesfloat color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };public Triangle() {// initialize vertex byte buffer for shape coordinatesByteBuffer bb = ByteBuffer.allocateDirect(// (number of coordinate values * 4 bytes per float)triangleCoords.length * 4);// use the device hardware's native byte orderbb.order(ByteOrder.nativeOrder());// create a floating point buffer from the ByteBuffervertexBuffer = bb.asFloatBuffer();// add the coordinates to the FloatBuffervertexBuffer.put(triangleCoords);// set the buffer to read the first coordinatevertexBuffer.position(0);} }

默認(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)系是以逆時針為順序。繪制的順序非常重要因?yàn)樗x那一邊是形狀的正面(正面會被繪制)以及哪一邊是形狀的背面(根據(jù)OpenGL ES剔除的特性,背面不會被繪制)。更多關(guān)于面(facing)和剔除(culling)見OpenGL ES 開發(fā)手冊。

(2) 定義矩形
在OpenGL中定義三角形相當(dāng)簡單,但當(dāng)定義圖形變得稍加復(fù)雜時應(yīng)該怎么定義?比如如,一個矩形。有幾種方式可以定義矩形,在OpenGL定義矩形最為典型的方式是定義兩個三角形來形成矩形。

圖1. 用兩個三角形繪制矩形

需要以逆時針的順序來定義組成矩形的兩個三角形,并將坐標(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);} }

此例給了一個怎么用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í)行作初始化工作。

public class MyGLRenderer implements GLSurfaceView.Renderer {...private Triangle mTriangle;private Square mSquare;public void onSurfaceCreated(GL10 unused, EGLConfig config) {...// initialize a trianglemTriangle = new Triangle();// initialize a squaremSquare = new Square();}... }

(2) 繪制形狀
繪制用OpenGL ES 2.0定義的形狀需要大量代碼,因?yàn)楸仨殲閳D形渲染管道提供許多細(xì)節(jié)。尤其是需要定義以下介個方面內(nèi)容:
- 頂點(diǎn)著色(Vertex Shader) - 渲染形狀頂點(diǎn)的OpenGL ES 圖形代碼。
- 片段著色(Fragment Shader) - 用顏色或紋理來渲染形狀各面的OpenGL ES代碼。
- 程序(Program) - 用包含著色的OpenGL ES 對象來繪制一個或多個形狀。

至少需要一個頂點(diǎn)著色來繪制形狀一個片段著色來為形狀著色。這些著色器必須被編譯然后增添到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)建一個方法來編譯以上著色器的代碼:

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ù)中完成這個過程,這樣此過程就只會被執(zhí)行一次。

注:編譯OpenGL ES著色器和連接程序?qū)τ贑PU周期來和處理時間來說是比較耗時的操作,所以要避免此操作被執(zhí)行多次。如果在運(yùn)行時不知著色器的內(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);} }

此時,到了可以調(diào)用方法來繪制形狀的時候了。用OpenGL ES需要用幾個參數(shù)來告知渲染管道將繪制的內(nèi)容并如何繪制它們。因?yàn)槔L制過程由形狀決定,所以在圖形類中包含圖形的繪制邏輯是個不錯的主意。

創(chuàng)建一個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ī)視圖下繪制的三角形

在以上代碼樣例中存在幾個問題。第一,此運(yùn)行結(jié)果不會給人留下深刻印象。第二,當(dāng)改變設(shè)備屏幕方向時三角形會有些變形。會變形的原因時對象的頂點(diǎn)沒有隨著屏幕變化而變化。可以通過下一節(jié)介紹的投影和相機(jī)視圖來解決這個問題。

第三,圖中的三角形是固定不動的,這顯得有些無聊。在“增添運(yùn)動”這一節(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()方法中的視圖比例改變時,投影變換將會重新計(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)用程序的動態(tài)改變時,相機(jī)視圖可能只會被計(jì)算一次。

此節(jié)描述如何創(chuàng)建投影和相機(jī)以及如何在GLSurfaceView中應(yīng)用它們。

(1) 定義投影
投影變換的數(shù)據(jù)在GLSurfaceView.Renderer類中的onSurfaceChanged()方法中計(jì)算。以下樣例代碼根據(jù)GLSurfaceView的高度和寬度用Matrix.frustumM()方法計(jì)算投影變換的Matrix:

// mMVPMatrix is an abbreviation for "Model View Projection Matrix" private final float[] mMVPMatrix = new float[16]; private final float[] mProjectionMatrix = new float[16]; private final float[] mViewMatrix = new float[16];@Override public void onSurfaceChanged(GL10 unused, int width, int height) {GLES20.glViewport(0, 0, width, height);float ratio = (float) width / height;// this projection matrix is applied to object coordinates// in the onDrawFrame() methodMatrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7); }

此段代碼計(jì)算了投影矩陣mProjectionMatrix,相機(jī)視圖將在onDrawFrame()方法中使用此矩陣,此將在下一節(jié)中介紹。

注:只對繪制對象應(yīng)用投影一般會導(dǎo)致圖形消失。通常,為了將圖形重新顯示在屏幕上還需要使用相機(jī)視圖。

(2) 定義相機(jī)視圖
為繪制對象增加相機(jī)視圖變換方才算完成了圖形的變換。在以下代碼示例中,在Matrix.setLookATM()方法中完成相機(jī)試圖變換并結(jié)合之前的投影變換矩陣。再將兩種變換結(jié)合得到矩陣傳遞給繪制對象:

@Override public void onDrawFrame(GL10 unused) {...// Set the camera position (View matrix)Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);// Calculate the projection and view transformationMatrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);// Draw shapemTriangle.draw(mMVPMatrix); }

(3) 請求投影和相機(jī)變換
欲結(jié)合投影和相機(jī)視圖變換矩陣,首先需要在之前的三角形類中定義頂點(diǎn)著色器矩陣:

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

然后,修改繪制對象的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)用正確的比例繪制圖形了,該往形狀增添動畫了。

2.5 增加運(yùn)動

學(xué)習(xí)如何用OpenGL來實(shí)現(xiàn)所繪對象基本的移動和動畫。

將對象繪制在屏幕之上只是OpenGL最基本的特定,安卓其它的圖形框架類如Canvas及Drawable也能夠完成此項(xiàng)工作。OpenGL ES還為圖形提供了移動、變換圖形到三維空間以及創(chuàng)造令人信服的用戶體驗(yàn)的功能。

此節(jié)將繼續(xù)學(xué)習(xí)OpenGL ES來通過旋轉(zhuǎn)的方式移動圖形。

(1) 旋轉(zhuǎn)圖形
用OpenGL ES 2.0來選中繪制對象比較簡單。在渲染器中創(chuàng)建一個轉(zhuǎn)換矩陣(旋轉(zhuǎn)矩陣)并將其跟投影和相機(jī)視圖變換矩陣結(jié)合到一塊:

private float[] mRotationMatrix = new float[16]; public void onDrawFrame(GL10 gl) {float[] scratch = new float[16];...// Create a rotation transformation for the trianglelong time = SystemClock.uptimeMillis() % 4000L;float angle = 0.090f * ((int) time);Matrix.setRotateM(mRotationMatrix, 0, angle, 0, 0, -1.0f);// Combine the rotation matrix with the projection and camera view// Note that the mMVPMatrix factor *must be first* in order// for the matrix multiplication product to be correct.Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);// Draw trianglemTriangle.draw(scratch); }

如果做了這些改變后三角形仍舊還沒有旋轉(zhuǎn),確保對GLSurfaceView.RENDERMODE_WHEN_DIRTY進(jìn)行了注釋,此內(nèi)容在下一節(jié)討論。

(2) 啟用連續(xù)渲染
如果您已孜孜不倦地昨晚了樣例代碼中的所有內(nèi)容,確保注釋了設(shè)置渲染模式為只在臟(dirty)才繪制的一行代碼,否則OpenGL只做一次旋轉(zhuǎn)然后就等待調(diào)用GLSurfaceView容器中的requestRender()方法:

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

除非在無用戶交互的情況下對象還有轉(zhuǎn)動,否則應(yīng)該將此語句的注釋去掉。做好取消此語句注釋的準(zhǔn)備,因?yàn)橄乱还?jié)將會在程序中使用此語句。

2.6 響應(yīng)觸摸事件

學(xué)習(xí)怎么和OpenGL圖形實(shí)現(xiàn)基本的互動。

移動預(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)的角度。

private final float TOUCH_SCALE_FACTOR = 180.0f / 320; private float mPreviousX; private float mPreviousY;@Override public boolean onTouchEvent(MotionEvent e) {// MotionEvent reports input details from the touch screen// and other input controls. In this case, you are only// interested in events where the touch position changed.float x = e.getX();float y = e.getY();switch (e.getAction()) {case MotionEvent.ACTION_MOVE:float dx = x - mPreviousX;float dy = y - mPreviousY;// reverse direction of rotation above the mid-lineif (y > getHeight() / 2) {dx = dx * -1 ;}// reverse direction of rotation to left of the mid-lineif (x < getWidth() / 2) {dy = dy * -1 ;}mRenderer.setAngle(mRenderer.getAngle() +((dx + dy) * TOUCH_SCALE_FACTOR));requestRender();}mPreviousX = x;mPreviousY = y;return true; }

注意在計(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)角度
以上代碼樣例需要通過增加一個公有的變量來揭露渲染器的旋轉(zhuǎn)角度。由于渲染代碼運(yùn)行于獨(dú)立于用戶主線程的線程中,必須將此變量聲明為volatile。以下代碼聲明了此變量且揭露了獲取和設(shè)置方法:

public class MyGLRenderer implements GLSurfaceView.Renderer {...public volatile float mAngle;public float getAngle() {return mAngle;}public void setAngle(float angle) {mAngle = angle;} }

(3) 應(yīng)用旋轉(zhuǎn)
欲應(yīng)用通過觸摸輸入產(chǎn)生的旋轉(zhuǎn),注釋掉產(chǎn)生角度的代碼并增加mAngle變量,此變量包含了觸摸事件生成的旋轉(zhuǎn)角度:

public void onDrawFrame(GL10 gl) {...float[] scratch = new float[16];// Create a rotation for the triangle// long time = SystemClock.uptimeMillis() % 4000L;// float angle = 0.090f * ((int) time);Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);// Combine the rotation matrix with the projection and camera view// Note that the mMVPMatrix factor *must be first* in order// for the matrix multiplication product to be correct.Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);// Draw trianglemTriangle.draw(scratch); }

當(dāng)完成以上描述的所有步驟后,運(yùn)行程序并用手指再屏幕上滑動來選擇三角形,運(yùn)行結(jié)果會類似下圖:

圖1. 觸摸輸入選擇三角形(圓圈表示觸摸位置)

2015.11.16

3. 使用場景和變換來實(shí)現(xiàn)動畫視圖

在視圖層次如何用轉(zhuǎn)換來讓動畫狀態(tài)改變。

活動對應(yīng)的用戶界面會常會因?yàn)轫憫?yīng)用戶輸入或其它事件而變化。例如,包含供用戶輸入查詢內(nèi)容的查詢條在用戶提交后可以隱藏查詢條而呈現(xiàn)查詢結(jié)果。

欲在這些情形下提供視覺上的連續(xù),可以在用戶界面的不同視圖層次中作動畫般的改變。這些動畫給以用戶動作上的響應(yīng)并幫助用戶學(xué)習(xí)應(yīng)用程序是怎么工作的。

安卓包含變換框架,此框架能夠很易在兩個視圖層次間作動畫改變。框架在運(yùn)行時通過改變視圖的某些特性來動畫視圖。框架中既包含針對于常見效果的內(nèi)建動畫也允許開發(fā)者自定義動畫和變換生命周期回調(diào)方法。

此節(jié)教您使用變換框架內(nèi)的內(nèi)建動畫來動畫改變兩個不同視圖層次的視圖。此節(jié)同樣包含如何創(chuàng)建自定義動畫。

注:對于在4.0(API level 14)和4.4.2(API level 19)的安卓版本,使用animateLayoutChanges屬性來動畫布局。欲獲取更多信息,見Property Animation及Animating Layout Changes。

3.1 變換框架

學(xué)習(xí)變換框架主要的特性和組件。

動畫應(yīng)用程序用戶界面不僅是視覺上的呼吁。動畫強(qiáng)調(diào)改變且提供了應(yīng)用程序是如何工作的視覺線索。

欲幫助開發(fā)者動畫在兩個視圖層次的改變,安卓提供了變換框架。此框架能應(yīng)用一個或多個動畫到有改變的層次中的所有視圖之間。

框架有以下特性:
組-級動畫
應(yīng)用一個或多個動畫去影響視圖層次中的所有視圖。

變換-基礎(chǔ)動畫動畫的運(yùn)行基于視圖特性值改變開始和結(jié)束之間。內(nèi)建動畫包含具常見印象的預(yù)定義動畫,諸如漸弱或移動。資源文件支持從布局資源文件載入視圖層和內(nèi)建動畫。生命周期定義回調(diào)方法提供堆動畫和層改變處理的更好控制。

(1) 概要
圖1的圖例展示動畫是如何提供視覺線索來幫助用戶的。當(dāng)應(yīng)用程序從搜索條屏幕改變到搜索結(jié)果屏幕時,屏幕漸弱不再使用的視圖而漸現(xiàn)幾個新的視圖。

用戶動畫界面:http://developer.android.com/images/transitions/transition_sample_video.mp4
圖1. 視覺線索使用用戶界面動畫。點(diǎn)擊設(shè)備屏幕放映動畫。

2015.11.16
此動畫是使用變換框架的一個例子。框架動畫改變兩個視圖層次中的所有視圖。一個視圖層次可以簡單得只有一個視圖也可以復(fù)雜到像ViewGroup包含復(fù)雜的視圖樹。框架在視圖層的開始和結(jié)束期間通過改變視圖的特性值來動畫每個視圖。

變換框架以并行的方式工作于視圖層和動畫。框架的目的是存儲視圖層的狀態(tài),在這些層之間作改變以修改屏幕的顯示,通過存儲和應(yīng)用動畫定義進(jìn)行動畫改變。

圖2中所示的框圖能夠說明視圖層、框架對象以及動畫之間的關(guān)系:

圖2. 變換框架各部分之間的關(guān)系

變換框架為場景、變換以及變換方式提供了抽象的理念。在后續(xù)節(jié)中將會詳細(xì)描述三者。欲使用此框架,在應(yīng)用程序中為計(jì)劃改變的視圖層創(chuàng)建場景。然后,為欲使用的各個動畫創(chuàng)建變換。欲在兩個視圖層之間開始動畫,用變換方法來制定欲使用的變換和結(jié)束場景。此過程在此節(jié)余留部分詳細(xì)講解。

(2) 場景
場景用來存儲視圖層的狀態(tài),包括所有視圖以及它們的特性值。一個視圖可能是簡單的或是視圖和其子視圖的復(fù)雜的樹視圖。在場景中保存視圖狀態(tài)能夠使得從另外的場景變換到此種狀態(tài)。框架提供了Scene類來呈現(xiàn)場景。

變換框架能夠根據(jù)布局資源文件或代碼中的ViewGroup對象來創(chuàng)建場景。如果動態(tài)的創(chuàng)建或運(yùn)行時修改視圖層,那么在代碼中創(chuàng)建場景會很有用。

在大多數(shù)情況下,不會精確的創(chuàng)建開始場景。如果已經(jīng)應(yīng)用了變換,框架用之前的結(jié)束場景作為后續(xù)變換的開始場景。如果并未應(yīng)用變換,框架將會從屏幕當(dāng)前狀態(tài)收集 的信息。

也可以為場景定義場景自己的動作,當(dāng)場景改變時將會運(yùn)行此些動作。例如,在變換場景之后親你管理視圖設(shè)置。

除視圖層和其屬性值之外,場景還存儲父視圖層的引用。根視圖被稱為scene root。改變場景和動畫會影響根場景下的場景。

更多關(guān)于創(chuàng)建場景的信息見“創(chuàng)建場景”。

(3) 變換
在變換框架中,動畫創(chuàng)造了一系列描述各視圖層在開始和結(jié)束場景之間變化的框架。關(guān)于動畫的信息被保存在Transition對象中。用TransitionManager實(shí)例運(yùn)行動畫。框架能夠在不用場景之間變換也能夠在同一個場景的不同狀態(tài)間變換。

框架包含了一套用于常見動畫效果的內(nèi)建變換,如漸變和調(diào)整視圖尺寸。也可以用動畫框架中的APIs來自定義變換以創(chuàng)建動畫效果。變換框架同樣允許聯(lián)合包含內(nèi)建或自定義變換組的變換集中的不同的動畫。

變換的生命周期類似活動的生命周期,在動畫開始和完成期間由框架監(jiān)控生命周期對應(yīng)的變換狀態(tài)。一個重要的生命周期狀態(tài), 在變換階段可以實(shí)現(xiàn)框架會調(diào)用的回調(diào)方法來調(diào)整用戶界面。

更多關(guān)于變換的信息,見Applying a Transition及Creating Custom Transitions。

(4) 限制
以下理解了變換框架的一些知名的限制:
- 動畫應(yīng)用到SurfaceView可能不會正確的顯示。SurfaceView實(shí)例在非用戶界面線程中更新,所以更新可能超出其它視圖的動畫的異步范圍。
- 當(dāng)應(yīng)用于TextureView時,一些特殊的變換類型可能不會產(chǎn)生應(yīng)有的動畫效果。
- 從AdapterView擴(kuò)展類,如ListView,管理子視圖的方法與變化框架不兼容。如果在AdapterView上實(shí)現(xiàn)動畫視圖,設(shè)備顯示可能會被掛起。
- 如果通過動畫來調(diào)整TextView的尺寸,在對象尺寸被調(diào)整完成之前文本將會突然跑到一個新的區(qū)域。欲避免此問題,不要用動畫調(diào)整包含文本的視圖。

3.2 創(chuàng)建場景

學(xué)習(xí)如何創(chuàng)建場景來存儲視圖層次的狀態(tài)。

場景存儲視圖層的狀態(tài),包括所有視圖和其特性值。變換框架在開始場景和結(jié)束場景之間運(yùn)行動畫。開始場景從用戶當(dāng)前界面的當(dāng)前狀態(tài)獲得。對于結(jié)束場景,框架讓開發(fā)者從布局資源文件或代碼中的一組視圖創(chuàng)建結(jié)束場景。

此節(jié)演示如何在應(yīng)用程序中創(chuàng)建場景以及如何定義場景動作。下一節(jié)將演示如何在兩個場景之間變換。

注:框架能夠在一個無場景的視圖層中動畫改編,在Apply a Transition Without Scenes一節(jié)中已描述過。然而,理解此節(jié)內(nèi)容對于變換來說是必要的。

(1) 從布局資源創(chuàng)建場景
可以根據(jù)布局資源文件直接創(chuàng)建場景。當(dāng)文件中的視圖層大多是靜態(tài)時可以使用此技術(shù)。場景實(shí)例中的結(jié)果代表視圖層的某時刻的狀態(tài)。欲改變視圖層,就需要重建場景。框架根據(jù)文件中的整個視圖層來創(chuàng)建場景;不能只根據(jù)布局文件的某一部分創(chuàng)建場景。

欲根據(jù)布局資源文件創(chuàng)建場景,檢索ViewGroup實(shí)例布局文件的場景根,然后用包含視圖層的布局文件的場景根和資源ID調(diào)用Scene.getSceneForLayout()方法來創(chuàng)建場景。

[1] 為場景定義布局
此節(jié)后續(xù)部分代碼將演示如何用相同的場景根元素來創(chuàng)建兩個不同的場景。這些代碼片段同時演示在不用聲明場景彼此的相關(guān)性而載入多個不相關(guān)的場景對象。

樣例有以下的布局定義組成:
- 活動的擁有一個文本標(biāo)簽和子布局的主布局文件。
- 第一個場景的有兩個文本域的關(guān)系布局。
- 第二個場景的擁有與第一個布局相同內(nèi)容但內(nèi)容不同順序的關(guān)系布局。

樣例設(shè)計(jì)來讓動畫發(fā)生在活動的主布局的子布局中。在主布局中的文本標(biāo)簽仍然是靜態(tài)的。

活動的主布局定義如下:
res/layout/activity_main.xml

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

布局文件的定義包含了一個文本域和一個場景根的子布局。第一個場景的布局文件被包含在了主布局文件中。此允許應(yīng)用程序?qū)⒋俗鳛橛脩艚缑娴囊徊糠謥碚故就瑫r也能夠?qū)⒋溯d入到場景中,因?yàn)榭蚣苤荒軐⒄麄€布局文件載入場景中。

第一個場景的布局文件定義如下:
res/layout/a_scene.xml

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

擁有與第一個場景布局文件相同的文本域(相同的ID)但放置順序不同的第二個場景的布局文件內(nèi)容如下:
res/layout/another_scene.xml

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

[2] 根據(jù)布局生成場景
為兩個關(guān)系布局創(chuàng)建定義之后,就可以為每個布局文件各獲取一個場景了。這能夠使得稍后在兩個用戶界面配置之間作變換。欲獲得場景,需要場景根的引用和布局資源ID。

以下代碼片段展示了如何獲取場景根的引用,并根據(jù)布局文件創(chuàng)建兩個場景對象:

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

此時,在應(yīng)用程序中已有基于視圖層的兩個場景對象。兩個場景都使用在res/layout/activity_main.xml中被FrameLayout元素定義的場景根。

(2) 在代碼中創(chuàng)建場景
也可以根據(jù)ViewGroup對象用代碼創(chuàng)建場景實(shí)例。當(dāng)用代碼直接修改視圖層或動態(tài)創(chuàng)建視圖時可使用此項(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)建一個視圖實(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)建場景動作
框架允許定義在場景進(jìn)入運(yùn)行或場景退出運(yùn)行時的自定義場景動作。在許多情況下,自定義場景動作都沒必要,因?yàn)榭蚣茉趫鼍爸g自動的動畫改變。

場景動作在處理以下情況中顯得有用:
- 動畫視圖不在相同的視圖層上。用進(jìn)入或退出場景動作引起場景開始或結(jié)束以使用動畫視圖。
- 變換框架不能自動進(jìn)行動畫視圖,如ListView對象。更多信息見Limitations。

欲提供自定義的場景動作,以Runnable對象定義動作并將動作傳遞給Scene.setExitAction()或Scene.setEnterAction()方法。框架在開始場景即運(yùn)行變換動畫之前調(diào)用setExitAction()方法,在結(jié)束場景即運(yùn)行變換動畫之后調(diào)用setEnterAction()方法。

注:不要用場景動作在開始視圖和結(jié)束視圖之間傳遞數(shù)據(jù)。更多信息見Defining Transition Lifecycle Callbacks。

2015.11.17

3.3 請求轉(zhuǎn)換

學(xué)習(xí)如何變換視圖層次的兩個場景。

在變換框架中,動畫創(chuàng)建了一系列描繪在開始和結(jié)束場景中的視圖層的改變的幀。框架代表的動畫作為變換對象,其中包含動畫的信息。欲運(yùn)行動畫,需提供要使用的變換以及結(jié)束場景給變換方式。

此節(jié)向您展示用內(nèi)建的變換在兩個場景動畫即移動、調(diào)整尺寸以及漸褪視圖。下一節(jié)將向您演示如何自定義變換。

(1) 創(chuàng)建變換
在上一節(jié)中,您學(xué)會了如何創(chuàng)建代表不同視圖層狀態(tài)的場景。一旦定義了欲改變的開始場景和結(jié)束場景,就再需要創(chuàng)建一個定義動畫的變換對象。框架能夠在資源文件中指定內(nèi)建的變換并且能將此關(guān)聯(lián)到到代碼中或直接在代碼中定義一個內(nèi)建變換的實(shí)例。

[1] 根據(jù)資源文件創(chuàng)建變換實(shí)例
此項(xiàng)技術(shù)能夠在不修改活動代碼的情況下就能夠修改變換定義。此項(xiàng)技術(shù)也能夠?qū)?fù)雜的變換定義跟應(yīng)用程序代碼獨(dú)立開來,如 Specify Multiple Transitions中所述。

欲在資源文件中指定內(nèi)建變換,跟隨以下步驟:
- 增加/res/transition/目錄到工程 中。
- 在剛所建的目錄中新建一個XML資源文件。
- 將內(nèi)建變換作為節(jié)點(diǎn)添加到XML文件中。

例如,以下資源文件指定了消退(Fade)變換:
res/transition/fade_transition.xml

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

以下代碼片段演示如何將資源文件中的變換實(shí)例關(guān)聯(lián)到活動的代碼中:

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

[2] 在代碼中創(chuàng)建變換實(shí)例
此項(xiàng)技術(shù)對于在代碼中修改用戶界面動態(tài)創(chuàng)建變換對象非常有用,對于創(chuàng)建簡單的內(nèi)建變換實(shí)例不需要或只需要很少的參數(shù)。

欲創(chuàng)建內(nèi)建變換實(shí)例,調(diào)用Transition類子類的其中一個構(gòu)造函數(shù)即可。例如,以下代碼片段創(chuàng)建了一個消退(fade)變換的實(shí)例:

Transition mFadeTransition = new Fade();

(2) 請求變換
一般來說,變換應(yīng)用于改變不同的視圖層以響應(yīng)諸如用戶動作這樣的事件。例如,一個搜索應(yīng)用程序:當(dāng)用戶在搜索條中輸入內(nèi)容并點(diǎn)擊搜索按鈕時,當(dāng)應(yīng)用消退(fade)變換時,應(yīng)用程序改變到顯示搜索結(jié)果的場景之上,在此場景中搜索條消失不見。

當(dāng)應(yīng)用變換來響應(yīng)活動中的某些事件來實(shí)現(xiàn)場景變換,需要用結(jié)束場景和變換實(shí)例來調(diào)用TransitionManager.go()靜態(tài)方法來實(shí)現(xiàn)動畫,代碼如下所示:

TransitionManager.go(mEndingScene, mFadeTransition);

在根據(jù)指定變換實(shí)例運(yùn)行動畫時,框架根據(jù)結(jié)束場景的視圖層改變場景根下的視圖層。開始場景為上一次變換的結(jié)束場景。如果之前無任何變換,系統(tǒng)根據(jù)用戶界面當(dāng)前狀態(tài)作為開始場景。

如果沒有指定變換實(shí)例,變換管理器能夠?qū)⒆詣討?yīng)用一個能夠響應(yīng)大多數(shù)情形的變換。更多信息見API參考TransitionManager類。

(3) 選擇特定的目標(biāo)視圖
框架應(yīng)用變換到默認(rèn)的開始場景和結(jié)束場景中的所有視圖。在某些清醒下,可能只想將變換應(yīng)用到場景中的某部分子視圖上。例如,框架不支持ListView對象的動畫改變,所以在變換期間不會動畫ListView對象。框架能夠只選擇欲動畫的部分視圖。

將每個會進(jìn)行變換的視圖稱作目標(biāo)。只能將場景視圖層中的某些視圖作為目標(biāo)。

欲從目標(biāo)列表中移除一個或多個視圖,在開始變換前調(diào)用removeTarget()方法。欲增加視圖到目標(biāo)列表中,調(diào)用addTart()方法。更多信息見API 參考Transition類。

(4) 指定多個變換
欲獲得動畫的最大效果,應(yīng)將動畫跟場景間的變化匹配。例如,如果您正在場景間移除某些視圖的同時又在增添其它的視圖,消退(fade out)/消失(fade in)動畫提供某些視圖不在可用的顯著提示。如果您正將視圖移到屏幕的不同點(diǎn)處,一個更好的選擇時動畫移動以讓用戶注意視圖的新位置。

不必只選擇一個動畫,因?yàn)樽儞Q框架能夠在包含內(nèi)建或自定義變換組的變換集中結(jié)合動畫效果。

欲根據(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對象中,在活動中調(diào)用 TransitionInflater.from()方法。TransitionSet類從Transition類繼承,所以可以像其它變換實(shí)例一樣用變換管理器來使用它。

(5) 請求無場景變換
改變視圖層不是修改用戶界面的唯一方式。亦可以在當(dāng)前層次中通過增加、修改以及移除子視圖的方式改變用戶界面。例如,可以將搜索條實(shí)現(xiàn)在一個單獨(dú)的布局文件中。開始顯示搜索條。欲改變用戶界面來顯示搜索結(jié)果,當(dāng)用戶點(diǎn)擊搜索條時調(diào)用ViewGroup.removeView()方法來將搜索條移除,并通過調(diào)用ViewGroup.addView()方法將搜索結(jié)果增加到用戶界面中。

如果會相互替代的兩層的內(nèi)容幾乎相同可用此方法來實(shí)現(xiàn)用戶界面的改變。否則,還是創(chuàng)建動畫來實(shí)現(xiàn)用戶界面的改變,這樣就可以只用包含可在代碼中修改的視圖層的一個布局文件。

如果以上述方法來改變當(dāng)前視圖層,就不再需要創(chuàng)建場景。可以創(chuàng)建并應(yīng)用延遲變換到視圖層的兩個狀態(tài)中。框架的這個特點(diǎn)開始于當(dāng)前視圖狀態(tài),記錄對視圖作的改變,當(dāng)系統(tǒng)重繪用戶界面是變換將動畫改變。

欲在單個視圖中創(chuàng)建延遲變換,需以下步驟:
[1]. 當(dāng)觸發(fā)變換的事件發(fā)生時,調(diào)用方法來提供欲用變換來改變的所有子視圖的父視圖。框架存儲子視圖當(dāng)前的狀態(tài)和特性值。
[2]. 欲改變子視圖需要用戶使用子視圖。框架記錄用戶對子視圖所作的改變及其特性。
[3]. 當(dāng)系統(tǒng)根據(jù)改變重繪用戶界面時,框架從原始狀態(tài)動畫改變到新狀態(tài)。

以下代碼樣例展示如何使用延遲變換將一個文本視圖動畫的添加到視圖層中。第一段代碼片段展示的布局文件中的定義:
res/layout/activity_main.xml

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

第二個代碼片段展示動畫增加文本視圖的過程:
MainActivity.java

private TextView mLabelText; private Fade mFade; private ViewGroup mRootView; ...// Load the layout this.setContentView(R.layout.activity_main); ...// Create a new TextView and set some View properties mLabelText = new TextView(); mLabelText.setText("Label").setId("1");// Get the root view and create a transition mRootView = (ViewGroup) findViewById(R.id.mainLayout); mFade = new Fade(IN);// Start recording changes to the view hierarchy TransitionManager.beginDelayedTransition(mRootView, mFade);// Add the new TextView to the view hierarchy mRootView.addView(mLabelText);// When the system redraws the screen to show this update, // the framework will animate the addition as a fade in

(6) 定義變換聲明周期回調(diào)方法
變換的生命周期類似活動的生命周期。它代表框架在調(diào)用TransitionManager.go()方法和完成動畫期間所監(jiān)控的變換的狀態(tài)。在重要的生命周期狀態(tài)中,框架調(diào)用被TransitionListener接口定義的回調(diào)方法。

變換的生命周期回調(diào)函數(shù)很有用,例如,在場景改變時復(fù)制從開始場景到結(jié)束場景某個視圖的特性值。不可簡單的在視圖層中復(fù)制開始視圖和結(jié)束視圖,因?yàn)榻Y(jié)束視圖層在變換完成前沒有被關(guān)聯(lián)。科學(xué)的做法是,將值保存在某變量中在框架完成變換后再將此變量拷貝到結(jié)束場景中。欲獲得變換結(jié)束的通知,在活動中實(shí)現(xiàn)TransitionListener.onTransitionEnd()方法。

更多信息見API參考TransitionListener類。

3.4 創(chuàng)建自定義變換

學(xué)習(xí)如何創(chuàng)建不屬于變換框架中的其它的動畫效果。

自定義變換創(chuàng)建的動畫對于任何內(nèi)建變換類都不可用。例如,可以定義一個變換來返回文本的前景色并將輸入域設(shè)置為灰色以按時此域在屏幕上已經(jīng)失去了輸入功能。這中效果將幫助用戶理解此域失去了輸入功能。

就像內(nèi)建變換類型一樣,自定義的變換可以應(yīng)用動畫到開始和結(jié)束場景中的子視圖中。然而,也不像內(nèi)建變換類型,需要提供來獲取特性值和產(chǎn)生動畫的代碼。也可以圍動畫定義視圖目標(biāo)的子集。

此節(jié)教您獲取特性值和產(chǎn)生動畫來創(chuàng)建自定義變換。

(1) 擴(kuò)展變換類
欲創(chuàng)建自定義變換,增加擴(kuò)展Transition類的類到工程中并重寫方法,如以下代碼所示:

public class CustomTransition extends Transition {@Overridepublic void captureStartValues(TransitionValues values) {}@Overridepublic void captureEndValues(TransitionValues values) {}@Overridepublic Animator createAnimator(ViewGroup sceneRoot,TransitionValues startValues,TransitionValues endValues) {} }

后續(xù)內(nèi)容解釋如何重寫這些方法。

(2) 獲取視圖屬性值
變換動畫用屬性動畫中的屬性動畫系統(tǒng)。屬性動畫在開始和結(jié)束值中的一段特殊時間改變視圖屬性,所以框架需要屬性的開始和結(jié)束值來構(gòu)建動畫。

然而,屬性動畫通常只需要視圖屬性值的一個子集。例如,顏色動畫需要顏色屬性值,移動動畫需要位置屬性值。由于動畫所需的屬性值對于變化來說特殊,變化框架不會為變換提供每一個屬性值。取而代之的是,框架將調(diào)用可以為變換獲取變換所需的屬性值的回調(diào)方法并將屬性值存儲在框架中。

[1] 獲取開始值
欲傳遞開始視圖值給框架,需實(shí)現(xiàn)captureStartValues(transitionValues)方法。框架將調(diào)用此方法來獲取開始場景中的每一視圖。此方法的參數(shù)是一個Transitionvalues對象,器包含一個視圖引用和一個能夠存儲視圖值的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é)束場景中為每個目標(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)建自定義動畫
欲動畫視圖在其開始和結(jié)束場景中的改變,需要重寫createAnimator()方法來提供動畫器。當(dāng)框架調(diào)用此方法時,它將傳遞動畫器給場景根視圖和所捕獲的包含開始和結(jié)束場景給TransitionValues對象。

框架調(diào)用createAnimator()方法的次數(shù)基于開始和結(jié)束場景的改變。例如,自定義實(shí)現(xiàn)的消退/消失變換。如在開始場景中景中有5個目標(biāo)但在結(jié)束場景中會被移除兩個,那么在結(jié)束場景就只有三個目標(biāo),再在結(jié)束場景中添加一個新目標(biāo)。那么框架將會調(diào)用createAnimator()方法六次。三次調(diào)用用于兩個場景中的消退/消失;兩次調(diào)用為移除的目標(biāo)動畫;一次調(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)方法時,用捕獲到的視圖屬性值來創(chuàng)建Animator對象并將此返回給框架。一個實(shí)現(xiàn)的樣例,見自定義變換樣例中的ChangeColor類。更多關(guān)于屬性動畫的值 Property Animation見。

(4) 應(yīng)用自定義變換
自定義變換的工作機(jī)制跟內(nèi)建變換相同。可以用變換管理應(yīng)用于自定義變換,具體描述見 Applying a Transition。

4. 增加動畫

如何將漸變的動畫添加到用戶界面中。

動畫能夠?yàn)橥ㄖ脩絷P(guān)于應(yīng)用程序發(fā)生了啥增加微妙的視覺線索并且會增加應(yīng)用程序界面的心智模型(mental model)。當(dāng)屏幕改變狀態(tài)時動畫尤其有用,如當(dāng)內(nèi)容載入或新動作變得有用時。同時動畫也能夠?yàn)閼?yīng)用程序增加光滿的外觀,這可以給應(yīng)用程序一個更高質(zhì)量的感覺。

但仍需記住,過度的使用動畫或在錯誤的時間使用動畫也會帶來不利,如引起延遲。此節(jié)展示如何實(shí)現(xiàn)一些常見的能夠帶來實(shí)用并在不騷擾用戶的情況下增加流動性的動畫類型。

2015.11.18

4.1 兩視圖交叉淡入淡出

學(xué)習(xí)如何讓兩個重疊的視圖淡入淡出。此節(jié)展示如何讓進(jìn)度條淡入包含文本內(nèi)容的視圖。

淡入淡出動畫(亦稱溶解)漸漸的消退某用戶界面組件的同時漸入另一個組件。此動畫對于轉(zhuǎn)換內(nèi)容或視圖到應(yīng)用程序的情形很有用。淡入淡出非常微妙同時也很短暫但提供了屏幕到文本的流利的變換。當(dāng)不用淡入淡出動畫時,這些變換都會顯得有些突然。

此處有一個從進(jìn)度條淡出文本內(nèi)容的例子:http://developer.android.com/training/animation/anim_crossfade.mp4
淡入淡出動畫
點(diǎn)擊屏幕設(shè)備屏幕可放映動畫

如果您想跳過后續(xù)內(nèi)容并想看一個完整的代碼示例,下載并運(yùn)行樣例,選擇淡入淡出的例子。看一下幾個文件中的代碼實(shí)現(xiàn):
· src/CrossfadeActivity.java
· layout/activity_crossfade.xml
· menu/activity_crossfade.xml

(1) 創(chuàng)建視圖
創(chuàng)建欲淡入淡出的兩個視圖。以下代碼片段創(chuàng)建了一個進(jìn)度條和一個具滑動條的文本視圖:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><ScrollView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/content"android:layout_width="match_parent"android:layout_height="match_parent"><TextView style="?android:textAppearanceMedium"android:lineSpacingMultiplier="1.2"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/lorem_ipsum"android:padding="16dp" /></ScrollView><ProgressBar android:id="@+id/loading_spinner"style="?android:progressBarStyleLarge"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center" /></FrameLayout>

(2) 設(shè)置動畫
欲設(shè)置動畫,遵循以下步驟:
[1] 為欲淡入淡出的視圖創(chuàng)建成員變量。在動畫期間需要這些視圖的引用來修改視圖。
[2] 對于淡入的視圖,將其可見性設(shè)置為GONE。此值能夠避免視圖占據(jù)布局文件空間并忽略堆它們的計(jì)算以提高處理速度。
[3] 將config_shortAnimTime系統(tǒng)屬性緩存在成員變量中。此屬性為動畫定義了一個標(biāo)準(zhǔn)的“短”的持續(xù)時間。此持續(xù)時間對微妙的動畫或發(fā)生頻率較高的動畫比較理想。config_longAnimTime和config_mediumAnimTime也是可用的,如果您想用它們的話。

將前面代碼片段所定義的內(nèi)容作為以下代碼描述的活動的布局文件:

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] 對于淡入的視圖,動畫改變其alpha的值從0到1。同時,將淡出視圖的alpha值從1動畫改為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屏幕滑動

學(xué)習(xí)在滑動變換下動畫變化屏幕。

屏幕滑動是從一個屏幕頁面到另一個屏幕頁面的變換。此節(jié)內(nèi)容將展示如何用由support library提供的ViewPager來做到屏幕滑動變換。ViewPager自動掃描屏幕滑動。點(diǎn)擊下圖屏幕,由本頁屏幕會自動滑動到下一頁屏幕:

屏幕滑動動畫:http://developer.android.com/training/animation/anim_screenslide.mp4
屏幕滑動動畫
點(diǎn)擊屏幕設(shè)備屏幕可放映動畫

如果您想跳過后續(xù)內(nèi)容且想看完整的工程代碼,下載本節(jié)樣例應(yīng)用程序,選擇屏幕滑動(Screen Slide)例子。看以下幾個文件中的代碼實(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)建一個布局文件。以下實(shí)現(xiàn)的布局文件中包含一個顯示文本的文本視圖:

<!-- fragment_screen_slide_page.xml --> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/content"android:layout_width="match_parent"android:layout_height="match_parent" ><TextView style="?android:textAppearanceMedium"android:padding="16dp"android:lineSpacingMultiplier="1.2"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/lorem_ipsum" /> </ScrollView>

同時也在碎片中定義了一個字符串。

(2) 創(chuàng)建碎片
創(chuàng)建一個返回在onCreateView()方法中創(chuàng)建的布局的Fragment(碎片)類。然后,在需要向用戶展示新頁的時候就可以在碎片的父活動中創(chuàng)建此片段實(shí)例:

import android.support.v4.app.Fragment; ... public class ScreenSlidePageFragment extends Fragment {@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.fragment_screen_slide_page, container, false);return rootView;} }

(3) 增加ViewPager
ViewPager已經(jīng)被內(nèi)建在掃頁面變換中,它的默認(rèn)功能就是滑動屏幕動畫,所以不必重新創(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)建一個具以下功能的活動:
- 將活動的視圖內(nèi)容設(shè)置包含ViewPager的布局文件。
- 創(chuàng)建一個擴(kuò)展于FragmentStatePagerAdapter類的類并實(shí)現(xiàn)getItem()方法來提供一個ScreenSlidePageFragment實(shí)例作為新頁。頁面適配器同樣要求實(shí)現(xiàn)getCount()方法,此方法返回適配器所需創(chuàng)建的頁面數(shù)。
- 將PagerAdapter掛到ViewPager上。
- 在碎片的虛擬棧中通過回移來響應(yīng)設(shè)備的返回按鈕。如果用戶已經(jīng)在第一頁上,就返回到活動的棧底。

import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; ... public class ScreenSlidePagerActivity extends FragmentActivity {/*** The number of pages (wizard steps) to show in this demo.*/private static final int NUM_PAGES = 5;/*** The pager widget, which handles animation and allows swiping horizontally to access previous* and next wizard steps.*/private ViewPager mPager;/*** The pager adapter, which provides the pages to the view pager widget.*/private PagerAdapter mPagerAdapter;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_screen_slide);// Instantiate a ViewPager and a PagerAdapter.mPager = (ViewPager) findViewById(R.id.pager);mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());mPager.setAdapter(mPagerAdapter);}@Overridepublic void onBackPressed() {if (mPager.getCurrentItem() == 0) {// If the user is currently looking at the first step, allow the system to handle the// Back button. This calls finish() on this activity and pops the back stack.super.onBackPressed();} else {// Otherwise, select the previous step.mPager.setCurrentItem(mPager.getCurrentItem() - 1);}}/*** A simple pager adapter that represents 5 ScreenSlidePageFragment objects, in* sequence.*/private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {public ScreenSlidePagerAdapter(FragmentManager fm) {super(fm);}@Overridepublic Fragment getItem(int position) {return new ScreenSlidePageFragment();}@Overridepublic int getCount() {return NUM_PAGES;}} }

(4)用PageTransformer自定義動畫
欲在默認(rèn)的屏幕滑動動畫中展示不同的動畫頁面,實(shí)現(xiàn)ViewPager.PagerTransformer接口并將此接口提供給視圖頁。此接口指暴露了transformPage()一個方法。每當(dāng)屏幕變換時,每個可見視圖(通常屏幕上只有一個可見頁面)以及相鄰的沒有在屏幕上的頁面就會調(diào)用此方法一次。例如,若當(dāng)前屏幕為頁面三,用戶欲拖拽出頁面四,在每一個手勢發(fā)生時,transformPage()會被頁面二、三、四調(diào)用。

在transformPage()的實(shí)現(xiàn)中,根據(jù)屏幕上頁面的位置參數(shù)通過判斷哪一個頁面需要轉(zhuǎn)換可以創(chuàng)建自定義的動畫滑動,位置可從ransformPage()方法的position參數(shù)獲得。

位置position參數(shù)會表明一個頁面跟屏幕中心的位置關(guān)系。當(dāng)用戶滑動屏幕時此參數(shù)是一個動態(tài)值。當(dāng)某頁面填充到屏幕中時,其位置參數(shù)值為0。當(dāng)一個頁面剛好從屏幕右邊消失時,其位置值為1。如果用戶在頁面1和頁面2中滑動一半,頁面1的位置值為-0.5,頁面2的位置值為0.5。基于頁面在屏幕上的具體位置,可以通過setAlpha()、setTranslationX()或setScaleY()方法設(shè)置頁面屬性值來自定義動畫。

當(dāng)實(shí)現(xiàn)PagerTransformer后,用此實(shí)現(xiàn)調(diào)用setPageTransformer()來應(yīng)用自定義的動畫。例如,假設(shè)有一個名為ZoomOutPageTransformer的PagerTransformer,可以像以下這樣設(shè)置自定義動畫:

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)頁面接近屏幕中心時,此頁面將回到正常尺寸并漸入。

頁面縮小變換動畫:http://developer.android.com/training/animation/anim_page_transformer_zoomout.mp4
ZoomOutPageTransformer示例
點(diǎn)擊設(shè)備屏幕放映動畫

public class ZoomOutPageTransformer implements ViewPager.PageTransformer {private static final float MIN_SCALE = 0.85f;private static final float MIN_ALPHA = 0.5f;public void transformPage(View view, float position) {int pageWidth = view.getWidth();int pageHeight = view.getHeight();if (position < -1) { // [-Infinity,-1)// This page is way off-screen to the left.view.setAlpha(0);} else if (position <= 1) { // [-1,1]// Modify the default slide transition to shrink the page as wellfloat scaleFactor = Math.max(MIN_SCALE, 1 - Math.abs(position));float vertMargin = pageHeight * (1 - scaleFactor) / 2;float horzMargin = pageWidth * (1 - scaleFactor) / 2;if (position < 0) {view.setTranslationX(horzMargin - vertMargin / 2);} else {view.setTranslationX(-horzMargin + vertMargin / 2);}// Scale the page down (between MIN_SCALE and 1)view.setScaleX(scaleFactor);view.setScaleY(scaleFactor);// Fade the page relative to its size.view.setAlpha(MIN_ALPHA +(scaleFactor - MIN_SCALE) /(1 - MIN_SCALE) * (1 - MIN_ALPHA));} else { // (1,+Infinity]// This page is way off-screen to the right.view.setAlpha(0);}} }

[2] 頁面深度變換
當(dāng)用“depth”動畫滑動頁面到右邊時,頁面用默認(rèn)的動畫變換將滑動頁面移到左邊。深度變換將頁面淡出并線性的減小其范圍。

頁面深度變換示例:http://developer.android.com/training/animation/anim_page_transformer_depth.mp4
DepthPageTransformer 示例
點(diǎn)擊設(shè)備屏幕放映動畫

注:在深度動畫期間,默認(rèn)的動畫(屏幕滑動)仍舊發(fā)生了,所以必須構(gòu)建一個X負(fù)方向的變換。例如:

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

以下代碼演示如何在正移動的頁面變換中抵消默認(rèn)的屏幕動畫滑動:

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)式動畫

學(xué)習(xí)在翻轉(zhuǎn)運(yùn)動下如何實(shí)現(xiàn)兩視圖之間的動畫。

此節(jié)將展示如何用自定義碎片動畫來實(shí)現(xiàn)卡片翻轉(zhuǎn)動畫。視圖內(nèi)容間的卡片翻轉(zhuǎn)通過模擬卡片翻轉(zhuǎn)過來實(shí)現(xiàn)。

卡片翻轉(zhuǎn)的過程如下所示:http://developer.android.com/training/animation/anim_card_flip.mp4
卡片翻轉(zhuǎn)動畫,點(diǎn)擊設(shè)備屏幕放映動畫

欲跳過后續(xù)內(nèi)容而想看完整的樣例,下載本節(jié)樣例選擇Card Flip樣例打開看以下幾個文件中的代碼實(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)建動畫
欲創(chuàng)建卡片式動畫翻轉(zhuǎn),需要兩個動畫場景,當(dāng)前面的卡片動畫從向左翻轉(zhuǎn)出去時另外一個動畫要從左方顯示進(jìn)來。同時,當(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)的視圖。動畫變換時將會用存儲在碎片中的視圖布局。以下布局創(chuàng)建了卡片用用于顯示文本的一面:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:background="#a6c"android:padding="16dp"android:gravity="bottom"><TextView android:id="@android:id/text1"style="?android:textAppearanceLarge"android:textStyle="bold"android:textColor="#fff"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/card_back_title" /><TextView style="?android:textAppearanceSmall"android:textAllCaps="true"android:textColor="#80ffffff"android:textStyle="bold"android:lineSpacingMultiplier="1.2"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="@string/card_back_description" /></LinearLayout>

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

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

(3) 創(chuàng)建碎片
為卡片的前面和背面創(chuàng)建碎片類。此類返回之前在每個片段中onCreateView()方法中所創(chuàng)建的布局。之后,可在欲顯示卡片的碎片的父活動中聲明此碎片實(shí)例。以下代碼展示了在父活動中嵌套碎片類的實(shí)現(xiàn):

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

(4) 動畫卡片翻轉(zhuǎn)
至此,可以在父活動中展示片段了。欲此,首先為活動創(chuàng)建布局文件。以下代碼創(chuàng)建了可以在運(yùn)行時添加碎片的包含F(xiàn)rameLayout元素的布局文件:

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

在活動類代碼中,將剛創(chuàng)建的布局文件加載到活動類中。在活動被創(chuàng)建時顯示默認(rèn)的片段也是個不錯的主意,所以以下活動類中的代碼展示如何將卡片前面作為默認(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候可以用翻轉(zhuǎn)動畫來顯示卡片的背面。遵循以下步驟來顯示卡片的另一面:
- 設(shè)置之前為碎片變換創(chuàng)建的自定義動畫。
- 用新的碎片代替當(dāng)前碎片的顯示并觸發(fā)自定義動畫。
- 將之前的碎片置于棧頂?shù)南乱粚?#xff0c;這樣只要用戶按下返回按鈕,卡片就翻轉(zhuǎn)過來了。

private void flipCard() {if (mShowingBack) {getFragmentManager().popBackStack();return;}// Flip to the back.mShowingBack = true;// Create and commit a new fragment transaction that adds the fragment for the back of// the card, uses custom animations, and is part of the fragment manager's back stack.getFragmentManager().beginTransaction()// Replace the default fragment animations with animator resources representing// rotations when switching to the back of the card, as well as animator// resources representing rotations when flipping back to the front (e.g. when// the system Back button is pressed)..setCustomAnimations(R.animator.card_flip_right_in, R.animator.card_flip_right_out,R.animator.card_flip_left_in, R.animator.card_flip_left_out)// Replace any fragments currently in the container view with a fragment// representing the next page (indicated by the just-incremented currentPage// variable)..replace(R.id.container, new CardBackFragment())// Add this transaction to the back stack, allowing users to press Back// to get to the front of the card..addToBackStack(null)// Commit the transaction..commit(); }

4.4 縮放視圖

學(xué)習(xí)在縮放觸摸動畫操作下如何放大視圖。

此節(jié)演示如何實(shí)現(xiàn)觸摸-縮放動畫,此動畫對于像相片畫廊引用程序非常有用 - 從縮略圖視圖動畫到全尺寸以填充屏幕。

這里有一個觸摸-縮放動畫:http://developer.android.com/training/animation/anim_zoom.mp4
縮放動畫,點(diǎn)擊屏幕放映動畫

如果想直接看本節(jié)代碼示例,下載并選擇Zoom樣例,見以下幾個文件中的代碼實(shí)現(xiàn):
- src/TouchHighlightImageButton.java(一個幫助類,當(dāng)按圖片按鈕時高亮點(diǎn)擊的地方)
- src/ZoomActivity.java
- layout/activity_zoom.xml

(1) 創(chuàng)建視圖
創(chuàng)建一個包含縮放所需的小和大尺寸視圖的布局文件。以下代碼創(chuàng)建了一個具點(diǎn)擊響應(yīng)事件的ImageButton按鈕以及一個展示擴(kuò)大圖片的的ImageView視圖:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/container"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayout android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"android:padding="16dp"><ImageButtonandroid:id="@+id/thumb_button_1"android:layout_width="100dp"android:layout_height="75dp"android:layout_marginRight="1dp"android:src="@drawable/thumb1"android:scaleType="centerCrop"android:contentDescription="@string/description_image_1" /></LinearLayout><!-- This initially-hidden ImageView will hold the expanded/zoomed version ofthe images above. Without transformations applied, it takes up the entirescreen. To achieve the "zoom" animation, this view's bounds are animatedfrom the bounds of the thumbnail button above, to its final laid-outbounds.--><ImageViewandroid:id="@+id/expanded_image"android:layout_width="match_parent"android:layout_height="match_parent"android:visibility="invisible"android:contentDescription="@string/description_zoom_touch_close" /></FrameLayout>

(2) 設(shè)置縮放動畫
一旦應(yīng)用布局文件,即可設(shè)置出發(fā)縮放動畫的事件。以下代碼增加View.onClickListener事件給ImageButton,如此,當(dāng)用戶點(diǎn)擊此按鈕時即實(shí)現(xiàn)縮放動畫:

public class ZoomActivity extends FragmentActivity {// Hold a reference to the current animator,// so that it can be canceled mid-way.private Animator mCurrentAnimator;// The system "short" animation time duration, in milliseconds. This// duration is ideal for subtle animations or animations that occur// very frequently.private int mShortAnimationDuration;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_zoom);// Hook up clicks on the thumbnail views.final View thumb1View = findViewById(R.id.thumb_button_1);thumb1View.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {zoomImageFromThumb(thumb1View, R.drawable.image1);}});// Retrieve and cache the system's default "short" animation time.mShortAnimationDuration = getResources().getInteger(android.R.integer.config_shortAnimTime);}... }

(3) 縮放視圖
在恰當(dāng)?shù)臅r機(jī)下需要從正常尺寸的視圖縮放到某個尺寸的視圖。通常來講,需要根據(jù)視圖的邊界來動畫。以下方法展示了怎么實(shí)現(xiàn)縮放動畫(從縮略視圖到更大尺寸):
[1]. 分配高分辨率圖片到隱藏的“放大”(擴(kuò)大)ImageView中。以下的樣例代碼將一張大型圖片簡單的載入到用戶界面線程中。更科學(xué)的做法是在獨(dú)立的線程中載入圖片并在用戶界面線程中設(shè)置位圖以防止阻礙用戶界面線程。理想情況下,位圖不應(yīng)該比屏幕尺寸大。
[2]. 計(jì)算ImageView開始和結(jié)束時的邊界。
[3]. 根據(jù)開始邊界和結(jié)束邊界,同時動畫四個位置和尺寸值X,Y(SCALE_X和SCALE_Y)。四個動畫被增加到AnimatorSet中,這樣他們可以在同時開始。
[4]. 當(dāng)圖片被放大用戶再點(diǎn)擊屏幕時運(yùn)行類似的動畫將視圖縮小(還原)。可以為ImageView增加View.onClickListerer來監(jiān)聽用戶的點(diǎn)擊。當(dāng)用戶點(diǎn)擊時,ImageView會還原縮略圖大小并將其可見性設(shè)置為GONE來隱藏。

private void zoomImageFromThumb(final View thumbView, int imageResId) {// If there's an animation in progress, cancel it// immediately and proceed with this one.if (mCurrentAnimator != null) {mCurrentAnimator.cancel();}// Load the high-resolution "zoomed-in" image.final ImageView expandedImageView = (ImageView) findViewById(R.id.expanded_image);expandedImageView.setImageResource(imageResId);// Calculate the starting and ending bounds for the zoomed-in image.// This step involves lots of math. Yay, math.final Rect startBounds = new Rect();final Rect finalBounds = new Rect();final Point globalOffset = new Point();// The start bounds are the global visible rectangle of the thumbnail,// and the final bounds are the global visible rectangle of the container// view. Also set the container view's offset as the origin for the// bounds, since that's the origin for the positioning animation// properties (X, Y).thumbView.getGlobalVisibleRect(startBounds);findViewById(R.id.container).getGlobalVisibleRect(finalBounds, globalOffset);startBounds.offset(-globalOffset.x, -globalOffset.y);finalBounds.offset(-globalOffset.x, -globalOffset.y);// Adjust the start bounds to be the same aspect ratio as the final// bounds using the "center crop" technique. This prevents undesirable// stretching during the animation. Also calculate the start scaling// factor (the end scaling factor is always 1.0).float startScale;if ((float) finalBounds.width() / finalBounds.height()> (float) startBounds.width() / startBounds.height()) {// Extend start bounds horizontallystartScale = (float) startBounds.height() / finalBounds.height();float startWidth = startScale * finalBounds.width();float deltaWidth = (startWidth - startBounds.width()) / 2;startBounds.left -= deltaWidth;startBounds.right += deltaWidth;} else {// Extend start bounds verticallystartScale = (float) startBounds.width() / finalBounds.width();float startHeight = startScale * finalBounds.height();float deltaHeight = (startHeight - startBounds.height()) / 2;startBounds.top -= deltaHeight;startBounds.bottom += deltaHeight;}// Hide the thumbnail and show the zoomed-in view. When the animation// begins, it will position the zoomed-in view in the place of the// thumbnail.thumbView.setAlpha(0f);expandedImageView.setVisibility(View.VISIBLE);// Set the pivot point for SCALE_X and SCALE_Y transformations// to the top-left corner of the zoomed-in view (the default// is the center of the view).expandedImageView.setPivotX(0f);expandedImageView.setPivotY(0f);// Construct and run the parallel animation of the four translation and// scale properties (X, Y, SCALE_X, and SCALE_Y).AnimatorSet set = new AnimatorSet();set.play(ObjectAnimator.ofFloat(expandedImageView, View.X,startBounds.left, finalBounds.left)).with(ObjectAnimator.ofFloat(expandedImageView, View.Y,startBounds.top, finalBounds.top)).with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X,startScale, 1f)).with(ObjectAnimator.ofFloat(expandedImageView,View.SCALE_Y, startScale, 1f));set.setDuration(mShortAnimationDuration);set.setInterpolator(new DecelerateInterpolator());set.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {mCurrentAnimator = null;}@Overridepublic void onAnimationCancel(Animator animation) {mCurrentAnimator = null;}});set.start();mCurrentAnimator = set;// Upon clicking the zoomed-in image, it should zoom back down// to the original bounds and show the thumbnail instead of// the expanded image.final float startScaleFinal = startScale;expandedImageView.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {if (mCurrentAnimator != null) {mCurrentAnimator.cancel();}// Animate the four positioning/sizing properties in parallel,// back to their original values.AnimatorSet set = new AnimatorSet();set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left)).with(ObjectAnimator.ofFloat(expandedImageView, View.Y,startBounds.top)).with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal)).with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));set.setDuration(mShortAnimationDuration);set.setInterpolator(new DecelerateInterpolator());set.addListener(new AnimatorListenerAdapter() {@Overridepublic void onAnimationEnd(Animator animation) {thumbView.setAlpha(1f);expandedImageView.setVisibility(View.GONE);mCurrentAnimator = null;}@Overridepublic void onAnimationCancel(Animator animation) {thumbView.setAlpha(1f);expandedImageView.setVisibility(View.GONE);mCurrentAnimator = null;}});set.start();mCurrentAnimator = set;}}); }

4.5 動畫布局文件的改變

學(xué)習(xí)當(dāng)在布局文件中增加、移除以及更新子視圖時如何開啟內(nèi)建動畫。

布局動畫是對布局文件配置更改之前預(yù)先載入動畫。開發(fā)者需要做的就是在布局文件中設(shè)置屬性來告知安卓系統(tǒng)動畫改變布局文件的改變,系統(tǒng)用默認(rèn)的動畫效果動畫顯示它們。

提示:若欲實(shí)現(xiàn)自定義布局動畫,需創(chuàng)建LayoutTransition對象并需通過setLayoutTransiton()方法將此對象應(yīng)用到布局文件中。

此處有一個當(dāng)增加列表中的條目時默認(rèn)的布局動畫:http://developer.android.com/training/animation/anim_layout_changes.mp4
布局動畫,點(diǎn)擊設(shè)備屏幕放映動畫

若想直接看此部分的代碼樣例, 下載本節(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)建布局
在活動的布局XML文件中,將布局文件中欲開啟的動畫的android:animateLayoutChanges屬性設(shè)置為true。例:

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

(2) 從布局文件中增加、 更新或移除內(nèi)容
至此,所有需要做的操作就是增加、移除或更新布局文件中的內(nèi)容,布局文件中的內(nèi)容將會自動的以動畫形式實(shí)現(xiàn):

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

[2015.11.18-16:35]

總結(jié)

以上是生活随笔為你收集整理的pAdTy_1 构建图形和动画应用程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。

九九久久精品视频 | 国产黑丝袜在线 | adn—256中文在线观看 | 麻豆mv在线观看 | 免费观看午夜视频 | 在线观看av片 | 免费黄色激情视频 | 97超碰中文字幕 | 91久久精品日日躁夜夜躁国产 | 国内精品小视频 | 中文免费在线观看 | 国产三级国产精品国产专区50 | 五月婷婷激情网 | 国产麻豆精品免费视频 | 日本久草电影 | 日日弄天天弄美女bbbb | 日韩伦理一区二区三区av在线 | 麻豆果冻剧传媒在线播放 | 综合婷婷丁香 | 色网站在线免费 | 欧美一级视频一区 | 探花视频在线观看 | 日韩欧美亚州 | 免费在线观看黄网站 | 韩国精品一区二区三区六区色诱 | 在线免费av观看 | 国产高清av在线播放 | 欧美伦理一区 | 2019免费中文字幕 | 日韩理论电影在线 | 18国产精品白浆在线观看免费 | 制服丝袜在线91 | 91日韩在线专区 | 成人a免费看 | 五月婷婷,六月丁香 | 国产高清在线不卡 | 91色网址| 国产高清久久 | av怡红院 | 91免费在线播放 | 天天插综合 | 色在线视频| 91久久精品一区 | 欧美男同网站 | 四虎免费av | 亚洲精品永久免费视频 | 亚洲 欧美变态 另类 综合 | 激情视频在线观看网址 | 久久人视频 | 一本一本久久aa综合精品 | 国产一区欧美在线 | 久久久91精品国产一区二区精品 | 日韩欧美在线免费观看 | 国产日韩精品一区二区三区 | 黄色成品视频 | 福利区在线观看 | 丁香婷婷社区 | 国产精品久久久久久久久久白浆 | 婷婷激情五月 | 午夜精品久久久 | 特级aaa毛片 | 亚洲a网| 亚洲另类视频在线观看 | 国内99视频 | 69国产成人综合久久精品欧美 | 久久午夜网 | 日日噜噜噜噜夜夜爽亚洲精品 | 少妇性bbb搡bbb爽爽爽欧美 | 一本一道久久a久久综合蜜桃 | 国产精品久久久久久久毛片 | 久久久久久精 | 亚洲精品乱码久久久久久蜜桃不爽 | 日韩电影在线观看一区二区 | 天天综合网~永久入口 | 日本久久精 | 日本护士三级少妇三级999 | 天天操网 | 激情导航 | 欧产日产国产69 | 日韩在线中文字幕 | 欧美aⅴ在线观看 | 精品专区一区二区 | 精品国产伦一区二区三区观看体验 | av黄在线播放 | freejavvideo日本免费 | 亚洲丝袜中文 | 91av欧美 | 亚洲激情在线观看 | 一区二区在线影院 | 91系列在线 | 久久黄色片 | 中文字幕在线日亚洲9 | 婷婷久久国产 | 一区二区三区四区五区在线 | 奇米四色影狠狠爱7777 | 日本性久久 | 久久精品第一页 | 黄色一级在线免费观看 | 亚洲电影图片小说 | 国产精品第7页 | 热久久免费国产视频 | 欧美日韩一区久久 | 最近免费中文字幕 | 日本三级香港三级人妇99 | www.狠狠插.com | 国产激情久久久 | 中文字幕在线观看一区二区 | 人人dvd | 不卡电影一区二区三区 | 久久免费成人网 | 国产精品福利无圣光在线一区 | 99国产精品免费网站 | 欧美性网站 | 久久成人综合 | 欧美韩国日本在线观看 | 青青草国产成人99久久 | 九色在线| 免费网站观看www在线观看 | 久久激情五月婷婷 | 四虎5151久久欧美毛片 | 欧美中文字幕久久 | 日本aaaa级毛片在线看 | 97在线影视 | 欧美精品国产综合久久 | 2019免费中文字幕 | 丝袜美腿亚洲综合 | 超碰人人在线观看 | 亚洲欧美日韩精品一区二区 | 成人在线视频免费观看 | 精品久久精品久久 | 国产精品va在线观看入 | 欧美激情另类 | 91九色蝌蚪国产 | 日韩在线免费高清视频 | 91在线视频观看免费 | 97在线视频网站 | 91网页版在线观看 | 99久精品 | 成人影视片| 日韩免费一区二区在线观看 | 黄色av一区二区 | 在线观看亚洲免费视频 | 日本精品中文字幕在线观看 | 99c视频在线 | www.天天草 | 日韩在线播放欧美字幕 | 日韩黄色在线电影 | 六月色 | 久久在线电影 | 综合色天天 | 九九激情视频 | 中文字幕美女免费在线 | 亚洲在线日韩 | 天天激情综合网 | 亚洲欧洲久久久 | 久久久久欧美精品 | www.com.黄| 在线观看中文字幕dvd播放 | 激情av一区二区 | 中文字幕欧美日韩va免费视频 | 免费av 在线| 亚洲视频 一区 | 精品福利国产 | 一级黄色大片在线观看 | 国产在线一卡 | 免费观看一区 | а天堂中文最新一区二区三区 | 特级a毛片| 欧美激情综合色 | 日韩精品一区二区三区在线播放 | 国产欧美日韩精品一区二区免费 | 国产精品自产拍在线观看中文 | 欧美成人精品xxx | 很黄很污的视频网站 | 天天射综合网站 | 在线中文字幕播放 | 婷婷久久综合九色综合 | 中文字幕在线免费观看 | 激情小说网站亚洲综合网 | 麻豆成人精品视频 | 91污污视频在线观看 | 色综合天天在线 | 日韩高清在线一区 | 97视频免费在线看 | 夜色资源站wwwcom | 国产免费片 | 日韩欧美视频在线 | 日韩一级片网址 | 三级免费黄色 | 亚洲精品在线观看免费 | 99热精品视 | 国产精品久久久久久久妇 | 高清av影院 | 国产一区二区三区高清播放 | 成人va在线观看 | 99久久精品国产亚洲 | 三级黄色大片在线观看 | 麻豆小视频在线观看 | 99成人在线视频 | 99色在线视频 | 国产一区 在线播放 | 国产人免费人成免费视频 | 久久久久久久久久福利 | 欧美一级片在线观看视频 | 免费热情视频 | 在线观看精品视频 | 人人爱人人爽 | 久久精品精品 | 91精品国自产拍天天拍 | 日韩欧美视频免费看 | 91精品国产乱码 | 中文字幕日韩伦理 | 中文字幕文字幕一区二区 | 在线精品在线 | 成人午夜电影久久影院 | 国内精品99 | 国内一区二区视频 | 免费一级片在线 | 在线观看视频你懂的 | 国产成本人视频在线观看 | 国产精品18久久久久白浆 | 国产精品不卡视频 | 亚洲欧美视频一区二区三区 | 一区二区三区四区五区在线 | 蜜臀av性久久久久蜜臀aⅴ流畅 | 偷拍精品一区二区三区 | 国精产品999国精产品岳 | 国产一二区精品 | 日韩有色 | 成人免费网视频 | 亚洲一级二级三级 | av片在线观看 | 中文字幕一区二区三区久久 | 日韩在线中文字幕视频 | 97视频入口免费观看 | 99在线精品观看 | 久久久久久毛片精品免费不卡 | 国产精品久久久久久久久毛片 | 国产热re99久久6国产精品 | 欧美久久久久久久久久久久 | 久久免费视频4 | 欧美日韩国产三级 | 午夜免费在线观看 | 91av手机在线观看 | 色偷偷网站视频 | 日韩一级片网址 | 中文字幕丝袜 | 国产资源网 | 热久久国产精品 | 日韩r级电影在线观看 | 国产成人一区二区三区 | 日韩二区在线播放 | 91在线入口 | 免费电影一区二区三区 | 伊人天天狠天天添日日拍 | 婷婷在线免费观看 | 国产黄大片在线观看 | 成人午夜精品久久久久久久3d | 亚洲欧洲在线视频 | 日本三级大片 | 国产成人久久精品77777综合 | 波多野结衣久久资源 | 91完整视频 | 久久人人精 | 四虎国产精品成人免费4hu | 91在线视频一区 | 久久久久一区二区三区四区 | 天天玩天天干 | 中文在线亚洲 | 亚洲国产午夜精品 | 欧美在线你懂的 | 中国美女一级看片 | 色在线国产 | 97在线影视 | 国产乱码精品一区二区蜜臀 | 福利区在线观看 | av在线看片 | 99久热在线精品视频观看 | 97在线免费观看视频 | 国产99久久久精品 | 国产日韩精品一区二区三区在线 | 国产自在线观看 | 免费国产一区二区 | 久久人人爽人人爽 | 久久久国产一区 | 天天色天天操综合网 | 日本性xxx | 国产精品永久免费视频 | 色婷婷www| 久久综合色8888 | 国产一区欧美一区 | av超碰在线 | 亚洲九九精品 | 91精品国产电影 | 成人在线观看你懂的 | 国产精品资源网 | 色黄久久久久久 | 91.精品高清在线观看 | 日本女人的性生活视频 | 国产亚洲精品久久久久久移动网络 | 夜夜婷婷| 午夜久久| 久久综合影音 | 六月丁香社区 | 91一区啪爱嗯打偷拍欧美 | 国产精品欧美一区二区三区不卡 | aaawww| 国产在线观看一 | www天天操 | 亚洲成av人片在线观看香蕉 | 免费久久久久久久 | 国产综合片 | 久久蜜臀av | 9在线观看免费高清完整版在线观看明 | 婷婷丁香av | 最新国产精品亚洲 | 欧美成人日韩 | 国产不卡在线视频 | 天天干天天拍 | 婷婷5月色 | 亚洲激情国产精品 | 999在线精品 | 国产精品视频永久免费播放 | 中文在线8新资源库 | 免费观看完整版无人区 | 中文区中文字幕免费看 | 97碰碰视频| 久久免费电影 | 久久精品伊人 | bbbb操bbbb | av在线一 | 亚洲综合视频在线观看 | 国产精品99久久久久久久久 | 国产无遮挡又黄又爽在线观看 | 国产黄色免费看 | 国产精品99久久久久 | 欧美日韩视频在线播放 | 国产精品成人一区二区三区吃奶 | 久人人 | 91精品国| 色九九影院 | 日本99热| 天天干天天操天天爱 | 99国产精品一区二区 | 精品国产日本 | 人人爽人人爽人人片av免 | 99久久精品国 | 国产又粗又猛又黄又爽 | 国产精品1000| a久久久久 | 久久久久成人精品 | 中文字幕制服丝袜av久久 | 久久精品成人欧美大片古装 | 99av在线视频 | 久久精品综合一区 | 欧美一级艳片视频免费观看 | 在线综合 亚洲 欧美在线视频 | 99精品热视频只有精品10 | 国产看片免费 | 成年人在线观看 | 激情综合啪| 丁香av| 婷婷国产精品 | 国产美女主播精品一区二区三区 | 午夜精品久久久久久久久久久 | 少妇精品久久久一区二区免费 | 中文字幕影片免费在线观看 | 久久天天操 | 中文字幕精品在线 | 日韩在线观看三区 | 最新av观看 | 九九久久久 | 中国一级片在线观看 | 国产精品久久久久久久久久直播 | 国语久久 | 久久精品伊人 | 亚洲人成网站精品片在线观看 | 中文字幕国产精品 | 国产精品久久久久久久久大全 | 国产小视频免费在线网址 | 国产精品video | 又黄又爽又无遮挡的视频 | 亚洲伊人天堂 | 婷婷精品进入 | 国产精品美女久久久网av | 日韩精品不卡在线观看 | av不卡免费在线观看 | 国产午夜精品一区二区三区四区 | 久久精品视频在线观看 | 亚洲aⅴ免费在线观看 | 亚欧日韩av| h视频在线看 | 日本mv大片欧洲mv大片 | 激情 婷婷 | 99精品国产免费久久久久久下载 | 成人中文字幕在线观看 | 成人在线电影观看 | 精品产品国产在线不卡 | 久久精品国产美女 | 在线观看午夜 | 97精品超碰一区二区三区 | 日本中文字幕网站 | 久久精品中文视频 | 在线高清一区 | 亚洲国产丝袜在线观看 | 深爱五月激情五月 | 日韩h在线观看 | 亚洲精品成人免费 | 亚洲精品国产片 | www.色在线| 成年人免费在线观看 | 久久精品99国产精品亚洲最刺激 | av看片在线| 激情亚洲综合在线 | 日韩欧美在线影院 | 中文字幕视频观看 | 成人在线观看免费视频 | 激情视频综合网 | 一区二区三区四区五区在线 | 国产黄色在线网站 | 久久久久久国产精品美女 | 婷婷色中文字幕 | 成人在线一区二区 | 日本在线视频网址 | 国产成人精品在线观看 | 免费合欢视频成人app | 亚洲激情精品 | 成人在线免费观看视视频 | 一级片免费视频 | 五月天av在线 | 国产精品99蜜臀久久不卡二区 | 日本护士撒尿xxxx18 | 免费黄色一区 | 欧洲精品久久久久毛片完整版 | 中文字幕a∨在线乱码免费看 | 国产不卡精品 | 国产福利一区在线观看 | 婷婷激情五月 | 精品久久一区二区三区 | 天堂网一区二区三区 | 久久经典国产视频 | 久久久国产精品久久久 | 国产一区黄色 | 午夜精品久久久久久久99 | 久久免费观看少妇a级毛片 久久久久成人免费 | 欧美午夜久久 | 精品福利网 | 亚洲午夜电影网 | 国产99免费 | 天天激情 | 日韩视频在线播放 | 日韩区欠美精品av视频 | 亚州av一区 | 天天操天天爱天天干 | 日韩在线视频精品 | 国产麻豆视频在线观看 | 亚洲精品在线观看免费 | 五月天婷婷在线播放 | 免费精品人在线二线三线 | 在线观看av网 | 国产精品久久久久久久久久久久 | 中文字幕电影在线 | 黄在线免费观看 | 中文字幕你懂的 | 成人午夜电影免费在线观看 | 久久久影院一区二区三区 | 欧美日本在线观看视频 | 日韩在线网址 | 亚洲精品一区二区三区高潮 | 日韩国产欧美在线播放 | 色偷偷网站视频 | 亚洲精品一区二区三区在线观看 | 91亚洲精品久久久久图片蜜桃 | 一区免费视频 | 色欧美综合| 丁香九月婷婷综合 | 久久久国产精品人人片99精片欧美一 | 婷婷久久一区二区三区 | 四虎国产精品免费 | 99久精品 | 国产成人av在线 | 少妇bbr搡bbb搡bbb | 国产在线v | 免费av在线网站 | 欧洲色综合 | 久久人人爽av | 天天躁日日躁狠狠 | 骄小bbw搡bbbb揉bbbb | 美女免费视频一区 | 国产精品福利一区 | 国产在线视频一区二区三区 | 最新国产精品亚洲 | 在线免费色视频 | 黄色资源网站 | 久久综合色综合88 | 中文字幕中文字幕 | 国产一区二区成人 | 五月婷网站 | 久久人人爽人人爽人人片av免费 | 日日干干夜夜 | 激情开心网站 | 99精品国产一区二区 | 91资源在线播放 | 天天操天天操天天操天天 | 99在线精品免费视频九九视 | 婷婷在线免费视频 | 波多野结衣电影一区二区三区 | 日本午夜免费福利视频 | 中文字幕在线久一本久 | 久久国产一区二区三区 | 综合网av | www.久草视频| 玖玖玖精品 | 中文在线字幕免费观 | 超碰人人干人人 | 999久久国精品免费观看网站 | 国产999在线 | 午夜精品久久久99热福利 | 好看的国产精品视频 | 国产午夜精品免费一区二区三区视频 | 四虎影视精品永久在线观看 | 91你懂的| 日韩av在线小说 | 二区三区中文字幕 | 久久人人爽人人爽人人 | 麻豆国产精品视频 | 久久久久一区二区三区 | a久久免费视频 | 亚洲一级片av | 丝袜美女在线观看 | 99久精品视频 | 日韩高清免费无专码区 | 精品国产伦一区二区三区观看说明 | 亚洲首页 | 亚洲综合成人专区片 | 97超碰人人模人人人爽人人爱 | 日批网站在线观看 | 韩日精品中文字幕 | 91一区二区三区在线观看 | 国产日产精品一区二区三区四区 | 天天操网址 | 免费久久片 | 欧美日韩精品区 | 综合色中文 | 99视频精品免费观看, | 久久精品综合一区 | 激情久久久久久久久久久久久久久久 | 四虎最新域名 | 国产精品乱码高清在线看 | 色停停五月天 | 蜜臀av性久久久久蜜臀aⅴ涩爱 | 视频一区二区三区视频 | 国产视频在线观看一区二区 | 国产系列精品av | 最新动作电影 | 97在线视频免费看 | 日韩在线观看你懂得 | 在线97 | 国产一级一片免费播放放 | 国产麻豆果冻传媒在线观看 | 在线观看不卡视频 | 美女黄频视频大全 | 爱情影院aqdy鲁丝片二区 | 国产精品嫩草影院9 | 9999精品免费视频 | 日韩免 | av在线最新 | 免费看黄的 | 最新国产精品拍自在线播放 | www亚洲精品| 色91av| 久久国产精品久久w女人spa | 久久综合婷婷 | 婷婷丁香在线视频 | 日本xxxx裸体xxxx17 | 国产在线观看,日本 | 99精品热视频只有精品10 | 欧美激情亚洲综合 | 色婷婷激情五月 | 中文字幕亚洲综合久久五月天色无吗'' | 黄色在线观看污 | 五月婷婷激情综合 | 麻豆国产网站 | 国产精品电影一区二区 | 天天插狠狠插 | 国产精品久久影院 | 在线观看日韩精品视频 | 亚洲最大av在线播放 | 毛片网站观看 | 99热.com| 久草爱 | 成人精品一区二区三区电影免费 | 中文成人字幕 | 欧美国产日韩中文 | 日日草夜夜操 | 欧美日韩高清一区二区 国产亚洲免费看 | av电影中文字幕在线观看 | 伊人久在线 | 伊人色综合网 | 97免费在线观看视频 | 在线视频一区观看 | 96国产在线| 日本中文一区二区 | 国产精品久久久久久久久久不蜜月 | 亚洲精品视 | 免费看特级毛片 | 亚洲综合欧美日韩狠狠色 | 成人免费在线播放 | 亚洲 欧美 综合 在线 精品 | 日日弄天天弄美女bbbb | 亚洲国产日韩在线 | 免费日韩电影 | 久久久久福利视频 | 免费观看视频的网站 | 97精产国品一二三产区在线 | jizz欧美性9| 国产精品一区在线 | 日韩精品免费在线观看视频 | 麻豆超碰| 国产精品午夜久久 | 久久精品欧美视频 | 国产91精品看黄网站在线观看动漫 | 午夜久久久久久久久久影院 | 久久国产免 | 久久久高清一区二区三区 | 久久中文网 | 成人免费视频网站在线观看 | 国产91国语对白在线 | 日日干 天天干 | 狠狠色丁香| 久久免费电影网 | 天天天天天天干 | 成人黄色影片在线 | 久久午夜影院 | 免费看久久 | 国产精品免费久久久久 | 国产aaa免费视频 | 久久久精品国产免费观看同学 | 人人爱在线视频 | a黄在线观看 | 国产精品视频999 | 欧美日韩网站 | 日韩在线播放av | 国产a视频免费观看 | 精品国产中文字幕 | 午夜.dj高清免费观看视频 | 国产视频二区三区 | 亚洲一区免费在线 | 亚洲干视频在线观看 | 久久男人免费视频 | 麻豆精品在线 | 国产成人久久av977小说 | 国产视频久 | 在线影院 国内精品 | 很黄很污的视频网站 | 精品久久91| 天天干.com| 在线观看免费高清视频大全追剧 | 狠狠干2018| 亚洲精品视频播放 | 久草精品视频在线观看 | 欧美天堂视频在线 | 亚洲经典中文字幕 | 91天堂素人约啪 | 日韩精品亚洲专区在线观看 | 91精品少妇偷拍99 | 99久久这里有精品 | 激情欧美国产 | 国产精品麻豆一区二区三区 | 波多野结衣资源 | 国内外成人免费在线视频 | 麻豆91精品 | 国产精品美女久久久久久久网站 | 久草在线资源观看 | 久久久不卡影院 | 69xx视频 | 日韩久久久久久 | 美女av免费看 | 中国一级片在线观看 | 欧美成年人在线观看 | 99视频国产精品 | 日韩精品一区二区三区中文字幕 | 国产在线高清视频 | 日韩电影在线观看一区二区三区 | 久久久久久久免费观看 | 九九久久成人 | 国产精品免费在线播放 | 亚洲毛片在线观看. | 中文字幕丝袜美腿 | 国产中文字幕一区二区 | 中文字幕在线播放一区二区 | 久久短视频| 国产精品高清免费在线观看 | 日本中文字幕在线电影 | 四虎影视8848aamm | 欧美国产在线看 | 成人在线播放av | 亚洲精品字幕在线观看 | 日韩成人精品在线观看 | 久久婷婷网 | 成人在线视频网 | 美女视频永久黄网站免费观看国产 | 日韩大陆欧美高清视频区 | 视频一区在线免费观看 | 国产精品123 | 中文字幕在线播放日韩 | 91高清免费在线观看 | 亚洲一区二区三区四区精品 | 中文字幕首页 | 国产中文字幕久久 | 香蕉网在线 | 一区二区精品国产 | 国产亚洲欧美日韩高清 | sesese图片 | 国产日韩欧美在线观看 | 中文成人字幕 | 久久99热久久99精品 | 麻豆视频免费版 | 97精品在线视频 | 日韩xxx视频| 美州a亚洲一视本频v色道 | 国内外激情视频 | 国产特级毛片aaaaaaa高清 | 午夜av在线播放 | 成人免费网站在线观看 | 久久久久久久久久免费 | 国产区高清在线 | 人人射av | 最近中文字幕第一页 | 99久久电影 | 天天射射天天 | 久久午夜网 | 日韩精品中文字幕久久臀 | 玖操| 日韩视频中文字幕 | 精品一二区 | 久久精品毛片基地 | 97超级碰碰 | 少妇av片 | 欧美一级乱黄 | 中文字幕在线观看免费观看 | 日本一区二区三区免费看 | 国产成人三级在线播放 | 国产91亚洲精品 | 国产精品免费大片视频 | 久久国产精品久久w女人spa | 婷婷六月在线 | 麻豆视频免费观看 | 五月婷婷,六月丁香 | 欧美精品国产精品 | 九九国产精品视频 | a级国产乱理伦片在线播放 久久久久国产精品一区 | a视频免费看 | 久久成人一区 | 手机av永久免费 | 国产在线国偷精品产拍 | 久久99久久99精品免费看小说 | 国产系列在线观看 | 国产精品视频久久 | 在线免费观看国产黄色 | 欧美精品v国产精品v日韩精品 | 一区二区三区日韩在线 | 欧美日韩国产精品爽爽 | 99色在线观看 | 黄色www免费 | 日韩电影在线一区 | 在线视频在线观看 | 久久久久久久国产精品视频 | 91av电影在线 | 成人av网站在线观看 | 成人精品国产 | 99久久久久免费精品国产 | 免费av片在线 | av免费在线免费观看 | 日韩欧美精品在线观看 | 天天干天天看 | 99精品在线直播 | 久久久国产精品久久久 | 91麻豆免费视频 | 欧美日韩一区二区三区不卡 | 精品一区二区三区在线播放 | 人人添人人澡人人澡人人人爽 | 欧美激情综合五月色丁香 | 91久久国产精品 | 久草免费福利在线观看 | 一本色道久久综合亚洲二区三区 | 亚州av成人 | 黄污视频网站大全 | www.亚洲黄色 | 国产二区视频在线观看 | 欧洲亚洲国产视频 | 久久99国产精品二区护士 | 九9热这里真品2 | 久久成人在线视频 | 色久天 | 国产成人综合精品 | 日本中文字幕久久 | 欧美一区二区三区不卡 | 国产午夜精品一区二区三区在线观看 | 国产又粗又猛又爽又黄的视频免费 | 亚洲 欧美 国产 va在线影院 | 五月激情站 | 国产性天天综合网 | 丁香婷婷激情网 | av中文字幕在线免费观看 | 国产伦精品一区二区三区在线 | 91在线视频精品 | 欧美日韩一级在线 | 五月婷婷视频在线观看 | 国产一区网址 | 香蕉视频啪啪 | 亚洲国产中文字幕在线观看 | 黄色一级大片在线免费看国产一 | 九色自拍视频 | 免费网站色 | 国产黄a三级三级三级三级三级 | 国产日韩欧美综合在线 | 国产精品色在线 | 91亚洲精品久久久 | 色综合天天狠狠 | 91九色蝌蚪视频网站 | 日本在线观看中文字幕无线观看 | 不卡的av电影 | 天天躁天天躁天天躁婷 | 午夜精品视频福利 | 亚洲乱亚洲乱妇 | 缴情综合网五月天 | 玖玖爱国产在线 | 亚洲色视频 | 97电影网站 | 亚洲精品www久久久久久 | 日韩一区二区三 | 国产精品毛片久久久久久 | se视频网址 | 热re99久久精品国产66热 | 国内免费久久久久久久久久久 | 一区二区在线不卡 | 五月激情六月丁香 | 国产主播99 | 在线观看岛国 | 成 人 免费 黄 色 视频 | 97超碰人人澡人人爱 | 91九色最新地址 | 成年人免费电影在线观看 | 欧美精品九九99久久 | 久久超 | 日本久久久久久久久 | 99久久精品免费看国产免费软件 | 中文一区二区三区在线观看 | 国产亚洲欧洲 | 亚洲欧美日韩一二三区 | 亚洲国产精品电影在线观看 | 日日夜操 | 日本精品视频在线观看 | 国产精品国产亚洲精品看不卡 | 麻豆影音先锋 | 中文字幕第 | 日韩欧美国产激情在线播放 | 欧美在线久久 | av免费线看 | 亚洲91中文字幕无线码三区 | 亚洲欧美日韩精品一区二区 | 欧美日韩精品在线观看 | 婷婷.com| 毛片基地黄久久久久久天堂 | 成人精品国产 | 欧美一级电影在线观看 | 77国产精品 | 国产中文字幕免费 | 欧美一区二区三区在线播放 | 亚洲v精品 | 欧美精品久久久久久 | 天天色天天爱天天射综合 | 国产成人精品久久久久 | 黄色最新网址 | 九九免费在线观看 | 涩涩在线 | av日韩不卡 | 九九视频网站 | 中日韩免费视频 | 久久理论电影网 | 91精品资源 | 日本中文字幕久久 | 天天干天天操av | 亚洲不卡av一区二区三区 | 中文字幕中文字幕在线中文字幕三区 | 91精品蜜桃| 国产四虎在线 | 99久久婷婷国产综合亚洲 | 精品日韩视频 | 欧美少妇xxxxxx | 日本不卡视频 | 国产精品免费观看视频 | 精品国产一区二区三区久久影院 | 中文资源在线观看 | 综合激情婷婷 | 在线观看www视频 | 麻豆系列在线观看 | 国产精品激情偷乱一区二区∴ | 中文资源在线官网 | 在线观看亚洲成人 | 午夜影院一区 | 在线免费黄网站 | 精品91视频 | 2019免费中文字幕 | 91福利国产在线观看 | 在线免费观看不卡av | 中文字幕在线观看视频网站 | 国产免费久久 | 国产成人在线免费观看 | 97国产一区二区 | 久久久999免费视频 日韩网站在线 | 俺要去色综合狠狠 | 欧美激情在线网站 | 在线精品观看国产 | 亚洲国产中文字幕 | 亚洲欧美色婷婷 | 超碰在线最新网址 | av一级久久 | 成人av网站在线播放 | av在线激情 | 亚洲动漫在线观看 | 人人草在线观看 | 国产老妇av | 日韩欧美xxxx | www.狠狠操.com | 亚洲综合爱 | 久久精品国亚洲 | 美女精品在线观看 | 欧美视频日韩视频 | 91黄色小视频 | 国产999精品视频 | 爱爱av网 | 91在线视频免费观看 | 亚洲精选在线 | 精品一二三四在线 | 日韩视频一区二区三区在线播放免费观看 | 99产精品成人啪免费网站 | 超碰99在线| 人人添人人澡 | 欧美视屏一区二区 | 欧美a视频| 99视频这里有精品 | av电影免费看 | 日本一区二区高清不卡 | 91在线公开视频 | 国产尤物在线观看 | 久草网首页 | 伊人亚洲综合网 | 成人a视频在线观看 | 中文字幕丝袜制服 | 久久久在线免费观看 | 免费视频 三区 | 欧美成人一区二区 | www.黄色网.com | www.xxxx变态.com | 日本夜夜草视频网站 | 丁香av在线 | 国产香蕉在线 | 日韩免费三区 | 国产免费中文字幕 | 激情欧美xxxx | 日韩欧美高清一区二区三区 | 久久夜夜爽 | 久久久国产精品一区二区三区 | 五月婷婷综合在线 | 久草9视频 | 欧美精品资源 | 日本深夜福利视频 | 欧美a在线免费观看 | 字幕网在线观看 | 成人黄色大片在线免费观看 | 爱爱一区 | 五月的婷婷 | 91亚色免费视频 | 人人爽人人爱 | 国产中文字幕一区二区 | 中文字幕成人在线观看 | 日韩中文字幕视频在线观看 | 亚州av免费 | 久久久www成人免费毛片麻豆 | 99精品偷拍视频一区二区三区 | 国产美女网站视频 | 亚洲精品视频在线观看免费视频 | 综合色综合色 | 国产精品原创视频 | 国产一区二区久久 | 天天操天天射天天 | 免费在线国产 | 中文字幕在线视频网站 | 久久久免费毛片 | 最新国产福利 | 久久免费精彩视频 | 97超碰在线人人 | 中文字幕在线观看第三页 | 久久久一本精品99久久精品 | 在线不卡中文字幕播放 | 麻豆免费精品视频 |