閒聊自定義控件之View滑動

Android的滾動(滑動)有很多種實現方式,如動態改變佈局參數,屬性動畫,Scroller等。這些方式大多是通過View的座標改變來實現的。

佈局參數相關實現方式

這一部分主要包括layout()方法以及設置LayoutParams兩種方式。
layout()方法實際就是直接控制View座標的四個點來實現的,上一篇文章(閒聊自定義控件之滑動衝突)中就是使用的這種方式。具體如下:

case MotionEvent.ACTION_MOVE:
    xMove = x;
    yMove = y;
    onceMoveX = xMove - xDown;
    onceMoveY = yMove - yDown;
    layout(getLeft() + onceMoveX, getTop() + onceMoveY, getRight() + onceMoveX, getBottom() + onceMoveY);
    break;

設置LayoutParams這種方式跟layout()方法類似,只是可以單獨控制任何一個參數,如果只要左右移動的話,只需要修改其leftMargin 參數即可。具體如下:

   LinearLayout.LayoutParams layoutParams =(LinearLayout.LayoutParams)getLayoutParams();
    layoutParams.leftMargin = getLeft() + onceMoveX;
    setLayoutParams(layoutParams);

offsetLeftAndRight()和offsetTopAndBottom()方式

這兩種方式其實跟上面兩種使用起來沒有太大區別,只需要將layout()方法或者設置LayoutParams參數的代碼替換爲一下即可。具體如下:

case MotionEvent.ACTION_MOVE:
    xMove = x;
    yMove = y;
    onceMoveX = xMove - xDown;
    onceMoveY = yMove - yDown;
    offsetLeftAndRight(onceMoveX);
    offsetTopAndBottom(onceMoveY);
break;

scrollTo相關——scrollBy、scrollTo、Scroller

scrollBy、scrollTo和Scroller雖然是官方提供給我們的三個不同的API用於View的滑動,但scrollBy和Scroller內部也是通過scrollTo實現的。
使用scrollBy實現滑動過程如下:

case MotionEvent.ACTION_MOVE:
    xMove = x;
    yMove = y;
    onceMoveX = xMove - xDown;
    onceMoveY = yMove - yDown;
    ((View) getParent()).scrollBy(-onceMoveX,-onceMoveY);  
    break;

scrollBy方法的內部實現如下:

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

通過上段代碼就可以明顯看出scrollBy和scrollTo的明顯關係了,當我們已知需要滾動的距離時可以通過scrollBy來實現,已知滾動的目的地時用scrollTo來實現。

需要注意的是scrollBy以及scrollTo的滾動方向,它與上面的幾種方式相反,這是因爲它移動的是內容,而不是控件本身,例如移動listview中的item。

Scroller跟上面兩種方式實現的效果有點差異,scrollBy和scrollTo是瞬間滑動,而Scroller則爲平滑移動。Scroller使用時需要通過其構造方法創建,其構造方法有三個如下:

 /**
     * Create a Scroller with the default duration and interpolator.
     */
    public Scroller(Context context) {
        this(context, null);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. Specify whether or
     * not to support progressive "flywheel" behavior in flinging.
     */
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

一般我們使用第一個構造方法,使用默認的插值器配置,當然如果需要特殊的滑動效果可以嘗試選用其它或者自定義插值器來實現。創建好Scroller就可以通過其startScroll方法以及View的computeScroll方法來完成滾動。Scroller的startScroll方法如下:

  /**
     * Start scrolling by providing a starting point and the distance to travel.
     * The scroll will use the default value of 250 milliseconds for the
     * duration.
     * 
     * @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.
     */
    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }

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

從上面的代碼中可以看到startScroll方法僅僅對一些變量進行了賦值,並沒有過多的操作,那滑動操作是怎樣完成的呢?這個過程需要對View的繪製有所瞭解。因爲在調用完startScroll方法後需要接着調用invalidate方法通知View進行重繪,而重繪的過程中會調用View的computeScroll方法。但這個方法在View中僅僅是個空方法,沒有進行任何內容的填充,我們要實現滑動過程就得重寫這個方法,下面是一個能夠完成滑動操作的computeScroll的一種常見寫法:

 @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            ((View) getParent()).scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

從上面的代碼可以看到一個熟悉的身影——scrollTo。但是,它是怎樣通過一個瞬間移動的方法來實現平滑移動的呢?其實,下面的invalidate方法已經告訴了我們一切:每次移動一小段,然後通知重繪,然後移動一小段,然後……,最終給我們一個視覺效果就是一個平滑過渡的過程。而這一小段距離在於mCurrX和mCurrY的改變,這個改變過程是通過computeScrollOffset方法利用插值器獲得的,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;
            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;
    }

這個方法比較容易理解,先用插值器計算出運行所佔的比例,然後乘以要移動的總距離( mDeltaX、 mDeltaY)計算出應該移動的距離,然後加上初始值(mStartX、mStartY),即得到了當前的值(mCurrX、mCurrY)。
爲了便於理解並掌握用法,做了一個演示demo,效果圖如下:
在這裏插入圖片描述
Demo中自定義了一個ScrollerTextView,它繼承自TextView,然後通過創建Scroller,重寫computeScroll以及調用startScroll方法完成了平滑移動的功能,代碼比較簡單,感興趣的可以自己拉下了看看。項目地址

動畫方式

熟悉動畫的同學都知道,補間動畫和屬性動畫都能夠實現View的移動,但補間動畫實質上並沒有改變View的座標,其響應事件也不會跟着移動,因此補間動畫在Android 3.0之後已經逐漸被屬性動畫替代。
屬性動畫是一個比較強大的動畫框架,種類、用法較多,所需篇幅較大,後期會有一篇專門的文章進行講解。下面是一個常用的能夠實現View的移動的屬性動畫代碼:

ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 300);  
animator.setDuration(500);  
animator.start(); 

這個代碼非常容易理解,就是將View在500ms內在X方向上移動300個像素點。

ViewDragHelper

ViewDragHelper這種方式可能大家也經常碰到,它能幫助我們用簡單的方式實現複雜的效果,限於篇幅,後面寫一個專門的文章進行講解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章