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是要执行的动画向左移动到看不见的位置(宽度为一整个页面),正好对应一个进入动画,一个退出动画。
下面提供几个动画实现:
页面变换器实现了,要与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);
}
}
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);
}
}
//这种转变需要结合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());
}
这个每个子View都居中,而下边这个第一个靠左,最后一个靠右,其他的才居中
实现这个的关键,是需要我们动态调整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值,不关闭的话,默认会剪切,那么负的就会被剪切掉;导致效果失效。