在Android中想要實現實現滑動有很多方法,這篇博客將提供一些實現滑動的思路,希望可以幫助到有需要的人。
一、Android坐標體系
在講解滑動之前,我們有必要簡單提一下Android的坐標體系,因為滑動的實質就是坐標的不斷改變,所以我們先來了解一下
Android坐標系
和視圖坐標系
兩個概念。直接放上兩張圖片吧,一目了然。
從上面的兩張圖可以看出,
Android坐標系
的坐標原點位于屏幕的左上角,而視圖坐標系
的原點位于父視圖的左上角,既然提供了兩種不同的坐標系,那么我們如何來獲取坐標呢,Android已經給我們提供了一些方法用于獲取這些坐標,看下面的圖便一目了然。
二、layout
方法
在View進行繪制時,是調用
onLayout()
方法來確定View的位置的,同樣我們也可以調用layout()
方法來傳入我們滑動后的坐標便可以實現View的滑動,當然坐標的獲取我們可以在觸控事件中進行獲取,下面我們做一個View隨手指進行滑動
的小例子來進行說明。
public class DragView extends View {
private int mLastX;
private int mLastY;
public DragView(Context context) {
this(context, null);
}
public DragView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int lastX = 0, lastY = 0;
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
}
上面我們在觸控事件中獲取到獲取到手指按下時的坐標(lastX, lastY),然后在手指移動時不斷計算X和Y方向上的偏移量,然后再調用
layout()
方法來改變View的位置從而實現滑動。當然上面我們是通過getX()
和getY()
來獲取視圖坐標來進行修改,我們也可以通過getRawX()
和getRawY()
來獲取絕對坐標來實現上面的效果。代碼如下:
private int mLastX;
private int mLastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
//重新設置初始坐標
mLastX = x;
mLastY = y;
break;
}
return true;
}
上面一定要注意,我們在改變完
View
的位置后必須調用設置初始坐標,這樣才能準確獲取偏移量。
三、offsetLeftAndRight
和offsetTopAndBottom
這一種方法和上一種方法大部分步驟都是相同的,只是在移動View上有所差別,代碼如下:
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
上面的這種方法只是多了一層封裝,可以實現比上面實現同樣的效果。
四、設置LayoutParams
LayoutParams
可以通過改變的布局參數,我們可以通過下面的代碼實現上面同樣的效果。
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
注意:我們的
LayoutParams
可以通過getLayoutParams()
方法來獲取,但是要注意,如果View的父布局是LinearLayout
,那么我們的LayoutParams
就是LinearLayout.LayoutParams
,如果View的父布局是RelativeLayout
,則我們的LayoutParams
就是RelativeLayout.LayoutParams
。當然我們還有一種簡單的方法,不用再管父布局的布局方式。代碼如下:
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
marginLayoutParams.leftMargin = getLeft() + offsetX;
marginLayoutParams.topMargin = getTop() + offsetY;
setLayoutParams(marginLayoutParams);
上面的這種方法不用管父布局的類型,使用起來更加方便。
五、scrollTo
和scrollBy
方法
關于這兩個方法我們需要仔細說一下其中的一些注意事項
1 . scrollTo
的參數是具體的一個坐標點(x, y), 而scrollBy
的參數是在x, y方向上的坐標偏移
2 . scrollTo
和scrollBy
移動的是View的內容。這一點很重要!!!!
如果我們對
ViewGroup
使用scrollTo
和scrollBy
則移動的是內部的所有子View, 如果對TextView
使用scrollTo
和scrollBy
則移動的是其中額文本。
3 . 視圖移動還有一個不太好理解的地方在于坐標,我們下面結合圖片來說明一下:
我們可以這樣理解,我們的手機屏幕作為一個蓋板,在手機屏幕下面是一個巨大的畫布,我們的手機屏幕這個蓋板是透明的,導致只有和手機屏幕重合的畫布部分才會被我們看到,我們調用
scrollTo
和scrollBy
也可以理解為是在移動手機上面的蓋板。如圖中所示,按鈕在ViewGroup
中的坐標是(20, 10)
,當我們調用scrollBy(20, 10)
之后,就相當于移動了屏幕上的蓋板,然后我們看到的按鈕就到了ViewGroup
的左上角。這樣如果我們想讓按鈕在水平和豎直方向上各移動20
和10
個單位,我們就必須調用scrollBy(-20, -10)
經過了上面的知識準備,我們這里也使用scrollBy
來實現前面實現的那個View隨手指移動
的小例子:
((View)getParent()).scrollBy(-offsetX, -offsetY);
六、使用Scroller
Scroller
也是滑動中很重要的一個角色,進過前面的scrollTo
和scrollBy
大家也會發現,它們的移動時瞬間完成的,滑動顯得十分突兀,Google為了改善用戶體驗,便給出了Scroller
,它可以實現平滑的移動,從而使滑動過程更加真實,用戶體驗更好,下面我們先簡單說說Scroller
的實現原理。
Scroller
的實現方式類似于scrollTo
和scrollBy
,scrollTo
和scrollBy
的移動都是從一個坐標點瞬間移動到另一個左邊點,而Scroller
則是將移動的這段距離切分成好幾段的微小的位移,然后每一段調用scrollTo
來不斷移動這些微小的位移,由于人眼的視覺暫留效果,就會給人平滑移動的視覺效果。
下面我們在上一步的基礎上增加一個小功能,第一部分還是View隨手指移動,但是當我們松開手指時,讓View自己平滑移動到最初始的位置(屏幕左上角),下面我們就來一步步介紹
Scroller
的用法
1 . 聲明Scroller
變量,并在構造方法中進行初始化
2 . 在觸控事件的ACTION_UP
(手指抬起)事件中傳入開始滑動的坐標和需要滑動的距離并觸發Scroller
的滑動事件
3 . 重寫computeScroll()
,實現真正的滑動
下面是完整的代碼示例:
public class DragView extends View {
private int mLastX;
private int mLastY;
//聲明Scroller變量
private Scroller mScroller;
public DragView(Context context) {
this(context, null);
}
public DragView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//在構造方法中初始化Scroller變量
mScroller = new Scroller(context);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//實現View跟隨手指移動的效果
((View)getParent()).scrollBy(-offsetX, -offsetY);
//重新設置初始坐標
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
//當手指抬起時執行滑動過程
View view = (View) getParent();
mScroller.startScroll(view.getScrollX(), view.getScrollY(),
view.getScrollX(), view.getScrollY(), 5000);
//調用重繪來間接調用computeScroll()方法
invalidate();
break;
}
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
//判斷滑動過程是否完成
if (mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//通過重繪來不斷調用computeScroll()方法
invalidate();
}
}
}
上面的代碼View隨手指移動的代碼部分是與前面相同的,我們只說說
Scroller
的部分以及一些注意事項
1 . startScroll()
方法各參數的意義,我們可以看看下面的源碼:
/**
* Start scrolling by providing a starting point, the distance to travel,
* and the duration of the scroll.
*
* @param startX Starting horizontal scroll offset in pixels. Positive
* numbers will scroll the content to the left.
* @param startY Starting vertical scroll offset in pixels. Positive numbers
* will scroll the content up.
* @param dx Horizontal distance to travel. Positive numbers will scroll the
* content to the left.
* @param dy Vertical distance to travel. Positive numbers will scroll the
* content up.
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration)
可以看出startX
和startY
參數就是開始滾動的(x, y)
坐標,那么我們就可以通過ViewGroup(子View的父視圖)
的getScrollX()
和getScrollY()
來獲取,這里一定要注意,我們在滑動時的content
就是子View
,所以我們通過子View的父視圖(ViewGroup)的getScrollX()
和getScrollY()
獲取到的就是子View在X和Y方向上滑動的距離,即就是我們需要的當我們手指抬起時子View的(x, y)坐標。而如果我們對子View調用getScrollX()
和getScrollY()
方法,則獲得的是子View內部的視圖的滑動距離及坐標。
dx
和dy
分別是在X和Y方向上的偏移量,而且注釋中說了,如果我們傳入的dx
和dy
的值是正值,那么將會向上向左移動這個content(其實就是我們這里的View)
,即我們就可以讓子View回到左上角,這里我們還是可以借助于上一小節中提到的視圖移動的概念,我們想讓子View向坐上方移動,其實就是想讓覆蓋在上面的蓋板向右下角移動,我們可以將dx
和dy
理解為父視圖(覆蓋在上面的蓋板)的偏移量。
假設我們剛開始是讓子View隨手指向右下方移動,那么相當于覆蓋在上面的蓋板是向左上方移動,所以我們通過getScrollX()
和getScrollY()
獲得的值是負值,我們現在松開手指想讓子View向左上方移動(即回到屏幕左上角),那么就相當于蓋板向右下角移動,所以我們的dx
和dy
的值必須是-getScrollX()
和-getScrollY()
,此時的兩個值都是正值。
2 . 由于我們的computeScroll()
方法不會主動調用,但是我們又需要它不斷調用從而不斷進行微小移動從而實現平滑的滑動,所以我們可以通過下面的方法。
這三個按照以下順序進行調用
invalidate()
--->onDraw()
--->computeScroll()
,所以我們可以可以在ACTION_UP
中調用完startScroll()
方法后調用invalidate()
方法,然后在computeScroll()
方法中判斷滑動是否結束,如果沒結束,則通過getCurrX()
和getCurrY()
來獲得當前需要移動的微小的位移的坐標點,然后傳入scrollTo()
方法中,這時候子View還只是移動了一小段距離,然后我們再次調用invalidate()
方法,然后接著調用onDraw()
方法,然后再次進入computeScroll()
中再次讓子View移動一小段距離,直到滑動結束,computeScrollOffset()
返回false
,則這個循環調用的過程結束,從而完成平滑移動的過程。
七、屬性動畫
屬性動畫一樣可以實現View的滑動,但是由于屬性動畫涉及到的知識點也是眾多,這里不再展開來寫,只是提供一個思路,后續后專門寫一篇博客來說。
八、ViewDragHelper
ViewDragHelper可以幫助我們實現各種滑動需求,但是它的使用也相對較復雜,所以準備專門寫一篇博客來介紹他,這里只是給出一個概念