手勢滑動結束 Activity(一)基本功能的實現

喜歡聽音樂的朋友可能都看過天天動聽這款 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);
    }

因爲每個 Activity 都只有一個 Layout,所以這裏只有一個子 View,佈局和測量就顯得非常簡單。

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"
            />

再來看看效果,效果圖:
這裏寫圖片描述

完美實現!
好了,今天就到這裏,下期文章就是對這個功能的進一步優化和改善,如果感興趣,可以繼續關注我!

發佈了46 篇原創文章 · 獲贊 9 · 訪問量 33萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章