官术网_书友最值得收藏!

3.2 View的滑動(dòng)

3.1節(jié)介紹了View的一些基礎(chǔ)知識(shí)和概念,本節(jié)開(kāi)始介紹很重要的一個(gè)內(nèi)容:View的滑動(dòng)。在Android設(shè)備上,滑動(dòng)幾乎是應(yīng)用的標(biāo)配,不管是下拉刷新還是SlidingMenu,它們的基礎(chǔ)都是滑動(dòng)。從另外一方面來(lái)說(shuō),Android手機(jī)由于屏幕比較小,為了給用戶呈現(xiàn)更多的內(nèi)容,就需要使用滑動(dòng)來(lái)隱藏和顯示一些內(nèi)容。基于上述兩點(diǎn),可以知道,滑動(dòng)在Android開(kāi)發(fā)中具有很重要的作用,不管一些滑動(dòng)效果多么絢麗,歸根結(jié)底,它們都是由不同的滑動(dòng)外加一些特效所組成的。因此,掌握滑動(dòng)的方法是實(shí)現(xiàn)絢麗的自定義控件的基礎(chǔ)。通過(guò)三種方式可以實(shí)現(xiàn)View的滑動(dòng):第一種是通過(guò)View本身提供的scrollTo/scrollBy方法來(lái)實(shí)現(xiàn)滑動(dòng);第二種是通過(guò)動(dòng)畫(huà)給View施加平移效果來(lái)實(shí)現(xiàn)滑動(dòng);第三種是通過(guò)改變View的LayoutParams使得View重新布局從而實(shí)現(xiàn)滑動(dòng)。從目前來(lái)看,常見(jiàn)的滑動(dòng)方式就這么三種,下面一一進(jìn)行分析。

3.2.1 使用scrollTo/scrollBy

為了實(shí)現(xiàn)View的滑動(dòng),View提供了專門的方法來(lái)實(shí)現(xiàn)這個(gè)功能,那就是scrollTo和scrollBy,我們先來(lái)看看這兩個(gè)方法的實(shí)現(xiàn),如下所示。

        /**
         * 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();
                  }
              }
         }

         /**
          * 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);
         }

從上面的源碼可以看出,scrollBy實(shí)際上也是調(diào)用了scrollTo方法,它實(shí)現(xiàn)了基于當(dāng)前位置的相對(duì)滑動(dòng),而scrollTo則實(shí)現(xiàn)了基于所傳遞參數(shù)的絕對(duì)滑動(dòng),這個(gè)不難理解。利用scrollTo和scrollBy來(lái)實(shí)現(xiàn)View的滑動(dòng),這不是一件困難的事,但是我們要明白滑動(dòng)過(guò)程中View內(nèi)部的兩個(gè)屬性mScrollX和mScrollY的改變規(guī)則,這兩個(gè)屬性可以通過(guò)getScrollX和getScrollY方法分別得到。這里先簡(jiǎn)要概況一下:在滑動(dòng)過(guò)程中,mScrollX的值總是等于View左邊緣和View內(nèi)容左邊緣在水平方向的距離,而mScrollY的值總是等于View上邊緣和View內(nèi)容上邊緣在豎直方向的距離。View邊緣是指View的位置,由四個(gè)頂點(diǎn)組成,而View內(nèi)容邊緣是指View中的內(nèi)容的邊緣,scrollTo和scrollBy只能改變View內(nèi)容的位置而不能改變View在布局中的位置。mScrollX和mScrollY的單位為像素,并且當(dāng)View左邊緣在View內(nèi)容左邊緣的右邊時(shí),mScrollX為正值,反之為負(fù)值;當(dāng)View上邊緣在View內(nèi)容上邊緣的下邊時(shí),mScrollY為正值,反之為負(fù)值。換句話說(shuō),如果從左向右滑動(dòng),那么mScrollX為負(fù)值,反之為正值;如果從上往下滑動(dòng),那么mScrollY為負(fù)值,反之為正值。

為了更好地理解這個(gè)問(wèn)題,下面舉個(gè)例子,如圖3-3所示。在圖中假設(shè)水平和豎直方向的滑動(dòng)距離都為100像素,針對(duì)圖中各種滑動(dòng)情況,都給出了對(duì)應(yīng)的mScrollX和mScrollY的值。根據(jù)上面的分析,可以知道,使用scrollTo和scrollBy來(lái)實(shí)現(xiàn)View的滑動(dòng),只能將View的內(nèi)容進(jìn)行移動(dòng),并不能將View本身進(jìn)行移動(dòng),也就是說(shuō),不管怎么滑動(dòng),也不可能將當(dāng)前View滑動(dòng)到附近View所在的區(qū)域,這個(gè)需要仔細(xì)體會(huì)一下。

圖3-3 mScrollX和mScrollY的變換規(guī)律示意

3.2.2 使用動(dòng)畫(huà)

上一節(jié)介紹了采用scrollTo/scrollBy來(lái)實(shí)現(xiàn)View的滑動(dòng),本節(jié)介紹另外一種滑動(dòng)方式,即使用動(dòng)畫(huà),通過(guò)動(dòng)畫(huà)我們能夠讓一個(gè)View進(jìn)行平移,而平移就是一種滑動(dòng)。使用動(dòng)畫(huà)來(lái)移動(dòng)View,主要是操作View的translationX和translationY屬性,既可以采用傳統(tǒng)的View動(dòng)畫(huà),也可以采用屬性動(dòng)畫(huà),如果采用屬性動(dòng)畫(huà)的話,為了能夠兼容3.0以下的版本,需要采用開(kāi)源動(dòng)畫(huà)庫(kù)nineoldandroids(http://nineoldandroids.com/)。

采用View動(dòng)畫(huà)的代碼,如下所示。此動(dòng)畫(huà)可以在100ms內(nèi)將一個(gè)View從原始位置向右下角移動(dòng)100個(gè)像素。

        <? 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:interpolator="@android:anim/linear_interpolator"
              android:toXDelta="100"
              android:toYDelta="100" />
        </set>

如果采用屬性動(dòng)畫(huà)的話,就更簡(jiǎn)單了,以下代碼可以將一個(gè)View在100ms內(nèi)從原始位置向右平移100像素。

        ObjectAnimator.ofFloat(targetView, "translationX", 0, 100).setDuration
        (100).start();

上面簡(jiǎn)單介紹了通過(guò)動(dòng)畫(huà)來(lái)移動(dòng)View的方法,關(guān)于動(dòng)畫(huà)會(huì)在第5章中進(jìn)行詳細(xì)說(shuō)明。使用動(dòng)畫(huà)來(lái)做View的滑動(dòng)需要注意一點(diǎn),View動(dòng)畫(huà)是對(duì)View的影像做操作,它并不能真正改變View的位置參數(shù),包括寬/高,并且如果希望動(dòng)畫(huà)后的狀態(tài)得以保留還必須將fillAfter屬性設(shè)置為true,否則動(dòng)畫(huà)完成后其動(dòng)畫(huà)結(jié)果會(huì)消失。比如我們要把View向右移動(dòng)100像素,如果fillAfter為false,那么在動(dòng)畫(huà)完成的一剎那,View會(huì)瞬間恢復(fù)到動(dòng)畫(huà)前的狀態(tài);如果fillAfter為true,在動(dòng)畫(huà)完成后,View會(huì)停留在距原始位置100像素的右邊。使用屬性動(dòng)畫(huà)并不會(huì)存在上述問(wèn)題,但是在Android 3.0以下無(wú)法使用屬性動(dòng)畫(huà),這個(gè)時(shí)候我們可以使用動(dòng)畫(huà)兼容庫(kù)nineoldandroids來(lái)實(shí)現(xiàn)屬性動(dòng)畫(huà),盡管如此,在Android 3.0以下的手機(jī)上通過(guò)nineoldandroids來(lái)實(shí)現(xiàn)的屬性動(dòng)畫(huà)本質(zhì)上仍然是View動(dòng)畫(huà)。

上面提到View動(dòng)畫(huà)并不能真正改變View的位置,這會(huì)帶來(lái)一個(gè)很嚴(yán)重的問(wèn)題。試想一下,比如我們通過(guò)View動(dòng)畫(huà)將一個(gè)Button向右移動(dòng)100px,并且這個(gè)View設(shè)置的有單擊事件,然后你會(huì)驚奇地發(fā)現(xiàn),單擊新位置無(wú)法觸發(fā)onClick事件,而單擊原始位置仍然可以觸發(fā)onClick事件,盡管Button已經(jīng)不在原始位置了。這個(gè)問(wèn)題帶來(lái)的影響是致命的,但是它卻又是可以理解的,因?yàn)椴还蹷utton怎么做變換,但是它的位置信息(四個(gè)頂點(diǎn)和寬/高)并不會(huì)隨著動(dòng)畫(huà)而改變,因此在系統(tǒng)眼里,這個(gè)Button并沒(méi)有發(fā)生任何改變,它的真身仍然在原始位置。在這種情況下,單擊新位置當(dāng)然不會(huì)觸發(fā)onClick事件了,因?yàn)锽utton的真身并沒(méi)有發(fā)生改變,在新位置上只是View的影像而已。基于這一點(diǎn),我們不能簡(jiǎn)單地給一個(gè)View做平移動(dòng)畫(huà)并且還希望它在新位置繼續(xù)觸發(fā)一些單擊事件。

從Android 3.0開(kāi)始,使用屬性動(dòng)畫(huà)可以解決上面的問(wèn)題,但是大多數(shù)應(yīng)用都需要兼容到Android 2.2,在Android 2.2上無(wú)法使用屬性動(dòng)畫(huà),因此這里還是會(huì)有問(wèn)題。那么這種問(wèn)題難道就無(wú)法解決了嗎?也不是的,雖然不能直接解決這個(gè)問(wèn)題,但是還可以間接解決這個(gè)問(wèn)題,這里給出一個(gè)簡(jiǎn)單的解決方法。針對(duì)上面View動(dòng)畫(huà)的問(wèn)題,我們可以在新位置預(yù)先創(chuàng)建一個(gè)和目標(biāo)Button一模一樣的Button,它們不但外觀一樣連onClick事件也一樣。當(dāng)目標(biāo)Button完成平移動(dòng)畫(huà)后,就把目標(biāo)Button隱藏,同時(shí)把預(yù)先創(chuàng)建的Button顯示出來(lái),通過(guò)這種間接的方式我們解決了上面的問(wèn)題。這僅僅是個(gè)參考,面對(duì)這種問(wèn)題時(shí)讀者可以靈活應(yīng)對(duì)。

3.2.3 改變布局參數(shù)

本節(jié)將介紹第三種實(shí)現(xiàn)View滑動(dòng)的方法,那就是改變布局參數(shù),即改變LayoutParams。這個(gè)比較好理解了,比如我們想把一個(gè)Button向右平移100px,我們只需要將這個(gè)Button的LayoutParams里的marginLeft參數(shù)的值增加100px即可,是不是很簡(jiǎn)單呢?還有一種情形,為了達(dá)到移動(dòng)Button的目的,我們可以在Button的左邊放置一個(gè)空的View,這個(gè)空View的默認(rèn)寬度為0,當(dāng)我們需要向右移動(dòng)Button時(shí),只需要重新設(shè)置空View的寬度即可,當(dāng)空View的寬度增大時(shí)(假設(shè)Button的父容器是水平方向的LinearLayout), Button就自動(dòng)被擠向右邊,即實(shí)現(xiàn)了向右平移的效果。如何重新設(shè)置一個(gè)View的LayoutParams呢?很簡(jiǎn)單,如下所示。

        MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams();
        params.width += 100;
        params.leftMargin += 100;
        mButton1.requestLayout();
        //或者mButton1.setLayoutParams(params);

通過(guò)改變LayoutParams的方式去實(shí)現(xiàn)View的滑動(dòng)同樣是一種很靈活的方法,需要根據(jù)不同情況去做不同的處理。

3.2.4 各種滑動(dòng)方式的對(duì)比

上面分別介紹了三種不同的滑動(dòng)方式,它們都能實(shí)現(xiàn)View的滑動(dòng),那么它們之間的差別是什么呢?

先看scrollTo/scrollBy這種方式,它是View提供的原生方法,其作用是專門用于View的滑動(dòng),它可以比較方便地實(shí)現(xiàn)滑動(dòng)效果并且不影響內(nèi)部元素的單擊事件。但是它的缺點(diǎn)也是很顯然的:它只能滑動(dòng)View的內(nèi)容,并不能滑動(dòng)View本身。

再看動(dòng)畫(huà),通過(guò)動(dòng)畫(huà)來(lái)實(shí)現(xiàn)View的滑動(dòng),這要分情況。如果是Android 3.0以上并采用屬性動(dòng)畫(huà),那么采用這種方式?jīng)]有明顯的缺點(diǎn);如果是使用View動(dòng)畫(huà)或者在Android 3.0以下使用屬性動(dòng)畫(huà),均不能改變View本身的屬性。在實(shí)際使用中,如果動(dòng)畫(huà)元素不需要響應(yīng)用戶的交互,那么使用動(dòng)畫(huà)來(lái)做滑動(dòng)是比較合適的,否則就不太適合。但是動(dòng)畫(huà)有一個(gè)很明顯的優(yōu)點(diǎn),那就是一些復(fù)雜的效果必須要通過(guò)動(dòng)畫(huà)才能實(shí)現(xiàn)。

最后再看一下改變布局這種方式,它除了使用起來(lái)麻煩點(diǎn)以外,也沒(méi)有明顯的缺點(diǎn),它的主要適用對(duì)象是一些具有交互性的View,因?yàn)檫@些View需要和用戶交互,直接通過(guò)動(dòng)畫(huà)去實(shí)現(xiàn)會(huì)有問(wèn)題,這在3.2.2節(jié)中已經(jīng)有所介紹,所以這個(gè)時(shí)候我們可以使用直接改變布局參數(shù)的方式去實(shí)現(xiàn)。

針對(duì)上面的分析做一下總結(jié),如下所示。

· scrollTo/scrollBy:操作簡(jiǎn)單,適合對(duì)View內(nèi)容的滑動(dòng);

· 動(dòng)畫(huà):操作簡(jiǎn)單,主要適用于沒(méi)有交互的View和實(shí)現(xiàn)復(fù)雜的動(dòng)畫(huà)效果;

· 改變布局參數(shù):操作稍微復(fù)雜,適用于有交互的View。

下面我們實(shí)現(xiàn)一個(gè)跟手滑動(dòng)的效果,這是一個(gè)自定義View,拖動(dòng)它可以讓它在整個(gè)屏幕上隨意滑動(dòng)。這個(gè)View實(shí)現(xiàn)起來(lái)很簡(jiǎn)單,我們只要重寫(xiě)它的onTouchEvent方法并處理ACTION_MOVE事件,根據(jù)兩次滑動(dòng)之間的距離就可以實(shí)現(xiàn)它的滑動(dòng)了。為了實(shí)現(xiàn)全屏滑動(dòng),我們采用動(dòng)畫(huà)的方式來(lái)實(shí)現(xiàn)。原因很簡(jiǎn)單,這個(gè)效果無(wú)法采用scrollTo來(lái)實(shí)現(xiàn)。另外,它還可以采用改變布局的方式來(lái)實(shí)現(xiàn),這里僅僅是為了演示,所以就選擇了動(dòng)畫(huà)的方式,核心代碼如下所示。

        public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getRawX();
            int y = (int) event.getRawY();
            switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    Log.d(TAG, "move, deltaX:" + deltaX + " deltaY:" + deltaY);
                    int translationX = (int)ViewHelper.getTranslationX(this) + deltaX;
                    int translationY = (int)ViewHelper.getTranslationY(this) + deltaY;
                    ViewHelper.setTranslationX(this, translationX);
                    ViewHelper.setTranslationY(this, translationY);
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
                }
                default:
                    break;
                }

                mLastX = x;
                mLastY = y;
                return true;
            }

通過(guò)上述代碼可以看出,這一全屏滑動(dòng)的效果實(shí)現(xiàn)起來(lái)相當(dāng)簡(jiǎn)單。首先我們通過(guò)getRawX和getRawY方法來(lái)獲取手指當(dāng)前的坐標(biāo),注意不能使用getX和getY方法,因?yàn)檫@個(gè)是要全屏滑動(dòng)的,所以需要獲取當(dāng)前點(diǎn)擊事件在屏幕中的坐標(biāo)而不是相對(duì)于View本身的坐標(biāo);其次,我們要得到兩次滑動(dòng)之間的位移,有了這個(gè)位移就可以移動(dòng)當(dāng)前的View,移動(dòng)方法采用的是動(dòng)畫(huà)兼容庫(kù)nineoldandroids中的ViewHelper類所提供的setTranslationX和setTranslationY方法。實(shí)際上,ViewHelper類提供了一系列g(shù)et/set方法,因?yàn)閂iew的setTranslationX和setTranslationY只能在Android 3.0及其以上版本才能使用,但是ViewHelper所提供的方法是沒(méi)有版本要求的,與此類似的還有setX、setScaleX、setAlpha等方法,這一系列方法實(shí)際上是為屬性動(dòng)畫(huà)服務(wù)的,更詳細(xì)的內(nèi)容會(huì)在第5章進(jìn)行進(jìn)一步的介紹。這個(gè)自定義View可以在2.x及其以上版本工作,但是由于動(dòng)畫(huà)的性質(zhì),如果給它加上onClick事件,那么在3.0以下版本它將無(wú)法在新位置響應(yīng)用戶的點(diǎn)擊,這個(gè)問(wèn)題在前面已經(jīng)提到過(guò)。

主站蜘蛛池模板: 安图县| 商丘市| 浪卡子县| 芜湖县| 康平县| 应用必备| 正安县| 涟源市| 屏山县| 明光市| 郑州市| 达拉特旗| 秀山| 赫章县| 凌海市| 都匀市| 四平市| 清远市| 奉贤区| 武隆县| 宁城县| 周宁县| 麻城市| 宕昌县| 东丰县| 巴彦县| 雅安市| 红河县| 平定县| 长垣县| 武冈市| 泾阳县| 沁源县| 明星| 盘锦市| 英超| 平湖市| 监利县| 新邵县| 嘉祥县| 东乡县|