1. ViewPager 使用示例
2. 源码解析
示例
ViewPager 可以说是 IM 中必用的一个控件了,举例比如 WeChat,Weibo,其用法也非常之简单。
<android.support.v4.view.ViewPager android:id="@+id/viewpager" android:layout_width="fill_parent" android:layout_height="fill_parent"> </android.support.v4.view.ViewPager>
xml文件中定义完之后,再看java文件中代码
public class MainActivity extends AppCompatActivity {private ViewPager viewpager=null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); viewpager=(ViewPager)findViewById(R.id.viewpager); MyAdapter adapter=new MyAdapter(); LayoutInflater lf=getLayoutInflater(); adapter.addviews(lf.inflate(R.layout.page1,null)); adapter.addviews(lf.inflate(R.layout.page2,null)); adapter.addviews(lf.inflate(R.layout.page3,null)); viewpager.setAdapter(adapter); }
}
可以看到 ViewPager 的用法和 ListView 的用法是类似的,都需要一个 Adapter 来加载数据,下面看这个自定义的 MyAdapter。
public class MyAdapter extends PagerAdapter {private ArrayList<View> views; public void addviews(View v){ views.add(v); } @Override public int getCount() { return views.size(); } @Override public boolean isViewFromObject(View view, Object object) { return view==object; } @Override public void destroyItem(ViewGroup container, int position, Object object) { container.removeView(views.get(position)); } @Override public Object instantiateItem(ViewGroup container,int position){ container.addView(views.get(position)); return views.get(position); }
}
很简单吧?跟ListView的用法真是太相似了,ViewPager的内部还有一个接口
public interface OnPageChangeListener { public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); public void onPageSelected(int position); public void onPageScrollStateChanged(int state); }
接口的作用也很明显,最常用的就是用来加载各个tab的内容。
网上的用法有很多,这里就不细讲了,重点是下一部分。
开始看到ViewPager的滑动效果时,马上联想到了ListView的下拉刷新,想到ViewPager也是采用这种原理来实现的,后来一看源码,果然,其原理就是相当于将多个View平行的摆放在一起,滑动即时调整横向的offset,来实现滑屏效果。
初始化的代码跳过,直接看onTouchEvent函数的代码,
首先介绍一下ViewPager中比较重要的一些变量和类。
private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); static class ItemInfo { Object object; //View或者Image int position; //在mItems中的position,一般等同于offset boolean scrolling; float widthFactor; float offset; }
mItems是一个ItemInfo的ArrayList,很重要,是前一个Page,当前Page,后一个Page的集合。
ACTION_DOWN
case MotionEvent.ACTION_DOWN: { mScroller.abortAnimation(); mPopulatePending = false; populate();// Remember where the motion event started mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); break; }</pre>
重点是记录X,Y,PointerId,和populate()函数,前面的变量都好理解,这个populate函数还是比较复杂的,我们看看。
final int pageLimit = mOffscreenPageLimit;//偏离屏幕page限制,一般左右各一个 final int startPos = Math.max(0, mCurItem - pageLimit);左边的page final int N = mAdapter.getCount(); final int endPos = Math.min(N-1, mCurItem + pageLimit);右边的page ............ int curIndex = -1; ItemInfo curItem = null; for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); if (ii.position >= mCurItem) { if (ii.position == mCurItem) curItem = ii; break; } } //找到curItem ............. if (curItem != null) { float extraWidthLeft = 0.f; int itemIndex = curIndex - 1; ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; final int clientWidth = getClientWidth(); final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; for (int pos = mCurItem - 1; pos >= 0; pos--) { if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthLeft += ii.widthFactor; itemIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex + 1); extraWidthLeft += ii.widthFactor; curIndex++; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } }float extraWidthRight = curItem.widthFactor; itemIndex = curIndex + 1; if (extraWidthRight < 2.f) { ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; final float rightWidthNeeded = clientWidth <= 0 ? 0 : (float) getPaddingRight() / (float) clientWidth + 2.f; for (int pos = mCurItem + 1; pos < N; pos++) { if (extraWidthRight >= rightWidthNeeded && pos > endPos) { if (ii == null) { break; } if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { extraWidthRight += ii.widthFactor; itemIndex++; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } else { ii = addNewItem(pos, itemIndex); itemIndex++; extraWidthRight += ii.widthFactor; ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; } } }</pre>
这是核心代码,代码有点长,主要作用就是产生一个mItems,使得mItems中含有左右以及当前的Page,产生的依据是extraWidth和WidthNeeded的关系,这里注意,mItems有多个地方会产生调用,比如onMeasure中,所以一次touch滑屏,会分别调用多次populate(),一般调用两次吧。
extraWidth和WidthNeeded分Left和Right,现只拿Right来做讲解。
首先float extraWidthRight=curItem.widthFactor;
widthFactor宽度因子,一般为1,代表一个page的宽度。
final float rightWidthNeeded=(float)getPaddingRight()/(float)clientWidth+2.f;
rightWidthNeeded=2,很明显,不用计算。
进入for循环,先假设总共有3个Page,现在从2滑到1
先看第2次调用吧,即屏幕已经从2变成了1,但是mItems里的值还没有发生改变,但是curItem是1了。
pos=mCurItem+1=1,此时pos=1,endPos=1,ii代表的是itemIndex所对应的mItem,现在是page2,进入else if语句,extraWidthRight+=ii.widthFactor,此时extraWidthRight=2,即curItem和右边Page的宽度总和。
itemIndex++;得到page3所对应的mItem,pos=2>endPos,直接进入if语句,我们可以看到
mItems.remove(itemIndex); mAdapter.destroyItem(this, pos, ii.object);
成功删除了page3这个对象,mItems的大小变为了2。
以上就是ViewPager的核心--mItems,基本由populate掌控。
后面的就比较简单了,上面的代码看了比较久,主要是在观察各种width和mItems之间的关系,还有千万不要忽略了滑屏过后onMeasure方法会重新调用populate()。
ACTION_MOVE
if (mIsBeingDragged) { final int activePointerIndex = MotionEventCompat.findPointerIndex( ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); needsInvalidate |= performDrag(x); }
核心函数是performDrag(x),作用顾名思义,就是产生drag的效果。
private boolean performDrag(float x) { boolean needsInvalidate = false;final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); float scrollX = oldScrollX + deltaX; final int width = getClientWidth(); float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; boolean leftAbsolute = true; boolean rightAbsolute = true; final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); if (firstItem.position != 0) { leftAbsolute = false; leftBound = firstItem.offset * width; } if (lastItem.position != mAdapter.getCount() - 1) { rightAbsolute = false; rightBound = lastItem.offset * width; } if (scrollX < leftBound) { if (leftAbsolute) { float over = leftBound - scrollX; needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); } scrollX = leftBound; } else if (scrollX > rightBound) { if (rightAbsolute) { float over = scrollX - rightBound; needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); } scrollX = rightBound; } // Don't lose the rounded component mLastMotionX += scrollX - (int) scrollX; scrollTo((int) scrollX, getScrollY()); pageScrolled((int) scrollX); return needsInvalidate; }
别看代码很乱,主要就是leftBound,RightBound,leftAbsolute,rightAbsolute的关系,这里还要注意First和Last,千万不要以为是第一个Page和最后一个Page,这个是相对于mItems来说的,所以前面的mItems概念理解错了,这里也会理解错。
leftAbsolute=true表示,左边没有Page了,RightAbsolute也一样。
从上到下的这些if分别是判断,firstItem和lastItem是否在边缘,socrllX的判断,则是在leftAbsolute或者RightAbsolute为true时起作用,即是超过边缘,无效的滑动。最后scrollTo实现drag效果。
ACTION_UP
其实弹起后的代码就很好想了,无非是根据fling这类的手势或者offset来判断是否要跳到下一个page,代码也很简单了,大家自己看看吧。
其实我们也可以看到,android系统自带的ViewPager中mItems的使用是非常节约内存的。为了锻炼自己对View和ViewGroup的理解,决定下星期自己写一个ScrollLayou试试。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于