利用ViewPager實現3D畫廊效果及其圖片加載優化

MalDesmond 7年前發布 | 26K 次閱讀 ViewPager Bitmap Android開發 移動開發

前言

對于ViewPager,相信大家都已經很熟悉了,在各種切換場景比如Fragment切換、選項卡的切換或者頂部輪播圖片等都可以用ViewPager去實現。那么本篇文章帶來ViewPager的一種實現效果:3D畫廊。直接上圖來看:

ic.gif

從上面的圖我們可以看出,整個頁面分成三個部分,中間的是大圖,正中地顯示給用戶;而兩邊的是側圖,而這兩幅圖片又有著角度的旋轉,與大圖看起來不在同一平面上,這就形成了3D效果。接著拖動頁面,側面的圖慢慢移到中間,這個過程也是有著動畫的,包括了圖片的旋轉、縮放和平移。在欣賞了上面的效果后,話不多說,我們來看看是怎樣實現的。

實現原理

1、利用ViewGroup的clipChildren屬性。大家可能對ClipChildren屬性比較陌生,我們先來看看官方文檔對該屬性的描述:

Defines whether a child is limited to draw inside of its bounds or not. This is useful with animations that scale the size of the children to more than 100% for instance. In such a case, this property should be set to false to allow the children to draw outside of their bounds. The default value of this property is true.

上面的大意是說,ViewGroup的子View默認是不會繪制邊界意外的部分的,倘若將clipChildren屬性設置為false,那么子View會把自身邊界之外的部分繪制出來。

那么這個屬性跟我們的ViewPager又有什么關聯呢?我們可以這樣想,ViewPager自身是一個ViewGroup,如果將它的寬度限制為某一個大小比如200dp(我們通常是match_parent),這樣ViewPager的繪制區域就被限制在了240dp內(此時繪制的是ViewA),此時我們將它的父容器的clipChildren屬性設置為false,那么ViewPager未繪制的部分就會在兩旁得到繪制(此時繪制的是ViewA左右兩邊的Item View)。

那么我們的布局文件可以這樣寫,activity_main.xml:

<RelativeLayout xmlns:android="

<android.support.v4.view.ViewPager
    android:id="@+id/viewpager"
    android:layout_width="240dp"
    android:layout_height="match_parent"
    android:clipChildren="false"
    android:layout_centerInParent="true">
</android.support.v4.view.ViewPager>

</RelativeLayout></code></pre>

接著,我們需要為每個Item創建一個布局,這個很簡單,就是一個ImageView,新建item_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/iv"
        android:layout_width="240dp"
        android:layout_height="360dp"
        android:layout_centerInParent="true"/>
</RelativeLayout>

布局文件寫好后,我們接著完成MainActivity.java和MyPagerAdapter.java的內容:

MainActivity.java:

public class MainActivity extends AppCompatActivity {

//這里的圖片從百度圖片中下載,圖片規格是960*640
private static final int[] drawableIds = new int[]{R.mipmap.ic_01,R.mipmap.ic_02,R.mipmap.ic_03,
        R.mipmap.ic_04,R.mipmap.ic_05,R.mipmap.ic_06,R.mipmap.ic_07,R.mipmap.ic_08,R.mipmap.ic_09,
        R.mipmap.ic_10,R.mipmap.ic_11,R.mipmap.ic_12};
private ViewPager mViewPager;
private RelativeLayout mRelativeLayout;
private MyPagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    initViews();
}

private void initViews() {
    mViewPager = (ViewPager) findViewById(R.id.viewpager);
    mPagerAdapter = new MyPagerAdapter(drawableIds,this);
    mViewPager.setAdapter(mPagerAdapter);
}

}</code></pre>

MyPagerAdapter.java:

public class MyPagerAdapter extends PagerAdapter {

private int[] mBitmapIds;
private Context mContext;

public MyPagerAdapter(int[] data,Context context){
    mBitmapIds = data;
    mContext = context;
}

@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);
    imageView.setImageResource(mBitmapIds[position]);
    container.addView(view);
    return view;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View) object);
}

}</code></pre>

ok,到現在為止,我們先運行一下看看結果如何:

ic_01.png

從上圖可以看出,本來ViewPager設置的寬度是240dp,那么原來應該只會顯示一個Page的內容,但是由于clipChildren=false屬性的生效,使得ViewPager早240dp之外的部分也被繪制了出來。那么到目前為止,就實現了在一屏顯示多個Page的效果了,那么接下來的3D效果怎樣實現呢?

2、利用ViewPager.PageTransformer實現滑動動畫效果

PageTransformer是Android3.0之后加入的一個接口,通過該接口我們可以方便地為ViewPager添加滑動動畫,但是該接口只能用于Android3.0之后的版本,3.0之前的版本會被忽略。我們看看這個接口需要重寫的唯一一個方法:

/**

 * 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

 

 本文由用戶 MalDesmond 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!