主要總結了:
- View的基礎知識:
- View的mTop、mLeft、mRight、mBottom四個參數和對應的四個get()。
- View的getTanslationX() getTranslationY()、getX() getY()。
- MotionEvent的典型事件和getX()、getY()、getRawX()、getRawY()。
- TouchSlop最小滑動距離。
- Velocity Tracker滑動速度。
- GestureDetector和它的回調接口OnGestureListener、OnDoubleTapListener。
- View的滑動:
- scrollTo()、scrollBy()的使用和實現,mScrollX、mScrollY參數。
- View動畫和屬性動畫實現滑動。
- 改變參數布局實現滑動。
- View的彈性滑動:
- Scroller實現彈性動畫和原理。
- 利用動畫特性實現彈性動畫。
- 其他方法實現彈性動畫。
View的基礎知識
View的位置參數
mTop mLeft mRight mBottom
View的位置主要通過它的四個頂點來決定,對應View的四個屬性。
- mTop 左上角縱坐標
- mLeft 左上角橫坐標
- mRight 右下角橫坐標
- mBottom 右下角縱坐標
這四個參數指的是View的原始位置信息,平移并不會改變這四個參數的值。
看到View的源碼中,比如說mLeft,注釋中說mLeft是從父布局的左邊緣到這個View的左邊的像素。
/**
* The distance in pixels from the left edge of this view's parent
* to the left edge of this view.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "layout")
protected int mLeft;
這四個坐標是相對于這個View的父容器來說的,所以它是一種相對坐標。
View中提供了四個get()來獲得這四個參數,比如下面的getTop()。
/**
* Top position of this view relative to its parent.
*
* @return The top of this view, in pixels.
*/
@ViewDebug.CapturedViewProperty
public final int getTop() {
return mTop;
}
可以從上面的四個參數計算出View的寬高。
width = right - left;
height = bottom - top;
getTanslationX() getTranslationY()
Android3.0之后提供的兩個方法,getTranslationX()和getTranslationY(),它們不同于上面的四個參數,這兩個參數會由于 View的平移而變化,表示View左上角坐標相對于left、top(原始左上角坐標)的偏移量。
/**
* The horizontal location of this view relative to its {@link #getLeft() left} position.
* This position is post-layout, in addition to wherever the object's
* layout placed it.
*
* @return The horizontal position of this view relative to its left position, in pixels.
*/
@ViewDebug.ExportedProperty(category = "drawing")
public float getTranslationX() {
return mRenderNode.getTranslationX();
}
getX() getY()
Android3.0之后提供了getX()和getY()兩個方法。
/**
* The visual x position of this view, in pixels. This is equivalent to the
* {@link #setTranslationX(float) translationX} property plus the current
* {@link #getLeft() left} property.
*
* @return The visual x position of this view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "drawing")
public float getX() {
return mLeft + getTranslationX();
}
代碼是將mLeft加上translationX得到x的,可以看出來,x和y代表的就是當前View左上角相對于父布局的偏移量。
上面三組參數可以得到兩組等式。
x = left + translationX;
y = top + translationY
MotionEvent
手指接觸屏幕后產生的一系列事件中,典型的事件如下:
- ACTION_DOWN——手指剛接觸屏幕。
- ACTION_MOVE——在屏幕上移動。
- ACTION_DOWN——從屏幕上松開。
這些事件對應MotionEvent類中的幾個靜態常量。
public static final int ACTION_DOWN = 0;
public static final int ACTION_UP = 1;
public static final int ACTION_MOVE = 2;
正常情況下的一些列點擊事件:
- 點擊屏幕后立即松開,ACTION_DOWN->ACTION_UP
- 點擊屏幕滑動后再松開,ACTION_DOWN->ACTION_MOVE->......->ACTION_MOVE->ACTION_UP
可以通過MotionEvent對象調用getX()、getY()、getRawX()、getRawY()獲取觸碰點的位置參數。
- getX()、getY() 相對于當前View左上角的x、y值。
- getRawX()、getRawY() 相對于手機屏幕左上角的x、y值。
這四個方法都是去調用native方法。
public final float getRawX() {
return nativeGetRawAxisValue(mNativePtr, AXIS_X, 0, HISTORY_CURRENT);
}
@FastNative
private static native float nativeGetRawAxisValue(long nativePtr,
int axis, int pointerIndex, int historyPos);
TouchSlop
TouchSlop是系統能識別的最小滑動距離,如果小于這個值,則不認為是滑動。這是一個常量和設備有關,可以通過以下方式獲得。
ViewConfiguration.get(getContext()).getScaledTouchSlop();
public int getScaledTouchSlop() {
return mTouchSlop;
}
這個mTouchSlop在ViewConfiguration的無參構造器中用一個常量賦了初始值為8。
private static final int TOUCH_SLOP = 8;
@Deprecated
public ViewConfiguration() {
//...
mTouchSlop = TOUCH_SLOP;
//...
}
有參構造器中初始化為資源文件的一個值,這個值也是8。
<!-- Base "touch slop" value used by ViewConfiguration as a
movement threshold where scrolling should begin. -->
<dimen name="config_viewConfigurationTouchSlop">8dp</dimen>
private ViewConfiguration(Context context) {
//...
mTouchSlop = res.getDimensionPixelSize(com.android.internal.R.dimen.config_viewConfigurationTouchSlop);
//...
}
在處理滑動的時候可以使用這個值來做一些過濾,過濾掉滑動距離小于這個值,會有更好的用戶體驗。
Velocity Tracker
用來獲取手指滑動過程中的速度,包括水平速度和垂直速度。
用法
在onTouchEvent()中追蹤當前單擊事件的速度。
- 首先獲得一個VelocityTracker對象,再將當前時間加入進去。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
- 計算自定義時間內的速度,再調用get獲得定義時間內劃過的像素點。
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
- 計算真正的速度。
int xV = xVelocity / 1;//這里的1是上面計算時間時定義的時間間隔1000ms
int yV = yVelocity / 1;
- 回收資源。
velocityTracker.clear();
velocityTracker.recycle();
注意
- 獲取速度之前必須要調用computeCurrentVelocity()計算速度。
- getXVelocity()\getYVelocity()獲取到的是計算單位時間內滑過的像素值,并不是速度。
GestureDetector
GestureDetector用于檢測用戶的單擊、滑動、長按、雙擊等行為。
GestureDetector內部有兩個監聽接口,OnGestureListener和OnDoubleTapListener,里面的方法可以根據需求去實現。
public interface OnGestureListener {
boolean onDown(MotionEvent e);//手指輕輕觸摸屏幕的一瞬間,一個ACTION_DOWN觸發
void onShowPress(MotionEvent e);//手指輕觸屏幕,沒有松開或挪動
boolean onSingleTapUp(MotionEvent e);//輕觸后松開,單擊行為,伴隨一個ACTION_UP觸發
boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY);//拖動行為,由一個ACTION_DOWN和一系列ACTION_MOVE觸發
void onLongPress(MotionEvent e);//長按
boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY);//按下快速滑動后松開,一個ACTION_DOWN、多個ACTION_MOVE和一個ACTION_UP觸發
}
public interface OnDoubleTapListener {
boolean onSingleTapConfirmed(MotionEvent e);//嚴格的單擊行為,不能是雙擊中的其中一次單擊,onSingleTapUp可以是雙擊中的其中一次。
boolean onDoubleTap(MotionEvent e);//雙擊,兩次單擊,不可能和onSingleTapConfirmed共存
boolean onDoubleTapEvent(MotionEvent e);//雙擊行為,雙擊期間ACTION_DOWN ACTION_MOVE ACTION_UP都會觸發此回調。
}
使用
創建一個GestureDetector,根據需要實現接口并傳入GestureDetector。
gestureDetector = new GestureDetector(context, gestureListener);
gestureDetector.setOnDoubleTapListener(doubleTapListener);
gestureDetector.setIsLongpressEnabled(false);//解決長按屏幕后無法拖動的現象
接管View的onTouchEvent(),GestureDetector的onTouchEvent()中會根據event來回調上面說的兩個接口方法。
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean consume = gestureDetector.onTouchEvent(event);
return consume;
}
注意
并不是必須要用GestureDetector來實現所需的監聽,完全也可以直接在View的onTouchEvent()中做判斷并實現需求。所以,如果只需要監聽簡單的單擊事件就可以直接使用View的onTouchEvent(),如果需要監聽復雜一點的一系列事件,就可以使用GestureDetector。
View的滑動
scrollTo()/scrollBy()
scrollTo和scrollBy可以改變View內容的位置,舉例來說就是如果對ViewGroup調用scrollTo只會改變其子View的位置,如果對View,比如TextView調用,那么只會改變這個TextView文字的位置。
1. 使用
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bt.scrollTo(100, 200);
tv.scrollBy(-5, -5);
}
});
直接使用View對象去調用兩個方法,傳入位移像素值就可以了。scrollTo()是內容的絕對移動,scrollBy()是內容的相對移動。
但是需要注意的是,這兩個方法在onCreate()中調用,可能不會成功,原因應該是因為那時View還沒有完全加載完畢,所以調用會不起作用。
2. scrollTo的實現
/**
* 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) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
這里有兩個量,mScrollX和mScrollY:
/**
* The offset, in pixels, by which the content of this view is scrolled
* horizontally.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "scrolling")
protected int mScrollX;
mScrollX表示View內容和View本身的橫向偏移量,mScrollY就是縱向偏移的像素值了。
- scorllTo()首先比較內容偏移量和傳入的x y是否相等,都不相等再操作。
- 它記錄了原始的兩個偏移量,之后將傳入的x y賦值給mScrollX和mScrollY。
- 接著調用了invalidateParentCaches(),方法注釋意思是當啟動了硬件加速時去通知此View的父容器清除緩存。
- 調用了onScrollChanged(mScrollX, mScrollY, oldX, oldY),這個方法內部會判斷我們是否有設置OnScrollChangeListener,如果有就調用它的回調方法。
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//......
if (mListenerInfo != null && mListenerInfo.mOnScrollChangeListener != null) {
mListenerInfo.mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt);
}
}
- awakenScrollBars()喚醒scrollbar去重新繪制,如果失敗返回false,就直接調用postInvalidateOnAnimation()重新繪制。所以不管怎么樣最終都會調用到postInvalidateOnAnimation()。
public void postInvalidateOnAnimation() {
// We try only with the AttachInfo because there's no point in invalidating
// if we are not attached to our window
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
}
}
判斷與Window的連接是否空,不空就調用ViewRootImpl的dispatchInvalidateOnAnimation()。
public void dispatchInvalidateOnAnimation(View view) {
mInvalidateOnAnimationRunnable.addView(view);
}
將這個View加入到了InvalidateOnAnimationRunnable這個Runnable中的集合中,在這個Runnable的run()中,遍歷了集合中的每個View,調用View的invalidate()后釋放。invalidate()就是去在UI線程中重繪View的,最后View就在新的位置顯示了。
@Override
public void run() {
//......
for (int i = 0; i < viewCount; i++) {
mTempViews[i].invalidate();
mTempViews[i] = null;
}
//......
}
總結一下,簡單來說邏輯就是改變mScrollX和mScrollY的值,之后刷新UI,顯示在新位置。
3. scrollBy()的實現
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
scrollBy()就是調用了scrollTo,只不過參數加上了當前已有的偏移量。所以可以猜到scrollBy()是相對于當前偏移的基礎上相對移動x y的像素值,而scrollTo()是相對于View的原始位置絕對移動。
4. mScrollX 和 mScrollY的正負
如下圖所示,白色框是View自身的位置,灰色是View的內容移動后的位置,那么假設偏移量都為100,mScrollX的值就是100,mScrollY的值是100,單位是像素,都是正值。
下面View的內容移動到了右下角,此時mScrollX和mScrollY的值就是負的了。
動畫
使用動畫來移動View,可以使用View動畫,也可以使用屬性動畫(3.0版本以下需要使用nineoldandroid)。
1. 使用View動畫
首先可以在xml中定義一個動畫集合。
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true"
android:zAdjustment="normal">
<translate
android:duration="100"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="100"
android:toYDelta="100"
android:interpolator="@android:anim/linear_interpolator"/>
</set>
這個動畫會讓View從原始位置向右下方平移100個像素。
再對View對象開始動畫,傳入加載進來的上面寫的動畫。
tv.startAnimation(AnimationUtils.loadAnimation(MainActivity.this, R.anim.anim_view_event));
2. 使用屬性動畫
使用ObjectAnimator類去設置動畫。
ObjectAnimator.ofFloat(tv, "translationX", 0, 10).setDuration(100).start();
3. 注意
- 使用View動畫其實并不是改變View的真正位置,而是移動View的影像,不會改變View的真實位置參數。
這就會導致一個問題,如果View有點擊事件,新位置并不能觸發點擊事件,而是原位置仍能觸發,盡管View看起來已經不在原先的位置上了。
- 屬性動畫改變View本身屬性只能兼容到Android3.0,所以如果需要兼容更低的版本,就必須要使用開源動畫庫nineoldandroid。
改變布局參數
使用
MarginLayoutParams params = (MarginLayoutParams) tv.getLayoutParams();
params.leftMargin += 100;
tv.requestLayout();
//tv.setLayoutParams(params); 也可以使用這個重新設置參數
改變布局參數的方法可以通過更改margin來改變View的位置達到移動的效果,這種方法需要根據實際去做不同的處理。
滑動對比
滑動方式 | 優點 | 缺點 |
---|---|---|
scrollTo() / scrollBy() | 簡單易使用,不影響點擊事件 | 只能移動View的內容,不能移動View本身 |
View動畫 | 能夠實現復雜的效果 | 只能改變View的影像,會影響View的點擊事件 |
屬性動畫 | 3.0以上移動View本身,能夠實現復雜的效果 | 3.0以下不能改變View本身屬性,需要nineoldandroid來兼容 |
改變參數 | 不會影響點擊事件,改變的是View自身的屬性 | 使用稍麻煩,需要根據需求來靈活應用 |
再總結一下適用場景:
- scrollTo() / scrollBy(): 操作簡單,適合對于View的內容的移動。
- 動畫:操作簡單,主要適用于對沒有交互的移動和復雜的動畫效果。
- 改變參數:操作稍微復雜,適用于有交互的移動。
彈性滑動
前面的方法其實只能叫做移動,并不能叫滑動。彈性滑動有一個共同的思想,在一段時間內將一次大的滑動分成若干次小的滑動來完成。
Scoller
Scroller本身無法實現彈性滑動,需要和View的computeScroll()配合使用。在最后通過分析可以發現也是通過scrollTo()實現滑動的,所以它也是View內容的滑動,而不是View本身的滑動。
使用
自定義一個TextView,實現TextView的文字向手指點擊的地方彈性滑動。
public class MyTextView extends TextView {
private Scroller mScroller;
private int xDown;
private int yDown;
//...
public MyTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);//初始化Scroller對象
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN://記錄點擊的相對坐標
xDown = (int) event.getX();
yDown = (int) event.getY();
break;
case MotionEvent.ACTION_UP:
smoothScroll(-xDown, -yDown);//調用自定義的彈性滑動
}
return true;
}
//自定義的彈性滑動方法
public void smoothScroll(int destX, int destY) {
//畫的初始滑動偏移
int scrollX = getScrollX();
int scrollY = getScrollY();
//計算需要滑動的兩個方向的大小
int deltaX = -destX - scrollX;
int deltaY = -destY - scrollY;
調用Scroller對象的startScroll()
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
invalidate();//重繪
}
//固定的重寫compuuteScroll
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
}
- 初始化Scroller對象。
- 實現computeScroll()。
- 自定義彈性滑動的方法,內部調用Scroller對象的startScroll()、invalidate()。
- 就可以調用自定義的彈性滑動方法進行彈性滑動了。
實現
1. startScroll()
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 = 1.0f / (float) mDuration;
}
startScroll()只是進行了一些計算和參數的記錄,并沒有進行真正的滑動工作。四個參數分別是其實位置的x、y坐標,x、y方向的滑動距離,滑動的時間間隔。
2. invalidate()
invalidate()會導致View的重繪調用View的draw(),View的draw()中又會去調用computeScroll(),computeScroll()在View中是一個空實現,所以需要我們自己去實現。
3. computeScroll()
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
如果想實現彈性滑動這樣的需求,其實computeScroll()的實現和上面寫成一樣就可以了,不需要做其他的改動。發現在這個方法里,還是調用了scrollTo(),所以Scroller彈性滑動也是用scrollTo()實現的。
就能猜到computeScrollOffset()是用來計算CurrX和CurY的,也就是最初提到的將一個大滑動拆分成小滑動,computeScrollOffset()就是去計算每一次小滑動的坐標的。
最后調用postInvalidate()進行下一次重繪,重復之前的操作。
4. computeScrollOffset()
最后再來單獨看一下computeScrollOffset()的實現。
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
//...
}
} else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
- 它首先判斷是否完成,如果已經完成就直接返回false。
- 如果還沒完成,計算過去的時間,如果還有剩余,就根據時間百分比計算下一個滑動位置,返回true。
- 如果已經超過時間,就賦值下一個滑動位置為目標位置,并將mFinished變成true,返回true。
- 在調用computeScrollOffset()的地方,如果computeScrollOffset()返回了true就進行scrollTo()并重新繪制。
動畫屬性
除了利用Scroller的computeScrollOffset()來分成小份計算位移,還可以利用動畫屬性。前面介紹的View動畫和屬性動畫都屬于彈性動畫。除了直接使用動畫,還可以利用動畫的特性。
使用
final int startX = 0;
final int deltaX = -100;
final ValueAnimator animator = ValueAnimator.ofInt(0, 1).setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
tv.scrollTo(startX + (int)(deltaX * fraction), 0);
}
});
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animator.start();
}
});
利用動畫的回調,實現像Scroller類似的,在動畫改變的時候通過onAnimationUpdate()監聽,獲得百分比,調用scrollTo()滑動一小步,也是View內容的滑動。
延時策略
通過發送延時消息從而達到漸近式的效果。可以使用Handler、View的postDelayed()、Thread的sleep()。具體的思路其實和上面是一樣的,只不過這里需要自己去實現延時,而上面的方法已經內部實現,只需要計算小段位移后進行小段滑動就可以了。