1.寫在前面
通過整個部落格以及例子的布局回彈效果了解以及搞清楚以下幾個點:
1. Scroller的使用(這裡學習了谷歌的小弟 這個大神的,在他的部落格中學到了很多關于Scroller知識,但是因為他水準太高,很多初級的東西沒講解,是以通過打斷點以及寫log弄明白很多未知的知識);
2. GestureDetector.OnGestureListener中onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)方法中參數的意義;
3. scrollTo以及scrollBy的差別以及scrollTo使用時總是感覺方向錯了;
4. Scroller.getFinalY()以及Scroller.getCurrY()非常實在的意思,并不是很抽象的最後的位置以及目前位置的解釋;
5. 布局回彈效果的了解;
2.幹貨
A. 都說scrollTo移動的并不是view而且view裡面的内容,但是使用時内容總是會往相反的方向跑,這裡那位大佬已經解釋的非常清楚了,不是本文的重點直接上圖了
scrollTo和scrollBy的差別直接舉例子,假設布局的初始位置是(0,0),我要将布局向下移動30,則應該是scrollTo(0,-30),但是這裡我們使用scrollBy(0,-30)也是樣的效果,因為初始位置是0,scrollBy就算是累加的也沒用,加的是0,但是在目前位置,我還想要布局往下30,則可以再使用scrollBy(0,-30),但是如果還是使用scrollTo(0,-30)則不行,如果要使用scrollTo則應該是scrollTo(0,-60),這樣子說應該很好了解了,因為scrollBy(x,y)是把初始位置加上x,y,而scrollTo(x,y)則是跳到x,y位置。
B. 剩下的都要通過回彈這個例子來了解了。首先對于這個回彈效果要知道可以分為兩部分,第一是下拉,整個布局下移,第二是松開手,布局上移。因為整個布局是繼承RelativeLayout的,對于第一部分,看到的随着手指下拉,布局下移其實是通過scrollTo使布局移動的。
對于手勢的監聽,這裡使用class GestureListenerImpl implements GestureDetector.OnGestureListener,對于裡面onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) 參數的了解很重要,
還是用這個代碼測試下點選這裡 代碼如下
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.d(TAG, "onScroll: " + e1.toString()+" "+e2.toString()+" distance-->"+distanceX+" "+distanceY);
Log.d(TAG, "e1 y--->" + e1.getRawY() + " " + e1.getY() + " e2 y-->" + e2.getRawY() + " " + e2.getY());
return true;
}
然後隻是手指在螢幕上滑動,并不快速“抛”,得到如下圖結果:
因為篇幅原因,并不能很好的看清楚,我整理其中的兩個好好看下,如下
MainActivity: onScroll:
MotionEvent { action=ACTION_DOWN, actionButton=, id[]=, x[]=, y[]=, toolType[]=TOOL_TYPE_FINGER, buttonState=, metaState=, flags=, edgeFlags=, pointerCount=, historySize=, eventTime=, downTime=, deviceId=, source= }
MotionEvent { action=ACTION_MOVE, actionButton=, id[]=, x[]=, y[]=, toolType[]=TOOL_TYPE_FINGER, buttonState=, metaState=, flags=, edgeFlags=, pointerCount=, historySize=, eventTime=, downTime=, deviceId=, source= }
distance-->2.481079 -24.953857
MainActivity: e1 y--->896.533 896.533 e2 y-->921.4869 921.4869
MainActivity: onScroll:
MotionEvent { action=ACTION_DOWN, actionButton=, id[]=, x[]=, y[]=, toolType[]=TOOL_TYPE_FINGER, buttonState=, metaState=, flags=, edgeFlags=, pointerCount=, historySize=, eventTime=, downTime=, deviceId=, source= }
MotionEvent { action=ACTION_MOVE, actionButton=, id[]=, x[]=, y[]=, toolType[]=TOOL_TYPE_FINGER, buttonState=, metaState=, flags=, edgeFlags=, pointerCount=, historySize=, eventTime=, downTime=, deviceId=, source= }
distance-->1.0904541 -5.329773
MainActivity: e1 y--->896.533 896.533 e2 y-->926.81665 926.81665
對于其中的參數MotionEvent e1, MotionEvent e2,經常說e1是滑動初始位置的相關資訊,e2是滑動結束位置的相關資訊,這是對整個過程來說的,但是分析整個滑動過程,根據檢查出來的onScroll節點,在我們手離開螢幕之前,e2并不确定,因為我們可能在其中的任何一個地方手指離開螢幕,是以才如下圖
因為我監聽的是整個父布局,是以getRawY()和getY()結果是一樣的,在滑動過程中每GestureDetector.OnGestureListener捕捉到的e2都是變化的,再仔細看下上面列出來的兩節點,distanceY = e1Y-e2Y,896.533-921.4869 = -24.953857,這就是distanceY,帶符号計算出來的距離。
C. 先貼出整個代碼
package guo.com.gesturepro;
/**
* Created by ${GuoZhaoHui} on 2017/4/27.
* email:[email protected]
*/
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.RelativeLayout;
import android.widget.Scroller;
/**
1. 原創作者:
2. 谷哥的小弟
3. 部落格位址:
4. http://blog.csdn.net/lfdfhl
*/
public class BounceableRelativeLayout extends RelativeLayout {
private Scroller mScroller;
private GestureDetector mGestureDetector;
private final String TAG="stay4it";
public BounceableRelativeLayout(Context context) {
this(context, null);
}
public BounceableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setClickable(true);
setLongClickable(true);
mScroller = new Scroller(context);
mGestureDetector = new GestureDetector(context, new GestureListenerImpl());
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.d(TAG,"-----getCurrX------ "+mScroller.getCurrX()+"-----getCurrY------ "+mScroller.getCurrY());
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP :
reset(, );
break;
default:
return mGestureDetector.onTouchEvent(event);
}
return super.onTouchEvent(event);
}
class GestureListenerImpl implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
int disY = (int) ((distanceY - ) / );
Log.d(TAG,"distanceY-->"+distanceY+" disY-->"+disY);
beginScroll(, disY);
return false;
}
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float x,float y) {
return false;
}
}
protected void reset(int x, int y) {
int dx = x - mScroller.getFinalX();
int dy = y - mScroller.getFinalY();
beginScroll(dx, dy);
}
protected void beginScroll(int dx, int dy) {
Log.d(TAG,"----getFinalX1----- "+mScroller.getFinalX()+"----getFinalY1------- "+mScroller.getFinalY());
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
Log.d(TAG,"----getFinalX2----- "+mScroller.getFinalX()+"----getFinalY2------- "+mScroller.getFinalY());
invalidate();
}
}
根據上面所說的onScroll就很好了解這一段代碼了
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
int disY = (int) ((distanceY - ) / );
Log.d(TAG,"distanceY-->"+distanceY+" disY-->"+disY);
beginScroll(, disY);
return false;
}
中間的對于 distanceY處理也很有道理,使的手指下滑和布局下移的頻率不一緻,有種下拉很“困難”的感覺。
通過如下log來了解Scroller.getFinalY()和Scroller.getCurrY()的差別以及Scroller.startScroll()前後getFinalY值的變化。
protected void beginScroll(int dx, int dy) {
Log.d(TAG,"----getFinalX1----- "+mScroller.getFinalX()+"----getFinalY1------- "+mScroller.getFinalY());
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy);
Log.d(TAG,"----getFinalX2----- "+mScroller.getFinalX()+"----getFinalY2------- "+mScroller.getFinalY());
invalidate();
}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = f / (float) mDuration;
}
先看看調用startScroll前後getFinalY的差別,根據源碼可以知道mFinalY = startY + dy; 就比如上面的,先看第一個紅色框在滑動之前最初getFinalY=0,然後加上dy=-14,是以調用startScroll方法後是getFinalY = -14,再看第二個框,當到這裡時說明布局已經下移了14,這個時候的getFinalY=-14,又需要下移4(移動-4),在調用startScroll後getFinalY = -14-4=-18,後面的以此類推,一直累加。
再看getCurrY,根據上面代碼可知,調用startScroll,又調用了invalidate(),在調用invalidate()後會導緻View的重繪進而調用computeScroll()。如下
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
Log.d(TAG,"-----getCurrX------ "+mScroller.getCurrX()+"-----getCurrY------ "+mScroller.getCurrY());
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
super.computeScroll();
}
其實根據上面startScroll的源碼可知,隻要調用了這個方法都會運作這行代碼
mFinished = false;,這樣子在進入computeScroll這個方法時mScroller.computeScrollOffset()就會為true,才有log才會scrollTo,這一點對于了解後面手指離開螢幕那段代碼非常重要。再看上面log,第一個框中的getCurrY = 0,因為确實是0,因為在得到這個結果時,并沒有執行scrollTo這行代碼,是以還是沒有移動的。剛好這個值和startScroll之前的getFinalY是一樣的,後面都符合這個規律,這也很好了解,startScroll隻是指派,也沒有移動,指派後得到getFinalY是需要移動的目标。
C. 因為前面寫了
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP :
reset(, );
break;
default:
return mGestureDetector.onTouchEvent(event);
}
return super.onTouchEvent(event);
}
隻要除了MotionEvent.ACTION_UP ,其他的操作比如down move,都是會重寫onScroll方法的。但是當手指離開時會運作 reset(0, 0);,如下
protected void reset(int x, int y) {
int dx = x - mScroller.getFinalX();
int dy = y - mScroller.getFinalY();
beginScroll(dx, dy);
}
這個時候得到的getFinalY是累加到手指離開螢幕最大的值(不含符号).如log圖
紅色框是布局移動到最後一個點,因為前面說了隻要調用了startScroll這個方法,在進入computeScroll後都能經過mScroller.computeScrollOffset()的判斷,是以這裡還是會運作scrollTo這行代碼,但是這行代碼完了後,手指離開就會運作如下代碼
case MotionEvent.ACTION_UP :
reset(, );
break;
首先要注意一點,每次scrollTo後都會postInvalidate()重新整理界面,既然重新整理了界面,那麼又将導緻View的重繪,則又将調用computeScroll()方法,前面的一直下拉累加和後面情況不一樣。比如以前,目标位置是 -112或者 而getCurrY=-110,經過postInvalidate();到達目标位置布局就會終止循環停止滾動,再等手指滑動。而這裡看第三個框,getFinalY1-115,然後經過startScroll,則getFinalY=-115+115=0,也是對的。又是調用了startScroll,則又會進入判斷,會執行scrollTo,因為這裡的目标位置是getFinalY2=0,是以不像以前樣,手指已經離開螢幕,不能控制,目前getCurrY=-115 目标是0,再調用postInvalidate()時一直不斷循環直到位置等于0 。
3.寫在最後
感謝http://blog.csdn.net/lfdfhl/article/details/53143114 ,通過它的部落格學到很多知識。