看了guolin大神的一篇博客,介紹的很詳細,不適合小白。 viewpager可以左右滑動,如何做的呢,viepager的實現代碼太多了3千多行,不做深究了。我們實現簡單的滑動即可。說到滑動大家一定會想到scrollTo(x,y)和scrollBy(x,y)?,F在來看一下他們的起源,從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源碼,他是存粹的類,它的主要作用就是計算時間段滑動多少距離??匆欢蜸croller中的代碼,
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); }}新聞熱點
疑難解答