關(guān)于Scroller的使用以及自己實(shí)現(xiàn)一個(gè)側(cè)滑菜單

在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>

getScrollX.png

簡(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ō)明:

scrollto.png

這兩個(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)看下效果圖:

scroll.gif

下面來(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)題就迎刃而解了均驶,下面如圖所示:

scroolX.png

很顯然,中間黃色和綠色的部分代表的是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è)方法:

startscroll.png

這個(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)原理鳍咱。

本文示例代碼地址:https://github.com/tracyxia716/ViewLearning

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末降盹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子谤辜,更是在濱河造成了極大的恐慌蓄坏,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑念,死亡現(xiàn)場(chǎng)離奇詭異剑辫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)渠欺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)椎眯,“玉大人挠将,你說(shuō)我怎么就攤上這事”嗾” “怎么了舔稀?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)掌测。 經(jīng)常有香客問(wèn)我内贮,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任夜郁,我火速辦了婚禮什燕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘竞端。我一直安慰自己屎即,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布事富。 她就那樣靜靜地躺著技俐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪统台。 梳的紋絲不亂的頭發(fā)上雕擂,一...
    開(kāi)封第一講書(shū)人閱讀 52,394評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音贱勃,去河邊找鬼井赌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛募寨,可吹牛的內(nèi)容都是我干的族展。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拔鹰,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼仪缸!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起列肢,我...
    開(kāi)封第一講書(shū)人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恰画,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后瓷马,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體拴还,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年欧聘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了片林。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡怀骤,死狀恐怖费封,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蒋伦,我是刑警寧澤弓摘,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站痕届,受9級(jí)特大地震影響韧献,放射性物質(zhì)發(fā)生泄漏末患。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一锤窑、第九天 我趴在偏房一處隱蔽的房頂上張望璧针。 院中可真熱鬧,春花似錦果复、人聲如沸陈莽。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)走搁。三九已至,卻和暖如春迈窟,著一層夾襖步出監(jiān)牢的瞬間私植,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工车酣, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曲稼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓湖员,卻偏偏與公主長(zhǎng)得像贫悄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娘摔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容