
【本文出自大圣代的技術(shù)專欄 http://blog.csdn.net/qq_23191031】
【禁止任何商業(yè)活動。轉(zhuǎn)載煩請注明出處】
學(xué)前準(zhǔn)備
詳解Android控件體系與常用坐標(biāo)系
Android常用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker
前言
在前面的幾篇文章,我向大家介紹的都是單一View事件,從這篇文章開始,我將向大家介紹連續(xù)的事件 —— 滑動?;瑒邮且苿佣嗽O(shè)備提供的重要功能,正是由于強(qiáng)大的滑動事件讓我們小巧的屏幕可以展現(xiàn)無限的數(shù)據(jù)。而滑動事件沖突卻常常困擾著廣大開發(fā)者。孫子云:知己知彼,百戰(zhàn)不殆。想更好的協(xié)調(diào)滑動事件,不知道其中原理的確困難重重。當(dāng)你學(xué)習(xí)本篇文章之后你會發(fā)現(xiàn)其實(shí)Scroll很簡單,你只是被各種文章與圖書弄糊涂了。
在真正講解之前,我們需要掌握Android坐標(biāo)系與觸控事件相關(guān)知識,對此不太明確的同學(xué)請參見上文的 學(xué)前準(zhǔn)備
View滑動產(chǎn)生的原理
從原理上講View滑動的本質(zhì)就是隨著手指的運(yùn)動不斷地改變坐標(biāo)。當(dāng)觸摸事件傳到View時,系統(tǒng)記下觸摸點(diǎn)的坐標(biāo),手指移動時系統(tǒng)記下移動后的觸摸的坐標(biāo)并算出偏移量,并通過偏移量來修改View的坐標(biāo),不斷的重復(fù)這樣的過程,從而實(shí)現(xiàn)滑動過程。
1 scrollTo 與 scrollBy
說到Scroll就不得不提到scrollTo()與scrollBy()這兩個方法。
1.1 scrollTo
首先我們要知道Android每一個控件都有滾動條,只不過系統(tǒng)對我們隱藏了,所以我們看不見。
對于控件來說它的大小是有限的,(例如我們指定了大小、屏幕尺寸的束縛等),系統(tǒng)在繪制圖像的時候只會在這個有限的控件內(nèi)繪制,但是內(nèi)容(content)的載體Canvas在本質(zhì)上是無限的,例如我們的開篇圖片,控件仿佛就是一個窗口我們只能通過它看到這塊畫布。
/**
* 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) {//滾動到目標(biāo)位置
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX; // 已經(jīng)滾動到的X
int oldY = mScrollY; //已經(jīng)滾動到的Y
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);//回調(diào)方法,通知狀態(tài)改變
if (!awakenScrollBars()) {
postInvalidateOnAnimation(); //重新繪制
}
}
}
通過注釋Set the scrolled position of your view我們可以清楚的得知 scrollTo(x,y)的作用就是將View滾動到(x,y)這個點(diǎn),注意是滾動(scroll本意滾動,滑動是translate)。
在初始時 mScrollX 與mScrollY均為0,表示著View中展示的是從畫布左上角開始的內(nèi)容(如圖 1),當(dāng)調(diào)用scrollTo(100,100)時相當(dāng)于將View的坐標(biāo)原點(diǎn)滾動到(100,100)這個位置,展示畫布上從(100,100)開始的內(nèi)容(如圖2),但是事實(shí)上View是靜止不動的,所以最終的效果是View的內(nèi)容平移了(-100,-100)的偏移量(如圖3)

1.2 scrollBy
/**
* 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
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
學(xué)習(xí)scrollTo在學(xué)習(xí)scrollBy就簡單了,通過源碼可以看到它里面調(diào)用了ScrollTo(),傳入的參數(shù)是mScrollX+x,也就是說這次x是一個增量,所以scrollBy實(shí)現(xiàn)的效果就是,在當(dāng)前位置上,再偏移x距離
這是ScrollTo()和ScrollBy()的重要區(qū)別。
1.3 小結(jié):
- scrollTo與scrollBy都會另View立即重繪,所以移動是瞬間發(fā)生的
- scrollTo(x,y):指哪打哪,效果為View的左上角滾動到(x,y)位置,但由于View相對與父View是靜止的所以最終轉(zhuǎn)換為相對的View的內(nèi)容滑動到(-x,-y)的位置。
- scrollBy(x,y): 此時的x,y為偏移量,既在原有的基礎(chǔ)上再次滾動
- scrollTo與scrollBy的最用效果會作用到View的內(nèi)容,所以要是想滑動當(dāng)前View,就需要對其父View調(diào)用二者。也可以在當(dāng)前View中使用
((View)getParent).scrollXX(x,y)達(dá)到同樣目的。
2 Scroller
OK,通過上面的學(xué)習(xí)我們知道scrollTo與scrollBy可以實(shí)現(xiàn)滑動的效果,但是滑動的效果都是瞬間完成的,在事件執(zhí)行的時候平移就已經(jīng)完成了,這樣的效果會讓人感覺突兀,Google建議使用自然過渡的動畫來實(shí)現(xiàn)移動效果。因此,Scroller類這樣應(yīng)運(yùn)而生了。
2.1 簡單實(shí)例
舉一個簡單的實(shí)例方便大家的理解與學(xué)習(xí) Scroller
主要代碼
public class CustomScrollerView extends LinearLayout {
private Scroller mScroller;
private View mLeftView;
private View mRightView;
private float mInitX, mInitY;
private float mOffsetX, mOffsetY;
public CustomScrollerView(Context context) {
this(context, null);
}
public CustomScrollerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
this.setOrientation(LinearLayout.HORIZONTAL);
mScroller = new Scroller(getContext(), null, true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 2) {
throw new RuntimeException("Only need two child view! Please check you xml file!");
}
mLeftView = getChildAt(0);
mRightView = getChildAt(1);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mInitX = ev.getX();
mInitY = ev.getY();
super.dispatchTouchEvent(ev);
return true;
case MotionEvent.ACTION_MOVE:
//>0為手勢向右下
mOffsetX = ev.getX() - mInitX;
mOffsetY = ev.getY() - mInitY;
//橫向手勢跟隨移動
if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
int offset = (int) -mOffsetX;
if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
return true;
}
this.scrollBy(offset, 0);
mInitX = ev.getX();
mInitY = ev.getY();
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//松手時刻滑動
int offset = ((getScrollX() / (float) mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
// this.scrollTo(offset, 0);
mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset - this.getScrollX(), 0);
invalidate();
mInitX = 0;
mInitY = 0;
mOffsetX = 0;
mOffsetY = 0;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡化我們在非UI線程更新view的步驟
}
}
}
主要布局
<com.im_dsd.blogdemo.CustomScrollerView
android:layout_width="200sp"
android:layout_height="200sp"
android:layout_centerInParent="true"
android:orientation="horizontal"
>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_light"/>
<TextView
android:layout_width="100sp"
android:layout_height="match_parent"
android:background="@android:color/holo_green_light"/>
</com.im_dsd.blogdemo.CustomScrollerView>

通過上面實(shí)例我們可以發(fā)現(xiàn)在自定義View的過程中使用Scroller的流程如下圖所示:

下面我們就按照這個流程進(jìn)行源碼分析吧
2.2 源碼分析
對于Scroller類 Google給出的如下解釋:
This class encapsulates scrolling. You can use scrollers ( Scroller or OverScroller) to collect the data you need to produce a scrolling animation
for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.
我們中可以看出:Scroller 是一個工具類,它只是產(chǎn)生一些坐標(biāo)數(shù)據(jù),而真正讓View平滑的滾動起來還需要我們自行處理。我們使用的處理工具就是—— scrollTo與scrollBy
2.2.1 構(gòu)造方法分析
public Scroller(Context context) {
this(context, null);
}
public Scroller(Context context, Interpolator interpolator) {
this(context, interpolator,
context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
mFinished = true;
if (interpolator == null) {
mInterpolator = new ViscousFluidInterpolator();
} else {
mInterpolator = interpolator;
}
mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
//摩擦力計算單位時間減速度
mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
mFlywheel = flywheel;
mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
}
Scroller的構(gòu)造方法沒啥特殊的地方只不過第二個參數(shù)interpolator是插值器,不同的插值器實(shí)現(xiàn)不同的動畫算法(這里不是重點(diǎn)不做展開,以后重點(diǎn)講解),如果我們不傳,則默認(rèn)使用ViscousFluidInterpolator()插值器。
2.2.2 startScroll與fling
/**
* 使用默認(rèn)滑動時間完成滑動
*/
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
/**
* 在我們想要滾動的地方調(diào)運(yùn),準(zhǔn)備開始滾動,手動設(shè)置滾動時間
*
* @param startX 滑動起始X坐標(biāo)
* @param startY 滑動起始Y坐標(biāo)
* @param dx X方向滑動距離
* @param dy Y方向滑動距離
* @param duration 完成滑動所需的時間
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();//獲取當(dāng)前時間作為滑動的起始時間
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
/**
* 開始基于滑動手勢的滑動。根據(jù)初始的滑動手勢速度,決定滑動的距離(滑動的距離,不能大于設(shè)定的最大值,不能小于設(shè)定的最小值)
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
......
mMode = FLING_MODE;
mFinished = false;
......
mStartX = startX;
mStartY = startY;
......
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
......
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
在這兩個方法中,都是一些全局變量的賦值,果真沒有實(shí)現(xiàn)滾動的方法,也佐證了Scroller是一個工具的解讀。而要實(shí)現(xiàn)滑動還是要依靠我們手動調(diào)用View的invalidated()方法觸發(fā)computeScroll()方法。
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡化我們在非UI線程更新view的步驟
}
}
一旦觸發(fā)成功就會調(diào)用Scroller.computeScrollOffset()方法,返回結(jié)果如果為true表示當(dāng)前的滑動尚未結(jié)束,如果返回false表示滑動完成。
在Scroller類中,最最重要的就是這個computeScrollOffset方法,看上去只是返回了一個boolean類型,但他卻是Scroller的核心,所有的坐標(biāo)與滑動時間都由它計算完成。他將原本瞬間的滑動拆分成連續(xù)平滑的過程。
/**
* 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.
* 調(diào)用這個函數(shù)獲得新的位置坐標(biāo)(滑動過程中)。如果它返回true,說明滑動沒有結(jié)束。
* getCurX(),getCurY()方法就可以獲得計算后的值。
*/
public boolean computeScrollOffset() {
if (mFinished) {//是否結(jié)束
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//滑動開始,經(jīng)過了多長時間
if (timePassed < mDuration) {//如果經(jīng)過的時間小于動畫完成所需時間
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)//如果沒有設(shè)置插值器,利用默認(rèn)算法
x = viscousFluid(x);
else//否則利用插值器定義的算法
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);//計算當(dāng)前X坐標(biāo)
mCurrY = mStartY + Math.round(x * mDeltaY);//計算當(dāng)前Y坐標(biāo)
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE[index];
final float d_sup = SPLINE[index + 1];
final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
從代碼可以看到,如果我們沒有設(shè)置插值器,就會調(diào)用內(nèi)部默認(rèn)算法。
/**
* 函數(shù)翻譯是粘性流體
* 估計是一種算法
*/
static float viscousFluid(float x)
{
x *= sViscousFluidScale;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
x *= sViscousFluidNormalize;
return x;
}
接著是兩個重要的get方法
/**
* Returns the current X offset in the scroll.
*
* @return The new X offset as an absolute distance from the origin.
* 獲得當(dāng)前X方向偏移
*/
public final int getCurrX() {
return mCurrX;
}
/**
* Returns the current Y offset in the scroll.
*
* @return The new Y offset as an absolute distance from the origin.
* 獲得當(dāng)前Y方向偏移
*/
public final int getCurrY() {
return mCurrY;
}
2.2.3 其他方法
public class Scroller {
......
public Scroller(Context context) {}
public Scroller(Context context, Interpolator interpolator) {}
public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
//設(shè)置滾動持續(xù)時間
public final void setFriction(float friction) {}
//返回滾動是否結(jié)束
public final boolean isFinished() {}
//強(qiáng)制終止?jié)L動
public final void forceFinished(boolean finished) {}
//返回滾動持續(xù)時間
public final int getDuration() {}
//返回當(dāng)前滾動的偏移量
public final int getCurrX() {}
public final int getCurrY() {}
//返回當(dāng)前的速度
public float getCurrVelocity() {}
//返回滾動起始點(diǎn)偏移量
public final int getStartX() {}
public final int getStartY() {}
//返回滾動結(jié)束偏移量
public final int getFinalX() {}
public final int getFinalY() {}
//實(shí)時調(diào)用該方法獲取坐標(biāo)及判斷滑動是否結(jié)束,返回true動畫沒結(jié)束
public boolean computeScrollOffset() {}
//滑動到指定位置
public void startScroll(int startX, int startY, int dx, int dy) {}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
//快速滑動松開手勢慣性滑動
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {}
//終止動畫,滾到最終的x、y位置
public void abortAnimation() {}
//延長滾動的時間
public void extendDuration(int extend) {}
//返回滾動開始經(jīng)過的時間
public int timePassed() {}
//設(shè)置終止時偏移量
public void setFinalX(int newX) {}
public void setFinalY(int newY) {}
}
3 總結(jié):
- 滑動的本質(zhì)就是View隨著手指的運(yùn)動不斷地改變坐標(biāo)
- scrollTo(x,y)指的就是
View滾動到(x,y)這個位置,但是View 要相當(dāng)于父控件靜止不懂,所以相對的View的內(nèi)容就會滑動到(-x, -y)的位置 - scrollTo、scrollBy移動是瞬間的
- 滑動效果作用的對象是View內(nèi)容
- Scroller類其實(shí)是一個工具類,生產(chǎn)滑動過程的平滑坐標(biāo),但最終的滑動動作還是需要我們自行處理
- Scroller類的使用流程:

參考
《Android群英傳》
http://blog.csdn.net/crazy__chen/article/details/45896961
http://blog.csdn.net/yanbober/article/details/49904715
版權(quán)聲明:
禁止一切商業(yè)行為,轉(zhuǎn)載請著名出處 http://blog.csdn.net/qq_23191031。作者: 大圣代
Copyright (c) 2017 代圣達(dá). All rights reserved.