1. 程式人生 > >手勢滑動Activity之常見效果-- Slide,陰影效果與動畫實現

手勢滑動Activity之常見效果-- Slide,陰影效果與動畫實現

轉自:http://blog.csdn.net/jxxfzgy/article/details/44266163

1.手勢滑動基礎

喜歡聽音樂的朋友可能都看過天天動聽這款 app, 這款 app 有一個亮點就是在切換頁面(Fragment)的時候可以通過手勢滑動來結束當前頁面,這裡先說一下,我為什麼會這麼關心這個功能呢,因為前兩天 PM說我們即將開始做的這款app 也要實現頁面能通過手勢滑動來結束的功能,所以我就拿著這款 app 滑了一上午;但是我要實現的跟天天動聽這款 app又有點不同,細心觀察的朋友可能會發現,天天動聽是 Fragment 之間的切換,而我這裡要實現的是 Activity 之間的切換,不過,不管是哪種,最終效果都是一樣,就是頁面能隨著手勢的滑動而滑動,最終達到某個特定條件,結束此頁面。 
要實現這個功能其實也不是特別難,這裡我把這個功能的實現分為了以下兩個步驟: 
1、識別手勢滑動自定義ViewGroup 的實現 
2、實現自定義 ViewGroup 和 Activity 繫結

根據以上兩個步驟,我們發現,這其中涉及到的知識點有:Android 事件處理機制、自定義 View(ViewGroup)的實現,Activity Window的知識,在開發的過程中還涉及到Activity 主題的配置。Android 事件處理和自定義 View 都在我前面的 blog 中有講到,如果不瞭解的朋友可以去看看。下面開始按步驟來實現功能

一、自定義 ViewGroup 
這個 ViewGroup 的功能只要是對事件的攔截,能夠實現手勢滑動效果;顯示 Activity 的內容包括 ActionBar 和內容區。 
1、實現測量和佈局

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*獲取預設的寬度*/
        int width = getDefaultSize(0, widthMeasureSpec);
        /*獲取預設的高度*/
        int height = getDefaultSize(0, heightMeasureSpec);
        /*設定ViewGroup 的寬高*/
        setMeasuredDimension(width, height);
        /*獲取子 View 的寬度*/
        final int contentWidth = getChildMeasureSpec(widthMeasureSpec, 0, width);
        /*獲取子View 的高度*/
        final int contentHeight = getChildMeasureSpec(heightMeasureSpec, 0, height);
        /*設定子View 的大小*/
        mContent.measure(contentWidth, contentHeight);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = r - l;
        final int height = b - t;
        mContent.layout(0, 0, width, height);
    }
<span style="font-family: 'microsoft yahei'; background-color: rgb(255, 255, 255);">因為每個 Activity 都只有一個 Layout,所以這裡只有一個子 View,佈局和測量就顯得非常簡單。</span>

2、事件攔截

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnable) {
            return false;
        }
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
                || action != MotionEvent.ACTION_DOWN && mIsUnableToDrag) {
            /*結束手勢的滑動,不攔截*/
            endToDrag();
            return false;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                /*計算 x,y 的距離*/
                int index = MotionEventCompat.getActionIndex(ev);
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                if (mActivePointerId == INVALID_POINTER)
                    break;
                mLastMotionX = mInitialMotionX = MotionEventCompat.getX(ev, index);
                mLastMotionY = MotionEventCompat.getY(ev, index);
                /*這裡判讀,如果這個觸控區域是允許滑動攔截的,則攔截事件*/
                if (thisTouchAllowed(ev)) {
                    mIsBeingDragged = false;
                    mIsUnableToDrag = false;
                } else {
                    mIsUnableToDrag = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                /*繼續判斷是否需要攔截*/
                determineDrag(ev);
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_POINTER_UP:
                /*這裡做了對多點觸控的處理,當有多個手指觸控的時候依然能正確的滑動*/
                onSecondaryPointerUp(ev);
                break;

        }
        if (!mIsBeingDragged) {
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(ev);
        }
        return mIsBeingDragged;
    }

事件攔截,是攔截而是其不會向子 View 分發,直接執行本級 View的 onTouchEvent方法;

3、事件處理

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnable) {
            return false;
        }
        if (!mIsBeingDragged && !thisTouchAllowed(event))
            return false;
        final int action = event.getAction();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        switch (action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                /*按下則結束滾動*/
                completeScroll();
                int index = MotionEventCompat.getActionIndex(event);
                mActivePointerId = MotionEventCompat.getPointerId(event, index);
                mLastMotionX = mInitialMotionX = event.getX();
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                /*有多個點按下的時候,取最後一個按下的點為有效點*/
                final int indexx = MotionEventCompat.getActionIndex(event);
                mLastMotionX = MotionEventCompat.getX(event, indexx);
                mActivePointerId = MotionEventCompat.getPointerId(event, indexx);
                break;

            }
            case MotionEvent.ACTION_MOVE:
                if (!mIsBeingDragged) {
                    determineDrag(event);
                    if (mIsUnableToDrag)
                        return false;
                }
                /*如果已經是滑動狀態,則根據手勢滑動,而改變View 的位置*/
                if (mIsBeingDragged) {
                    // 以下程式碼用來判斷和執行View 的滑動
                    final int activePointerIndex = getPointerIndex(event, mActivePointerId);
                    if (mActivePointerId == INVALID_POINTER)
                        break;
                    final float x = MotionEventCompat.getX(event, activePointerIndex);
                    final float deltaX = mLastMotionX - x;
                    mLastMotionX = x;
                    float oldScrollX = getScrollX();
                    float scrollX = oldScrollX + deltaX;
                    final float leftBound = getLeftBound();
                    final float rightBound = getRightBound();
                    if (scrollX < leftBound) {
                        scrollX = leftBound;
                    } else if (scrollX > rightBound) {
                        scrollX = rightBound;
                    }

                    mLastMotionX += scrollX - (int) scrollX;
                    scrollTo((int) scrollX, getScrollY());

                }
                break;
            case MotionEvent.ACTION_UP:
                /*如果已經是滑動狀態,擡起手指,需要判斷滾動的位置*/
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaxMunVelocity);
                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                            velocityTracker, mActivePointerId);
                    final int scrollX = getScrollX();
                    final float pageOffset = (float) (-scrollX) / getContentWidth();
                    final int activePointerIndex = getPointerIndex(event, mActivePointerId);
                    if (mActivePointerId != INVALID_POINTER) {
                        final float x = MotionEventCompat.getX(event, activePointerIndex);
                        final int totalDelta = (int) (x - mInitialMotionX);
                        /*這裡判斷是否滾動到下一頁,還是滾回原位置*/
                        int nextPage = determineTargetPage(pageOffset, initialVelocity, totalDelta);
                        setCurrentItemInternal(nextPage, true, initialVelocity);
                    } else {
                        setCurrentItemInternal(mCurItem, true, initialVelocity);
                    }
                    mActivePointerId = INVALID_POINTER;
                    endToDrag();
                } else {
//                    setCurrentItemInternal(0, true, 0);
                    endToDrag();
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                /*這裡有事多點處理*/
                onSecondaryPointerUp(event);
                int pointerIndex = getPointerIndex(event, mActivePointerId);
                if (mActivePointerId == INVALID_POINTER)
                    break;
                mLastMotionX = MotionEventCompat.getX(event, pointerIndex);
                break;
        }

        return true;
    }

因為這裡加入了多點控制,所以程式碼看起來有點複雜,其實原理很簡單,就是不斷的判斷是否符合滑動的條件。其他就不細講了,來看看這個自定義 ViewGroup 的效果 

這裡寫圖片描述

可以看到,這裡我們已經實現了手勢識別的 ViewGroup,其實這個ViewGroup如果發揮想象,它能實現很多效果,不單單是我今天要講的效果,還可以用作側拉選單,或者是做 QQ5.0版本側滑效果都可以實現的。

二、側滑 View繫結 Activity 
這裡為了程式碼的簡潔,還是通過一個 ViewGroup 來封裝了一層。

/**
 * Created by moon.zhong on 2015/3/13.
 */
public class SlidingLayout extends FrameLayout {
    /*側滑View*/
    private SlidingView mSlidingView ;
    /*需要側滑結束的Activity*/
    private Activity mActivity ;

    public SlidingLayout(Context context) {
        this(context, null);
    }

    public SlidingLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlidingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSlidingView = new SlidingView(context) ;
        addView(mSlidingView);
        mSlidingView.setOnPageChangeListener(new SlidingView.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                if (position == 1){                    Log.v("zgy","========position=========") ;
                    mActivity.finish();
                }
            }
            @Override
            public void onPageSelected(int position) {
            }
        });
        mActivity = (Activity) context;
        bindActivity(mActivity) ;
    }

    /**
     * 側滑View 和Activity 繫結
     * @param activity
     */
    private void bindActivity(Activity activity){
        /*獲取Activity 的最頂級ViewGroup*/
        ViewGroup root = (ViewGroup) activity.getWindow().getDecorView();
        /*獲取Activity 顯示內容區域的ViewGroup,包行ActionBar*/
        ViewGroup child = (ViewGroup) root.getChildAt(0);
        root.removeView(child);
        mSlidingView.setContent(child);
        root.addView(this);
    }
}


測試 Activity 這事就變的非常簡單了

public class SecondActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        /*繫結Activity*/
        new SlidingLayout(this) ;
    }

}


來看看效果怎麼樣: 

這裡寫圖片描述 
咦!能滑動結束頁面,但為什麼邊滑走的同時看不到第一個 Acitivity,而是要等結束了才能看到呢?我們猜測,應該是滑動的時候,這個 Activity 還有哪裡把第一個 Activity 覆蓋了,每個 Activity 都是附在一個 Window 上面,所以這裡就涉及到一個 Activity 的 window背景顏色問題, OK,把第二個 Activity 的 window 背景設為透明

 <style name="TranslucentTheme" parent="AppTheme">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:windowContentOverlay">@null</item>
    </style>
        <activity android:name=".SecondActivity"
                  android:label="SecondActivity"
                  android:screenOrientation="portrait"
                  android:theme="@style/TranslucentTheme"
            />

再來看看效果,效果圖: 

這裡寫圖片描述

完美實現! 


2.手勢滑動Activity應用 -- 陰影效果與動畫實現

上面講了如何實現手勢滑動來銷燬頁面,再來回顧一下實現的效果 

這裡寫圖片描述 
具體實現請看上一篇文章 手勢滑動結束 Activity(一)基本功能的實現,不過這只是實現了最基本的功能,還有很多地方需要優化和完善的,這篇文章主要是在原來實現的基礎上做優化和特效; 
先來看效果:

1、效果圖1:側滑顯示陰影

這裡寫圖片描述

2、效果圖2:改變滑動動畫效果

這裡寫圖片描述

先來看看效果圖1的實現方式: 
根據效果圖,我們應該能想到他的實現原理,就是在手勢不斷的滑動的同時繪製已經滑走部分的顏色,並且不斷改變透明度。 
所以我們需要重寫 ViewGroup 中的 drawShadow(Canvas canvas) 方法,在裡面繪製我們要顯示的遮罩層,

 /**
     * 繪製背景顏色,隨著距離的改變而改變
     * @param canvas
     */
    private void drawBackground(Canvas canvas) {
        mShadowPaint.setAlpha((int) ((1 - mRatio) * 180));
        canvas.drawRect(-mPix, 0, 0, getHeight(), mShadowPaint);
    }

這裡有三個變數我們還不知道的,mRatio、mPix、mShadowPaint; 

1、mShadowPaint:是繪製陰影的畫筆,這個我們可以直接在初始化的時候 new 出來,並設定畫筆的顏色為黑色,畫出來的圖就是黑色的。

  mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mShadowPaint.setColor(0xff000000);

2、mRatio:滑動的梯度值,但開始滑動的時候這個值為0,而滑動到最大值的時候,也就是 Activity 頁面小時不見了時,這個值為1,所以這個值的範圍0.0f~1.0f。所以通過一下程式碼來改變畫筆的透明度,使得在滑動的過程中,陰影層會逐漸變化

mShadowPaint.setAlpha((int) ((1 - mRatio) * 180));

3、mPix:這個變數是滑動距離原始位置的距離。通俗點講就是已經滑動了多遠的距離,可以通過這個值來確定繪製陰影部分的面積
canvas.drawRect(-mPix, 0, 0, getHeight(), mShadowPaint);

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mShouldDraw){
            drawBackground(canvas);
        }
    }

以上程式碼只是繪製了滑動部分的陰影,如果仔細看了效果圖1的朋友會發現在被滑走頁面的邊緣也存在一條漸變的陰影,這又怎麼繪製呢? 

首先,定義一個 Drawable 資原始檔left_shadow.xml

<?xml version="1.0" encoding="utf-8"?>
    <!--形狀為矩形-->
<shape android:shape="rectangle"
  xmlns:android="http://schemas.android.com/apk/res/android">
    <!--顏色漸變範圍-->
    <gradient android:startColor="#00000000" android:endColor="#33000000" />
</shape>

接著繪製 Drawable,在繪製 Drawable 之前先要設定其大小
 /**
     * 繪製shadow陰影
     * @param canvas
     */
    private void drawShadow(Canvas canvas){
        /*儲存畫布當前的狀態,這個用法在我前面將自定義View 的時候將的很詳細*/
        canvas.save() ;
        /*設定 drawable 的大小範圍*/
        mLeftShadow.setBounds(0, 0, mShadowWidth, getHeight());
        /*讓畫布平移一定距離*/
        canvas.translate(-mShadowWidth,0);
        /*繪製Drawable*/
        mLeftShadow.draw(canvas);
        /*恢復畫布的狀態*/
        canvas.restore();
    }

所以dispatchDraw(Canvas canvas)的方法實現為
@Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mShouldDraw){
            /*繪製偏移的背影顏色*/
            drawBackground(canvas);
            /*繪製邊緣的陰影*/
            drawShadow(canvas) ;
        }
    }

到這裡就完成了效果圖1的功能實現。

滑動動畫的實現: 
在實現可滑動的自定義 ViewGroup 的時候,我把滑動的距離做了一個介面回調出來,類似 ViewPage控制元件的 PageChange 監聽事件,這裡也對頁面滑動做了滑動監聽,雖然只有一頁,但是有了這個監聽事件,對做很多操作都十分方便,比如我們前面講到的 mRatio、mPix 都可以直接通過監聽滑動事件來獲取,介面定義如下:

 public interface OnPageChangeListener {

        /*滑動頁面滑動狀態,當前頁和頁面的偏移梯度,頁面的偏移位置*/
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
        /*當前頁面*/
        public void onPageSelected(int position);
    }

介面呼叫通過重寫scrollTo(int x, int y)方法來獲取滑動監聽的引數

    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo(x, y);
        /*設定回撥事件的值*/
        pageScrolled(x);
    }

    private void pageScrolled(int xpos) {
        final int widthWithMargin = getWidth();
        /*獲取當前頁面*/
        int position = Math.abs(xpos) / widthWithMargin;
        /*獲取當前滑動的距離*/
        final int offsetPixels = Math.abs(xpos) % widthWithMargin;
        /*通過滑動的距離來獲取梯度值*/
        final float offset = (float) offsetPixels / widthWithMargin;
        /*這裡需要做特殊處理,因為只有一個頁面*/
        position = mIsBeingDragged ? 0 : position;
        onPageScrolled(position, offset, offsetPixels);
    }

    protected void onPageScrolled(int position, float offset, int offsetPixels) {
        if (mListener != null) {
            mListener.onPageScrolled(position, offset, offsetPixels);
        }
        mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
    }

動畫效果的實現我這裡做了處理,放在了 SlidingLayout類中實現
/**
 * Created by moon.zhong on 2015/3/13.
 */
public class SlidingLayout extends FrameLayout {

    /*可根據手勢滑動的View*/
    private SlidingView mSlidingView ;
    /*需要繫結的Activity*/
    private Activity mActivity ;
    /*滑動View 的滑動監聽*/
    private SlidingView.OnPageChangeListener mPageChangeListener ;
    /*這個是當Activity 滑動到可以結束的時候用到的常量*/
    private final int POSITION_FINISH = 1 ;
    /*頁面的View*/
    private View mContextView  ;
    /*用於定製自己的動畫效果的介面*/
    private OnAnimListener mAnimListener ;

    public SlidingLayout(Context context) {
        this(context, null);
    }

    public SlidingLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlidingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        /*初始化可滑動的View*/
        mSlidingView = new SlidingView(context) ;
        /*吧可滑動的View 新增到當前Layout 中*/
        addView(mSlidingView);
        /*設定滑動監聽*/
        mPageChangeListener = new SlidingOnPageChangeListener() ;
        mSlidingView.setOnPageChangeListener(mPageChangeListener);
        mActivity = (Activity) context;
        /*繫結Activity 到可滑動的View 上面*/
        bindActivity(mActivity) ;
    }

    /**
     * 側滑View 和Activity 繫結
     * @param activity
     */
    private void bindActivity(Activity activity){
        /*獲取Activity 的最頂級ViewGroup*/
        ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        /*獲取Activity 顯示內容區域的ViewGroup,包行ActionBar*/
        ViewGroup child = (ViewGroup) decorView.getChildAt(0);
        decorView.removeView(child);
        setContentView(child) ;
        decorView.addView(this);
    }

    private void setContentView(View view){
        mContextView = view ;
        mSlidingView.setContent(view);

    }
    private class SlidingOnPageChangeListener implements SlidingView.OnPageChangeListener{
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            /*到達了結束Activity 的條件*/
            if (position == POSITION_FINISH){
                /*結束當前 */
                mActivity.finish();
            }
            if (mAnimListener != null){
                mAnimListener.onAnimationSet(mContextView,positionOffset,positionOffsetPixels);
            }
        }

        @Override
        public void onPageSelected(int position) {

        }
    }

    /**
     * 設定自己定義的動畫
     * @param listener
     */
    public void setOnAnimListener(OnAnimListener listener){
        mAnimListener = listener ;
        mSlidingView.setShouldDraw(false);
    }

    /**
     * 定製動畫的介面
     */
    public interface OnAnimListener {
        /**
         * 重寫這個方法來定製動畫
         * @param view
         * @param offSet
         * @param offSetPix
         */
        void onAnimationSet(View view, float offSet,int offSetPix) ;
    }

    /**
     * 預設的動畫效果
     */
    public static class SimpleAnimImpl implements OnAnimListener{
        private final int MAX_ANGLE = 25 ;
        @Override
        public void onAnimationSet(View view, float offSet,int offSetPix) {
            view.setPivotX(view.getWidth()/2.0F);
            view.setPivotY(view.getHeight());
            View.ROTATION.set(view,MAX_ANGLE*offSet);
        }
    }

}

Activity 中應用:

public class SecondActivity extends ActionBarActivity {;

    SlidingLayout mSlidingLayout ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mSlidingLayout = new SlidingLayout(this);
        /*設定預設的動畫效果*/
        mSlidingLayout.setOnAnimListener(new SlidingLayout.SimpleAnimImpl());
    }

}

執行就是我們看到的效果圖2的動畫效果;
如果想要自定義動畫該怎麼做呢,只要在 Activity 中設定

mSlidingLayout.setOnAnimListener(OnAnimListener anim)

anim需要自己實現,來看看。


public class SecondActivity extends ActionBarActivity {;

    SlidingLayout mSlidingLayout ;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        mSlidingLayout = new SlidingLayout(this) ;
        /*設定自己定義的動畫效果*/
        mSlidingLayout.setOnAnimListener(new SlidingLayout.OnAnimListener() {
            final int MAX_DEGREE = 180 ;
            @Override
            public void onAnimationSet(View view, float offSet, int offSetPix) {
                view.setPivotX(view.getWidth()/2.0F);
                view.setPivotY(view.getHeight() / 2.0F);
                View.TRANSLATION_X.set(view, (float) -offSetPix);
                View.ROTATION_Y.set(view,MAX_DEGREE*offSet);
                view.setAlpha((1-offSet));
            }
        });

    }

}

執行效果 

這裡寫圖片描述

下面把可滑動的自定義 ViewGroup 的程式碼貼出來

/**
 * Created by moon.zhong on 2015/3/13.
 */
public class SlidingView extends ViewGroup {

    /*無效的點*/
    private static final int INVALID_POINTER = -1;
    /*滑動動畫執行的時間*/
    private static final int MAX_SETTLE_DURATION = 600; // ms
    /*最小滑動距離,結合加速度來判斷需要滑動的方向*/
    private static final int MIN_DISTANCE_FOR_FLING = 25; // dips
    /*定義了一個時間插值器,根據ViewPage控制元件來定義的*/
    private static final Interpolator sInterpolator = new Interpolator() {
        public float getInterpolation(float t) {
            t -= 1.0f;
            return t * t * t * t * t + 1.0f;
        }
    };

    /*內容View*/
    private View mContent;
    /*是否開始滑動*/
    private boolean mIsBeingDragged;
    /*是否停止滑動*/
    private boolean mIsUnableToDrag;
    /*自動滾動工具*/
    private Scroller mScroller;
    /*判斷是否已經在滾動*/
    private boolean mScrolling;
    /*共外面呼叫的監聽事件*/
    private OnPageChangeListener mListener;
    /*內部監聽事件*/
    private OnPageChangeListener mInternalPageChangeListener;

    /*記錄上一次手指觸控的點*/
    private float mLastMotionX;
    private float mLastMotionY;
    /*記錄最初觸控的點*/
    private float mInitialMotionX;

    /*當前活動的點Id,有效的點的Id*/
    protected int mActivePointerId = INVALID_POINTER;
    /*加速度工具類*/
    protected VelocityTracker mVelocityTracker;
    /*最大加速度的值*/
    private int mMinMunVelocity;
    /*最小加速度的值*/
    private int mMaxMunVelocity;
    /*滑動的距離*/
    private int mFlingDistance;
    /*開始滑動的標誌距離*/
    private int mTouchSlop;
    /*是否可以觸控滑動的標誌*/
    private boolean isEnable = true;
    /*沒有滑動,正常顯示*/
    private int mCurItem = 0;
    /*用於繪製陰影時的梯度變化*/
    private float mRatio;
    /*頁面滑動的距離*/
    private int mPix;
    /*繪製陰影背景的畫筆*/
    private Paint mShadowPaint;
    /*頁面邊緣的陰影圖*/
    private Drawable mLeftShadow ;
    /*頁面邊緣陰影的寬度預設值*/
    private final int SHADOW_WIDTH = 6 ;
    /*頁面邊緣陰影的寬度*/
    private int mShadowWidth ;
    /*這個標誌是用來判斷是否需要繪製陰影效果*/
    private boolean mShouldDraw = true;

    public SlidingView(Context context) {
        this(context, null);
    }

    public SlidingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlidingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initCustomView(context);
    }

    private void initCustomView(Context context) {
        setWillNotDraw(false);
        mScroller = new Scroller(context, sInterpolator);
        mInternalPageChangeListener = new InternalPageChangeListener();
        mShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mLeftShadow = getResources().getDrawable(R.drawable.left_shadow) ;

        mShadowPaint.setColor(0xff000000);
        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        mMinMunVelocity = configuration.getScaledMinimumFlingVelocity();
        mMaxMunVelocity = configuration.getScaledMaximumFlingVelocity();
        final float density = context.getResources().getDisplayMetrics().density;
        mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);
        mShadowWidth = (int) (SHADOW_WIDTH*density);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int width = r - l;
        final int height = b - t;
        mContent.layout(0, 0, width, height);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (!isEnable) {
            return false;
        }
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP
                || action != MotionEvent.ACTION_DOWN && mIsUnableToDrag) {
            endToDrag();
            return false;
        }
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                /*計算 x,y 的距離*/
                int index = MotionEventCompat.getActionIndex(ev);
                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
                if (mActivePointerId == INVALID_POINTER)
                    break;
                mLastMotionX = mInitialMotionX = MotionEventCompat.getX(ev, index);
                mLastMotionY = MotionEventCompat.getY(ev, index);
                /*這裡判讀,如果這個觸控區域是允許滑動攔截的,則攔截事件*/
                if (thisTouchAllowed(ev)) {
                    mIsBeingDragged = false;
                    mIsUnableToDrag = false;
                } else {
                    mIsUnableToDrag = true;
                }
                if (!mScroller.isFinished()) {
                    startDrag();
                }
                break;
            case MotionEvent.ACTION_MOVE:
               /*繼續判斷是否需要攔截*/
                determineDrag(ev);
                break;
            case MotionEvent.ACTION_UP:
                break;
            case MotionEvent.ACTION_POINTER_UP:
                /*這裡做了對多點觸控的處理,當有多個手指觸控的時候依然能正確的滑動*/
                onSecondaryPointerUp(ev);
                break;

        }
        if (!mIsBeingDragged) {
            if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
            }
            mVelocityTracker.addMovement(ev);
        }
        return mIsBeingDragged;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (!isEnable) {
            return false;
        }
        if (!mIsBeingDragged && !thisTouchAllowed(event))
            return false;
        final int action = event.getAction();

        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        switch (action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                /*按下則結束滾動*/
                completeScroll();
                int index = MotionEventCompat.getActionIndex(event);
                mActivePointerId = MotionEventCompat.getPointerId(event, index);
                mLastMotionX = mInitialMotionX = event.getX();
                break;
            case MotionEventCompat.ACTION_POINTER_DOWN: {
                /*有多個點按下的時候,取最後一個按下的點為有效點*/
                final int indexx = MotionEventCompat.getActionIndex(event);
                mLastMotionX = MotionEventCompat.getX(event, indexx);
                mActivePointerId = MotionEventCompat.getPointerId(event, indexx);
                break;

            }
            case MotionEvent.ACTION_MOVE:
                if (!mIsBeingDragged) {
                    determineDrag(event);
                    if (mIsUnableToDrag)
                        return false;
                }
                /*如果已經是滑動狀態,則根據手勢滑動,而改變View 的位置*/
                if (mIsBeingDragged) {
                    // 以下程式碼用來判斷和執行View 的滑動
                    final int activePointerIndex = getPointerIndex(event, mActivePointerId);
                    if (mActivePointerId == INVALID_POINTER)
                        break;
                    final float x = MotionEventCompat.getX(event, activePointerIndex);
                    final float deltaX = mLastMotionX - x;
                    mLastMotionX = x;
                    float oldScrollX = getScrollX();
                    float scrollX = oldScrollX + deltaX;
                    final float leftBound = getLeftBound();
                    final float rightBound = getRightBound();
                    if (scrollX < leftBound) {
                        scrollX = leftBound;
                    } else if (scrollX > rightBound) {
                        scrollX = rightBound;
                    }

                    mLastMotionX += scrollX - (int) scrollX;
                    scrollTo((int) scrollX, getScrollY());

                }
                break;
            case MotionEvent.ACTION_UP:
                /*如果已經是滑動狀態,擡起手指,需要判斷滾動的位置*/
                if (mIsBeingDragged) {
                    mIsBeingDragged = false;
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaxMunVelocity);
                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                            velocityTracker, mActivePointerId);
                    final int scrollX = getScrollX();
                    final float pageOffset = (float) (-scrollX) / getContentWidth();
                    final int activePointerIndex = getPointerIndex(event, mActivePointerId);
                    if (mActivePointerId != INVALID_POINTER) {
                        final float x = MotionEventCompat.getX(event, activePointerIndex);
                        final int totalDelta = (int) (x - mInitialMotionX);
                         /*這裡判斷是否滾動到下一頁,還是滾回原位置*/
                        int nextPage = determineTargetPage(pageOffset, initialVelocity, totalDelta);
                        setCurrentItemInternal(nextPage, true, initialVelocity);
                    } else {
                        setCurrentItemInternal(mCurItem, true, initialVelocity);
                    }
                    mActivePointerId = INVALID_POINTER;
                    endToDrag();
                } else {
//                    setCurrentItemInternal(0, true, 0);
                    scrollTo(getScrollX(), getScrollY());
                    endToDrag();
                }
                break;
            case MotionEventCompat.ACTION_POINTER_UP:
                onSecondaryPointerUp(event);
                int pointerIndex = getPointerIndex(event, mActivePointerId);
                if (mActivePointerId == INVALID_POINTER)
                    break;
                mLastMotionX = MotionEventCompat.getX(event, pointerIndex);
                break;
        }

        return true;
    }

    public void setOnPageChangeListener(OnPageChangeListener listener) {
        mListener = listener;
    }

    private float getLeftBound() {
        return -mContent.getWidth();
    }

    private float getRightBound() {
        return 0;
    }

    private int getContentWidth() {
        return mContent.getWidth();
    }

    public void setEnable(boolean isEnable) {
        this.isEnable = isEnable;
    }

    /**
     * 通過事件和點的 id 來獲取點的索引
     * @param ev
     * @param id
     * @return
     */
    private int getPointerIndex(MotionEvent ev, int id) {
        int activePointerIndex = MotionEventCompat.findPointerIndex(ev, id);
        if (activePointerIndex == -1)
            mActivePointerId = INVALID_POINTER;
        return activePointerIndex;
    }

    /**
     * 結束拖拽
     */
    private void endToDrag() {
        mIsBeingDragged = false;
        mIsUnableToDrag = false;
        mActivePointerId = INVALID_POINTER;
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

    /**
     * 可以拖拽
     */
    private void startDrag() {
        mIsBeingDragged = true;
    }

    /**
     * 這裡是多多點觸控的控制
     * @param ev
     */
    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = MotionEventCompat.getActionIndex(ev);
        final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex);
            mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
            if (mVelocityTracker != null) {
                mVelocityTracker.clear();
            }
        }
    }

    /**
     * 決定是否可以拖拽
     * @param event
     */
    private void determineDrag(MotionEvent event) {
        /*這麼一大串程式碼只有一個目的,就是用來獲取和判斷手指觸控的位置*/
        int pointIndex = MotionEventCompat.getActionIndex(event);
        int pointId = MotionEventCompat.getPointerId(event, pointIndex);
        if (pointId == INVALID_POINTER) {
            return;
        }
        final float x = MotionEventCompat.getX(event, pointIndex);
        final float y = MotionEventCompat.getY(event, pointIndex);
        final float dx = x - mLastMotionX;
        final float dy = y - mLastMotionY;
        final float xDiff = Math.abs(dx);
        final float yDiff = Math.abs(dy);
        /*如果滑動的距離大於我們規定的預設位置,並且水平滑動的幅度大於垂直滑動的幅度,則說明可以滑動此View*/
        if (xDiff > mTouchSlop && xDiff > yDiff && thisSlideAllowed(dx)) {
            startDrag();
            mLastMotionX = x;
            mLastMotionY = y;
        } else if (xDiff > mTouchSlop) {
            mIsUnableToDrag = true;
        }
    }

    /**
     * 這個方法是待擴充套件用的,主要是想要來過濾某些特殊情況
     * @param ev
     * @return
     */
    private boolean thisTouchAllowed(MotionEvent ev) {

        return true;
    }

    /**
     * 如果手勢是向有滑動返回為 true
     * @param dx
     * @return
     */
    private boolean thisSlideAllowed(float dx) {
        return dx > 0;
    }

    @Override
    public void computeScroll() {
        if (!mScroller.isFinished()) {
            if (mScroller.computeScrollOffset()) {
                int oldX = getScrollX();
                int oldY = getScrollY();
                int x = mScroller.getCurrX();
                int y = mScroller.getCurrY();

                if (oldX != x || oldY != y) {
                    scrollTo(x, y);
//                    pageScrolled(x);
                }


                invalidate();
                return;
            }
        }


        completeScroll();
    }

    private void pageScrolled(int xpos) {
        final int widthWithMargin = getWidth();
        /*獲取當前頁面*/
        int position = Math.abs(xpos) / widthWithMargin;
        /*獲取當前滑動的距離*/
        final int offsetPixels = Math.abs(xpos) % widthWithMargin;
        /*通過滑動的距離來獲取梯度值*/
        final float offset = (float) offsetPixels / widthWithMargin;
        /*這裡需要做特殊處理,因為只有一個頁面*/
        position = mIsBeingDragged ? 0 : position;
        onPageScrolled(position, offset, offsetPixels);
    }

    protected void onPageScrolled(int position, float offset, int offsetPixels) {
        if (mListener != null) {
            mListener.onPageScrolled(position, offset, offsetPixels);
        }
        mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels);
    }

     private void setCurrentItemInternal(int item, boolean smoothScroll, int velocity) {
        mCurItem = item;
        final int destX = getDestScrollX(item);

        if (mListener != null) {
            mListener.onPageSelected(mCurItem);
        }
        mInternalPageChangeListener.onPageSelected(mCurItem);
        if (smoothScroll) {
            /*執行滑動滾動*/
            smoothScrollTo(destX, 0, velocity);
        } else {
            /*結束滑動滾動*/
            completeScroll();
            /*直接滾動到指定位置*/
            scrollTo(destX, 0);
        }
    }

    /**
     * 根據當前頁面來獲取需要滾動的目的位置
     * @param page
     * @return
     */
    public int getDestScrollX(int page) {
        switch (page) {
            case 0:
                return mContent.getLeft();
            case 1:
                return -mContent.getRight();
        }
        return 0;
    }

    /**
     * 通過偏移位置和加速度來確定需要滾動的頁
     * @param pageOffset
     * @param velocity
     * @param deltaX
     * @return
     */
    private int determineTargetPage(float pageOffset, int velocity, int deltaX) {
        int targetPage = 0;
        /*這裡判斷是否需要滾動到下一頁*/
        if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinMunVelocity) {
            if (velocity > 0 && deltaX > 0) {
                targetPage = 1;
            } else if (velocity < 0 && deltaX < 0) {
                targetPage = 0;
            }
        } else {
            /*根據距離來判斷滾動的頁碼*/
            targetPage = (int) Math.round(targetPage + pageOffset);
        }
        return targetPage;
    }

    private void completeScroll() {
        boolean needPopulate = mScrolling;
        if (needPopulate) {
            mScroller.abortAnimation();
            int oldX = getScrollX();
            int oldY = getScrollY();
            int x = mScroller.getCurrX();
            int y = mScroller.getCurrY();
            if (oldX != x || oldY != y) {
//                scrollTo(x, y);
            }
        }

        mScrolling = false;
    }

    @Override
    public void scrollTo(int x, int y) {
        super.scrollTo(x, y);
        /*設定回撥事件的值*/
        pageScrolled(x);
    }

    private void smoothScrollTo(int x, int y, int velocity) {
        if (getChildCount() == 0) {

            return;
        }
        int sx = getScrollX();
        int sy = getScrollY();
        int dx = x - sx;
        int dy = y - sy;
        if (dx == 0 && dy == 0) {
            completeScroll();
            /*這裡為了解決一個bug,當用手指觸控滑到看不見的時候再用力滑動,如果不做此操作,那麼不會回撥position = 1*/
            scrollTo(sx,sy);
            return;
        }

        mScrolling = true;

        final int width = getContentWidth();
        final int halfWidth = width / 2;
        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
        final float distance = halfWidth + halfWidth *
                distanceInfluenceForSnapDuration(distanceRatio);

        int duration = 0;
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
        } else {
            final float pageDelta = (float) Math.abs(dx) / width;
            duration = (int) ((pageDelta + 1) * 100);
            duration = MAX_SETTLE_DURATION;
        }
        duration = Math.min(duration, MAX_SETTLE_DURATION);
        /*開始自定滾動到指定的位置*/
        mScroller.startScroll(sx, sy, dx, dy, duration);
        invalidate();
    }

    float distanceInfluenceForSnapDuration(float f) {
        f -= 0.5f;
        f *= 0.3f * Math.PI / 2.0f;
        return (float) FloatMath.sin(f);
    }

    public void setContent(View v) {
        if (mContent != null)
            this.removeView(mContent);
        mContent = v;
        addView(mContent);
    }

    public View getContentView(){
        return mContent ;
    }

    public void setShouldDraw(boolean shouldDraw){
        mShouldDraw = shouldDraw ;
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /*獲取預設的寬度*/
        int width = getDefaultSize(0, widthMeasureSpec);
        /*獲取預設的高度*/
        int height = getDefaultSize(0, heightMeasureSpec);
        /*設定ViewGroup 的寬高*/
        setMeasuredDimension(width, height);
        /*獲取子 View 的寬度*/
        final int contentWidth = getChildMeasureSpec(widthMeasureSpec, 0, width);
        /*獲取子View 的高度*/
        final int contentHeight = getChildMeasureSpec(heightMeasureSpec, 0, height);
        /*設定子View 的大小*/
        mContent.measure(contentWidth, contentHeight);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mShouldDraw){
            /*繪製偏移的背影顏色*/
            drawBackground(canvas);
            /*繪製邊緣的陰影*/
            drawShadow(canvas) ;
        }
    }

    /**
     * 繪製背景顏色,隨著距離的改變而改變
     * @param canvas
     */
    private void drawBackground(Canvas canvas) {
        mShadowPaint.setAlpha((int) ((1 - mRatio) * 180));
        canvas.drawRect(-mPix, 0, 0, getHeight(), mShadowPaint);
    }

    /**
     * 繪製shadow陰影
     * @param canvas
     */
    private void drawShadow(Canvas canvas){
        /*儲存畫布當前的狀態,這個用法在我前面將自定義View 的時候將的很詳細*/
        canvas.save() ;
        /*設定 drawable 的大小範圍*/
        mLeftShadow.setBounds(0, 0, mShadowWidth, getHeight());
        /*讓畫布平移一定距離*/
        canvas.translate(-mShadowWidth,0);
        /*繪製Drawable*/
        mLeftShadow.draw(canvas);
        /*恢復畫布的狀態*/
        canvas.restore();
    }

    private class InternalPageChangeListener implements OnPageChangeListener {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            if (mShouldDraw){
                mRatio = positionOffset;
                mPix = positionOffsetPixels;
                invalidate();
            }
        }

        @Override
        public void onPageSelected(int position) {

        }
    }

    public interface OnPageChangeListener {

        /*滑動頁面滑動狀態,當前頁和頁面的偏移梯度,頁面的偏移位置*/
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
        /*當前頁面*/
        public void onPageSelected(int position);
    }
}

點選下載原始碼