View的滑動(dòng)

目前移動(dòng)設(shè)備流行,我們要在如此小的屏幕上盡可能給用戶展現(xiàn)更多的內(nèi)容坞嘀,就需要在應(yīng)用上通過(guò)滑動(dòng)來(lái)顯示和隱藏部分內(nèi)容丽涩,View作為呈現(xiàn)內(nèi)容的媒介裁蚁,具備滑動(dòng)功能就無(wú)可厚非了。

三種方式實(shí)現(xiàn)View的滑動(dòng):

  • 通過(guò)View本身提供的scrollTo/scrollBy方法
  • 通過(guò)動(dòng)畫給View添加平移動(dòng)畫
  • 通過(guò)改變View的LayoutParams使得View重新布局

通過(guò)使用scrollTo/scrollBy方法

先看這兩個(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)刽严。在滑動(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ù)值盆均。

情景推理

比如我們有一個(gè)自定義View,占滿整個(gè)屏幕漱逸,那么這個(gè)View的的位置是固定了的泪姨,四個(gè)頂點(diǎn)分別是屏幕的四個(gè)頂點(diǎn)游沿,這個(gè)是View怎么滑動(dòng)都改變不了的,這個(gè)場(chǎng)景中View的邊緣就是屏幕的四條邊肮砾,不滑動(dòng)時(shí)诀黍,這個(gè)View的邊緣和View的內(nèi)容邊緣是重合的。當(dāng)我下拉時(shí)仗处,View向下移動(dòng)蔗草,此時(shí)View的邊緣還是屏幕的四條邊,而View內(nèi)容的上邊緣變了疆柔。按照上面說(shuō)的規(guī)則咒精,此時(shí)View的上邊緣在View內(nèi)容上邊緣的上邊,即從上往下滑動(dòng)時(shí)旷档,mScrollY是負(fù)值模叙,同理推得從左往右滑動(dòng)時(shí),mScrollX也是負(fù)值鞋屈。

通過(guò)使用動(dòng)畫實(shí)現(xiàn)

使用平移動(dòng)畫主要是操作View的translationXtranslationY這兩個(gè)屬性范咨,那我們就有兩種選擇了,傳統(tǒng)的補(bǔ)間動(dòng)畫和屬性動(dòng)畫厂庇,如果使用屬性動(dòng)畫渠啊,為了兼容Android3.0,需要采用開源動(dòng)畫庫(kù)nineoldandroids权旷。

在3.0以下系統(tǒng)的手機(jī)上通過(guò)nineoldandroids實(shí)現(xiàn)的屬性動(dòng)畫本質(zhì)上還是補(bǔ)間動(dòng)畫替蛉。

之前的兩篇關(guān)于動(dòng)畫的文章中有實(shí)例平移動(dòng)畫的,這里就不貼代碼了拄氯。

記得那時(shí)候有個(gè)問(wèn)題不是很理解躲查,就是當(dāng)使用補(bǔ)間動(dòng)畫來(lái)對(duì)View做平移或者縮放時(shí),加入View上有點(diǎn)擊事件译柏,那么這個(gè)有效點(diǎn)擊區(qū)域還是原來(lái)View原始的位置區(qū)域镣煮,現(xiàn)在更加容易理解了,因?yàn)閂iew的尺寸都沒(méi)改變鄙麦,如果是使用屬性動(dòng)畫典唇,那么這個(gè)有效點(diǎn)擊區(qū)域就變了。

通過(guò)改變View的LayoutParams

這種方式其實(shí)就是改變View的外邊距胯府,也就是margin值:

ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) v.getLayoutParams();
params.leftMargin += 10;
params.width += 10;
v.setLayoutParams(params);
// 或者
//v.requestLayout();

幾種方式的對(duì)比

  • scrollTo/scrollBy:優(yōu)點(diǎn)是它是View提供的原生方法介衔,專門用于View的滑動(dòng),可以比較方便地實(shí)現(xiàn)滑動(dòng)效果且不影響內(nèi)部元素的點(diǎn)擊事件盟劫;缺點(diǎn)是只能滑動(dòng)View的內(nèi)容夜牡,并不能滑動(dòng)View本身。

  • 動(dòng)畫方式:如果是Android3.0以上且采用屬性動(dòng)畫,則沒(méi)有明顯的缺點(diǎn)塘装,但如果采用補(bǔ)間動(dòng)畫或者在Android3.0以下的版本使用屬性動(dòng)畫急迂,則不能改變View本身的屬性。如果動(dòng)畫元素不需要響應(yīng)用戶的交互蹦肴,使用動(dòng)畫來(lái)滑動(dòng)比較合適僚碎,否則就不太合適。動(dòng)畫有一個(gè)很明顯的優(yōu)勢(shì)就是一些復(fù)雜的滑動(dòng)效果必須使用動(dòng)畫才能實(shí)現(xiàn)阴幌。

  • 改變布局方式:除了使用比較麻煩勺阐,沒(méi)有明顯的缺點(diǎn),主要適用對(duì)象是一些具有交互性的View矛双。

Demo:采用動(dòng)畫的方式實(shí)現(xiàn)View的全屏滑動(dòng)

思路就是重寫View的onTouchEvent方法渊抽,處理其中的ACTION_MOVE事件:

private int mLastX, mLastY;

@Override
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;
            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;
    }

    mLastX = x;
    mLastY = y;

    return true;
}

首先我們通過(guò)event.getRawX()和event.getRawY()獲取手指當(dāng)前的坐標(biāo),不能使用getX/getY方法议忽,因?yàn)槭且粱瑒?dòng)懒闷,所以需要獲取當(dāng)前點(diǎn)擊事件在屏幕中的坐標(biāo)而不是相對(duì)于View本身的坐標(biāo)。然后計(jì)算出兩次滑動(dòng)之間的距離栈幸,通過(guò)nineoldandroids庫(kù)提供的setTranslationX/setTranslationY方法來(lái)實(shí)現(xiàn)愤估。

nineoldandroids的jar包下載地址:https://github.com/JakeWharton/NineOldAndroids/downloads

彈性滑動(dòng)

實(shí)現(xiàn)彈性滑動(dòng)的思路就是將一次大的滑動(dòng)分成若干次小的滑動(dòng)在一段時(shí)間內(nèi)完成,實(shí)現(xiàn)方式有Scroller速址、Handler#postDelayed玩焰、Thread#sleep等。

使用Scroller

其實(shí)上面已經(jīng)貼過(guò)使用Scroller實(shí)現(xiàn)滑動(dòng)的核心代碼:

mScroller = new Scroller(context);

private void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    mScroller.startScroll(scrollX, 0, delta, 0, 1000);
    invalidate();
}


@Override
public void computeScroll() {
    if (mScroller.computeScrollOffset()) {
        scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
        postInvalidate();
    }
}

當(dāng)我們構(gòu)建了一個(gè)Scroller并調(diào)用其startScroll方法時(shí)芍锚,Scroller內(nèi)部其實(shí)什么都沒(méi)做昔园,它只是保存了我們傳進(jìn)去的幾個(gè)參數(shù):

/**
 * Start scrolling by providing a starting point, the distance to travel,
 * and the duration of the scroll.
 * 
 * @param startX Starting horizontal scroll offset in pixels. Positive
 *        numbers will scroll the content to the left.
 * @param startY Starting vertical scroll offset in pixels. Positive numbers
 *        will scroll the content up.
 * @param dx Horizontal distance to travel. Positive numbers will scroll the
 *        content to the left.
 * @param dy Vertical distance to travel. Positive numbers will scroll the
 *        content up.
 * @param duration Duration of the scroll in milliseconds.
 */
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;
}

這里面startX、startY表示滑動(dòng)的起點(diǎn)闹炉,dx蒿赢、dy表示要滑動(dòng)的距離,duration表示滑動(dòng)的時(shí)間渣触,需要注意的是這里的滑動(dòng)是指View內(nèi)容的滑動(dòng)而不是View本身位置的改變。所以僅僅調(diào)用startScroll時(shí)無(wú)法讓View滑動(dòng)的壹若,因?yàn)樗鼉?nèi)部沒(méi)有做滑動(dòng)相關(guān)的事嗅钻。使View滑動(dòng)的真正起作用的代碼是invalidate方法,調(diào)用invalidate表示要重繪View店展,在View的draw方法中又會(huì)去調(diào)用computeScroll方法养篓,computeScroll在View中是一個(gè)空實(shí)現(xiàn),因此需要我們自己去動(dòng)手實(shí)現(xiàn)赂蕴。正是因?yàn)檫@個(gè)computeScroll方法柳弄,View才能實(shí)現(xiàn)彈性滑動(dòng)。過(guò)程是這樣的:當(dāng)View重繪后再draw方法中調(diào)用computeScroll方法,而computeScroll又會(huì)去向Scroller獲取當(dāng)前的scrollX和scrollY碧注,然后通過(guò)scrollTo實(shí)現(xiàn)滑動(dòng)嚣伐,接著又調(diào)用postInvalidate方法再進(jìn)行一次重繪,和上次一樣萍丐,使View滑動(dòng)到新的位置轩端,一直重復(fù)這個(gè)過(guò)程,知道滑動(dòng)結(jié)束逝变。

下面是Scroller的computeScrollOffset方法:

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */ 
public boolean computeScrollOffset() {
    if (mFinished) {
        return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    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;
            ...
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        mFinished = true;
    }
    return true;
}

這個(gè)方法會(huì)根據(jù)時(shí)間的流逝計(jì)算出當(dāng)前的scrollX和scrollY基茵,計(jì)算方法是根據(jù)時(shí)間流逝的百分比計(jì)算出scrollX和scrollY改變的百分比并計(jì)算出當(dāng)前的值,是不是感覺(jué)和動(dòng)畫中的插值器類似壳影?沒(méi)錯(cuò)拱层,這里正是使用了叫ViscousFluidInterpolator插值器:代碼如下:

static class ViscousFluidInterpolator implements Interpolator {
    /** Controls the viscous fluid effect (how much of it). */
    private static final float VISCOUS_FLUID_SCALE = 8.0f;

    private static final float VISCOUS_FLUID_NORMALIZE;
    private static final float VISCOUS_FLUID_OFFSET;

    static {

        // must be set to 1.0 (used in viscousFluid())
        VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
        // account for very small floating-point error
        VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
    }

    private static float viscousFluid(float x) {
        x *= VISCOUS_FLUID_SCALE;
        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);
        }
        return x;
    }

    @Override
    public float getInterpolation(float input) {
        final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
        if (interpolated > 0) {
            return interpolated + VISCOUS_FLUID_OFFSET;
        }
        return interpolated;
    }
}

這個(gè)插值器的實(shí)現(xiàn)過(guò)程我們先不管,繼續(xù)看computeScrollOffset方法宴咧,當(dāng)它的返回值是true時(shí)表示滑動(dòng)還未結(jié)束舱呻,此時(shí)還要繼續(xù)滑動(dòng),當(dāng)返回false時(shí)表示滑動(dòng)結(jié)束悠汽。

總結(jié):Scroller的工作原理

Scroller本身并不能實(shí)現(xiàn)View的滑動(dòng)箱吕,而是要結(jié)合View的computeScroll方法才能完成彈性滑動(dòng)的效果,它不斷讓View去重繪柿冲,而每一次重繪距滑動(dòng)起始時(shí)間會(huì)有一個(gè)時(shí)間間隔茬高,通過(guò)這個(gè)時(shí)間間隔就可以計(jì)算出View當(dāng)前的滑動(dòng)位置,知道了滑動(dòng)位置就可以通過(guò)scrollTo方法來(lái)完成View的滑動(dòng)假抄。

延時(shí)策略

延時(shí)策略實(shí)現(xiàn)彈性滑動(dòng)的核心思想就是通過(guò)發(fā)送一系列延時(shí)消息來(lái)達(dá)到彈性的效果怎栽,具體可以使用Handler.postDelayed方法或者Thread.sleep方法。對(duì)于postDelayed方法宿饱,不斷的發(fā)送延時(shí)消息熏瞄,就可以在消息中進(jìn)行View的移動(dòng),從而形成彈性滑動(dòng)谬以,而對(duì)于sleep方法强饮,使用while循環(huán)不斷的滑動(dòng)View然后sleep,同樣可以達(dá)到彈性的效果为黎。

使用動(dòng)畫

動(dòng)畫本身就有一個(gè)duration屬性邮丰,相當(dāng)于它滑動(dòng)時(shí)自帶了彈性效果,這部分在上面已經(jīng)說(shuō)過(guò)了铭乾,就不再贅述了剪廉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市炕檩,隨后出現(xiàn)的幾起案子斗蒋,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泉沾,死亡現(xiàn)場(chǎng)離奇詭異捞蚂,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)爆哑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門洞难,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人揭朝,你說(shuō)我怎么就攤上這事队贱。” “怎么了潭袱?”我有些...
    開封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵柱嫌,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我屯换,道長(zhǎng)编丘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任彤悔,我火速辦了婚禮嘉抓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘晕窑。我一直安慰自己抑片,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開白布杨赤。 她就那樣靜靜地躺著敞斋,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疾牲。 梳的紋絲不亂的頭發(fā)上植捎,一...
    開封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音阳柔,去河邊找鬼焰枢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盔沫,可吹牛的內(nèi)容都是我干的医咨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼架诞,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了干茉?” 一聲冷哼從身側(cè)響起谴忧,我...
    開封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后沾谓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體委造,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年均驶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昏兆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡妇穴,死狀恐怖爬虱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腾它,我是刑警寧澤跑筝,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站瞒滴,受9級(jí)特大地震影響曲梗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妓忍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一虏两、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧世剖,春花似錦定罢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至境蜕,卻和暖如春蝙场,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背粱年。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工售滤, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人台诗。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓完箩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親拉队。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弊知,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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