一、座標系
1、上圖圓點是手指觸摸點,藍色的是MotionEvent的方法,點擊事件走到onTouchEvent,獲得點擊事件的各種座標:
getX、getY是相對view;getRawX、getRawY是相對屏幕。
2、綠色的是View獲取座標的方法。ViewGroup是View的父佈局。getLeft、getRight、getTop、getBottom相對父佈局。
width = getRight()- getLeft()
height = getTop()- getBottom()
二、VelocityTracker、GestureDetector
1、VelocityTracker
用於追蹤手指滑動速度的。例如相冊的圖片,手指快速左右滑動會切換圖片,慢則不會切換。
獲取速度前,要先調用computeCurrentVelocity計算速度,如下代碼。效果是手指滑的快時,就會彈Toast。
private void init() {
//獲取速度追蹤器
mVelocityTracker = VelocityTracker.obtain();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
//獲取觸摸點座標
int x = (int) event.getX();
int y = (int) event.getY();
switch (action) {
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);
//速度器添加事件
mVelocityTracker.addMovement(event);
break;
case MotionEvent.ACTION_UP:
//計算手指滑動速度:1000ms內滑過的像素,(終點-起點)/時間段,所以從右向左滑 爲負值。
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
Log.i(TAG, "onTouchEvent: xVelocity = " + xVelocity);
if (Math.abs(xVelocity) > 100) {
Toast.makeText(getContext(), "滑的有點快!", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
2、GestureDetector
手勢檢測。通常監聽雙擊 才使用GestureDetector,其他的滑動就在onTouchEvent中實現(DOWN、MOVE、UP)就可以了。
private void init() {
//要設置兩個監聽OnGestureListener、OnDoubleTapListener
mGestureDetector = new GestureDetector(getContext(),this);
mGestureDetector.setOnDoubleTapListener(this);
}
@Override
public boolean onDown(MotionEvent e) {
//手指觸摸的一瞬間,由1個DOWN觸發
Log.i(TAG, "onDown: ");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
//手指觸摸的狀態,由1個DOWN觸發,強調的是沒有拖動的狀態,就是按着沒動。
Log.i(TAG, "onShowPress: ");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
//單擊,UP觸發
Log.i(TAG, "onSingleTapUp: ");
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//滾動,1個DOWN,多個MOVE觸發
Log.i(TAG, "onScroll: ");
return false;
}
@Override
public void onLongPress(MotionEvent e) {
//長按
Log.i(TAG, "onLongPress: ");
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//快速move後up
Log.i(TAG, "onFling: ");
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
//確認的單擊,不是雙擊中的某一擊
Log.i(TAG, "onSingleTapConfirmed: ");
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
//雙擊,兩個單擊組成。 和onSingleTapConfirmed不能共存
Log.i(TAG, "onDoubleTap: ");
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
//發生了雙擊行爲,雙擊期間DOWN、MOVE、UP都會觸發此回調
Log.i(TAG, "onDoubleTapEvent: ");
return false;
}
三、View的滑動
滑動是自定義view的基礎。共有6種滑動方法。
1、layout()
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
//獲取觸摸點座標
int x = (int) event.getX();
int y = (int) event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
//DOWN時,即剛開始的觸摸點相對view的座標。
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
//滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//所以View也要跟上這個滑動距離——有多重方式:
//方法一,layout()
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
每次移動都會調layout重新佈局,也就是滑動的效果。
效果如圖,隨着手指滑動:
2、offsetLeftAndRight、offsetTopAndBottom
case MotionEvent.ACTION_MOVE:
//滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//所以View也要跟上這個滑動距離——有多重方式:
//方法一,layout()
// layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//方法二,offsetLeftAndRight、offsetTopAndBottom
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
3、LayouParams
case MotionEvent.ACTION_MOVE:
//滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//所以View也要跟上這個滑動距離——有多重方式:
//方法一,layout()
// layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
//方法二,offsetLeftAndRight、offsetTopAndBottom
// offsetLeftAndRight(offsetX);
// offsetTopAndBottom(offsetY);
//方法三,LayoutParams
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
4、動畫
關於動畫,後面單獨寫一篇詳細講。現在知道有 :View動畫(不能改變位置參數)、屬性動畫可以實現。
//方法四,動畫(一般在外面調用)
//1、view動畫(最終效果是滑動第一下可以滑動,後面再滑不行,因爲view不能改變view的位置參數,不能真正的交互。)
float toXDelta = offsetX / getWidth();
float toYDelta = offsetY / getHeight();
TranslateAnimation animation = new TranslateAnimation(
TranslateAnimation.RELATIVE_TO_SELF, 0,
TranslateAnimation.RELATIVE_TO_SELF, toXDelta,
TranslateAnimation.RELATIVE_TO_SELF, 0,
TranslateAnimation.RELATIVE_TO_SELF, toYDelta);
animation.setDuration(0);
animation.setFillAfter(true);
startAnimation(animation);
//2、屬性動畫(橫移,貌似不適合放這裏使用,效果會閃)
ObjectAnimator.ofFloat(this,"translationX",0, offsetX).setDuration(0).start();
5、scrollTo、scrollBy
scrollTo(x,y)是瞬間移動到(x,y),scrollBy(deltaX,deltaY)是移動增量。代碼使用方法:
case MotionEvent.ACTION_MOVE:
//滑動的距離 = 觸摸點滑動到的座標 - 開始觸摸的座標 (都是相對於view本身)
int offsetX = x - mLastX;
int offsetY = y - mLastY;
//所以View也要跟上這個滑動距離——有多重方式:
//方法五,scrollTo、scrollBy。
// ((View) getParent()).scrollTo(getScrollX() - offsetX, getScrollY() - offsetY);
//scrollBy同理:
((View)getParent()).scrollBy(-offsetX, -offsetY);
說明1:
scrollBy還是調的scrollTo :
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
mScrollX:View左邊緣和 view的內容(即view的子view)左邊緣的距離。負值表示 view左邊緣在view內容的左側。
mScrollY:VIew上邊緣和 view的內容(即view的子view)上邊緣的距離。負值表示 view上邊緣在view內容的上面。
說明2:
scrollBy、scrollTo移動的是view的內容,如果是ViewGroup使用,即移動其所有的子view,若沒有子view就沒有效果。所以上面代碼使用getParent()來調用。
或者,換一種理解方式(個人感覺這個更好理解):scrollBy、scrollTo移動的就是view本身,而view的內容不動,只不過此時屏幕也隨view本身一起移動,視覺上就是 view的內容 就會反向移動。 例如,使用view.scrollBy(100,0),那麼view和屏幕一起右移100,即視覺上view的內容左移100。所以要讓view的內容視覺上右移100,需要view的父view左移100,view.scrollBy(-100,0)。 所以,上面用getParent,而且傳的是負值。
6、Scroller
Scroller是處理滑動效果的工具類,來實現有過度效果的彈性滑動(有個過程,不是瞬間完成的)。
Scroller本身不能實現彈性,需要結合View的computeScroll()方法。基本是固定的代碼:先new 一個Scroller,然後重寫computeScroll(),在寫一個方法供調用,這裏是smoothScrollTo。
private void init() {
mScroller = new Scroller(getContext());
}
/**
* 彈性滑動
*
* @param desX 目標X
* @param desY 目標Y
* @param duration 時間
*/
private void smoothScrollTo(int desX, int desY, int duration) {
int deltaX = desX - getScrollX();
int deltaY = desY - getScrollY();
//前兩個是起點左邊,中間兩個是滑動距離,duration是時間。此時僅僅是存入數據,並沒有滑動。
mScroller.startScroll(getScrollX(), getScrollY(), deltaX, deltaY, duration);
//invalidate會導致重新繪製,即走draw(),然後走computeScroll()
invalidate();
}
@Override
public void computeScroll() {
//計算本次滾動的位置,數據保存在Scroller中。返回true表示滾動未結束。
if (mScroller.computeScrollOffset()) {
//從Scroller中取出計算好的位置,並使用父view調scrollerTo來滑動 本身。
((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//再次調繪製,又會走computeScroll(),繼續這個過程,直到mScroller.computeScrollOffset()返回false結束滑動。
invalidate();
}
}
看下startScroll方法,僅僅保存了傳入的起點、移動距離、時間,並沒有真正移動。
computeScroll方法:根據時間計算當前一個滑動的距離,返回true表示滑動還沒結束。
所以,整個過程是:調用startScroll向Scroller傳入滑動的距離和時間,然後調用了invalidate(),invalidate會導致重新繪製,即走draw(),然後走computeScroll(),computeScroll中計算本次滾動的位置,數據保存在Scroller中,返回true表示滾動未結束。然後就調用scrollTo傳入計算好的當前的滑動距離,這樣就是實現了一小段的滑動。然後又調用invalidate(),就會繼續這個過程。最終實現彈性動畫。
參考資料:
《Android進階之光》第三章
《Android開發藝術探索》第三章