Android ViewPager用法

1.适配器PagerAdapter

ViewPager使用适配器类将数据和view的处理分离,ViewPager的适配器叫PagerAdapter,这是一个抽象类,不能实例化,所以它有两个子类:FragmentPagerAdapter 和 FragmentStatePagerAdapter,这两个都是处理页面为Fragment的情况。而PagerAdapter则是处理通用View的适配器,但是使用它的时候一定要自定义一个类继承这个基类。

private class Adapter extends PagerAdapter {

    public Object instantiateItem(ViewGroup container, int position) {

        //container其实就是ViewPager

        View itemView = LayoutInflater.from( context).inflate(R.layout.item_pager, null);

        container.addView(itemView);

        return itemView;

    }

    @Override

    public void destroyItem(ViewGroup container, int position, Object object) {

        // object为instantiateItem返回的object对象

        container.removeView((View) object);

    } 

    @Override

    public int getCount() { 

        return 10; //获取viewpager页数

    }

    @Override

    public boolean isViewFromObject(View view, Object o) {

      //判断view和o是否存在对应关系,内部是通过view找到对应的object的关联关系(instantiateItem中返回的object)。如果在instantiateItem方法中返回的是view,那这里就返回view == o

        return view == o;

    }

}

PageAdapter主要有以下几个方法:

①Object instantiateItem(ViewGroup container, int position)

这个方法在ViewPager需要加载某个页面时调用,container就是ViewPager自己,position是页面索引;

这个方法需要实现的是添加一个view到container中,然后返回一个跟这个view能够关联起来的对象,这个对象可以是view自身,也可以是其他对象(比如FragmentPagerAdapter返回的就是一个Fragment),关键是在isViewFromObject能够将view和这个object关联起来。

②void destroyItem(ViewGroup container, int position, Object object)

当ViewPager需要销毁一个页面时调用,在这个方法里需要将position对应的view从container中移除。

这个方法有三个参数,container就是ViewPager自己,position是页面索引,第三个参数object就是instantiateItem方法返回的对象。

在这个方法里需要通过object找到对应的View,然后将其移除掉,如果instantiateItem方法返回的就是View,就直接将object强转成View移除即可:container.removeView((View) object);如果不是,一般会自己创建一个List缓存view列表,然后根据position从List中找到对应的view移除(如果不移除,会产生内存泄漏)。FragmentPagerAdapter的实现是:mCurTransaction.detach((Fragment)object),其实也就是将fragemnt的view从container中移除

③isViewFromObject(View view, Object object)

这个方法用来说明instantiateItem方法中向container中添加的view和方法返回的对象两者之间一对一的关系。因为在ViewPager内部有个方法叫infoForChild,这个方法是通过view去找到对应页面信息缓存类ItemInfo(内部调用了isViewFromObject),如果找不到,ViewPager会认为不是Adapter提供的View,所以这个View不会显示出来。所以说,isViewFromObject方法是让view和object(内部为ItemInfo)一一对应起来。

④int getItemPosition(Object object)

该方法是判断当前object对应的View是否需要更新,在调用notifyDataSetChanged时会间接触发该方法。如果返回POSITION_UNCHANGED,表示该页面不需要更新;如果返回POSITION_NONE则表示该页面无效了,需要销毁并触发destroyItem方法(并且有可能调用instantiateItem重新初始化这个页面)。

从这几个方法就可以看出ViewPager的工作流程:在注册Adapter和draw绘制等时机,会调用 getCount()确定需要显示的子View的数量,然后在真正addView()加载子View的时候会调用instantiateItem()获取具体要显示的View,然后在装载显示的时候又会调用isViewFromObject()方法再次确认类型正确,然后在ViewPager不需要某个View的时候,会调用destroyItem()方法请求销毁。

 

2.FragmentPagerAdapter

子View是Fragment的情景比较常见,而Fragment的管理是个麻烦事,意味着Adapter中更多的代码量,针对这种情况,谷歌推荐开发者直接继承PagerAdapter的两个直接子类FragmentPagerAdapter和FragmentStatePagerAdapter,这样就不用关注Fragment的管理,只需要提供两个方法就行了。

FragmentPagerAdapter是一个不管理Fragment状态的适配器。这就意味着,许多碎片的Fragment会占用大量的内存(在少数情况下Fragment还是有可能会丢失的),如果在Fragment页面较少,或者每个Fragment页面中持有的数据较少的情况下,可以选择使用较为简单的适配器,不用关注并管理 Fragment 的状态,加快业务开发。

另一种情况是当大量碎片的Fragment占用的内存是不能被容忍,这时FragmentStatePagerAdapter就派上用场了,这个适配器保存并绑定了状态,在状态为销毁的状态下,可以去重新获取。但是频繁创建又会导致切换页面的性能消耗。所以在选择适配器的时候,要根据具体的业务逻辑进行判断,选择合适的适配器。

这两个子类的区别主要是Fragment内存管理状态的不同。为了验证,在Fragment的生命周期中添加Log。

采用了FragmentPagerAdapter 的Log记录:

//滑到第一页

MessageFragment: ---onAttach----

MessageFragment: ---onCreate----

FriendFragment: ---onAttach----

FriendFragment: ---onCreate----

MessageFragment: ---onCreateView---

FriendFragment: ---onCreateView---

//滑到第二页

CircleFragment: ---onAttach----

CircleFragment: ---onCreate----

CircleFragment: ---onCreateView---

//滑到第三页

AccountFragment: ---onAttach----

AccountFragment: ---onCreate----

MessageFragment: ----onDestroyView---

AccountFragment: ---onCreateView---

//滑到第四页

FriendFragment: ----onDestroyView---

 

采用了FragmentStatePagerAdapter的Log记录:

//滑到第一页

MessageFragment: ---onAttach----

MessageFragment: ---onCreate----

FriendFragment: ---onAttach----

FriendFragment: ---onCreate----

MessageFragment: ---onCreateView---

FriendFragment: ---onCreateView---

//滑到第二页

CircleFragment: ---onAttach----

CircleFragment: ---onCreate----

CircleFragment: ---onCreateView---

//滑到第三页

AccountFragment: ---onAttach----

AccountFragment: ---onCreate----

MessageFragment: ----onDestroyView---

MessageFragment: ----onDestroy--- (不同的地方)

MessageFragment: ----ondetach--- (不同的地方)

AccountFragment: ---onCreateView---

//滑到第四页

FriendFragment: ----onDestroyView---

FriendFragment: ----ondestroy--- (不同的地方)

FriendFragment: ----ondetach--- (不同的地方)

不更改ViewPager的预加载状态的情况下,翻到第一页(MessageFragment)和翻到第二页(FriendFragment)的时候,两个Adapter完全相同,这是因为这个时候根据ViewPager的缓存策略,会缓存3个子View,提高加载速度和显示流畅性。看日志可以证明:翻到第二页的时候第三个Fragment(CircleFragment)已经完成了加载操作,而且这个时候还没有回调Fragment卸载的相关方法。

翻到第三页(CircleFragment)和第四页(AccountFragment)的时候,出现了不同:当翻到第三页的时候,因为要提前加载第四页(AccountFragment),又由于缓存的数量是3,所以第一页(MessageFragment)开始回调卸载方法。使用FragmentPagerAdapter的回调了 onDestroyView卸载方法;使用FragmentStatePagerAdapter的回调了onDestroyView、onDestroy、onDetach卸载方法。当翻到第四页(AccountFragment)的时候,由于缓存的数量是3,而且是任一方向不保存1张以上,所以第二页开始回调卸载方法。使用FragmentPagerAdapter的回调了onDestroyView卸载方法;使用FragmentStatePagerAdapter的回调了onDestroyView、onDestroy、onDetach卸载方法。

可以看出来,采用FragmentStatePagerAdapter的ViewPager由于在意Fragment的State,为了节省内存,它回调了更彻底的onDestroy和onDetach方法,因此当需要重新使用完全卸载掉的Fragment时就需要通过getItem方法重新获取实例。而采用FragmentPagerAdapter的ViewPager,只回调onDestroyView方法,因此当需要显示或者提前加载这个Fragment的时候重新走onCreateView迅速创建显示,同时也就会一直驻留在内存里(在一般情况下)。

总结一下,在使用ViewPager选择PagerAdapter的时候应遵循这样的原则:

①当子View是普通View而非Fragment的时候,继承基类PagerAdapter实现必须实现的四个方法;

② 当子View是Fragment,并且子View中保存的内容比较少、轻量级、占用内存较小的时候,为了提高加载流畅性,使用FragmentPagerAdapter;

③当子View是Fragment,并且其中有些子View保存的内存较多、占用内存较大时,如果经大量测试发现不会出现out of memory,那为了保证流畅性,还是建议是用FragmentPagerAdapter,但是如果发现很容易oom,或者频率很大,那就一定要抛弃FragmentPagerAdapter,而采用FragmentStatePagerAdapter来辅助ViewPager加载子View。这样做出的牺牲就是数据每次都要重新加载,页面也需要重新加载初始化。

 

3.换页监听与换页方法

在viewpager滑动的时候需要给页面设置页码或者标题,页码是为了提醒用户浏览的总量以及当前所处的位置,而标题是为了给用户文字信息提示,并且也可以在广告场景作为图片的补充。

1)页面监听

实现页面监听,调用的是ViewPager的下列方法:

addOnAdapterChangeListener(OnAdapterChangeListener listener) 通过页面改变监听器进行监听

OnPageChangeListener是ViewPager的内部类,它是一个接口,有三个必须实现的方法:

①void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

这个方法是在滚动的时候进行回调,分两种情况,一种是代码控制的滚动,一种是用户手划滚动,都能监听的到,这个方法可以做一些滚动时的过程控制。注意:在初始化viewpager和Adapter后会回调这个方法一次。

②void onPageSelected(int position);

这个方法是在新页面已经呈现,并且动作还没有执行完全的时候回调的,这个方法就是用来动态设置页码的方法,只需要对position+1,就能对应当前页码,设置页面的标题也是在这个时候做。注意:这个方法在第一次加载viewpager后不会回调。所以无论是设置页码还是设置标题都需要在viewpager初始化之后手动设置一次,也就是手动设置首先呈现的页面的相关信息。

③void onPageScrollStateChanged(int state);

这个方法是对滑动方式状态的监听,可以根据state值来做判断。其中SCROLL_STATE_IDLE为切换页面动画结束,或者处于idle闲置状态;CROLL_STATE_DRAGGING为用户正在滑动时的状态回调,此时滑动未结束;SCROLL_STATE_SETTLING为非用户滑动,代码设置切换的状态,注意:这个状态会在手势滑动翻页(onPageSelected回调)之前回调一次。这个方法可以做一些关于不同滑动方式的滑动方案的区别设计,用户滑动的时候执行一套逻辑,代码切换的时候执行一套逻辑,并且还可以判断切换动画是否已经结束,是否处于idle状态。

如果只是改变当前页码并设置标题并不需要上面的三个方法都监听,这时就用到Listener的缺省适配SimpleOnPageChangeListener了,这是ViewPager的一个静态内部类,实现了上述接口的三个方法,所以继承它就可以在回调的时候只写出需要的回调方法,而不会出现编译报错。

对于页面切换的监听注册还有个过期的方法:

setOnPageChangeListener(OnAdapterChangeListener listener)

这个方法尽量不要用,是独占式的,不能注册多个,如果在不同的界面想要同时监听,就没办法了。

addOnPageChangeListener实际上是维护了一个list,调用这个方法就是往list里加元素,同时还有移除方法removeOnPageChangeListener,如果注册了多个,调用clearOnPageChangeListeners()就可以一次清除所有注册的监听。而setOnPageChangeListener是维护了一个单独的变量。

每次观察者模式通知事件的时候,先回调单独的,然后遍历list里的元素,分别回调。

2)换页方法

ViewPager允许代码控制翻页,并且可以设置翻页的状态,有2个方法:

setCurrentItem(int item) //设置对应位置显示

setCurrentItem(int item, boolean smoothScroll) //设置对应的位置显示,并且是否开启缓慢滑动

设置跳转页面的时候(比如界面上增加一个上一页按钮和下一页按钮)可以调用这两个方法来设置代码跳转。第一个方法,不传boolean值,默认值为判断是否第一次布局的状态,并取反。所以除了一进来是false,之后的都是true(取反后的)。

调用这个方法,需要在全局存一个当前位置int类型的position变量(如果不想保存,还可以调用ViewPager的getCurrentItem()方法获取当前位置),在监听按钮的时候需要对当前的全局位置变量+1或者-1,虽然在ViewPager的内部是有做越界判断的,但是如果你想提醒用户是否到了第一页和最后一页,就需要自己代码做判断。

而通过下面的方法只需要根据返回的布尔值做相应的提示:

boolean arrowScroll(int direction) //控制翻页

其中direction用的是View的几个静态常量:

表示上一页:View.FOCUS_LEFT , View.FOCUS_BACKWARD(二选一)

表示下一页:View.FOCUS_RIGHT, View.FOCUS_FORWARD(二选一)

调用上述方法传相应的值,然后就能翻页了,根据返回值判断是否到了第一页或者最后一页(如果是向前翻,返回false则是到了第一页;如果是向后翻,则是到了最后一页)。

上边两种方法都能实现相邻翻页,但是前边方法setCurrentItem可以跳转到任何一页,进入页面不想显示第一页可以通过这个方法设置(注意数组越界),并且当用户想要去哪一页也可指定跳转。而后边的方法arrowScroll只能相邻跳转,但是好处就是根据返回的布尔值判断是否到头了,不用自己去做判断和控制。

3)假滑动

boolean beginFakeDrag() //开始假滑

Void fakeDragBy(float xOffset) //控制假滑移动水平方向距离

void endFakeDrag() //结束假滑

boolean isFakeDragging() //返回当前是否假滑状态中

以上方法是完成一套完整的假滑流程操作。

执行具体动作的方法是第二个fakeDragBy(float xOffset),并将滑动距离的参数传入后,viewPager就会执行模拟滑动操作了。

但是随意调用这个方法,会抛出一个IllegalStateException异常:

No fake drag in progress. Call beginFakeDrag first.

这个时候就该第一个方法出场了。所以以上的四个方法的顺序是:

①在假滑之前判断一下当前是否正处于假滑的过程中,是否还没有结束。isFakeDragging()返回的boolean值就表示是否已经结束,能不能安全开始启动一个新的假滑。在返回false后再执行beginFakeDrag()启动假滑(重置参数,保证一次只能有一个假滑动作存在);

②启动假滑后调用fakeDragBy(float xOffset)传入想要滑动的距离,然后viewpager就会执行;

③在最后不需要假滑或者停止假滑的时候一定不要忘记调用endFakeDrag()释放当前假滑流程,并且这个时候仍然需要通过isFakeDragging()来判断当前状态,如果返回true,就能安全的停止了,如果是false的话,就不用调了,因为没有执行任何动作当然是不用停止的。如果不做判断的调,还会抛出和上边一样的异常来。而这个时候的判断正好与准备启动那个假滑开始判断相反。

这一套方法究竟用来干什么?

在某些场景下,用户可能不想要直接滑动Viewpager,但是在操作其他控件的时候也能让Viewpager执行滑动操作,好像有人在滑动它一样。所以说,这个是google为Viewpager和其他控件实现联动机制的方法。

再简单说下源码:

在beginFakeDrag里边有一句很重要的代码:

MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0);

mVelocityTracker.addMovement(ev);

在fakeDragBy(float xOffset)里边有这么几句代码,其中mLastMotionX是根据传入的移动偏量重新计算的移动变量:

MotionEvent ev = MotionEvent.obtain( mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, mLastMotionX, 0, 0);

mVelocityTracker.addMovement(ev);

而在endFakeDrag()方法中只是在判断当前滑动是否到了可以翻页的边界值,停止滑动后具体显示哪一页,其次就是讲一些过程控制变量重置。

 

4.预加载

懒加载和预加载是两部分,懒加载是针对具体页面加载内容的时机,而预加载指ViewPager这个容器在什么时机加载它的子View和加载多少的问题。

ViewPager的优势就是可以顺滑的处理多个页面的切换,这个情况就是充分的运用了ViewPager的预加载功能。ViewPager特有的预加载机制,在默认情况下当前页面左右两侧的1个页面会被加载,以方便用户滑动切换到相邻的界面时,可以更加顺畅的显示出来。

通过ViewPager的setOffscreenPageLimit(int limit)方法可以设置预加载页面数量。mOffscreenPageLimit意思就是:屏幕外的页面限制数量,即当前页面相邻的limit个页面会被预加载进内存。默认的预加载个数limit为1,所以一次性前后至少要预加载一页,如果目前处在第一页或者最后一页,将没有前一页或者没有后一页了,那这个时候,将默认最少加载两页了。

如果想获取当前的预加载数量,可以用下面的方法:

public int getOffscreenPageLimit() {

    return mOffscreenPageLimit;

}

如果把limit设置为 0 ,是不是就没有预加载了呢?答案是否定的,limit设置为0依然会预加载 ,可以看ViewPager的源码:

//默认的缓存页面数量(常量)

private static final int DEFAULT_OFFSCREEN_PAGES = 1;

//缓存页面数量(变量)

private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES;

public void setOffscreenPageLimit(int limit) {

    //当设置的limit数小于默认值1时,limit值会自动被赋值为默认值1

    if (limit < DEFAULT_OFFSCREEN_PAGES) {

        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "+ DEFAULT_OFFSCREEN_PAGES);

        limit = DEFAULT_OFFSCREEN_PAGES;

    }

    if (limit != mOffscreenPageLimit) {

        //经过前面的拦截判断后,将limit的值设置给mOffscreenPageLimit

        mOffscreenPageLimit = limit;

        populate();

    }

}

可以看到,就算是设置limit等于0,默认内部也会被修改成1。也就是说,如果设置小于默认的值,这个时候会使用默认值1。所以ViewPager的设计最低是缓存前后各一页,不允许不缓存。这个其实可以理解,不缓存、不预加载的话,如果每个子View加载数据比较费时间,这个时候就会在一个空页面等待很久,这个体验是非常差的。

预加载会带来问题:

①预加载的越多就会越卡;比如一个Fragment占用 1M,5个就(5*1M),累计到后面就会OOM;

②如果预加载的Fragment请求网络,不仅浪费流量,还会很卡顿.......

解决办法:

使用懒加载,来解决预加载带来的问题。

 

5.懒加载

懒加载就是为了防止预加载,可以用到才加载,可见才加载,不可见就不加载。

所以懒加载,其实也就是延迟加载,就是等到该页面的UI展示给用户时,再加载该页面的数据(从网络、数据库等),而不是依靠ViewPager预加载机制提前加载两三个,甚至更多页面的数据。这样可以提高所属Activity的初始化速度,也可以为用户节省流量。

其实懒加载更准确的应该是针对每个子View是Fragment的情况来说的,其他简单的View并不会占用太大的内存,提前加载会提高app显示速度,提升用户性能。而子View是Fragment的时候,由于ViewPager的预加载机制,导致无法通过ViewPager达到想要的懒加载效果,那么就得从Fragment自身入手了。

所以懒加载主要是通过Fragment的机制实现,在Fragment的回调方法中有这样一个方法:

public void setUserVisibleHint(boolean isVisibleToUser) {

    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED && mFragmentManager != null && isAdded()) {

        mFragmentManager. performPendingDeferredStart(this);

    }

    mUserVisibleHint = isVisibleToUser;

    mDeferStart = mState < STARTED && !isVisibleToUser;

    if (mSavedFragmentState != null) {

        mSavedUserVisibleHint = mUserVisibleHint;

    }

}

这个方法用来判断当前的Fragment界面是否对用户可见,当该页面对用户可见或不可见时,系统都会回调此方法。因此,可以重写该方法,然后根据回调的isVisibleToUser参数来进行相关的逻辑判断,以达到懒加载的效果,比如isVisibleToUser==true的时候再去加载页面数据。另外,由于ViewPager内会装载多个Fragment,而这种懒加载机制对于各个Fragment属于共同操作,因此适合将其抽取到BaseFragment中。

其实这个方法并不会像其他的生命周期方法按顺序执行,Fragment的这个方法主要用于用户自己控制当前View是否可见,而恰恰在Viewpager的两个FragmentPagerAdapter中就有对当前ViewPager子Fragment的可见性,如下边源码是FragmetPagerAdapter的部分源码:

@Override

public Object instantiateItem(ViewGroup container, int position) {

    if (mCurTransaction == null) {

        mCurTransaction = mFragmentManager.beginTransaction();

    }

    final long itemId = getItemId(position);

    String name = makeFragmentName( container.getId(), itemId);

    Fragment fragment = mFragmentManager. findFragmentByTag(name);

    if (fragment != null) {

        mCurTransaction.attach(fragment);

    } else {

        fragment = getItem(position);

        mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));

    }

    //关键在这

    if (fragment != mCurrentPrimaryItem) {

        fragment.setMenuVisibility(false);

        fragment.setUserVisibleHint(false);//调用这个方法,但是设置为false,这里其实是初始化所有的Fragment的状态为不可见

    }

    return fragment;

}

下边这个方法就是ViewPager源码中会调用到的方法,而这个方法调用的时机恰巧是要显示的那个页面:

@Override

public void setPrimaryItem(ViewGroup container, int position, Object object) {

    Fragment fragment = (Fragment)object;

    if (fragment != mCurrentPrimaryItem) {

        if (mCurrentPrimaryItem != null) {

            //将之前可见的页面重置为不可见

            mCurrentPrimaryItem.setMenuVisibility( false);

           mCurrentPrimaryItem.setUserVisibleHint( false);

        }

        if (fragment != null) {

            //将调用这个方法的当前页面设置为可见,也就是即将要显示的页面

            fragment.setMenuVisibility(true);

            fragment.setUserVisibleHint(true);

        }

        mCurrentPrimaryItem = fragment;

    }

}

instantiateItem方法就是具体加载子View。ViewPager创建每一个页面的时候会先将UserVisibleHint置为false,当前显示的页面会再置为true。

setPrimaryItem()方法是ViewPager在计算要显示的Item信息并保存的时候调用的。之后ViewPager就会执行绘制方法绘制子View,所以将要绘制的马上就要可见了,所以FragmentPagerAdapter中的这个方法的具体实现就是将上一次可见的置为不可见,然后将传过来的Fragment置为可见,就会看到当前的setUserVisableHint(true)方法回调了。

可以看到,谷歌对PagerAdapter的两个实现做了很多事情,里边集成了懒加载或者叫缓加载的策略,当然最后需要真正实现懒加载需要在具体的Fragmen中覆写setUserVisableHint这个方法。

这里有个坑要注意:这个方法回调的时候,可能在onCreateView之前,这个时候View还没有初始化,所以会报空指针。这个是一定要注意的,因为有时候可能会在这个方法里边进行网络请求,在网络回调方法中进行View的数据映射,这个时候报了空指针是有可能的,要知道原因,在Fragment的setUserVisableHint这个方法的源码中也是用Note的标签来引来开发者注意,着意强调这个方法不是生命周期方法,所以一定要自己控制,如果不用系统的FragmentPagerAdapter,而是实现PagerAdapter自己来控制,那需要在onCreateView里边View初始化完成后自行调用setUserVisibleHint(true)方法来执行具体网络加载。

和此类似的Fragment还有一个方法,就是setMenuVisiblity(Boolean visible),这个方法是为了控制菜单的懒加载,这个也是由我们自己控制,会控制Menu的显示和隐藏。

 

6.设置间距与添加转场动画

默认的ViewPager是从左到右依次连接所有子View,如果要让子View之间拉开一定的距离,在一般的XML布局之中,通常是设置子View之间的margin,但是ViewPager并没有将子View的layout暴露出来。但是依然可以通过代码调整子View的布局,并且还可以添加一些炫酷或者美观的分界线,通过以下几个方法:

int getPageMargin() 获得margin数值

void setPageMargin(int marginPixels) 设置margin相应的像素

setPageMarginDrawable(Drawable d) 设置相应的margin的drawable

setPageMarginDrawable(int resId) 设置相应margin的drawable的resId

在设置之前最好获取一下,就用第一个方法,如果新设置的值和获得的值一样,就不重新设置像素值得子margin了。

第二个方法就是设置具体像素的margin了,注意是像素,所以适配的话最好通过屏幕密度换算一下对应的dp值。

第三个方法和第四个方法其实是一个,是利用重载方便用户加载drawable资源,但这里应该注意一下,如果单独设置Drawable是不会生效的,必须和setPageMargin一起使用,也就是绘制的具体的间隔drawable高是ViewPager的高,宽就是通过setPageMargin设置的宽。所以如果不设置像素间距,设置的drawable就会落在子View的下面,那么就不用画了,现在来看ViewPager三段源码来验证下:

public void setPageMargin(int marginPixels) {

   //…

    mPageMargin = marginPixels;

    //…

}

第一个方法我们只保留了关键的给属性赋值

public void setPageMarginDrawable(Drawable d) {

    mMarginDrawable = d;

   //…

}

第二个方法也是只保留了关键的给属性赋值

@Override

protected void onDraw(Canvas canvas) {

    super.onDraw(canvas);

   //如果有需要的话,在子页面之间绘制间隔图案

    if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {

        //具体绘制drawable省略,这里只分析绘制drawable的启动机制

    }

}

真正的drawable就是通过这三个方法绘制出来的。

因为是分析为什么只设置drawable不会生效,我们看绘制onDraw方法的条件判断,这句是判断是否绘制的关键,他有四个因素,之间是与的关系,所以必须同时满足才行:

①间隔的像素值必须大于0,这个值默认就是0,通过setPageMargin设置(见第一段源码)

②间隔的图案不能为空,这个值默认就是null,通过setPageMarginDrawable设置(见第二段源码)

③必须要有子View,没有子View就不存在什么子View间隔了,这个好理解

④适配器必须不为空,不设置适配器也相当于没有子View

①②两点证明必须是要一起使用,drawable才能生效。

那四个条件如果只设置margin的像素值,而看源码是不执行绘制的,那么应该不会有间隔吧?实际上是有的,因为间隔值不需要绘制,通过布局将距离拉开就好了,不用绘制任何东西,显示的就是背景色。。

其实,子View是Fragment的情况,并不需要设置间隔,这样有画蛇添足之嫌。而设置Margin通常是在一个画廊(也就是ViewPager)里边包含了很多图片,这样滑动的时候希望它能有更出色的交互体验,这里就引出了另一个技巧就是转场动画,其实margin是配合着转场动画使用的,从而可以实现更加丰富多彩的交互方式。

ViewPager将动画交给了一个接口去处理,既然是接口,如果使用就一定要去实现:

public interface PageTransformer {

    void transformPage(View page, float position);

}

PageTransformer指页面变换器,专注处理动画。

注意:由于ViewPager的转场动画利用的是属性动画的原理,而属性动画在不引入第三方支持包的基础上,是支持3.0及以上的,所以在3.0以下的手机,转场动画会被忽视,并不会报错。

页面变换器只需要实现一个方法transformPage即可,这个方法有两个参数:

参数page:即将把动画赋予的子页面。

参数position:是float值,就要和作为int值的它区分开,float是个相对位置,它是一个-1到1的值,相对位置提供给开发者,可以控制动画进度和方式。具体的:0的时候是要执行动画的页面处于页面前端并居中的位置,1是要执行动画的页面向右移动到看不见(宽度为一整个页面),-1是要执行的动画向左移动到看不见的位置(宽度为一整个页面),正好对应一个进入动画,一个退出动画。

Android ViewPager用法_第1张图片

下面提供几个动画实现:

页面变换器实现了,要与ViewPager建立联系才能发挥作用,可以通过以下方法注入:

setPageTransformer(boolean reverseDrawingOrder,PageTransformer transformer)

setPageTransformer(boolean reverseDrawingOrder,PageTransformer transformer, int pageLayerType) 

以上设置变换器有两个重载方法,一个是两个参数,一个是三个参数

参数reverseDrawingOrder:是否执行相反的绘制命令,绘制后面的page在前面page之上,就为false,反之则为true。

参数PageTransformer:实现的页面变换器接口,这个提供一个实例。

参数pageLayerType:这个是和SurfaceView绘制相关,主要有以下几种图层类型

View.LAYER_TYPE_HARDWARE = 2; 采用硬件加速图层

View.LAYER_TYPE_SOFTWARE = 1; 不论硬件加速是否打开,都会使用软件渲染pipe通道

View.LAYER_TYPE_NONE = 0; 指示View没有图层

为什么会区分这三种模式呢?这里涉及复杂的绘制原理,这里只要记住,如果ViewGroup中包含了SurfaceView并且没有调用setZOrderOnTop(boolean)方法,那么就会出一些bug了。这个时候为了避免这种情况,需要调用三个参数的方法,并且需要传递LAYER_TYPE_NONE这个值给pageLayerType。两个参数时,系统会默认采用LAYER_TYPE_HARDWARE图层。除此之外,没有影响,就调用默认两个参数的方法。

看下面的实际效果:

//旋转缩放效果

class MyPageTransformer implements ViewPager.PageTransformer {

    private final float MIN_SCALE = 0.75f;

    @Override

    public void transformPage(View page, float position) {

        /*缩放旋转*/

        if (position <= 0f) {

            page.setTranslationX(0f);

            page.setScaleX(1f);

            page.setScaleY(1f);

        } else if (position <= 1f) {

            final float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));

            page.setAlpha(1 - position);

            page.setPivotY(0.5f * page.getHeight());

            page.setTranslationX(page.getWidth() * -position);

            page.setScaleX(scaleFactor);

            page.setScaleY(scaleFactor);

        }

       page.setRotation(360 * position);

    }

}

Android ViewPager用法_第2张图片

 

class MyPageTransformer implements ViewPager.PageTransformer {

    @Override

    public void transformPage(View page, float position) {

       //3D旋转

        int width = page.getWidth();

        int pivotX = 0;

        if (position <= 1 && position > 0) {// right scrolling

            pivotX = 0;

        } else if (position == 0) {

 

        } else if (position < 0 && position >= -1) {// left scrolling

            pivotX = width;

        }

        //设置x轴的锚点

        page.setPivotX(pivotX);

        //设置绕Y轴旋转的角度

        page.setRotationY(90f * position);

    }

}

Android ViewPager用法_第3张图片

 

//这种转变需要结合margin以及padding来实现

//在此之前我们需要在layout.xml文档中关闭剪切子View开关

//android:clipChildren="false"

//并且注意,是ViewPager及其父布局都要设置,否则不生效

    android:layout_width="match_parent"

    android:clipChildren="false"

    android:layout_height="match_parent">

   

        android:id="@+id/banner"

        android:layout_width="match_parent"

        android:clipChildren="false"

        android:layout_height="match_parent"/>

//接下来是转场变换器

class MyPageTransformer implements ViewPager.PageTransformer {

    // private final float MIN_SCALE = 0.75f;

    private final float MIN_SCALE = 0.5f;

    private final float MIN_ALPHA = 0.5f;

    @Override

    public void transformPage(View page, float position) {

        float scaleFactor = MIN_SCALE + (1 - MIN_SCALE) * (1 - Math.abs(position));

        float alphaFactor = MIN_ALPHA + (1 - MIN_ALPHA) * (1 - Math.abs(position));

        page.setScaleY(scaleFactor);

        page.setAlpha(alphaFactor);

    }

}

//然后还有适配器加载子View方法

class MyBaseAdapter extends PagerAdapter {

  //其他必须继承的方法这里省略

    @Override

    public Object instantiateItem(ViewGroup container, int position) {

        ImageView image = mImageList.get(position);

        container.addView(image);

        ViewGroup.LayoutParams params = image.getLayoutParams();

        params.width = 1080;

        params.height = ViewGroup.LayoutParams.MATCH_PARENT;

        image.setLayoutParams(params);

        //对,关键是instantiateItem的这个padding的设置方法

        image.setPadding(200, 80, 200, 80);

        image.setScaleType( ImageView.ScaleType.FIT_XY);

       image.setImageDrawable( getResources().getDrawable(mPicIds[position]));

        return image;

    }

}

    

 @Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

  //最后通过设置margin生效

   mBaseAdapter = new MyBaseAdapter();

    mPager.setPageMargin(-300);

    mPager.setOffscreenPageLimit(3);

    mPager.setAdapter(mBaseAdapter);

    mPager.setPageTransformer(true, new MyPageTransformer());

}

Android ViewPager用法_第4张图片

这个每个子View都居中,而下边这个第一个靠左,最后一个靠右,其他的才居中

Android ViewPager用法_第5张图片

 实现这个的关键,是需要我们动态调整padding,同时还需要匹配子View的width,覆写适配器的另一个方法,好了下面看代码:

//除了layout.xml,转换器,初始化设置保持相同外,以下是不同之处,只贴这部分代码:

//主要是适配器的两个方法

class MyBaseAdapter extends PagerAdapter {

  //其他方法省略

    @Override

    public Object instantiateItem(ViewGroup container, int position) {

        ImageView image = mImageList.get(position);

        container.addView(image);

        ViewGroup.LayoutParams params = image.getLayoutParams();

        params.width = 1080;

        params.height = ViewGroup.LayoutParams.MATCH_PARENT;

        image.setLayoutParams(params);

        //这里设置第一页靠左,最后一页靠右,其他居中的效果

        if (position == 0) {

            image.setPadding(50, 80, 200, 80);

        } else if (position == mImageList.size() - 1) {

            image.setPadding(200, 80, 50, 80);

        } else {

            image.setPadding(200, 80, 200, 80);

        }

        image.setScaleType( ImageView.ScaleType.FIT_XY);

        image.setImageDrawable( getResources().getDrawable(mPicIds[position]));

        return image;

    }

    @Override

    public float getPageWidth(int position) {

        super.getPageWidth(position);

        float width = 1.0f;

        if (position == 0 || position == mImageList.size() - 1) {

        //这个是ViewPager在测量确定子View所占宽度的时候用到的,

        //由于在instantiateItem方法中第一个和最后一个特殊处理了,导致在展示的时候,

        //第一个的右边间距过大,最后一个左边间距过大,为了调整这部分差异,

        //需要实现这个方法,并且对第一个和最后一个宽度进行计算

        //下边代码中150,正是第一个和最后一个异常间距的差异值

            width = (float) (DensityUtil.getScreenWidth( AdapterActivity.this) - 150) / DensityUtil.getScreenWidth( AdapterActivity.this);

        }

        return width;

    }

总结:

①增加转场效果,需要实现ViewPager.PageTransformer的唯一方法,在这个方法中的参数值区间是[-1,1],其中-1是从左边退出到头,-1到0,是左边到中间的一个进度值,0是正好稳定显示的位置,0到1是中间到右边的一个进度值,1就是正好右边退出到头;根据这个关系,结合属性动画,就能实现更加丰富多彩的转场动画效果了;

②padding可以在适配器的加载方法中设置,通过view.setPadding();方法能够调整当前页面的相对位置;

③如果想让左右两边的View部分进入当前View,首先需要关闭clipChildren开关,然后设置负的margin值,不关闭的话,默认会剪切,那么负的就会被剪切掉;导致效果失效。

你可能感兴趣的:(笔记,android)