編輯:關於Android編程
本文是android滾動相關的系列文章的第二篇,主要總結一下使用手勢相關的代碼邏輯。主要是單點拖動,多點拖動,fling和OveScroll的實現。每個手勢都會有代碼片段。
對android滾動相關的知識還不太了解的同學可以先閱讀一下文章:
《Android-MotionEvent詳解》 《Android Scroll詳解(一):基礎知識》為了節約你的時間,我特地將文章大致內容總結如下:
手勢Drag的實現和原理 手勢Fling的實現和原理 OverScroll效果和EdgeEffect效果的實現和原理。詳細代碼請查看我的github
?Drag是最為基本的手勢:用戶可以使用手指在屏幕上滑動,以拖動屏幕相應內容移動。實現Drag手勢其實很簡單,步驟如下:
在ACTION_DOWN
事件發生時,調用getX
和getY
函數獲得事件發生的x,y坐標值,並記錄在mLastX
和mLastY
變量中。 在ACTION_MOVE
事件發生時,調用getX
和getY
函數獲得事件發生的x,y坐標值,將其與mLastX
和mLastY
比較,如果二者差值大於一定限制(ScaledTouchSlop),就執行scrollBy
函數,進行滾動,最後更新mLastX
和mLastY
的值。 在ACTION_UP
和ACTION_CANCEL
事件發生時,清空mLastX
,mLastY
。
@Override
public boolean onTouchEvent(MotionEvent event) {
int actionId = MotionEventCompat.getActionMasked(event);
switch (actionId) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
mIsBeingDragged = true;
if (getParent() != null) {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
case MotionEvent.ACTION_MOVE:
float curX = event.getX();
float curY = event.getY();
int deltaX = (int) (mLastX - curX);
int deltaY = (int) (mLastY - curY);
if (!mIsBeingDragged && (Math.abs(deltaX)> mTouchSlop ||
Math.abs(deltaY)> mTouchSlop)) {
mIsBeingDragged = true;
// 讓第一次滑動的距離和之後的距離不至於差距太大
// 因為第一次必須>TouchSlop,之後則是直接滑動
if (deltaX > 0) {
deltaX -= mTouchSlop;
} else {
deltaX += mTouchSlop;
}
if (deltaY > 0) {
deltaY -= mTouchSlop;
} else {
deltaY += mTouchSlop;
}
}
// 當mIsBeingDragged為true時,就不用判斷> touchSlopg啦,不然會導致滾動是一段一段的
// 不是很連續
if (mIsBeingDragged) {
scrollBy(deltaX, deltaY);
mLastX = curX;
mLastY = curY;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mLastY = 0;
mLastX = 0;
break;
default:
}
return mIsBeingDragged;
}
?上邊的代碼只適用於單點觸控的手勢,如果你是兩個手指觸摸屏幕,那麼它只會根據你第一個手指滑動的情況來進行屏幕滾動。更為致命的是,當你先松開第一個手指時,由於我們少監聽了ACTION_POINTER_UP
事件,將會導致屏幕突然滾動一大段距離,因為第二個手指移動事件的x,y值會和第一個手指移動時留下的mLastX
和mLastY
比較,導致屏幕滾動。
?如果我們要監聽並處理多觸點的事件,我們還需要對ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件進行監聽,並且在ACTION_MOVE
事件時,要記錄所有觸摸點事件發生的x,y值。
ACTION_POINTER_DOWN
事件發生時,我們要記錄第二觸摸點事件發生的x,y值為mSecondaryLastX
和mSecondaryLastY
,和第二觸摸點pointer的id為mSecondaryPointerId
當ACTION_MOVE
事件發生時,我們除了根據第一觸摸點pointer的x,y值進行滾動外,也要更新mSecondayLastX
和mSecondaryLastY
當ACTION_POINTER_UP
事件發生時,我們要先判斷是哪個觸摸點手指被抬起來啦,如果是第一觸摸點,那麼我們就將坐標值和pointer的id都更換為第二觸摸點的數據;如果是第二觸摸點,就只要重置一下數據即可。
switch (actionId) {
.....
case MotionEvent.ACTION_POINTER_DOWN:
activePointerIndex = MotionEventCompat.getActionIndex(event);
mSecondaryPointerId = MotionEventCompat.findPointerIndex(event,activePointerIndex);
mSecondaryLastX = MotionEventCompat.getX(event,activePointerIndex);
mSecondaryLastY = MotionEventCompat.getY(event,mActivePointerId);
break;
case MotionEvent.ACTION_MOVE:
......
// handle secondary pointer move
if (mSecondaryPointerId != INVALID_ID) {
int mSecondaryPointerIndex = MotionEventCompat.findPointerIndex(event, mSecondaryPointerId);
mSecondaryLastX = MotionEventCompat.getX(event, mSecondaryPointerIndex);
mSecondaryLastY = MotionEventCompat.getY(event, mSecondaryPointerIndex);
}
break;
case MotionEvent.ACTION_POINTER_UP:
//判斷是否是activePointer up了
activePointerIndex = MotionEventCompat.getActionIndex(event);
int curPointerId = MotionEventCompat.getPointerId(event,activePointerIndex);
Log.e(TAG, "onTouchEvent: "+activePointerIndex +" "+curPointerId +" activeId"+mActivePointerId+
"secondaryId"+mSecondaryPointerId);
if (curPointerId == mActivePointerId) { // active pointer up
mActivePointerId = mSecondaryPointerId;
mLastX = mSecondaryLastX;
mLastY = mSecondaryLastY;
mSecondaryPointerId = INVALID_ID;
mSecondaryLastY = 0;
mSecondaryLastX = 0;
//重復代碼,為了讓邏輯看起來更加清晰
} else{ //如果是secondary pointer up
mSecondaryPointerId = INVALID_ID;
mSecondaryLastY = 0;
mSecondaryLastX = 0;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsBeingDragged = false;
mActivePointerId = INVALID_ID;
mLastY = 0;
mLastX = 0;
break;
default:
}
?當用戶手指快速劃過屏幕,然後快速立刻屏幕時,系統會判定用戶執行了一個Fling手勢。視圖會快速滾動,並且在手指立刻屏幕之後也會滾動一段時間。Drag表示手指滑動多少距離,界面跟著顯示多少距離,而fling是根據你的滑動方向與輕重,還會自動滑動一段距離。Filing手勢在android交互設計中應用非常廣泛:電子書的滑動翻頁、ListView滑動刪除item、滑動解鎖等。所以如何檢測用戶的fling手勢是非常重要的。
?在檢測Fling時,你需要檢測手指在屏幕上滑動的速度,這是你就需要VelocityTracker
和Scroller
這兩個類啦。
VelocityTracker.obtain()
這個方法獲得其實例 然後每次處理觸摸時間時,我們將觸摸事件通過addMovement
方法傳遞給它 最後在處理ACTION_UP
事件時,我們通過computeCurrentVelocity
方法獲得滑動速度; 我們判斷滑動速度是否大於一定數值(MinFlingSpeed),如果大於,那麼我們調用Scroller
的fling
方法。然後調用invalidate()
函數。 我們需要重載computeScroll
方法,在這個方法內,我們調用Scroller
的computeScrollOffset()
方法啦計算當前的偏移量,然後獲得偏移量,並調用scrollTo
函數,最後調用postInvalidate()
函數。 除了上述的操作外,我們需要在處理ACTION_DOWN
事件時,對屏幕當前狀態進行判斷,如果屏幕現在正在滾動(用戶剛進行了Fling手勢),我們需要停止屏幕滾動。
?具體這一套流程是如何運轉的,我會在下一篇文章中詳細解釋,大家也可以自己查閱代碼或者google來搞懂其中的原理。
@Override
public boolean onTouchEvent(MotionEvent event) {
.....
if (mVelocityTracker == null) {
//檢查速度測量器,如果為null,獲得一個
mVelocityTracker = VelocityTracker.obtain();
}
int action = MotionEventCompat.getActionMasked(event);
int index = -1;
switch (action) {
case MotionEvent.ACTION_DOWN:
......
if (!mScroller.isFinished()) { //fling
mScroller.abortAnimation();
}
.....
break;
case MotionEvent.ACTION_MOVE:
......
break;
case MotionEvent.ACTION_CANCEL:
endDrag();
break;
case MotionEvent.ACTION_UP:
if (mIsBeingDragged) {
//當手指立刻屏幕時,獲得速度,作為fling的初始速度 mVelocityTracker.computeCurrentVelocity(1000,mMaxFlingSpeed);
int initialVelocity = (int)mVelocityTracker.getYVelocity(mActivePointerId);
if (Math.abs(initialVelocity) > mMinFlingSpeed) {
// 由於坐標軸正方向問題,要加負號。
doFling(-initialVelocity);
}
endDrag();
}
break;
default:
}
//每次onTouchEvent處理Event時,都將event交給時間
//測量器
if (mVelocityTracker != null) {
mVelocityTracker.addMovement(event);
}
return true;
}
private void doFling(int speed) {
if (mScroller == null) {
return;
}
mScroller.fling(0,getScrollY(),0,speed,0,0,-500,10000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
?在Android手機上,當我們滾動屏幕內容到達內容邊界時,如果再滾動就會有一個發光效果。而且界面會進行滾動一小段距離之後再回復原位,這些效果是如何實現的呢?我們需要使用Scroller
和scrollTo
的升級版OverScroller
和overScrollBy
了,還有發光的EdgeEffect
類。
?我們先來了解一下相關的API,理解了這些接口參數的含義,你就可以輕松使用這些接口來實現上述的效果啦。
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent)
int deltaX,int deltaY : 偏移量,也就是當前要滾動的x,y值。 int scrollX,int scrollY : 當前的mScrollX和mScrollY的值。 int maxOverScrollX,int maxOverScrollY: 標示可以滾動的最大的x,y值,也就是你視圖真實的長和寬。也就是說,你的視圖可視大小可能是100,100,但是視圖中的內容的大小為200,200,所以,上述兩個值就為200,200 int maxOverScrollX,int maxOverScrollY:允許超過滾動范圍的最大值,x方向的滾動范圍就是0~maxOverScrollX,y方向的滾動范圍就是0~maxOverScrollY。 boolean isTouchEvent:是否在onTouchEvent
中調用的這個函數。所以,當你在computeScroll
中調用這個函數時,就可以傳入false。
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)
int scrollX,int scrollY:就是x,y方向的滾動距離,就相當於mScrollX
和mScrollY
。你既可以直接把二者賦值給相應的成員變量,也可以使用scrollTo
函數。 boolean clampedX,boolean clampY:表示是否到達超出滾動范圍的最大值。如果為true,就需要調用OverScroll
的springBack
函數來讓視圖回復原來位置。
public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY)
int startX,int startY:標示當前的滾動值,也就是mScrollX
和mScrollY
的值。 int minX,int maxX:標示x方向的合理滾動值 int minY,int maxY:標示y方向的合理滾動值。
?相信看完上述的API之後,大家會有很多的疑惑,所以這裡我來舉個例子。
?假設視圖大小為100*100。當你一直下拉到視圖上邊緣,然後在下拉,這時,mScrollY
已經達到或者超過正常的滾動范圍的最小值了,也就是0,但是你的maxOverScrollY傳入的是10,所以,mScrollY
最小可以到達-10,最大可以為110。所以,你可以繼續下拉。等到mScrollY
到達或者超過-10時,clampedY就為true,標示視圖已經達到可以OverScroll的邊界,需要回滾到正常滾動范圍,所以你調用springBack(0,0,0,100)。
?然後我們再來看一下發光效果是如何實現的。
?使用EdgeEffect
類。一般來說,當你只上下滾動時,你只需要兩個EdgeEffect
實例,分別代表上邊界和下邊界的發光效果。你需要在下面兩個情景下改變EdgeEffect
的狀態,然後在draw()
方法中繪制EdgeEffect
ACTION_MOVE
時,如果發現y方向的滾動值超過了正常范圍的最小值時,你需要調用上邊界實例的onPull
方法。如果是超過最大值,那麼就是調用下邊界的onPull
方法。 在computeScroll
函數中,也就是說Fling手勢執行過程中,如果發現y方向的滾動值超過正常范圍時的最小值時,調用onAbsorb
函數。
?然後就是重載draw
方法,讓EdgeEffect
實例在畫布上繪制自己。你會發現,你必須對畫布進行移動或者旋轉來讓EdgeEffect
繪制出上邊界或者下邊界的發光的效果,因為EdgeEffect
對象自己是沒有上下左右的概念的。
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (mEdgeEffectTop != null) {
final int scrollY = getScrollY();
if (!mEdgeEffectTop.isFinished()) {
final int count = canvas.save();
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
canvas.translate(getPaddingLeft(),Math.min(0,scrollY));
mEdgeEffectTop.setSize(width,getHeight());
if (mEdgeEffectTop.draw(canvas)) {
postInvalidate();
}
canvas.restoreToCount(count);
}
}
if (mEdgeEffectBottom != null) {
final int scrollY = getScrollY();
if (!mEdgeEffectBottom.isFinished()) {
final int count = canvas.save();
final int width = getWidth() - getPaddingLeft() - getPaddingRight();
canvas.translate(-width+getPaddingLeft(),Math.max(getScrollRange(),scrollY)+getHeight());
canvas.rotate(180,width,0);
mEdgeEffectBottom.setSize(width,getHeight());
if (mEdgeEffectBottom.draw(canvas)) {
postInvalidate();
}
canvas.restoreToCount(count);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
......
case MotionEvent.ACTION_MOVE:
.....
if (mIsBeingDragged) {
overScrollBy(0,(int)deltaY,0,getScrollY(),0,getScrollRange(),0,mOverScrollDistance,true);
final int pulledToY = (int)(getScrollY()+deltaY);
mLastY = y;
if (pulledToY<0) {
mEdgeEffectTop.onPull(deltaY/getHeight(),event.getX(mActivePointerId)/getWidth());
if (!mEdgeEffectBottom.isFinished()) {
mEdgeEffectBottom.onRelease();
}
} else if(pulledToY> getScrollRange()) {
mEdgeEffectBottom.onPull(deltaY/getHeight(),1.0f-event.getX(mActivePointerId)/getWidth());
if (!mEdgeEffectTop.isFinished()) {
mEdgeEffectTop.onRelease();
}
}
if (mEdgeEffectTop != null && mEdgeEffectBottom != null &&(!mEdgeEffectTop.isFinished()
|| !mEdgeEffectBottom.isFinished())) {
postInvalidate();
}
}
.....
}
....
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
if (!mScroller.isFinished()) {
int oldX = getScrollX();
int oldY = getScrollY();
scrollTo(scrollX,scrollY);
onScrollChanged(scrollX,scrollY,oldX,oldY);
if (clampedY) {
Log.e("TEST1","springBack");
mScroller.springBack(getScrollX(),getScrollY(),0,0,0,getScrollRange());
}
} else {
// TouchEvent中的overScroll調用
super.scrollTo(scrollX,scrollY);
}
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
int oldX = getScrollX();
int oldY = getScrollY();
int x = mScroller.getCurrX();
int y = mScroller.getCurrY();
int range = getScrollRange();
if (oldX != x || oldY != y) {
overScrollBy(x-oldX,y-oldY,oldX,oldY,0,range,0,mOverFlingDistance,false);
}
final int overScrollMode = getOverScrollMode();
final boolean canOverScroll = overScrollMode == OVER_SCROLL_ALWAYS ||
(overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
if (canOverScroll) {
if (y<0 && oldY >= 0) {
mEdgeEffectTop.onAbsorb((int)mScroller.getCurrVelocity());
} else if (y> range && oldY < range) {
mEdgeEffectBottom.onAbsorb((int)mScroller.getCurrVelocity());
}
}
}
}
本篇文章是系列文章的第二篇,大家可能已經知道如何實現各類手勢,但是對其中的機制和原理還不是很了解,之後的第三篇會講解從本篇代碼的視角講解一下android視圖繪制的原理和Scroller的機制,希望大家多多關注。
概覽http是現在主流應用使用的網絡請求方式, 用來交換數據和內容, 有效的使用HTTP可以使你的APP 變的更快和減少流量的使用OkHttp 是一個很棒HTTP客戶端(
現在的智能手機不敢說百分百的都是觸摸屏,也應該是百分之九九以上為觸摸屏了,觸摸屏為我們操作無鍵盤、無鼠標的手機系統帶來了很多的便利。當用戶觸摸屏幕時會產生很多
一.概述1.Android Studio中做混淆,基本就是對Proguard-rules.pro文件的操作。混淆的過程也是有規律可循 2.寫出適合自己代碼的混淆規則 3.
導航菜單的制作方式多種多樣,網上也有各種炫酷效果的具體實現方式,那麼今天我主要是想來說說Google在Android5.0之後推出的NavigationView的具體使用