/**
* A PageTransformer is invoked whenever a visible/attached page is scrolled.
* This offers an opportunity for the application to apply a custom transformation
* to the page views using animation properties.
*
* <p>As property animation is only supported as of Android 3.0 and forward,
* setting a PageTransformer on a ViewPager on earlier platform versions will
* be ignored.</p>
*/
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
void transformPage(View page, float position);
}</code></pre>
通過官方的注釋,我們可以獲得如下信息:①PageTransformer在可見Item或者被添加到ViewPager的Item的位置發生改變的時候,就會回調該方法。可見Item很容易理解,就是當前被選中的Page,那么attached page怎樣理解呢?我們知道, ViewPager有著預加載機制 ,默認的預加載數量是1,即中心Item向左的一個Item以及向右的一個Item,由于預加載機制的存在使得ViewPager在滑動的過程中不會感到卡頓,因為需要展示的頁面已經提前準備好了。
②關注transformPage(page,position)的方法參數,這里的position是存在一個范圍的,0代表當前被選中的Page的位置,位于中心,如果當前Page向左滑動,那么position會從0減到-1,當Page向右滑動,position會從0增加到1。當一個page的position變為-1的時候,這個page便位于中心Item的左邊了,相對的,position變成1的時候,這個page便位于中心Item的右邊。利用這個position變化的性質,我們可以很輕松地對View的某些屬性進行改變了。
接下來,新建RotationPageTransformer.java文件:
public class RotationPageTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE=0.85f;
@Override
public void transformPage(View page, float position) {
float scaleFactor = Math.max(MIN_SCALE,1 - Math.abs(position));
float rotate = 10 * Math.abs(position);
//position小于等于1的時候,代表page已經位于中心item的最左邊,
//此時設置為最小的縮放率以及最大的旋轉度數
if (position <= -1){
page.setScaleX(MIN_SCALE);
page.setScaleY(MIN_SCALE);
page.setRotationY(rotate);
}//position從0變化到-1,page逐漸向左滑動
else if (position < 0){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(rotate);
}//position從0變化到1,page逐漸向右滑動
else if (position >=0 && position < 1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(-rotate);
}//position大于等于1的時候,代表page已經位于中心item的最右邊
else if (position >= 1){
page.setScaleX(scaleFactor);
page.setScaleY(scaleFactor);
page.setRotationY(-rotate);
}
}
}
接著,我們為ViewPager設置這樣一個屬性即可:
mViewPager.setPageTransformer(true,new RotationPageTransformer());
mViewPager.setOffscreenPageLimit(2); //下面會說到
我們運行一下代碼,會發現結果跟最上面展示的效果圖是一樣的,此時滑動ViewPager,各個Item之間的切換也會有動畫的出現,呈現出了3D效果。
3、setPageMargin(int)方法,PageMargin屬性用于設置兩個Page之間的距離,有需要的可以加上該屬性,使得兩個Page的區分更加明顯。
4、setOffscreenPageLimit(int)方法,OffscreenPageLimit屬性用于設置預加載的數量,比如說這里設置了2,那么就會預加載中心item左邊兩個Item和右邊兩個Item。那么這里這個屬性對于我們的3D效果有什么影響呢?我們來試驗一下,首先調用mViewPager.setOffscreenPageLimit(1),把預加載數量設置為1,然后運行程序,向左右滑動幾次,會發現出現了下面的問題:

ic_02.png
即左邊或者右邊的Item在滑動的過程中有可能出現不正確的顯示, 這是為什么呢? 其實這是預加載的數量的問題,當前如果處于position為0的情況下,此時已經預加載了position為1的Item,那么該Item能正常顯示,然而當滑動的時候,由于ViewPager是停止滑動的時候才會加載需要的Item,導致滑動到item1的時候,已經沒有需要顯示的Item2了(因此此時尚未加載),但是當手指松開的時候,Item2得到加載,但是此時不再調用transformPage()方法來調整自身的顯示,所以造成了上面的錯誤顯示。解決的辦法是可以把預加載的數量設置為2或者3,這樣得到的效果更好。
優化
在實現以上效果后,我們需要重新審視一遍我們的代碼,看看是否還有優化的空間。
1、我們在Adapter中的instantiateItem()方法內加載一個View,并用了ImageView的setImageResource()方法來加載圖片,其實查看該方法的源碼可知,這個方法是在UI線程內加載圖片的,如果加載的是很大的一張圖片,那么就造成了UI線程的擁堵。
2、對于已經加載的圖片,沒有得到充分的利用,而是每次都加載一次,而舊的圖片由于失去了引用又處于待回收的狀態,這樣不斷的加載和回收無疑是加重了系統的負擔。
3、如果ImageView的寬高小于圖片的規格,那么把完整的一個大圖加載到ImageView內,顯然也是不合適的。因為圖片越大的話,其占用的內存也越大。
針對上述所說的情況,我們可以一一找到對應的解決辦法:
1、對于在UI線程加載圖片的情況,我們可以考慮在子線程加載圖片,等圖片加載完畢后在通知主線程把圖片設置進ImageView內即可。自然我們會想到使用Handler來進行線程之間的通信。但是這又引發一個問題,如果每一次的instantiateItem()方法內我們都新開一條線程去加載圖片,那么最終的結果是創建了很多只用了一次的線程,這樣的開銷更大了。那有沒有可以控制子線程的方法呢?答案是線程池。線程池通過合理調度線程的使用,使得線程達到最大的使用效率。那么我們可以直接使用 AsyncTask 來實現以上功能,因為AsyncTask內部也用到了線程池。
我們在MyPagerAdapter.java內新建一個內部類:
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
private ImageView imageView;
public LoadBitmapTask(ImageView imageView){
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(Integer... params) {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0]);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}
然后在instantiateItem()方法內添加如下代碼: new LoadBitmapTask(imageView).execute(mBitmapIds[position]); 這樣便開啟了異步任務,在后臺線程內加載我們的圖片。
2、對于高效利用已經加載好的圖片,我們可以這樣理解:因為如果一個Item被destroy后,它就會從它的父容器中移除,然后它的drawable(已經設置好的Bitmap)接著會在某個時刻被gc回收。但是,用戶可能會來回滑動頁面,那么之前的無用Bitmap其實可以再度利用,而不是重新加載一遍。自然,我們可以想到的是利用LruCache來進行內存緩存,對Bitmap保存一個強引用,這樣就不會被gc回收,等到需要用的時候再返回這個Bitmap,對不常用的bitmap進行回收即可。這樣便提高了Bitmap的利用效率,不會重復加載Bitmap,也能使內存的消耗保存在一個合理的范圍之內。使用LruCache也很簡單:
①首先我們在MyPagerAdapyer的構造方法內初始化LruCache:
public MyPagerAdapter(int[] data,Context context){
mBitmapIds = data;
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory * 3 / 8; //緩存區的大小
mCache = new LruCache<Integer, Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight(); //返回Bitmap的大小
}
};
}
②新建一個方法:
public void loadBitmapIntoTarget(Integer id,ImageView imageView){
//首先嘗試從內存緩存中獲取是否有對應id的Bitmap
Bitmap bitmap = mCache.get(id);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
//如果沒有則開啟異步任務去加載
new LoadBitmapTask(imageView).execute(id);
}
}
③對LoadBitmapTask作微小的修改,主要是在異步加載任務之后,向內存緩存中添加bitmap:
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
@Override
protected Bitmap doInBackground(Integer... params) {
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0]);
//把加載好的Bitmap放進LruCache內
mCache.put(params[0],bitmap);
return bitmap;
}
}
④最后,在我們的instantiate()方法內調用我們的loadBitmapIntoTarget方法即可:
loadBitmapIntoTarget(mBitmapIds[position],imageView);
3、對于最后一種情況,我們可以考慮在加載圖片之前,對圖片進行縮放,使得圖片的規格符合ImageView,那么就不會造成內存的浪費了,那么怎樣對一個Bitmap進行縮放呢?
我們知道,一般加載圖片都是利用BitmapFactory的幾個decode方法來加載,但我們觀察這幾個方法,會發現它們各自還有一個帶options參數的重載方法,即BitmapFactory.Options,那么Bitmap的縮放玄機就在這個Options內。Options有一個成員變量:inSampleSize,采樣率,即設置對Bitmap的采樣率,比如說inSampleSize默認為1,此時Bitmap的采樣寬高等于原始寬高,不做任何改變。如果inSampleSize等于2,那么采樣寬高都為原始寬高的1/2,那么大小就變成了原始大小的1/4,因此利用好這個inSampleSize能很好地控制一個Bitmap的大小。具體的使用方法可參考如下:
private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height >= reqHeight || width > reqWidth){
while ((height / (2 * inSampleSize)) >= reqHeight
&& (width / (2 * inSampleSize)) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
//dp轉換成px
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
@Override
protected Bitmap doInBackground(Integer... params) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //1、inJustDecodeBounds置為true,此時只加載圖片的寬高信息
BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
options.inSampleSize = calculateInSampleSize(options,
dp2px(mContext,240),
dp2px(mContext,360)); //2、根據ImageView的寬高計算所需要的采樣率
options.inJustDecodeBounds = false; //3、inJustDecodeBounds置為false,正常加載圖片
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
//把加載好的Bitmap放進LruCache內
mCache.put(params[0],bitmap);
return bitmap;
}
}
有一點要說明的是,筆者這里使用的圖片是960 * 640的,比ImageView的寬高要小,所以體現不出圖片的縮放,讀者可以自行改變ImageView的大小,或者加載一張更大規格的圖片。
最后,放上修改后MyPagerAdapter.java的完整代碼,以供讀者參考:
public class MyPagerAdapter extends PagerAdapter {
private int[] mBitmapIds;
private Context mContext;
private LruCache<Integer,Bitmap> mCache;
public MyPagerAdapter(int[] data,Context context){
mBitmapIds = data;
mContext = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory * 3 / 8; //緩存區的大小
mCache = new LruCache<Integer, Bitmap>(cacheSize){
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
};
}
@Override
public int getCount() {
return mBitmapIds.length;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = LayoutInflater.from(mContext).inflate(R.layout.item_main,container,false);
ImageView imageView = (ImageView) view.findViewById(R.id.iv);
loadBitmapIntoTarget(mBitmapIds[position],imageView);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
public void loadBitmapIntoTarget(Integer id,ImageView imageView){
//首先嘗試從內存緩存中獲取是否有對應id的Bitmap
Bitmap bitmap = mCache.get(id);
if (bitmap != null){
imageView.setImageBitmap(bitmap);
}else {
//如果沒有則開啟異步任務去加載
new LoadBitmapTask(imageView).execute(id);
}
}
private int calculateInSampleSize(BitmapFactory.Options options,int reqWidth,int reqHeight){
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height >= reqHeight || width > reqWidth){
while ((height / (2 * inSampleSize)) >= reqHeight
&& (width / (2 * inSampleSize)) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
public static int dp2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
private class LoadBitmapTask extends AsyncTask<Integer,Void,Bitmap>{
private ImageView imageView;
public LoadBitmapTask(ImageView imageView){
this.imageView = imageView;
}
@Override
protected Bitmap doInBackground(Integer... params) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //1、inJustDecodeBounds置為true,此時只加載圖片的寬高信息
BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
options.inSampleSize = calculateInSampleSize(options,
dp2px(mContext,240),
dp2px(mContext,360)); //2、根據ImageView的寬高計算所需要的采樣率
options.inJustDecodeBounds = false; //3、inJustDecodeBounds置為false,正常加載圖片
Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(),params[0],options);
//把加載好的Bitmap放進LruCache內
mCache.put(params[0],bitmap);
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
imageView.setImageBitmap(bitmap);
}
}
}
最后,感謝你的閱讀,希望這篇文章對你有所幫助~
來自:http://www.jianshu.com/p/1f6c5764dd72