码迷,mamicode.com
首页 > 其他好文 > 详细

利用ViewPager实现3D画廊效果及其图片加载优化

时间:2017-04-13 18:13:27      阅读:319      评论:0      收藏:0      [点我收藏+]

标签:试验   res   tag   tran   type   .net   versions   sch   可见   

前言

对于ViewPager,相信大家都已经很熟悉了,在各种切换场景比如Fragment切换、选项卡的切换或者顶部轮播图片等都可以用ViewPager去实现。那么本篇文章带来ViewPager的一种实现效果:3D画廊。直接上图来看:
技术分享
从上面的图我们可以看出,整个页面分成三个部分,中间的是大图,正中地显示给用户;而两边的是侧图,而这两幅图片又有着角度的旋转,与大图看起来不在同一平面上,这就形成了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="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false">

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

接着,我们需要为每个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);
    }
}

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

ok,到现在为止,我们先运行一下看看结果如何:
技术分享
从上图可以看出,本来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);
    }

通过官方的注释,我们可以获得如下信息:①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,然后运行程序,向左右滑动几次,会发现出现了下面的问题:
技术分享
即左边或者右边的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);
        }

    }
}

最后,感谢你的阅读,希望这篇文章对你有所帮助~

利用ViewPager实现3D画廊效果及其图片加载优化

标签:试验   res   tag   tran   type   .net   versions   sch   可见   

原文地址:http://blog.csdn.net/a553181867/article/details/70158854

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!