在Android應(yīng)用當(dāng)中陨溅,滑動(dòng)基本是每個(gè)應(yīng)用的標(biāo)配针余。對(duì)于滑動(dòng),我們主要可以通過(guò)Scroller谬以、屬性動(dòng)畫(huà)或者動(dòng)態(tài)改變view的LayoutParams來(lái)實(shí)現(xiàn)强饮。今天就主要來(lái)針對(duì)Scroller的使用進(jìn)行一番分析,并通過(guò)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的側(cè)滑菜單为黎,將Scroller應(yīng)用到我們的開(kāi)發(fā)當(dāng)中邮丰。
預(yù)備知識(shí):坐標(biāo)系
在分析Scroller的使用之前,我們需要明確一些關(guān)于坐標(biāo)系的問(wèn)題铭乾,這里主要針對(duì)X軸來(lái)進(jìn)行分析剪廉,Y軸同理。首先炕檩,我們來(lái)看谷歌官方文檔對(duì)于getScrollX()的解釋?zhuān)?/p>
簡(jiǎn)單來(lái)說(shuō)斗蒋,這個(gè)方法返回的是view左邊緣的X軸減去該view的內(nèi)容左邊緣X軸的值。那到底什么是對(duì)view內(nèi)容進(jìn)行滑動(dòng)呢笛质?下面我們先來(lái)看兩個(gè)方法泉沾,再舉例來(lái)說(shuō)明:
這兩個(gè)方法都是使view內(nèi)容進(jìn)行滑動(dòng)的,但是用法的區(qū)別在于经瓷,scrollBy是基于view內(nèi)容當(dāng)前位置進(jìn)行增量式的相對(duì)滑動(dòng)爆哑,而scrollTo則是相對(duì)于view的絕對(duì)坐標(biāo)進(jìn)行滑動(dòng)。說(shuō)了這么多舆吮,到底什么是對(duì)內(nèi)容進(jìn)行滑動(dòng)以及相對(duì)滑動(dòng)、絕對(duì)滑動(dòng)队贱,大家可能都比較模糊色冀,下面那我就通過(guò)舉例來(lái)進(jìn)行說(shuō)明。
首先柱嫌,我們來(lái)看下布局以及核心代碼:
<RelativeLayout
android:id="@+id/main_area"
android:layout_width="300dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="#fff000">
<TextView
android:id="@+id/main_text"
android:layout_width="150dp"
android:layout_height="match_parent"
android:background="#ffffff"
android:gravity="center"
android:text="left"
android:textColor="#000000"
android:textSize="32sp" />
<TextView
android:layout_width="150dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="#f0f"
android:gravity="center"
android:text="right"
android:textSize="32sp" />
</RelativeLayout>
<Button
android:id="@+id/scrollby"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/main_area"
android:layout_centerHorizontal="true"
android:text="scroolby" />
<Button
android:id="@+id/scrollTo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/scrollby"
android:layout_centerHorizontal="true"
android:text="scrollT0" />
</RelativeLayout>
mScrollBy.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMainArea.scrollBy(100, 0);
}
});
mScrollTo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mMainArea.scrollTo(0, 0);
}
});
然后锋恬,來(lái)看下效果圖:
下面來(lái)簡(jiǎn)單分析一下,該布局文件中是在一個(gè)相對(duì)布局里包裹著兩個(gè)TextView平分它的寬度编丘,并將其高度撐滿(mǎn)与学。在scroolBy這個(gè)按鈕,則是調(diào)用的RelativieLayout的scrollBy(100,0)方法嘉抓。正如前面所說(shuō)索守,scrollBy是基于view內(nèi)容的相對(duì)位置進(jìn)行滑動(dòng),我們發(fā)現(xiàn)每點(diǎn)擊一次抑片,這個(gè)RelativeLayout的內(nèi)容都會(huì)向左偏移100px卵佛。而scrollTo這個(gè)按鈕則是調(diào)用的scrollTo(0,0)方法,我們發(fā)現(xiàn)它就直接將兩個(gè)TextView返回到(0,0)點(diǎn)了截汪,這也就是前面所說(shuō)的是相對(duì)于view的絕對(duì)坐標(biāo)對(duì)view的內(nèi)容進(jìn)行偏移疾牲。講到這里,相信很多朋友都明白了什么是view內(nèi)容進(jìn)行滑動(dòng)衙解,
現(xiàn)在通過(guò)上面的例子阳柔,我們已經(jīng)知道了scrollTo、scrollBy的作用蚓峦,那么大家有沒(méi)有想過(guò)舌剂,為什么我們?cè)谡{(diào)用scrollBy(100,0)的時(shí)候view的內(nèi)容是向左邊跑呢?x軸的正方向明明是在右邊的胺阖摇架诞?這也正是我剛開(kāi)始在學(xué)習(xí)Scroller的時(shí)候最困惑的地方。
需要再次強(qiáng)調(diào)的是干茉,滑動(dòng)的是view的內(nèi)容而不是view本身谴忧,我們從上面的例子可以看到,不管我們?nèi)绾握{(diào)用角虫,調(diào)用者RelativeLayout是沒(méi)有任何變化的沾谓。getScrollX()返回的滑動(dòng)距離是view左邊緣的X軸減去它的父view內(nèi)容左邊緣的X軸,如果理解了這一句話戳鹅,那么這個(gè)問(wèn)題就迎刃而解了均驶,下面如圖所示:
很顯然,中間黃色和綠色的部分代表的是view的內(nèi)容(特意將高度縮小以達(dá)到顯示效果)枫虏,第一次調(diào)用scrollBy(100,0)以后妇穴,view的內(nèi)容就會(huì)距離view左邊緣距離為100px,測(cè)試X坐標(biāo)應(yīng)該為-100隶债,而用view的左邊緣X軸的值減去view內(nèi)容:0-(-100)等于100腾它,剛好就是我們滑動(dòng)的相對(duì)距離,如果第二次死讹、第三次調(diào)用瞒滴,則view內(nèi)容的X軸的值就會(huì)變成-200、-300赞警,這樣我們就很好理解了妓忍。而scrollTo是滑動(dòng)到view的絕對(duì)坐標(biāo)位置,這里我傳的值是(0愧旦,0)世剖,所以它就滑動(dòng)到了它在view的起始位置了。
關(guān)于Scroller實(shí)現(xiàn)彈性滑動(dòng)效果的原理
很顯然忘瓦,我們通過(guò)定時(shí)調(diào)用scrollBy就可以來(lái)實(shí)現(xiàn)滑動(dòng)效果了搁廓,然而Scroller類(lèi)里為我們提供了下面的一個(gè)方法:
這個(gè)方法可以為我們提供在某一段時(shí)間內(nèi)滑動(dòng)一段距離的彈性滑動(dòng)效果引颈,下面來(lái)看以下它的典型用法:
private void smoothScroll(int dx,int dy) {
int deltaX = dx-getScrollX();
int deltaY = dx-getScrollX();
mScrooler.startScroll(getScrollX(),getScrollY(),deltaX,deltaY,1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScrooler.computeScrollOffset()) {
scrollTo(mScrooler.getCurrX(), mScrooler.getCurrY());
postInvalidate();
}
}
特別提醒一下,dx,dy代表的不是終點(diǎn)坐標(biāo)而是我們滑動(dòng)的距離境蜕,這一點(diǎn)我們看源碼就很容易得出結(jié)論:
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;
}
在上面的smoothscroll方法中我們最后調(diào)用到了invalidate蝙场,這是為什么呢?因?yàn)檎{(diào)用invalidate會(huì)導(dǎo)致view的重繪在view的draw方法里粱年,我們可以發(fā)現(xiàn)它會(huì)調(diào)用到我們復(fù)寫(xiě)的computeScroll方法售滤,而這個(gè)方法又會(huì)調(diào)用postinvalidate方法來(lái)進(jìn)行重繪,這樣view的彈性滑動(dòng)就實(shí)現(xiàn)了台诗。但是它又是如何根據(jù)我們的時(shí)間來(lái)進(jìn)行一點(diǎn)點(diǎn)的滑動(dòng)的呢完箩?下面我們來(lái)看下computeScrollOffset()方法:
public boolean computeScrollOffset() {
//已經(jīng)完成了則為true,則不會(huì)調(diào)用postinvalidate方法就不會(huì)重繪拉队,滑動(dòng)就此停止
if (mFinished) {
return false;
}
//timerPassed就是根據(jù)當(dāng)前時(shí)間減去起始時(shí)間來(lái)計(jì)算時(shí)間的流逝值
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
//當(dāng)還在滑動(dòng)時(shí)間范圍內(nèi)的時(shí)候就會(huì)根據(jù)時(shí)間進(jìn)行計(jì)算平移
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;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
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;
}
通過(guò)對(duì)上面代碼的分析弊知,我們可以發(fā)現(xiàn)google在設(shè)計(jì)Scroller的時(shí)候還是十分巧妙的,沒(méi)有使用到定時(shí)器粱快,但也做到了在一定時(shí)間內(nèi)進(jìn)行滑動(dòng)的效果秩彤。
利用Scroller來(lái)實(shí)現(xiàn)一個(gè)側(cè)滑菜單
通過(guò)上面對(duì)Scroller滑動(dòng)的使用以及原理的分析,想必大家已經(jīng)很了解該類(lèi)的使用了事哭,接下來(lái)我就通過(guò)該類(lèi)來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的側(cè)滑菜單漫雷,以達(dá)到應(yīng)用目的:
核心代碼如下:
public class SlideMenu extends ViewGroup {
// x軸的上一次位置
private int mLastx;
// 模擬彈性滑動(dòng)
private Scroller mScrooler;
// 標(biāo)記當(dāng)前現(xiàn)實(shí)狀態(tài),默認(rèn)為true顯示的是主界面
private boolean mIsMain = true;
public SlideMenu(Context context) {
this(context, null);
}
public SlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mScrooler = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
View left = this.getChildAt(0);
left.measure(left.getLayoutParams().width, heightMeasureSpec);
View main = this.getChildAt(1);
main.measure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View left = this.getChildAt(0);
left.layout(-left.getLayoutParams().width, 0, 0, b);
View main = this.getChildAt(1);
main.layout(l, t, r, b);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN :
// getX是返回的相對(duì)于當(dāng)前view左邊緣的橫軸距離,getRawx是返回的是相對(duì)于屏幕左邊緣的距離,
// 因?yàn)檫@里的viewgroup是撐滿(mǎn)全屏的所以用這兩個(gè)方法的效果都是一樣的
mLastx = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE :
int x = (int) event.getX();
//上一次x軸位置減去當(dāng)前X軸的值為這一次滑動(dòng)的增量
int deltaX = mLastx - x;
//加上原本內(nèi)容在X軸的偏移量得到它將要便宜的量
int scrollX = getScrollX() + deltaX;
if (scrollX > 0) {
//大于0的時(shí)候代表側(cè)滑菜單已經(jīng)出來(lái)了所以直接scrollTo(0,0)即可
scrollTo(0, 0);
} else if (scrollX < -getChildAt(0).getWidth()) {
//小于側(cè)滑菜單的寬度的負(fù)值的時(shí)候則代表側(cè)滑菜單已經(jīng)完全隱藏起來(lái)了
scrollTo(-this.getChildAt(0).getWidth(), 0);
} else {
//此時(shí)側(cè)滑菜單既沒(méi)有完全隱藏也沒(méi)有完全現(xiàn)實(shí)根據(jù)增量值進(jìn)行正常的scrollBy即可
scrollBy(deltaX, 0);
}
mLastx = x;
break;
case MotionEvent.ACTION_UP :
// 獲取手指抬起時(shí)候的內(nèi)容偏移量
int upX = getScrollX();
if (upX > -this.getChildAt(0).getWidth() / 2) {
// 滑動(dòng)距離大于了左側(cè)菜單的一半則將菜單顯示出來(lái)
mIsMain = true;
} else {
// 反之菜單收起
mIsMain = false;
}
switchScreen();
break;
}
return true;
}
private void switchScreen() {
int startX = getScrollX();// 起始的X軸偏移量
int deltaX;
if (mIsMain) {
// 如果是要顯示的主界面則用目標(biāo)的偏移x點(diǎn)是0
deltaX = 0 - startX;
} else {
// 如果要顯示的是菜單界面則目標(biāo)的偏移x點(diǎn)是菜單的左邊界
deltaX = -getChildAt(0).getLayoutParams().width - startX;
}
mScrooler.startScroll(startX, 0, deltaX, 0, Math.abs(deltaX) * 3);
invalidate();
}
@Override
public void computeScroll() {
if (mScrooler.computeScrollOffset()) {
scrollTo(mScrooler.getCurrX(), mScrooler.getCurrY());
postInvalidate();
}
}
public void switchMenu() {
mIsMain = !mIsMain;
switchScreen();
}
}
代碼加注釋總共也只有一百多行,通過(guò)上面的分析以及代碼的注釋?zhuān)嘈乓欢ê苋菀拙涂梢岳斫庹麄€(gè)側(cè)滑菜單的實(shí)現(xiàn)原理鳍咱。