看了guolin大神的一篇博客,介绍的很详细,不适合小白。 viewpager可以左右滑动,如何做的呢,viepager的实现代码太多了3千多行,不做深究了。我们实现简单的滑动即可。说到滑动大家一定会想到scrollTo(x,y)和scrollBy(x,y)。现在来看一下他们的起源,从View控件中可以找到。
public class View implements Drawable.Callback, KeyEvent.Callback,accessibilityEventSource { public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } }}代码可以看到这两个函数已经实现,不是抽象方法,而且我查了ViewGroup,LinearLayout,TextView里面都没有这两个方法的复写,只在TextView中使用过,所以可以这样认为,scrollTo(x,y)和scrollBy(x,y)在View类中就是最后的实现。所以查看它们到View中看就OK了。另一方面说明了,其他所有继承View控件都存在这两个方法,并且控件内容都可以移动。
说了这么多废话,现在进入正题,scrollTo(x,y)和scrollBy(x,y)有什么区别呢。从scrollBy(x,y)的实现上可以看到,scrollBy(x,y)其实内部调用的就是scrollTo(x,y),唯一的区别就是在原有移动距离上加上新的移动距离。假设现在x轴已经移动了sx,y轴移动sy,如果在次调用scrollBy(x,y),在x轴上的移动距离变成x+sx,y轴上的一定距离y+sy。如果是scrollTo(x,y)无论调用多少次,只会在第一次调用时移动,除非改变x,y值。
说道现在,大家可能已经明白了,viewpager的滑动与scrollTo(x,y)和scrollBy(x,y)有关。是的,就是他们实现了viewpager的滑动。下面是我写的滑动容器:
public class ScollerContainer extends ViewGroup { PRivate Scroller scroller; private float XDown; private float XMove; private float XLastMove; private int leftBorder; private int rightBorder; private int touchSlop; public ScollerContainer(Context context) { super(context); init(); } public ScollerContainer(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ScollerContainer(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ scroller = new Scroller(getContext()); ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); touchSlop = viewConfiguration.getScaledTouchSlop(); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { float diff = 0; switch (event.getAction()){ case MotionEvent.ACTION_DOWN: XDown = event.getRawX(); XLastMove = XDown; break; case MotionEvent.ACTION_MOVE: XMove = event.getRawX(); diff = Math.abs(XMove-XDown); XLastMove = XMove; if (diff>touchSlop){ return true; } break; } return super.onInterceptHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { float scrollerX ; float diff; switch (event.getAction()) { case MotionEvent.ACTION_MOVE: scrollerX = getScrollX(); XMove = event.getRawX(); diff = XLastMove-XMove; if (scrollerX+diff<leftBorder){ scrollTo(leftBorder,0); return true; }else if (scrollerX+diff+getWidth()>rightBorder){ scrollTo(rightBorder -getWidth(),0); return true; } scrollBy((int) diff,0); XLastMove = XMove; break; case MotionEvent.ACTION_UP: // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面 int targetIndex = (getScrollX() + getWidth() / 2) / getWidth(); int dx = targetIndex * getWidth() - getScrollX(); // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面 Log.d("moveX:","scrollX="+getScrollX()+" dx="+dx); scroller.startScroll(getScrollX(), 0, dx, 0); invalidate(); break; } return super.onTouchEvent(event); } @Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(),scroller.getCurrY()); invalidate(); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int count = getChildCount(); View child=null; for (int i=0;i<count;i++){ child = getChildAt(i); child.measure(widthMeasureSpec,heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); View child = null; for (int i=0;i<count;i++){ child = getChildAt(i); child.layout(child.getMeasuredWidth()*i,0,child.getMeasuredWidth()*(i+1),child.getMeasuredHeight()); } leftBorder = getChildAt(0).getLeft(); rightBorder = getChildAt(count-1).getRight(); }}上面的代码就可以实现滑动了,看图:
如果只使用了scrollTo(x,y),scrollBy(x,y),虽然可以实现滑动,但是不会出现粘性滑动,就是手指离开后,控件慢慢回到原位。这是怎么做到的呢?下面开始讲解。
要做到粘性滑动,就要使用Scroller,可以看下Scroller源码,他是存粹的类,它的主要作用就是计算时间段滑动多少距离。看一段Scroller中的代码,
public void startScroll(int startX, int startY, int dx, int dy) { startScroll(startX, startY, dx, dy, DEFAULT_DURATION); } public void startScroll(int startX, int startY, int dx, int dy, int duration) { mMode = SCROLL_MODE; mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; mDurationReciprocal = 1.0f / (float) mDuration; }上述两个函数都传入了距离,时间。手指离开手机后,控件“粘性还原”要用到“时间”(没有传入时间,使用默认值DEFAULT_DURATION)和“移动的距离”。依据”时间“和“移动距离”计算出每秒移动的距离(这句话不严格,真正实现算法很复杂,还有加速,减速情况,只不过这样说容易理解),然后通过scroller实例中的scroller.getCurrX()和scroller.getCurrY()方法取得计算后要移动的距离值。如此看来,Scroller就是个计算“移动距离”的工具类。看代码,
public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()){//取得计算后要移动的距离值 scrollTo(scroller.getCurrX(),scroller.getCurrY()); invalidate(); } }scroller.computeScrollOffset()判断scroller内部处理有没有结束(内部处理结束也就意味着,控件已经粘性复原了,因为内部处理依赖传入的距离和时间吗),如果结束,则scroller.computeScrollOffset()返回false
有人会问,computeScroll()为什么会循环调用呢?看到 invalidate()了吗,这个充当循环角色, invalidate()被执行后,ui界面会被重新绘制,这样的话,draw()函数就会被调用,我们看一下它的源码:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {...if (!drawingWithRenderNode) { computeScroll(); sx = mScrollX; sy = mScrollY; }...}draw()执行了computeScroll(),而computeScroll()中又存在invalidate()方法,所以构成了循环,不是吗。这只是粘性滑动实现的一部分,另一部分开代码(截取ScollerContainer中的代码),
case MotionEvent.ACTION_UP: // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面 int targetIndex = (getScrollX() + getWidth() / 2) / getWidth(); int dx = targetIndex * getWidth() - getScrollX(); // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面 Log.d("moveX:","scrollX="+getScrollX()+" dx="+dx); scroller.startScroll(getScrollX(), 0, dx, 0); invalidate(); break;int targetIndex = (getScrollX() + getWidth() / 2) / getWidth(); 这段代码如何解释,getScrollX() 已经滑动的距离, getWidth() / 2不滑动控件的宽的1/2。没有画图工具,大家自己画图思考,我用文字描述。 用viewpager解释,大家方便想象。假设viewpager中有10项,可以被滑动,分别标志0,1,2,3,4,5,6,7,8,9。假如当前滑动到第4项和第5项之间,手指不离开,脑洞打开想一下。当手指离开时,是让第4项显示还是第5项显示在手机屏幕上。在4,5之间,此时,getScrollX() >4*getWidth(),这里分两种情况, 第一种情况,如果4滑动过半了,getScrollX() + getWidth() / 2>5*getWidth(),那么,(getScrollX() + getWidth() / 2) / getWidth()值是不是5.xxx,取整后=5,再看, targetIndex * getWidth() - getScrollX()不就是4没有过半的距离值(targetIndex * getWidth()是第5项距离值),最后粘性结果手机屏幕上显示第5项。 第二种情况,如果4没有过半,getScrollX() + getWidth() / 2<5*getWidth(),同样,(getScrollX() + getWidth() / 2) / getWidth()值是不是4.xxx,取整后=4,在看targetIndex * getWidth() - getScrollX()不就是4移动的没过半的距离值。 在执行 ,scroller.startScroll(getScrollX(), 0, dx, 0); invalidate();后,手指离开后,粘性滑动并复位了吗。
剩余代码:
<?xml version="1.0" encoding="utf-8"?> <com.luo.usedemo.ScollerContainer xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scrollerContainer" android:layout_height="match_parent" android:layout_width="match_parent"> <Button android:layout_width="match_parent" android:layout_height="100dp" android:text="第一个view"/> <Button android:layout_width="match_parent" android:layout_height="100dp" android:text="第二个view"/> <Button android:layout_width="match_parent" android:layout_height="100dp" android:text="第三个view"/> </com.luo.usedemo.ScollerContainer>public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}新闻热点
疑难解答