美文网首页安卓动画效果Android动画自定义view
利用ViewPager实现3D画廊效果及其图片加载优化

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

作者: 丶蓝天白云梦 | 来源:发表于2017-04-13 15:55 被阅读2651次

    前言

    对于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="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,到现在为止,我们先运行一下看看结果如何:


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

    通过官方的注释,我们可以获得如下信息:①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);
            }
    
        }
    }
    

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

    相关文章

      网友评论

      • ArcherZang:RotaionPageTransformer没有实现第一个图的效果哦,下面我自己改的
        public class RotationPageTransformer implements ViewPager.PageTransformer {

        private static final float MIN_SCALE=0.15f;
        private static final float rotate = 10;

        @Override
        public void transformPage(View page, float position) {
        Log.e("transformPage", "position"+position+" page" + page.toString());
        //position小于等于1的时候,代表page已经位于中心item的最左边,
        //此时设置为最小的缩放率以及最大的旋转度数
        final float scaleFactor = 1 - MIN_SCALE * Math.abs(position);
        final float rotateFactor = rotate * Math.abs(position);
        if (position <= -1){
        page.setScaleX(Math.max(1-MIN_SCALE, scaleFactor));
        page.setScaleY(Math.max(1-MIN_SCALE, scaleFactor));
        page.setRotationY(rotate);
        }//position从0变化到-1,page逐渐向左滑动
        else if (position < 0 && position > -1){
        page.setScaleX(scaleFactor);
        page.setScaleY(scaleFactor);
        page.setRotationY(rotateFactor);
        }//position从0变化到1,page逐渐向右滑动
        else if (position >0 && position < 1){
        page.setScaleX(scaleFactor);
        page.setScaleY(scaleFactor);
        page.setRotationY(-rotateFactor);
        }//position大于等于1的时候,代表page已经位于中心item的最右边
        else if (position >= 1){
        page.setScaleX(Math.max(1-MIN_SCALE, scaleFactor));
        page.setScaleY(Math.max(1-MIN_SCALE, scaleFactor));
        page.setRotationY(-rotate);
        } else if (position == 0) {
        page.setScaleX(1);
        page.setScaleY(1);
        page.setRotationY(0);
        }
        }
        }
      • 大桥酱:写的真的很好,跟随作者的思路 一步步了解作者的思考过程,很受益。
      • 4341d723c7bf:楼主源码来一份啊
      • 阿西吧喽:一般viewpager加载图片都是用框架的吧
        丶蓝天白云梦:@阿西吧喽 嗯,用图片加载框架,比如Picasso

      本文标题:利用ViewPager实现3D画廊效果及其图片加载优化

      本文链接:https://www.haomeiwen.com/subject/gqkrattx.html