Android Scroll分析 (二) 教你使用七種方法實現滑動
實現滑動的基本思想是:當觸控View時,系統記下當前觸控點座標;當手指移動時,系統記下移動後的觸控點座標,從而獲取到相對於前一次座標點的偏移量,並通過偏移量來修改View的座標,這樣不斷重複,從而實現滑動過程.
2.1 Layout方法
在View進行繪製時,會呼叫onLayout()方法來設定顯示的位置
通過修改View的left,top,right,bottom四個屬性來控制View的座標,在每次回撥onTouchEvent的時候,獲取一下觸控點的座標:
// 檢視座標方式
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸控點座標
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在當前left、top、right、bottom的基礎上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
break;
}
return true;
}
//使用getX(),getY()方法來獲取座標值,即通過檢視座標來獲取偏移量
使用getRawX(),getRawY()來獲取座標,並使用絕對座標來計算偏移量,要在每次執行完ACTION_MOVE的邏輯後,一定要重新設定初始座標,這樣才能準確地獲取偏移量.
// 絕對座標方式
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) (event.getRawX());
int rawY = (int) (event.getRawY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸控點座標
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
// 在當前left、top、right、bottom的基礎上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
// 重新設定初始座標
lastX = rawX;
lastY = rawY;
break;
}
return true;
}
2.2 offsetLeftAndRight()與offsetTopAndBottom()
當計算出偏移量後,只需使用如下程式碼就可以完成View的重新佈局:
//同時對left和right進行偏移
offsetLeftAndRight(offsetX);
//同時對top和bottom進行偏移
offsetTopAndBottom(offsetY);
2.3 LayoutParams
LayoutParams儲存了一個View的佈局引數,通過LayoutParams來動態改變View的位置引數,從而改變View位置效果.
使用getLayoutParams()來獲取一個View的LayoutParams.
通過setLayoutParams來改變其LayoutParams.
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸控點座標
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
// LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}
通過getLayoutParams()獲取LayoutParams時,需要根據View所在父佈局的型別來設定不同的型別.
還可以使用ViewGroup.MarginLayoutParams來實現這樣的功能.
2.4 scrollTo與scrollBy
在View中,提供了scrollTo,scrollBy兩種方式來改變一個View的位置,兩者的區別:與英文中To與By的區別類似,scroll(x,y)表示移動到一個具體的座標點(x,y),而scrollBy(dx,dy)表示移動的增量為dx,dy.
scrollTo,scrollBy方法移動的是View的content,即讓View的內容移動,如果在ViewGroup中使用scrollTo,scrollBy方法,那麼移動的將是所有子View,但如果在View中使用,那麼移動的將是View的內容.
將scrollBy中的引數dx和dy設定為正數,那麼content將向座標軸負方向移動;如果將scrollBy中的引數dx和dy設定為負數,那麼content將向座標軸正方向移動.
因此,要實現跟隨手指移動而滑動的效果,就必須將偏移量改為負值:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
}
return true;
}
在使用絕對座標時,也可以通過使用scroll方法來實現.
2.5 Scroller
使用Scroller物件,需要三個步驟:
1.初始化Scroller
首先,通過它的構造方法來建立一個Scroller物件:
//初始化Scroller
mScroller=new Scroller(context);
2.重寫computeScroll()方法,實現模擬滑動
重寫computeScroll()方法,它是Scroller類的核心,系統在繪製View的時候會在draw()方法中呼叫該方法:
@Override
public void computeScroll() {
super.computeScroll();
// 判斷Scroller是否執行完畢
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 通過重繪來不斷呼叫computeScroll
invalidate();
}
}
Scroller類提供了computeScrollOffset()方法來判斷是否完成了整個滑動,通過getCurrX(),getCurrY()方法來獲得當前的滑動座標,注意invalidate()方法,因為只能在computeScroll()方法中獲取模擬過程中的scrollX和scrollY座標.但computeScroll()方法是不會自動呼叫的,只能通過invalidate()->draw()->computeScroll()來間接呼叫computeScroll()方法,所有需要呼叫invalidate()方法,實現迴圈獲取scrollX和scrollY的目的,而當模擬過程結束後,scroller.computeScrollOffset()方法會返回false,從而中斷迴圈,完成整個平滑移動過程.
3.startScroll開啟模擬過程
在需要使用平滑移動的事件中,使用Scroller類的startScroll()方法來開啟平滑移動過程.
startScroll()方法具有兩個過載方法:
public void startScroll(int startX,int startY,int dx,int dy,int duration)
public void startScroll(int startX,int startY,int dx,int dy)
它們的區別在於:是否具有指定的持續時長.其它引數分別為起始座標與偏移量.
在獲取座標時,使用getScrollX()和getScrollY()方法來獲取父檢視中content所滑動到的點的座標.注意正負值,與scrollBy,scrollTo的情況相同.
例子:
演示一下如何使用Scroller類實現平滑移動.在這個例項中,讓子View跟隨手指的滑動而滑動,但是在手指離開螢幕是,讓子View平滑的移動到初始位置,即螢幕左上角:
public class DragView extends View {
private int lastX;
private int lastY;
private Scroller mScroller;
public DragView(Context context) {
super(context);
ininView(context);
}
public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
ininView(context);
}
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ininView(context);
}
private void ininView(Context context) {
setBackgroundColor(Color.BLUE);
// 初始化Scroller
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
super.computeScroll();
// 判斷Scroller是否執行完畢
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo(
mScroller.getCurrX(),
mScroller.getCurrY());
// 通過重繪來不斷呼叫computeScroll
invalidate();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
// 手指離開時,執行滑動過程
View viewGroup = ((View) getParent());
mScroller.startScroll(
viewGroup.getScrollX(),
viewGroup.getScrollY(),
-viewGroup.getScrollX(),
-viewGroup.getScrollY());
invalidate();
break;
}
return true;
}
}
在startScroll()方法中,獲取子View移動的距離–getScrollX(),getScrollY(),並將偏移量設定為其相反數,從而將子View滑動到原位置.注意invalidate()方法,需要使用這個方法來通知View進行重繪,從而來呼叫computeScroll()的模擬過程.
2.6 ViewDragHelper
ViewDragHelper可以實現各種不同的滑動 拖放需求.
使用方法:
初始化ViewDragHelper
首先,自然是需要初始化ViewDragHelper.ViewDragHelper通常定義在一個ViewGroup的內部,並通過其靜態工廠方法進行初始化.
mViewDragHelper=ViewDragHelper.create(this,callback);
第一個引數是要監聽的View,通常需要是一個ViewGroup,即parentView;第二個引數是一個Callback回撥,這個回撥是整個ViewDragHelper核心.
攔截事件
接下來,重寫事件攔截方法,將事件傳遞給ViewDragHelper進行處理:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸控事件傳遞給ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
處理computeScroll()
因為ViewDragHelp內部是通過Scroller來實現平滑移動的,所以需要實現處理computeScroll().
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
處理回撥Callback
下面就是最關鍵的Callback實現:
private ViewDragHelper.Callback callback =
new ViewDragHelper.Callback() {
// 何時開始檢測觸控事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果當前觸控的child是mMainView時開始檢測
return mMainView == child;
}
// 觸控到View後回撥
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 當拖拽狀態改變,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 當位置改變的時候呼叫,常用與滑動時更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
// 處理垂直滑動
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 處理水平滑動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 拖動結束後呼叫
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指擡起後緩慢移動到指定位置
if (mMainView.getLeft() < 500) {
//關閉選單
//相當於Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//開啟選單
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
實現側滑選單的完整例子:
public class DragViewGroup extends FrameLayout {
private ViewDragHelper mViewDragHelper;
private View mMenuView, mMainView;
private int mWidth;
public DragViewGroup(Context context) {
super(context);
initView();
}
public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public DragViewGroup(Context context,
AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@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();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸控事件傳遞給ViewDragHelper,此操作必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}
private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}
private ViewDragHelper.Callback callback =
new ViewDragHelper.Callback() {
// 何時開始檢測觸控事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//如果當前觸控的child是mMainView時開始檢測
return mMainView == child;
}
// 觸控到View後回撥
@Override
public void onViewCaptured(View capturedChild,
int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}
// 當拖拽狀態改變,比如idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}
// 當位置改變的時候呼叫,常用與滑動時更改scale等
@Override
public void onViewPositionChanged(View changedView,
int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}
// 處理垂直滑動
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}
// 處理水平滑動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}
// 拖動結束後呼叫
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指擡起後緩慢移動到指定位置
if (mMainView.getLeft() < 500) {
//關閉選單
//相當於Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//開啟選單
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};
@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}