詳解Android應用開發(fā)中Scroller類的屏幕滑動功能運用
今天給大家介紹下Android中滑屏功能的一個基本實現(xiàn)過程以及原理初探,最后給大家重點講解View視圖中scrollTo 與scrollBy這兩個函數(shù)的區(qū)別 。
首先 ,我們必須明白在Android View視圖是沒有邊界的,Canvas是沒有邊界的,只不過我們通過繪制特定的View時對Canvas對象進行了一定的操作,例如 : translate(平移)、clipRect(剪切)等,以便達到我們的對該Canvas對象繪制的要求 ,我們可以將這種無邊界的視圖稱為“視圖坐標”-----它不受物理屏幕限制。通常我們所理解的一個Layout布局文件只是該視圖的顯示區(qū)域,超過了這個顯示區(qū)域將不能顯示到父視圖的區(qū)域中 ,對應的,我們可以將這種有邊界的視圖稱為“布局坐標”父視圖給子視圖分配的布局(layout)大小。而且, 一個視圖的在屏幕的起始坐標位于視圖坐標起始處,如下圖所示。
這么來說吧 ,世界本是無邊無界的,可是我們的眼睛我們的心約束了我們所看到的“世界” 。
如下所示:
黑色框框表示該子視圖的布局坐標, 褐色框框表示該子視圖的視圖坐標--該坐標是無限的,超過了父視圖給子視圖規(guī)定的區(qū)域后,不再顯示該超出內容。
那么下面的問題就是:如何將我們的視圖的任意坐標能顯示到該視圖的中心坐標上呢? 由于該布局位置是只能顯示特定的一塊視圖內容 ,因此我們需要通過scrollTo()或者scrollBy()方法將我們期望的視圖“滾動”至布局坐標上。
在View.java中提供了了如下兩個變量以及相應的屬性方法去讀取滾動值 ,如下: View.java類中
/** * The offset, in pixels, by which the content of this view is scrolled * horizontally. * {@hide} */ protected int mScrollX; //該視圖內容相當于視圖起始坐標的偏移量 , X軸 方向 /** * The offset, in pixels, by which the content of this view is scrolled * vertically. * {@hide} */ protected int mScrollY; //該視圖內容相當于視圖起始坐標的偏移量 , Y軸方向 /** * Return the scrolled left position of this view. This is the left edge of * the displayed part of your view. You do not need to draw any pixels * farther left, since those are outside of the frame of your view on * screen. * * @return The left edge of the displayed part of your view, in pixels. */ public final int getScrollX() { return mScrollX; } /** * Return the scrolled top position of this view. This is the top edge of * the displayed part of your view. You do not need to draw any pixels above * it, since those are outside of the frame of your view on screen. * * @return The top edge of the displayed part of your view, in pixels. */ public final int getScrollY() { return mScrollY; }
注意,所謂的“by which the content of this view is scrolled”表示該偏移量只針對于該View中onDraw()方法里的具體內容實現(xiàn),而不針對繪制背景圖片等 。具體原因可參考<Android中View繪制流程以及invalidate()等相關方法分析>
提示:下文中提到的當前視圖內容是在繪制在布局坐標處的內容。
public void scrollTo(int x, int y)
說明:在當前視圖內容偏移至(x , y)坐標處,即顯示(可視)區(qū)域位于(x , y)坐標處。
方法原型為: View.java類中
/** * Set 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 x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { //偏移位置發(fā)生了改變 if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; //賦新值,保存當前便宜量 mScrollY = y; //回調onScrollChanged方法 onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); //一般都引起重繪 } } } public void scrollBy(int x, int y)
說明:在當前視圖內容繼續(xù)偏移(x , y)個單位,顯示(可視)區(qū)域也跟著偏移(x,y)個單位。方法原型為: View.java類中
/** * 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 */ // 看出原因了吧 。。 mScrollX 與 mScrollY 代表我們當前偏移的位置 , 在當前位置繼續(xù)偏移(x ,y)個單位 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
第一個小Demo非常簡單 ,大家重點理解與掌握scrollTo() 與 scrollBy()函數(shù)的用法和區(qū)別。
第二個小Demo則有了Launcher的模樣,能夠左右切換屏幕 。實現(xiàn)功能如下: 采用了一個自定義ViewGroup,該ViewGroup對象包含了3個LinearLayout子視圖,并且以一定的布局坐標(由layout()方法指定)顯示在ViewGroup上。 接下來,即可調用該ViewGroup對象的scrollTo或者scrollBy()方法切換指定視圖內容了,即切換屏幕。 呵呵 ,挺好玩的吧 。
自定義ViewGroup如下:
//自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 public class MultiViewGroup extends ViewGroup { private Context mContext; private static String TAG = "MultiViewGroup"; public MultiViewGroup(Context context) { super(context); mContext = context; init(); } public MultiViewGroup(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } private void init() { // 初始化3個 LinearLayout控件 LinearLayout oneLL = new LinearLayout(mContext); oneLL.setBackgroundColor(Color.RED); addView(oneLL); LinearLayout twoLL = new LinearLayout(mContext); twoLL.setBackgroundColor(Color.YELLOW); addView(twoLL); LinearLayout threeLL = new LinearLayout(mContext); threeLL.setBackgroundColor(Color.BLUE); addView(threeLL); } // measure過程 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.i(TAG, "--- start onMeasure --"); // 設置該ViewGroup的大小 int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); int childCount = getChildCount(); Log.i(TAG, "--- onMeasure childCount is -->" + childCount); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); // 設置每個子視圖的大小 , 即全屏 child.measure(MultiScreenActivity.screenWidth, MultiScreenActivity.scrrenHeight); } } // layout過程 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub Log.i(TAG, "--- start onLayout --"); int startLeft = 0; // 每個子視圖的起始布局坐標 int startTop = 10; // 間距設置為10px 相當于 android:marginTop= "10px" int childCount = getChildCount(); Log.i(TAG, "--- onLayout childCount is -->" + childCount); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); child.layout(startLeft, startTop, startLeft + MultiScreenActivity.screenWidth, startTop + MultiScreenActivity.scrrenHeight); startLeft = startLeft + MultiScreenActivity.screenWidth ; //校準每個子View的起始布局位置 //三個子視圖的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960] } } }
關于scrollTo()和scrollBy()以及偏移坐標的設置/取值問題
scrollTo()和scrollBy()這兩個方法的主要作用是將View/ViewGroup移至指定的坐標中,并且將偏移量保存起來。另外:
mScrollX 代表X軸方向的偏移坐標,mScrollY 代表Y軸方向的偏移坐標
關于偏移量的設置我們可以參看下源碼:
package com.qin.customviewgroup; public class View { .... protected int mScrollX; //該視圖內容相當于視圖起始坐標的偏移量 , X軸 方向 protected int mScrollY; //該視圖內容相當于視圖起始坐標的偏移量 , Y軸方向 //返回值 public final int getScrollX() { return mScrollX; } public final int getScrollY() { return mScrollY; } public void scrollTo(int x, int y) { //偏移位置發(fā)生了改變 if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; //賦新值,保存當前便宜量 mScrollY = y; //回調onScrollChanged方法 onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { invalidate(); //一般都引起重繪 } } } // 看出原因了吧 。。 mScrollX 與 mScrollY 代表我們當前偏移的位置 , 在當前位置繼續(xù)偏移(x ,y)個單位 public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); } //... }
于是,在任何時刻我們都可以獲取該View/ViewGroup的偏移位置了,即調用getScrollX()方法和getScrollY()方法
Scroller類的介紹
在初次看Launcher滑屏的時候,我就對Scroller類的學習感到非常蛋疼,完全失去了繼續(xù)研究的欲望。如今,沒得辦法,得重新看Launcher模塊,基本上將Launcher大部分類以及功能給掌握了。當然,也花了一天時間來學習Launcher里的滑屏實現(xiàn),基本上業(yè)是撥開云霧見真知了。
我們知道想把一個View偏移至指定坐標(x,y)處,利用scrollTo()方法直接調用就OK了,但我們不能忽視的是,該方法本身來的的副作用:非常迅速的將View/ViewGroup偏移至目標點,而沒有對這個偏移過程有任何控制,對用戶而言可能是不太友好的。于是,基于這種偏移控制,Scroller類被設計出來了,該類的主要作用是為偏移過程制定一定的控制流程(后面我們會知道的更多),從而使偏移更流暢,更完美。
可能上面說的比較懸乎,道理也沒有講透。下面我就根據(jù)特定情景幫助大家分析下:
情景: 從上海如何到武漢?
普通的人可能會想,so easy : 飛機、輪船、11路公交車...
文藝的人可能會想, 小 case : 時空忍術(火影的招數(shù))、翻個筋斗(孫大圣的招數(shù))...
不管怎么樣,我們想出來的套路可能有兩種:
1、有個時間控制過程才能抵達(緩慢的前進)、對應于Scroller的作用
假設做火車,這個過程可能包括: 火車速率,花費周期等;
2、瞬間抵達(超神太快了,都眩暈了,用戶體驗不太好)、對應于scrollTo()的作用
模擬Scroller類的實現(xiàn)功能:
假設從上海做動車到武漢需要10個小時,行進距離為1000km ,火車速率200/h 。采用第一種時間控制方法到達武漢的整個配合過程可能如下:我們每隔一段時間(例如1小時),計算火車應該行進的距離,然后調用scrollTo()方法,行進至該處。10小時過完后,我們也就達到了目的地了。相信大家心里應該有個感覺了。我們就分析下源碼里去看看Scroller類的相關方法.
其源代碼(部分)如下:
路徑位于 \frameworks\base\core\java\android\widget\Scroller.java
public class Scroller { private int mStartX; //起始坐標點 , X軸方向 private int mStartY; //起始坐標點 , Y軸方向 private int mCurrX; //當前坐標點 X軸, 即調用startScroll函數(shù)后,經(jīng)過一定時間所達到的值 private int mCurrY; //當前坐標點 Y軸, 即調用startScroll函數(shù)后,經(jīng)過一定時間所達到的值 private float mDeltaX; //應該繼續(xù)滑動的距離, X軸方向 private float mDeltaY; //應該繼續(xù)滑動的距離, Y軸方向 private boolean mFinished; //是否已經(jīng)完成本次滑動操作, 如果完成則為 true //構造函數(shù) public Scroller(Context context) { this(context, null); } public final boolean isFinished() { return mFinished; } //強制結束本次滑屏操作 public final void forceFinished(boolean finished) { mFinished = finished; } public final int getCurrX() { return mCurrX; } /* Call this when you want to know the new location. If it returns true, * the animation is not yet finished. loc will be altered to provide the * new location. */ //根據(jù)當前已經(jīng)消逝的時間計算當前的坐標點,保存在mCurrX和mCurrY值中 public boolean computeScrollOffset() { if (mFinished) { //已經(jīng)完成了本次動畫控制,直接返回為false return false; } int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime); if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: float x = (float)timePassed * mDurationReciprocal; ... mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; ... } else { mCurrX = mFinalX; mCurrY = mFinalY; mFinished = true; } return true; } //開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,即到達坐標為(startX+dx , startY+dy)出 public void startScroll(int startX, int startY, int dx, int dy, int duration) { mFinished = false; mDuration = duration; mStartTime = AnimationUtils.currentAnimationTimeMillis(); mStartX = startX; mStartY = startY; mFinalX = startX + dx; mFinalY = startY + dy; mDeltaX = dx; mDeltaY = dy; ... } }
其中比較重要的兩個方法為:
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函數(shù)功能說明:根據(jù)當前已經(jīng)消逝的時間計算當前的坐標點,保存在mCurrX和mCurrY值中
public void startScroll(int startX, int startY, int dx, int dy, int duration)
函數(shù)功能說明:開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,到達坐標為 (startX+dx , startY+dy)處。
PS : 強烈建議大家看看該類的源碼,便于后續(xù)理解。
computeScroll()方法介紹
為了易于控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個流程。在繪制View時,會在draw()過程調用該方法。因此, 再配合使用Scroller實例,我們就可以獲得當前應該的偏移坐標,手動使View/ViewGroup偏移至該處。
computeScroll()方法原型如下,該方法位于ViewGroup.java類中 /** * Called by a parent to request that a child update its values for mScrollX * and mScrollY if necessary. This will typically be done if the child is * animating a scroll using a {@link android.widget.Scroller Scroller} * object. */由父視圖調用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 public void computeScroll() { //空方法 ,自定義ViewGroup必須實現(xiàn)方法體 }
為了實現(xiàn)偏移控制,一般自定義View/ViewGroup都需要重載該方法 。其調用過程位于View繪制流程draw()過程中,如下
:@Override protected void dispatchDraw(Canvas canvas){ ... for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } protected boolean drawChild(Canvas canvas, View child, long drawingTime) { ... child.computeScroll(); ... }
Demo說明:
我們簡單的復用了之前寫的一個自定義ViewGroup,與以前一次有區(qū)別的是,我們沒有調用scrollTo()方法去進行瞬間偏移。 本次做法如下:
第一、調用Scroller實例去產(chǎn)生一個偏移控制(對應于startScroll()方法)
第二、手動調用invalid()方法去重新繪制,剩下的就是在 computeScroll()里根據(jù)當前已經(jīng)逝去的時間,獲取當前應該偏移的坐標(由Scroller實例對應的computeScrollOffset()計算而得),
第三、當前應該偏移的坐標,調用scrollBy()方法去緩慢移動至該坐標處。
附:由于滑動截屏很難,只是簡單的截取了兩個個靜態(tài)圖片,觸摸的話可以實現(xiàn)左右滑動切屏了。
//自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 public class MultiViewGroup extends ViewGroup { ... //startScroll開始移至下一屏 public void startMove(){ curScreen ++ ; Log.i(TAG, "----startMove---- curScreen " + curScreen); //使用動畫控制偏移過程 , 3s內到位 mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000); //其實點擊按鈕的時候,系統(tǒng)會自動重新繪制View,我們還是手動加上吧。 invalidate(); //使用scrollTo一步到位 //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0); } // 由父視圖調用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 @Override public void computeScroll() { // TODO Auto-generated method stub Log.e(TAG, "computeScroll"); // 如果返回true,表示動畫還沒有結束 // 因為前面startScroll,所以只有在startScroll完成時 才會為false if (mScroller.computeScrollOffset()) { Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); // 產(chǎn)生了動畫效果,根據(jù)當前值 每次滾動一點 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); //此時同樣也需要刷新View ,否則效果可能有誤差 postInvalidate(); } else Log.i(TAG, "have done the scoller -----"); } //馬上停止移動,如果已經(jīng)超過了下一屏的一半,我們強制滑到下一個屏幕 public void stopMove(){ Log.v(TAG, "----stopMove ----"); if(mScroller != null){ //如果動畫還沒結束,我們就按下了結束的按鈕,那我們就結束該動畫,即馬上滑動指定位置 if(!mScroller.isFinished()){ int scrollCurX= mScroller.getCurrX() ; //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕 // 這樣的一個簡單公式意思是:假設當前滑屏偏移值即 scrollCurX 加上每個屏幕一半的寬度,除以每個屏幕的寬度就是 // 我們目標屏所在位置了。 假如每個屏幕寬度為320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏,索引值為1 //即(500 + 320/2)/320 = 1 int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ; Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX); Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen); mScroller.abortAnimation(); //停止了動畫,我們馬上滑倒目標位置 scrollTo(descScreen *getWidth() , 0); curScreen = descScreen ; //糾正目標屏位置 } else Log.i(TAG, "----OK mScroller.is finished ---- "); } } ... }
如何實現(xiàn)觸摸滑屏?
其實網(wǎng)上有很多關于Launcher實現(xiàn)滑屏的博文,基本上也把道理闡釋的比較明白了 。我這兒也是基于自己的理解,將一些
重要方面的知識點給補充下,希望能幫助大家理解。想要實現(xiàn)滑屏操作,值得考慮的事情包括如下幾個方面:
其中:onInterceptTouchEvent()主要功能是控制觸摸事件的分發(fā),例如是子視圖的點擊事件還是滑動事件。其他所有處理過程均在onTouchEvent()方法里實現(xiàn)了。
1、屏幕的滑動要根據(jù)手指的移動而移動 ---- 主要實現(xiàn)在onTouchEvent()方法中
2、當手指松開時,可能我們并沒有完全滑動至某個屏幕上,這是我們需要手動判斷當前偏移至去計算目標屏(當前屏或者前后屏),并且優(yōu)雅的偏移到目標屏(當然是用Scroller實例咯)。
3、調用computeScroll ()去實現(xiàn)緩慢移動過程。
知識點介紹:
VelocityTracker類
功能: 根據(jù)觸摸位置計算每像素的移動速率。
常用方法有:
public void addMovement (MotionEvent ev)
功能:添加觸摸對象MotionEvent , 用于計算觸摸速率。
public void computeCurrentVelocity (int units)
功能:以每像素units單位考核移動速率。額,其實我也不太懂,賦予值1000即可。
參照源碼 該units的意思如下:
參數(shù) units : The units you would like the velocity in. A value of 1
provides pixels per millisecond, 1000 provides pixels per second, etc.
public float getXVelocity ()
功能:獲得X軸方向的移動速率。
ViewConfiguration類
功能: 獲得一些關于timeouts(時間)、sizes(大小)、distances(距離)的標準常量值 。
常用方法:
public int getScaledEdgeSlop()
說明:獲得一個觸摸移動的最小像素值。也就是說,只有超過了這個值,才代表我們該滑屏處理了。
public static int getLongPressTimeout()
說明:獲得一個執(zhí)行長按事件監(jiān)聽(onLongClickListener)的值。也就是說,對某個View按下觸摸時,只有超過了這個時間值在,才表示我們該對該View回調長按事件了;否則,小于這個時間點松開手指,只執(zhí)行onClick監(jiān)聽
我能寫下來的也就這么多了,更多的東西參考代碼注釋吧。 在掌握了上面我羅列的知識后(重點scrollTo、Scroller類),其他方面的知識都是關于點與點之間的計算了以及觸摸事件的分發(fā)了。這方面感覺也沒啥可寫的。
//自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的布局位置,通過scrollBy或者scrollTo方法切換 public class MultiViewGroup extends ViewGroup { private static String TAG = "MultiViewGroup"; private int curScreen = 0 ; //當前屏幕 private Scroller mScroller = null ; //Scroller對象實例 public MultiViewGroup(Context context) { super(context); mContext = context; init(); } public MultiViewGroup(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } //初始化 private void init() { ... //初始化Scroller實例 mScroller = new Scroller(mContext); // 初始化3個 LinearLayout控件 ... //初始化一個最小滑動距離 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); } // 由父視圖調用用來請求子視圖根據(jù)偏移值 mScrollX,mScrollY重新繪制 @Override public void computeScroll() { // TODO Auto-generated method stub Log.e(TAG, "computeScroll"); // 如果返回true,表示動畫還沒有結束 // 因為前面startScroll,所以只有在startScroll完成時 才會為false if (mScroller.computeScrollOffset()) { Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY()); // 產(chǎn)生了動畫效果,根據(jù)當前值 每次滾動一點 scrollTo(mScroller.getCurrX(), mScroller.getCurrY()); Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight()); //此時同樣也需要刷新View ,否則效果可能有誤差 postInvalidate(); } else Log.i(TAG, "have done the scoller -----"); } //兩種狀態(tài): 是否處于滑屏狀態(tài) private static final int TOUCH_STATE_REST = 0; //什么都沒做的狀態(tài) private static final int TOUCH_STATE_SCROLLING = 1; //開始滑屏的狀態(tài) private int mTouchState = TOUCH_STATE_REST; //默認是什么都沒做的狀態(tài) //-------------------------- //處理觸摸事件 ~ public static int SNAP_VELOCITY = 600 ; //最小的滑動速率 private int mTouchSlop = 0 ; //最小滑動距離,超過了,才認為開始滑動 private float mLastionMotionX = 0 ; //記住上次觸摸屏的位置 //處理觸摸的速率 private VelocityTracker mVelocityTracker = null ; // 這個感覺沒什么作用 不管true還是false 都是會執(zhí)行onTouchEvent的 因為子view里面onTouchEvent返回false了 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // TODO Auto-generated method stub Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop); final int action = ev.getAction(); //表示已經(jīng)開始滑動了,不需要走該Action_MOVE方法了(第一次時可能調用)。 //該方法主要用于用戶快速松開手指,又快速按下的行為。此時認為是出于滑屏狀態(tài)的。 if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) { return true; } final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: Log.e(TAG, "onInterceptTouchEvent move"); final int xDiff = (int) Math.abs(mLastionMotionX - x); //超過了最小滑動距離,就可以認為開始滑動了 if (xDiff > mTouchSlop) { mTouchState = TOUCH_STATE_SCROLLING; } break; case MotionEvent.ACTION_DOWN: Log.e(TAG, "onInterceptTouchEvent down"); mLastionMotionX = x; mLastMotionY = y; Log.e(TAG, mScroller.isFinished() + ""); mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: Log.e(TAG, "onInterceptTouchEvent up or cancel"); mTouchState = TOUCH_STATE_REST; break; } Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST); return mTouchState != TOUCH_STATE_REST; } public boolean onTouchEvent(MotionEvent event){ super.onTouchEvent(event); Log.i(TAG, "--- onTouchEvent--> " ); // TODO Auto-generated method stub Log.e(TAG, "onTouchEvent start"); //獲得VelocityTracker對象,并且添加滑動對象 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(event); //觸摸點 float x = event.getX(); float y = event.getY(); switch(event.getAction()){ case MotionEvent.ACTION_DOWN: //如果屏幕的動畫還沒結束,你就按下了,我們就結束上一次動畫,即開始這次新ACTION_DOWN的動畫 if(mScroller != null){ if(!mScroller.isFinished()){ mScroller.abortAnimation(); } } mLastionMotionX = x ; //記住開始落下的屏幕點 break ; case MotionEvent.ACTION_MOVE: int detaX = (int)(mLastionMotionX - x ); //每次滑動屏幕,屏幕應該移動的距離 scrollBy(detaX, 0);//開始緩慢滑屏咯。 detaX > 0 向右滑動 , detaX < 0 向左滑動 , Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX ); mLastionMotionX = x ; break ; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker ; velocityTracker.computeCurrentVelocity(1000); //計算速率 int velocityX = (int) velocityTracker.getXVelocity() ; Log.e(TAG , "---velocityX---" + velocityX); //滑動速率達到了一個標準(快速向右滑屏,返回上一個屏幕) 馬上進行切屏處理 if (velocityX > SNAP_VELOCITY && curScreen > 0) { // Fling enough to move left Log.e(TAG, "snap left"); snapToScreen(curScreen - 1); } //快速向左滑屏,返回下一個屏幕) else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){ Log.e(TAG, "snap right"); snapToScreen(curScreen + 1); } //以上為快速移動的 ,強制切換屏幕 else{ //我們是緩慢移動的,因此先判斷是保留在本屏幕還是到下一屏幕 snapToDestination(); } //回收VelocityTracker對象 if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } //修正mTouchState值 mTouchState = TOUCH_STATE_REST ; break; case MotionEvent.ACTION_CANCEL: mTouchState = TOUCH_STATE_REST ; break; } return true ; } ////我們是緩慢移動的,因此需要根據(jù)偏移值判斷目標屏是哪個? private void snapToDestination(){ //當前的偏移位置 int scrollX = getScrollX() ; int scrollY = getScrollY() ; Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX); //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕 //直接使用這個公式判斷是哪一個屏幕 前后或者自己 //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕 // 這樣的一個簡單公式意思是:假設當前滑屏偏移值即 scrollCurX 加上每個屏幕一半的寬度,除以每個屏幕的寬度就是 // 我們目標屏所在位置了。 假如每個屏幕寬度為320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏 int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ; Log.e(TAG, "### onTouchEvent ACTION_UP### dx destScreen " + destScreen); snapToScreen(destScreen); } //真正的實現(xiàn)跳轉屏幕的方法 private void snapToScreen(int whichScreen){ //簡單的移到目標屏幕,可能是當前屏或者下一屏幕 //直接跳轉過去,不太友好 //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0); //為了友好性,我們在增加一個動畫效果 //需要再次滑動的距離 屏或者下一屏幕的繼續(xù)滑動距離 curScreen = whichScreen ; //防止屏幕越界,即超過屏幕數(shù) if(curScreen > getChildCount() - 1) curScreen = getChildCount() - 1 ; //為了達到下一屏幕或者當前屏幕,我們需要繼續(xù)滑動的距離.根據(jù)dx值,可能想左滑動,也可能像又滑動 int dx = curScreen * getWidth() - getScrollX() ; Log.e(TAG, "### onTouchEvent ACTION_UP### dx is " + dx); mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2); //由于觸摸事件不會重新繪制View,所以此時需要手動刷新View 否則沒效果 invalidate(); } //開始滑動至下一屏 public void startMove(){ ... } //理解停止滑動 public void stopMove(){ ... } // measure過程 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... } // layout過程 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { ... } }
相關文章
Android BroadcastReceiver廣播注冊方式總結
這篇文章主要介紹了Android BroadcastReceiver廣播注冊方式總結的相關資料,需要的朋友可以參考下2017-01-01Android Studio啟動報錯Java 1.8 or later is required的解決方法
這篇文章主要為大家詳細介紹了Android Studio啟動時報錯Java 1.8 or later is required的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-03-03Android實現(xiàn)長按back鍵退出應用程序的方法
這篇文章主要介紹了Android實現(xiàn)長按back鍵退出應用程序的方法,實例分析了Android按鈕事件的操作技巧,需要的朋友可以參考下2015-05-05Android實現(xiàn)漸變啟動頁和帶有指示器的引導頁
這篇文章主要為大家詳細介紹了Android實現(xiàn)漸變啟動頁和帶有指示器的引導頁,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-09-09Android利用EditText如何實現(xiàn)搜索框詳解
EditText 在開發(fā)中也是經(jīng)常用到的控件,也是一個比較必要的組件,下面這篇文章主要給大家介紹了關于Android利用EditText如何實現(xiàn)搜索框的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考下2018-07-07