一、為什么要學(xué)會(huì)Scroll
我們?cè)趯懽远xView的時(shí)候,避免不了要讓用戶能夠和這個(gè)View進(jìn)行交互,而因?yàn)锳ndroid以觸摸作為主要的操作手段,那么明白如何去自作一個(gè)可滑動(dòng)的的View就非常有必要了。
例如ListView、ViewPager等官方提供的View均實(shí)現(xiàn)了這以交互方式。
二、坐標(biāo)
要制作一個(gè)可滑動(dòng)的View,我們首先要明白Android系統(tǒng)中的坐標(biāo)系是怎樣的一個(gè)定義。
1、Android坐標(biāo)系(絕對(duì)坐標(biāo))
Android的坐標(biāo)系分為兩種,其中一種叫做Android坐標(biāo)系,或者稱為絕對(duì)坐標(biāo)可能更為合適。它以手機(jī)屏幕的左上角的頂點(diǎn)作為坐標(biāo)系的原點(diǎn)。該點(diǎn)向右是X軸,向下既是Y軸。在onTouchEvent事件中可以通過getRawX()和getRawY()來分別獲得觸發(fā)該事件的View相對(duì)于絕對(duì)坐標(biāo)系的X軸和Y軸坐標(biāo)。
2、視圖坐標(biāo)系(相對(duì)坐標(biāo))
Android坐標(biāo)系的另一種叫視圖坐標(biāo)系,它的坐標(biāo)原點(diǎn)永遠(yuǎn)是直接父節(jié)點(diǎn)(parentView)的左上角。所以也可以稱它為相對(duì)坐標(biāo)。onTouchEvent中可通過getX()和getY()分別獲得X軸和Y軸坐標(biāo)。
三、獲取坐標(biāo)
獲取坐標(biāo)的方法可以分為兩類:
1、View提供的獲取坐標(biāo)的方法。
getTop():獲取到的是View自身的頂邊到父節(jié)點(diǎn)頂邊的距離。
getBootom():獲取到的是View自身的底邊到父節(jié)點(diǎn)頂邊的距離。
getLeft():獲取到的是View自身的左邊到父節(jié)點(diǎn)左邊的距離。
getRight():獲取到的是View自身的右邊到父節(jié)點(diǎn)左邊的距離。
2、MotionEvent提供的方法(既觸摸事件中)
getX():獲取到的是觸摸點(diǎn)相對(duì)父節(jié)點(diǎn)的X軸相對(duì)坐標(biāo)。
getY():獲取到的是觸摸點(diǎn)相對(duì)父節(jié)點(diǎn)的Y軸相對(duì)坐標(biāo)。
getRawX():獲取到的是觸摸點(diǎn)相對(duì)屏幕的X軸絕對(duì)坐標(biāo)。
getRawY():獲取到的是觸摸點(diǎn)相對(duì)屏幕的Y軸絕對(duì)坐標(biāo)。
四、實(shí)現(xiàn)滑動(dòng)的幾種方法
1、layout方法
調(diào)用layout方法去重新布局這個(gè)View。layout的各個(gè)參數(shù)為當(dāng)前View的left、top、right、bottom加上我們收支滑動(dòng)的偏移量。下列代碼是一個(gè)View跟隨手指移動(dòng)的例子:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - downX;
int offsetY = y - downY;
layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
2、使用offsetLeftAndRight與offsetTopAndBottom兩個(gè)方法
這兩個(gè)方法為系統(tǒng)提供的一個(gè)專門用來移動(dòng)View的Api。只需要將計(jì)算出的偏移量作為參數(shù)即可。所以使用這兩個(gè)方法可將上面的例子改為如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - downX;
int offsetY = y - downY;
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
3、LayoutParams(偏方,不建議使用,不夠精確)
我們知道LayoutParams里面封裝了一個(gè)View的布局參數(shù),而里面剛好有包含margin值。所以我們可以通過修改margin來達(dá)到移動(dòng)的目的。但是移動(dòng)的效果并不好,很不精確。代買如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - downX;
int offsetY = y - downY;
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
4、scrollTo和scrollBy
這種呢,和前面的都不太相同。首先我們要明白,這兩個(gè)方法的區(qū)別,以及他們所移動(dòng)的到底是什么東西。
(1)兩個(gè)方法間的區(qū)別
首先scrollTo(x,y),這個(gè)方法呢,兩個(gè)參數(shù)分別為準(zhǔn)確的x、y軸坐標(biāo),你給它什么坐標(biāo)它就移動(dòng)到哪。
而scrollBy(dx,dy)呢,看參數(shù)名就知道是距離,也就是說它會(huì)在原來的x、y的基礎(chǔ)值上去增量移動(dòng)。打個(gè)比方,一個(gè)View受到scrollTo的作用移動(dòng)到了(100,100)的一個(gè)點(diǎn)。之后再次被scrollBy作用增量移動(dòng)(20,20)。那么此時(shí)這個(gè)View處在一個(gè)什么位置呢?答案是(120,120),因?yàn)閷?shí)在(100,100)這個(gè)原值上增量移動(dòng)了(20,20)。
以上就是這兩個(gè)方法之間的區(qū)別
(2)這兩個(gè)方法所移動(dòng)的對(duì)象是什么。
前面有提到,這種移動(dòng)方式和前面的幾個(gè)都不相同,前面幾個(gè)都是直接移動(dòng)的View本身;
而這兩個(gè)方法所移動(dòng)實(shí)際上是View的顯示區(qū)域。
打個(gè)比方:有一個(gè)望遠(yuǎn)鏡,你可以通過望遠(yuǎn)鏡看到這個(gè)世界,但是世界是很大很大的,望遠(yuǎn)鏡只能讓你看到這有限一小部分,如果你想看右邊點(diǎn)的,就要把望遠(yuǎn)鏡往右移動(dòng),想看上邊點(diǎn)的,就往上移。其他方向同理。
那么這個(gè)望遠(yuǎn)鏡的可視區(qū)域就是我們的View或者ViewGroup的可視區(qū)域。
而世界呢,就是View或ViewGroup背后所藏著的那一整個(gè)顯示內(nèi)容,非常非常大,只是你要去移動(dòng)我們的可視區(qū)域才能夠看到。
舉個(gè)栗子:
我有一個(gè)ViewGroup,這個(gè)ViewGroup里有一個(gè)TextView。我調(diào)用了這個(gè)ViewGroup的scrollBy方法移動(dòng)了(100,0)。但是呢這時(shí)并不是ViewGroup移動(dòng)了,而是這個(gè)ViewGroup所顯示出來的區(qū)域被移動(dòng)了。所以TextView應(yīng)該是到了更左邊的距離去了.
那同理,我去調(diào)用了TextView的scrollBy方法移動(dòng)(-20,0)。這時(shí)會(huì)是誰移動(dòng)了呢?是TextView中的顯示區(qū)域被移動(dòng)了(-20,0)。所以文字應(yīng)該是到了更右邊的位置去了。
(3)他們的移動(dòng)方向。
scrollTo和scrollBy的移動(dòng)方向我想大概是學(xué)習(xí)scroll最容易模糊的一個(gè)部分了,最初即便我在網(wǎng)上看了各種相關(guān)描述的圖之后我也是有迷迷糊糊。
實(shí)際上首先呢,你要記住前面所說的scrollTo和scrollBy這兩個(gè)方法所移動(dòng)的東西實(shí)際上是View的顯示區(qū)域,而不是當(dāng)前View本身。
舉個(gè)栗子:
我又有一個(gè)ViewGroup,里面有一個(gè)寫著helloword的TextView - - ,而這個(gè)ViewGroup會(huì)根據(jù)手指移動(dòng)的增量去調(diào)用scrollBy。我的手指向右移動(dòng)了30,此時(shí)TextView會(huì)顯示在什么位置呢?
我們分析一下,手指向右移動(dòng)了30,移動(dòng)的是顯示區(qū)域,那么此時(shí)顯示區(qū)域應(yīng)該是在(30,0)的位置,我們的TextView自然是往左偏了30啦。
ok!那如果手指向下移動(dòng)30呢?那么此時(shí)顯示區(qū)域向下移動(dòng)30,內(nèi)容當(dāng)然是向上偏了30啦。
(4)實(shí)現(xiàn)移動(dòng)。
下面就用scrollBy去實(shí)現(xiàn)跟隨手指移動(dòng)的效果。實(shí)際上知道了前面的原理之后,我們就知道了,要讓它跟隨手指移動(dòng)只要將偏移量改為負(fù)數(shù)即可。如下:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - downX;
int offsetY = y - downY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}