1.view的滑動,六種滑動方式:
一:通過layout來實現滑動效果
package com.example.testdragview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;public class DragView extends View{
private int lastX;
private int lastY;
public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}public DragView(Context context, AttributeSet attrs) {
super(context, attrs);
}public DragView(Context context) {
super(context);
}
public boolean onTouchEvent(MotionEvent event) {
// Log.d("付勇焜----->","TouchEvent");
// Log.d("付勇焜----->",super.onTouchEvent(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 offX = x - lastX;
int offY = y - lastY;
//調用layout方法來重新放置它的位置
layout(getLeft()+offX, getTop()+offY,
getRight()+offX , getBottom()+offY);
break;
}
return true;
}
}
二、offsetLeftAndRight()和offsetTopAndBottom()方法來實現
其實這兩個方法分別是對左右移動和上下移動的封裝,傳入的就是偏移量。此時將DragView中的onTouchEvent代碼簡單替換即可,如下:
1 public boolean onTouchEvent(MotionEvent event) {
2
3 //獲取到手指處的橫座標和縱座標
4 int x = (int) event.getX();
5 int y = (int) event.getY();
6
7 switch(event.getAction())
8 {
9 case MotionEvent.ACTION_DOWN:
10
11 lastX = x;
12 lastY = y;
13
14 break;
15
16 case MotionEvent.ACTION_MOVE:
17
18 //計算移動的距離
19 int offX = x - lastX;
20 int offY = y - lastY;
21
22 offsetLeftAndRight(offX);
23 offsetTopAndBottom(offY);
24
25 break;
26 }
27
28 return true;
29 }
紅色部分就是關鍵代碼了,運行一下程序,跟上面的效果是一樣的,不再貼圖。
三、使用LayoutParams來實現
依舊修改DragView的onTouchEvent代碼,如下:
1 public boolean onTouchEvent(MotionEvent event) {
2
3 //獲取到手指處的橫座標和縱座標
4 int x = (int) event.getX();
5 int y = (int) event.getY();
6
7 switch(event.getAction())
8 {
9 case MotionEvent.ACTION_DOWN:
10
11 lastX = x;
12 lastY = y;
13
14 break;
15
16 case MotionEvent.ACTION_MOVE:
17
18 //計算移動的距離
19 int offX = x - lastX;
20 int offY = y - lastY;
21
22 ViewGroup.MarginLayoutParams mlp =
23 (MarginLayoutParams) getLayoutParams();
24
25 mlp.leftMargin = getLeft()+offX;
26 mlp.topMargin = getTop()+offY;
27
28 setLayoutParams(mlp);
29
30 break;
31 }
32
33 return true;
34 }
紅色部分依舊是關鍵代碼。注意這裏我們一般通過改變view的Margin屬性來改變其位置的。
運行程序,結果依舊,不再貼圖。
四、通過scrollTo和scrollBy方法,不是彈性滑動,感覺會很突兀
在一個view中,系統也提供了scrollTo和scrollBy方法來移動view。很好理解,sceollTo(x,y)傳入的應該是移動的終點座標,而scrollBy(dx,dy)傳入的是
移動的增量。這兩個方法要在view所在的viewGroup中使用!但是一定要注意:通過scrollBy傳入的值應該是你需要的那個增量的相反數!這樣子才能達到你想
要的效果!!切記切記
依舊是hi修改DragView的onTouchEvent代碼,如下:
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 offX = x - lastX;
int offY = y - lastY;
((View) getParent()).scrollBy(-offX,- offY);
break;
}
return true;
}
五.使用Scroller來實現彈性滑動
六.使用動畫來實現滑動上面我們提到了使用scrollTo/scrollBy方法實現View的滑動效果不是平滑的,好消息是我們可以使用Scroller方法來輔助實現View的彈性滑動。使用Scroller實現彈性滑動的慣用代碼如下:
1 Scroller scroller = new Scroller(mContext); 2 3 private void smoothScrollTo(int dstX, int dstY) { 4 int scrollX = getScrollX(); 5 int delta = dstX - scrollX; 6 scroller.startScroll(scrollX, 0, delta, 0, 1000); 7 invalidate(); 8 } 9 10 @Override 11 public void computeScroll() { 12 if (scroller.computeScrollOffset()) { 13 scrollTo(scroller.getCurrX(), scroller.getCurY()); 14 postInvalidate(); 15 } 16 }
我們來看一下以上的代碼。第4行中,我們獲取到View的mScrollX參數並存到scrollX變量中。然後在第5行計算要滑動的位移量。第6行調用了startScroll方法,我們來看看startScroll方法的源碼:
1 public void startScroll(int startX, int startY, int dx, int dy, int duration) { 2 mMode = SCROLL_MODE; 3 mFinished = false; 4 mDuration = duration; 5 mStartTime = AnimationUtils.currentAnimationTimeMillis(); 6 mStartX = startX; 7 mStartY = startY; 8 mFinalX = startX + dx; 9 mFinalY = startY + dy; 10 mDeltaX = dx; 11 mDeltaY = dy; 12 mDurationReciprocal = 1.0f / (float) mDuration; 13 14 mViscousFluidScale = 8.0f; 15 16 mViscousFluidNormalize = 1.0f; 17 mViscousFluidNormalize = 1.0f / viscousFluid(1.0f); 18 }
從以上的源碼我們可以看到,startScroll方法中並沒有進行實際的滾動操作,而是把startX、startY、deltaX、deltaY等參數都保存了下來。那麼究竟怎麼實現View的滑動的呢?我們先回到Scroller慣用代碼。我們看到第7行調用了invalidate方法,這個方法會請求重繪View,這會導致View的draw的方法被調用,draw的方法內部會調用computeScroll方法。我們來看看第13行,調用了scrollTo方法,並傳入mScroller.getCurrX()和mScroller.getCurrY()方法作爲參數。那麼獲取到的這兩個參數是什麼呢?這兩個參數是在第12行調用的computeScrollOffset方法中設置的,我們來看看這個方法中設置這兩個參數的相關代碼:
1 public boolean computeScrollOffset() { 2 ... 3 int timePassed = (int) (AnimationUtils.currentAnimationTimeMillis() - mStartTime); 4 if (timePassed < mDuration) { 5 switch (mMode) { 6 case SCROLL_MODE: 7 final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); 8 mCurrX = mStartX + Math.round(x * mDeltaX); 9 mCurrY = mStartY + Math.rounc(y * mDeltaY); 10 break; 11 ... 12 } 13 } 14 return true; 15 }
以上代碼中第8行和第9行設置的mCurrX和mCurrY即爲以上scrollTo的兩個參數,表示本次滑動的目標位置。computeScrollOffset方法返回true表示滑動過程還未結束,否則表示結束。
通過以上的分析,我們大概瞭解了Scroller實現彈性滑動的原理:invaldate方法會導致View的draw方法被調用,而draw會調用computeScroll方法,因此重寫了computeScroll方法,而computeScrollOffset方法會根據時間的流逝動態的計算出很小的一段時間應該滑動多少距離。也就是把一次滑動拆分成無數次小距離滑動從而實現“彈性滑動”。
使用動畫來實現View的滑動主要通過改變View的translationX和translationY參數來實現,使用動畫的好處在於滑動效果是平滑的。上面我們提到過,View的x、y參數決定View的當前位置,通過改變translationX和translationY,我們就可以改變View的當前位置。我們可以使用屬性動畫或者補間動畫來實現View的平移。
首先,我們先來看一下如何使用補間動畫來實現View的平移。補間動畫資源定義如下(anim.xml):
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android" android:fillAfter="true"> <translate android:duration="100" android:fromXDelta="0" android:fromYDelta="0" android:interpolator="@android:anim/linear_interpolator" android:toXDelta="100" android:toYDelta="100"/> </set>
然後在onCreat方法中調用startAnimation方法即可。使用補間動畫實現View的滑動有一個缺陷,那就是移動的知識View的“影像”,這意味着其實View並未真正的移動,只是我們看起來它移動了而已。拿Button來舉例,假若我們通過補間動畫移動了一個Button,我們會發現,在Button的原來位置點擊屏幕會出發點擊事件,而在移動後的Button上點擊不會觸發點擊事件。
接下來,我們看看如何用屬性動畫來實現View的平移。使用屬性動畫實現View的平移更加簡單,只需要以下一條語句:
ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration(100).start();
以上代碼即實現了使用屬性動畫把targetView在100ms內向右平移100px。使用屬性動畫的限制在於真正的屬性動畫只可以在Android 3.0+使用(一些第三方庫實現的兼容低版本的屬性動畫不是真正的屬性動畫),優點就是它可以真正的移動View而不是僅僅移動View的影像。
經過以上的描述,使用屬性動畫實現View的滑動看起來是個不錯的選擇,而且一些View的複雜的滑動效果只有通過動畫才能比較方便的實現。
2.View的滑動衝突
在Android開發中,如果是一些簡單的佈局,都很容易搞定,但是一旦涉及到複雜的頁面,特別是爲了兼容小屏手機而使用了ScrollView以後,就會出現很多點擊事件的衝突,最經典的就是ScrollView中嵌套了ListView。我想大部分剛開始接觸Android的同學們都踩到過這個坑,下面跟着小編一起來看看解決方案吧。。
同方向滑動衝突
比如ScrollView嵌套ListView,或者是ScrollView嵌套自己
這裏先看一張效果圖
上圖是在購物軟件上常見的上拉查看圖文詳情,關於這中動畫效果的實現,其實實現整體的效果,辦法是有很多的,網上有很多相關的例子,但是對某些細節的處理不是很清晰,比如,下拉之後顯示的部分(例如底部的圖文詳情)又是一個類ScrollView的控件(比如WebView)的話,又會產生新的問題。這裏就以下拉查看圖文詳情爲背景做一下同方向滑動衝突的分析。
整體思路
這裏看下圖
多個ScrollView嵌套示意圖
首先,關於這張圖做一些設定:
1.黑色的框代表手機屏幕
2.綠色的框代表一個外層的ScrollView
3.兩個紅色的框代表嵌套在裏面的兩個類ScrollView控件,這裏我們就暫時簡稱爲 SUp,SDown
好了,接下來就分析一下實現整個流程的過程。
這裏必須明確的一點,無論何時,SUp和SDown可見的部分始終是手機屏幕的高度。知道了這一點,我們就可以按以下步驟展開
首先,我們確保外部的ScrollView不攔截滑動事件,這樣SUp必然獲得此次事件,然後根據其Action_Move事件,當其爲向下滑動且自身滑動距離+屏幕高度=其自身高度 時,即可認爲SUp滑動到了底部,此時外部ScrollView可攔截滑動事件,從而保證整個視圖能夠繼續向下滑動,這個時候底部SDown就顯示出來了。
同理,這時候不允許外部ScrollView攔截滑動事件,由SDown處理,根據其Action_move事件,當其爲向上滑動,且自身可滑動距離爲0時,就說明SDown已經滑動到了頂部,這時外部ScrollView又可以獲得攔截滑動事件的權利,從而保證整個視圖能夠向上繼續滑動,此時SUp再次顯示,又開始新一輪循環攔截。
這樣整體的一個流程就可以實現動圖中的效果。好了,說完原理,還是看代碼。
代碼實現
SUp實現
這裏在ACTION_MOVE裏做了減法,其實道理是一樣的。
onScrollChanged 是在View類中實現,查看其API可以看到其第二個參數t解釋
即爲當前View此次滑動的距離
SDown實現
以上看到,這裏底部的View並沒有繼承ScrollView,而是選擇繼承了WebView,這裏只是爲了方便,當然繼承ScrollView也是沒有問題。這裏只是需要按實際情況考慮,因爲底部圖文詳情的內容就是一個WebView加載數據。
這個類的實現,按照之前說的原理應該很好理解。
外部ScrollView
這個類的實現,就很靈活了,在onMeasure方法中初始化完內部的View之後,在OnTouch方法中就可以根據實際需求完成不同的邏輯實現,這裏只是爲了仿照查看圖文詳情的效果,對整個視圖通過ScrollView的smoothScrollTo方法進行位移變化,這個邏輯很簡單。
這裏重點說一下一個地方:
你可能會奇怪中間的child(1)去了哪裏?這裏還要從MainActivity的佈局文件說起
dual_scrollview_activity_layout1.xml
整個佈局文件可以看出,我們在CustomerScrollViews這個最外層的自定義ScrollView內部又放置了兩個自定義的ScrollView(就如我們看到的原理圖那樣),只不過在這兩個ScrollView類控件的中間通過layout又放置一個LinearLayout,裏面的內容就是在動圖中看到的那個中間的寫着qq,baidu字樣的用於切換WebView內容的一個View。這裏就不貼代碼了。
這樣,你就可以理解之前的child(1)爲什麼被跳過了吧。
使用
關於底部View內容更新,WebView 通過加載不同URL實現不同視圖效果,只是作爲Demo測試,實際中應考慮通過fragment切換實現。
這裏對滑動衝突的解決方法,是由內而外的展開,默認使頂層View失去攔截能力,在由底部View的滑動距離,做出不同邏輯判斷控制了頂層的攔截與否;這也是比較容易理解和實現的思路。當然,對於此類滑動衝突,有很多不同思路,這裏只是列舉其一。
實際開發開發中,這種帶有同方向滑動特性的控件嵌套時,產生的問題不只是滑動衝突,有時候會有內容顯示不全或不顯示的情況。
最常見如ScrollView嵌套ListView,這種情況只需自定義ListView使其高度計算爲一個很大的值,某種意義上讓其失去了滑動的特性,但是這樣也讓ListView貌似失去了視圖回收機制,這種時候如果加載很多很多很多圖片,效果就會很不理想。對於這種情況,通過對ListView添加headView及footView也是一種解決的辦法,但也得實際UI情況允許。
ScrollView嵌套RecyclerView時稍微麻煩一點,需要自定義ScrollView,還需要自定義實現LinearLayoutManager。
不同方向滑動衝突
比如ScrollView嵌套ViewPager,或者是ViewPager嵌套ScrollView,這種情況其實很典型。現在大部分應用最外層都是ViewPager+Fragment 的底部切換(比如微信)結構,這種時候,就很容易出現滑動衝突。不過ViewPager裏面無論是嵌套ListView還是ScrollView,滑動衝突是沒有的,畢竟是官方的東西,可能已經考慮到了這些,所以比較完善。
複雜一點的滑動衝突,基本上就是這兩個衝突結合的結果。
滑動衝突解決思路
滑動衝突,就其本質來說,兩個不同方向(或者是同方向)的View,其中有一個是占主導地位的,每次總是搶着去處理外界的滑動行爲,這樣就導致一種很彆扭的用戶體驗,明明只是橫向的滑動了一下,縱向的列表卻在垂直方向發生了動作。就是說,這個占主導地位的View,每一次都身不由己的攔截了這個滑動的動作,因此,要解決滑動衝突,就是得明確告訴這個占主導地位的View,什麼時候你該攔截,什麼時候你不應該攔截,應該由下一層的View去處理這個滑動動作。這裏不明白的同學,可以去了解一下Android Touch事件的分發機制,這也是解決滑動衝突的核心知識。
滑動衝突
這裏,說一下背景情況。之前做下拉刷新、上拉加載更多時一直使用的是PullToRefreshView這個控件,因爲很方便,不用導入三方工程。在其內部可以放置ListView,GridView及ScrollView,非常方便,用起來可謂是屢試不爽。但是直到有一天,因項目需要,在ListView頂部加了一個輪播圖控件BannerView(這個可以參考之前寫的一篇學習筆記)。結果發現輪播圖滑動的時候,和縱向的下拉刷新組件衝突了。
如之前所說,解決滑動衝突的關鍵,就是明確告知接收到Touch的View,是否需要攔截此次事件。
解決方法
解決方案1,從外部攔截機制考慮
這裏,相當於是PullToRefreshView嵌套了ViewPager,那麼每次優先接收到Touch事件的必然是PullToRefreshView。這樣就清楚了,看代碼:
在PullToRefreshView中:
這裏最關鍵的代碼就是這行
橫向滑動距離大於縱向時,無須攔截這次滑動事件。其實,就是這麼簡單,但前提是你必須明確瞭解Android Touch事件的傳遞機制,期間各個方法執行的順序及意義。
解決方案2,從內容逆向思維分析
有時候,我們不想去修改引入的第三方控件,或者說是無法修改時。就必須考慮從當前從Touch傳遞事件中最後的那個View逆向考慮。首先,由Android中View的Touch事件傳遞機制,我們知道Touch事件,首先必然由最外層View攔截,如果無法更改這個最外層View,那麼是不是就沒轍了呢?其實不然,Android這麼高大上的系統必然考慮到了這個問題,好了廢話不說,先看代碼
首先說一下這個方法
API裏的意思很明確,子View如果不希望其父View攔截Touch事件時,可調用此方法。當disallowIntercept這個參數爲true時,父View將不攔截。
PS:這個方法的命名和其參數的使用邏輯,讓我想到了一句很有意思的話,敵人的敵人就是朋友,真不知道Google的大神們怎麼想的,非要搞一個反邏輯。
言歸正傳。這裏攔截直接也很明確,在carouselView的onTouch方法中每次進入就設定父View不攔截此次事件,然後在MOTION_MOVE時候,根據滑動的距離判斷再決定是父View是否有權利攔截Touch事件(即滑動行爲)。
總結
好了,本文內容到這基本就結束了,本篇文章只是提供一種解決方法的思路,在具體的場景下,交互往往是貼合具體業務需求的。但是不管怎麼樣,找出點擊事件截斷和處理的時機是最重要的,圍繞這個關鍵點,總能找出相應的解決方法。