實現View滑動的七種方法
Android坐標系
在介紹如何實現View滑動之前先了解一下Android的坐標系,我們在初中數學就學過坐標系,有原點和X軸Y軸,不過屏幕上的坐標系稍微有點區別,移動設備一般將 屏幕的左上角 定義為原點,向右為X軸正方向,向下為Y軸正方向,如下圖:
View坐標系
與屏幕坐標系相同,View也有自己的坐標系,我們可以稱之為視圖坐標系,描述了本身和父布局的位置關系,原點在View的左上角:
View及MotionEvent坐標獲取
View自身坐標獲取方法
-
getTop():獲取到的,是view自身的頂邊到其父布局頂邊的距離
-
getLeft():獲取到的,是view自身的左邊到其父布局左邊的距離
-
getRight():獲取到的,是view自身的右邊到其父布局左邊的距離
-
getBottom():獲取到的,是view自身的底邊到其父布局頂邊的距離
MotionEvent坐標獲取
-
getX():獲取點擊事件相對控件左邊的x軸坐標,即點擊事件距離控件左邊的距離
-
getY():獲取點擊事件相對控件頂邊的y軸坐標,即點擊事件距離控件頂邊的距離
-
getRawX():獲取點擊事件相對整個屏幕左邊的x軸坐標,即點擊事件距離整個屏幕左邊的距離
-
getRawY():獲取點擊事件相對整個屏幕頂邊的y軸坐標,即點擊事件距離整個屏幕頂邊的距離
說了這么多方法都不如一張圖最直接:
觸控事件onTouch
學好觸控事件是掌握后續內容的重要基礎,觸控事件回調的MotionEvent封裝了一些常用的事件常量,定義了一些常見類型動作。
/**
- A pressed gesture has started, the motion contains the initial starting location.
*/
public static final int ACTION_DOWN = 0;
/**
- A pressed gesture has finished, the motion contains the final release location as well as any intermediate
- points since the last down or move event.
*/
public static final int ACTION_UP = 1;
/**
- A change has happened during a
- press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
*/
public static final int ACTION_MOVE = 2;
/**
- The current gesture has been aborted.
*/
public static final int ACTION_CANCEL = 3;
/**
- A movement has happened outside of the normal bounds of the UI element.
*/
public static final int ACTION_OUTSIDE = 4;
/**
- A non-primary pointer has gone down.
*/
public static final int ACTION_POINTER_DOWN = 5;
/**
- A non-primary pointer has gone up.
*/
public static final int ACTION_POINTER_UP = 6;</code></pre>
我們讓View滑動的大概思路是重寫View的onTouchEvent(MotionEvent event)方法,來控制View的移動,這個代碼模板基本固定的,show me the code :
@Override
public boolean onTouch(View v, MotionEvent event) {
// 記錄當前point所在的位置
x = (int) event.getX();
y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//處理按下事件
break;
case MotionEvent.ACTION_MOVE:
//處理移動事件
break;
case MotionEvent.ACTION_UP:
//處理松開事件
break;
}
// 事件處理完畢
return true;
}</code></pre>
該方法return true 代表觸控事件到這里就處理完畢了,不必要再繼續傳遞,不懂的可以去再回顧一下Android的 觸摸事件分發機制 。下面我們就可以進入主題,來看一下有哪些方法可以移動View。
實現滑動
我們了解了Android坐標系和觸控事件,接著我們可以模擬實現View的滑動了,思路是:當發生onTouch事件時,記錄下位置,當手指移動時,記錄移動的坐標,獲得一個相對偏移量,然后修改View的位置,不斷重復下去就實現了View的模擬滑動。那么,怎么改動View的位置呢,下面有介紹幾種方法可以設置View的位置。
##layout方法
- 在ACTION_DOWN里面,記錄下按下的坐標
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
- 每次onTouch回調記錄下該點的坐標
int x = (int) event.getX();
int y = (int) event.getY();
- 在ACTION_MOVE里面計算偏移量,然后調用layout方法
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
效果如圖:

offsetLeftAndRight()和offsetTopAndBottom()
看命名就知道這個方法的作用,這是系統提供的對View上下、左右同時進行移動的API,效果與上相同。就不贅述了。
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
LayoutParams
通過改變View的LayoutParams布局參數,就可以移動View的位置,這里通常修改View的Margin屬性,代碼如下:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
break;
其實根據父布局的類型,可以設置LinearLayout.LayoutParams或者RelativeLayout.LayoutParams,不過這樣就必須先知道父布局的類型,不如ViewGroup.MarginLayoutParams來的方便。
scrollTo和scrollBy
前者表示移動到具體的坐標點位置,后者表示在原有的位置基礎上再移動一個偏移量。但是與之前三個方法不同的是,前三個方法都是移動View自己本身,而這兩個方法移動的都是 View里面的內容 ,如果放在ViewGroup中使用,則移動的是ViewGroup里面 所有的子View 。
那我們的思路就要換一下了,我們為了移動View,那我們就來移動View所在的ViewGroup,但是要注意的是,移動的偏移量要 取反 ,為什么呢?這是因為本來是該View移動dx、dy,現在View保持不動,讓ViewGroup移動,則根據 相對運動原理 ,就相當于ViewGroup移動了-dx、-dy。
下面我們來舉個簡單的例子解釋scrollBy。如下圖:ViewGroup里面是可視區域,第二個小人的坐標是(200,100),現在我們把ViewGroup移到第二個小人的位置, scrollBy(200,100) ,效果如第二張圖:在可視區域內,相當于第二個小人的偏移量為 (-200,-100) 。


這么解釋一定明白多了,我們看一下實現代碼:
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;
效果如圖:

Scroller
通過Scroller類來實現一些平滑的動畫效果,可以設置動畫時間等等,簡直就是滑動利器!現在我們來實現一個效果: View跟著手指滑動,當松開手指時就讓View回到原始位置 。
- 在View構造函數里初始化Scroller
scroller = new Scroller(context);
- 重寫computeScroll方法
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()) {
offsetLeftAndRight(scroller.getCurrX()-getLeft());
offsetTopAndBottom(scroller.getCurrY()-getTop());
invalidate();
}
}</code></pre>
- 在ACTION_UP里啟動動畫
scroller.startScroll(getLeft(),getTop(),-getLeft()+initX,-getTop()+initY,2000);
startScroll前兩個參數是起始位置,后兩個參數為終點位置,第五個參數是動畫持續時間,可以省略。
效果如圖:

屬性動畫
這個后續再單獨寫一篇介紹屬性動畫的,跳過。
ViewDragHelper
在開發自定義ViewGroup的時候,經常要根據業務需求實現onInterceptTouchEvent和onTouch(很繁瑣啊!有木有!),不過Google在support庫中為我們提供了一個超級強大的類ViewDragHelper,可以實現諸多滑動布局,側滑菜單就是之一。這里奉上 官方介紹 ,可能需國內或許不能訪問?
下面我們舉個實現側滑菜單的例子:自定義ViewGroup布局,然后里面有MenuView和MainView,滑動MainView超過一定距離就顯示MenuView。
- 初始化
private View mMenuView,mMainView;
private int mWidth;
//布局完成后調用
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}
@Override
protected void onSizeChanged(int w,int h,int oldW,int oldH) {
super.onSizeChanged(w,h,oldW,oldH);
mWidth = mMenuView.getMeasuredWidth();//側滑菜單的寬度
}
//構造函數
public ViewDragLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewDragHelper = ViewDragHelper.create(this,callback);
}</code></pre>
其中,構造函數里的callback是我們要自己實現的業務邏輯。也是該類的重要 核心內容 !
- 攔截事件交給ViewDragHelper處理
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return mViewDragHelper.shouldInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mViewDragHelper.processTouchEvent(event);
return true;
}</code></pre>
- 重寫computeScroll方法
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
- 實現callback
private final ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
//觸摸的布局是否為MainView
return mMainView==child;
}
@Override
public int clampViewPositionVertical(View child,int top,int dy) {
//不需要檢測垂直滑動,直接返回0
return 0;
}
@Override
public int clampViewPositionHorizontal(View child,int left,int dx) {
return left;
}
@Override
public void onViewReleased(View child,float xVel,float yVel) {
super.onViewReleased(child,xVel,yVel);
//核心邏輯:滑動MainView超過一定距離就顯示MenuView
if (mMainView.getLeft() < mWidth) {
mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
} else {
mViewDragHelper.smoothSlideViewTo(mMainView,mWidth,0);
ViewCompat.postInvalidateOnAnimation(ViewDragLayout.this);
}
}
};</code></pre>
來看一下效果:

ViewDragHelper.Callback中定義有大量的回調方法,就不一一介紹了。
最后
到這里,我們介紹的View滑動方法就學習完了,最后我們來實現一個滑動ViewGroup,來模擬微信下拉的粘性動畫,直接上代碼:
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (scroller.computeScrollOffset())
scroller.forceFinished(true);
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
int scrollY = y - lastY;
offsetTopAndBottom(scrollY/3-getTop());
break;
case MotionEvent.ACTION_UP:
scroller.startScroll(getLeft(),getTop(),0,-getTop()+initY,duration);
invalidate();
break;
}
return true;
}
在ACTION_DOWN里判斷動畫有沒有結束,可以強制結束,這樣就可以連續向下拖動。在ACTION_MOVE里設置偏移量,除以3可以調節偏移量滑動的比例。最后在松手時回到原位置,一起來看一下效果吧。

來自:http://www.biglong.cc/android/2016/09/23/實現View滑動的七種方法