Android中View滑動方式的選擇
View的滑動是我們開發中需要的一項基本技能,當然,Android在這方面做的還是比較出色, 提供了多種實現方式。
- 重寫View的onTouchEvent或設置View的setOnTouchListener(),在MotionEvent.MotionEvent.ACTION_MOVE中做相應的滑動處理;
- 采用動畫的方式(View動畫或屬性動畫)實現;
不論哪種方式,其實質都是可以調用scrollTo/scrollBy,或者setLayoutParams()或layout()來實現的。
下面我們對這幾種情況進行詳細說明:
由于onTouchEvent和setOnTouchListener()可實現相同的功能,但setOnTouchListener()使用起來更簡單,因為不需要自定義View, 同時setOnTouchListener中的onTouch比onTouchEvent優先執行,從View的dispatchTouchEvent()就可看出:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
// 沒有設置mOnTouchListener的時候再執行onToucheEvent()
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
那么下面就以setOnToucheListener()為例,來實現第一種滑動方案:
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
startX = motionEvent.getX();
startY = motionEvent.getY();
break;
case MotionEvent.ACTION_MOVE:
float distanceX = motionEvent.getX() - startX;
float distanceY = motionEvent.getY() - startY;
// 處理滑動事件,其滑動距離為distance
startX = motionEvent.getX();
startY = moitonEvent.getY();
break;
default:
break;
}
return true;
}
});
上面的代碼是實現滑動的一個基本框架,其滑動方式的差異就存在于處理滑動事件的部分。下面我們通過具體的方式來實現:
scrollTo/scrollBy方式
在使用這種方式的時候需要注意以下問題:
-
它引起的移動僅僅是內容的移動,View本身是不移動的。例如,對于TextView,使用這種方式滑動時,移動的只是文字,而TextView本身不會移動;而對于ViewGroup, 移動的當然也就是childView了。
-
scrollTo表示移動至指定位置,scrollBy表示移動指定偏移量。這兩個函數中的參數需要特別注意一下,參數>0表示沿著坐標軸反方向移動,反之向坐標軸正方向移動,與我們理解的坐標總是反的。
-
由于移動的是View的內容而不是View本身,所以移動之后View的事件觸發區域還停留在移動之前的位置,也就是說,移動之后,View的內容所在的位置可能會出現無法響應事件的情況。
了解了上面的基本知識,下面我們來通過這種方式來完善我們的代碼,下面我們只展示處理滑動事件的代碼:
scrollBy(distanceX, distanceY);
/* 當然也可以通過scrollTo來實現
int scrollX = view.getScrollX();
int scrollY = view.getScrollY();
scrollTo(scrollX + distanceX, scrollY + distanceY);
*/
毫無疑問,scrollBy比scrollTo實現起來要簡單,畢竟scrollBy也是通過scrollTo來實現的,源碼如下:
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
這種方式實現滑動很簡單,但局限性也很大,因為它只能移動View的內容,而不能移動View本身,所以經常使用在ViewGroup的滑動處理中。
LayoutParams方式
在開發過程中,我們經常遇到需要動態修改View位置或大小的情況,一般我們都是通過獲取其LayoutParams,然后設置相應的屬性,最后調用setLayoutParams()來完成。那在滑動處理過程中,也可以使用這種方式,動態地修改其LayoutParams的屬性來實現滑動。具體代碼如下:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
params.leftMargin += distanceX;
params.topMargin += distanceY;
view.setLayoutParams(params);
與scrollBy/scrollTo不同的是,這種方式移動的是View本身,而不是其內容。同時,其中的distanceX/distanceY大于0表示向坐標軸正方向移動,反之表示向坐標軸反方向移動。
調用View的layout()方法
View的layout()方法用來動態設置View的邊距,通過重復調用這個方法,也可以達到滑動的方式。具體代碼如下:
view.layout((int)(view.getLeft() + distanceX), (int)(view.getTop() + distanceY, 0, 0));
view.requestLayout(); // 注意調用layout后需要調用requestLayout刷新布局
與LayoutParams方式一樣,這種方式移動的也是View本身。從本質上來講,這兩種方式沒什么區別,都是通過影響Layout過程來實現的。
從onLayout()的源碼中我們可以看出, 影響View位置的幾個因素:
- 對齊方式;
- 邊距,如left, top, right, bottom;
- params參數的margin屬性;
- parentView的padding屬性;
理論上來將,修改這幾種因素都可引起View位置的變化,但是對齊方式局限性太大,而且各ViewGroup還存在差異,顯然不可取。修改邊距其實就是layout()方式,params參數當然也就是LayoutParams方式。而parentView的padding屬性影響所有的childView, 所以不能通過這種方式來修改某個View的位置。
以上方式都是在涉及滑動手勢的情況下的解決方案,其實在日常開發中,我們還會遇到其他需要View移動的場景,如點擊引起View的移動。在這種情況下,我們就可以考慮使用動畫方式來解決了。
View動畫方式
Android提供了縮放,漸變,平移和旋轉四種View動畫,這種動畫的特點是可重復播放,但動畫結束后View將恢復位置。
屬性動畫方式
屬性動畫可認為是View動畫的擴充,因為它可以通過漸變的方式來修改View的屬性。這樣就可解決scrollBy/scrollTo/LayoutParmas/layout()這幾種方式單次移動引起的動畫的生硬感。例如:將一個view向右移動200px,這里我們選擇layoutParams的方式,那么偽代碼應該如下所示:
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
params.leftMargin += 200;
view.setLayoutParams(leftMargin);
如果這種改變是在點擊事件下觸發的,那么就會發現view瞬間向右移動了200px, 這種感覺很生硬,那如何解決了,這里屬性動畫就派上用場了。修改代碼如下:
ValueAnimator animator = ValueAnimator.ofInt(200);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (int) valueAnimator.getAnimatedValue();
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams)view.getLayoutParams();
params.leftMargin += (value - lastValue);
view.setLayoutParams(leftMargin);
lastValue = value;
}
});
屬性動畫將根據動畫的執行時間和插值器將200px分成若干份,這樣通過多次setLayoutParams的方式就實現了平滑移動的效果。
注意:本文的主線是分析View的滑動方案的選擇,所以不對View動畫和屬性動畫做更詳細的介紹,對于這部分,更詳細的內容可自行了解。
總結:
通過對Android提供的幾種移動方案的分析,我們可以總結出以下結論:
- 如果需要跟隨手勢移動,移動的如果是View的內容(針對ViewGroup是childView), 那么選擇scrollBy/scrollTo來處理;如果移動的是View本身,那么選擇LayoutParams或者layout()的方式來實現;
- 如果是單次移動,且移動完成后恢復位置或重復性的移動效果,使用View動畫或動畫集合實現;
- 如果是點擊,長按,雙擊等事件引起的View移動,使用屬性動畫是一個不錯的選擇;
以上僅是個人使用過程中的一些簡單總結,如有不恰當之處,歡迎指正~
來自:http://juhonggang.github.io/2017/01/07/Android中View滑動方式的選擇/