1. 程式人生 > >自定義控制元件之側滑關閉 Activity 控制元件

自定義控制元件之側滑關閉 Activity 控制元件

隔壁 iOS 的小夥伴有一個功能就是左手向右手一個慢動作,輕輕一劃就可以關閉介面,這種操作感覺還是很絲滑的,而且這還是 iOS 系統自帶的功能,由於 Android 手機早期是有 back 鍵,home 鍵 和選單鍵(現在大部分手機都只保留一個鍵了),所以 Android 是沒有這個功能的。現在使用者越來越注重體驗,一般為了降低設計成本,在 App 的設計上 iOS 與 Android 也力求風格統一,那麼如果需要我們也實現這樣的功能怎麼辦?程式猿可是不會被難倒的一個物種,有很多這樣功能的開源控制元件,目前公司專案也有用到,但是用起來卻是有點問題,於是決定自己試著實現一下,也是一種學習。

 

1 思路

首先,側滑這個動作並不難,我們監聽到一個控制元件的觸控事件,然後改變它的橫縱座標即可(這個我之前有寫過可以自由移動的控制元件,http://blog.csdn.net/zgcqflqinhao/article/details/72731633,準備重新改一下這個控制元件,但是原理是一樣的),問題是我們應該監聽哪個控制元件的觸控。我們可以獲取到 Activity 的根檢視,於是我就想著把這個根檢視再放到一個自定義的容器上,那樣就好處理了。然後除了側滑的效果,我們還得處理它的事件衝突(鄙人對事件衝突也簡單學習過 http://blog.csdn.net/zgcqflqinhao/article/details/72110352),畢竟 Android 中可以滑動的控制元件還不少,比如 ViewPager、SeekBar 等等。OK,既然有思路了,那就可以開始我們的光榮之路了。

 

2 側滑的基本實現

public class SlideBackLayout extends FrameLayout {
    private boolean startSlide = false;
    private float lastX;
    private Activity activity;

    public SlideBackLayout(@NonNull Context context) {
        this(context, null);
    }

    public SlideBackLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideBackLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getX() < getMeasuredWidth() / 10) {
                    startSlide = true;
                    lastX = event.getRawX();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (startSlide) {
                    float distanceX = event.getRawX() - lastX;
                    float nextX = getX() + distanceX;
                    setX(nextX);
                    lastX = event.getRawX();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (startSlide && event.getRawX() > getMeasuredWidth() / 2) {
                    setX(getMeasuredWidth());
                    //Finish activity
                    activity.finish();
                } else {
                    setX(0);
                }
                startSlide = false;
                break;
        }
        return true;
    }

    public void bindActivity(Activity activity) {
        this.activity = activity;
        ViewGroup mDecorView = (ViewGroup) activity.getWindow().getDecorView();
        View mRootView = mDecorView.getChildAt(0);
        mDecorView.removeView(mRootView);
        addView(mRootView);
        mDecorView.addView(this);
    }

    public void unbindActivity() {
        this.activity = null;
    }
}

 

在需要側滑關閉的 Activity 中(一般會在 BaseActivity 中)新增如下程式碼(在 setContentView() 方法後呼叫就行,其他地方暫時還未測試):

        SlideBackLayout mSlideBackLayout = new SlideBackLayout(this);
        mSlideBackLayout.bindActivity(this);

 

3 攔截子 View 的觸控事件

可以看到我們實現了基本的側滑操作了,滑完後未超過一半自動回到原來的樣子,超過一半則關閉 Activity,這中間我還點了一下按鈕,不是我手賤,是想說明子控制元件的點選事件依然可以響應,但是這就有個問題了,如果這個控制元件的寬高全屏了,那就沒有辦法執行側滑動作了。如下圖:

 

這是因為子 View 把我們的觸控事件給消費了,那麼應該在適當的時機來攔截一下觸控事件,讓我們側滑控制元件自己消費而不下發到子 View,重寫 onInterceptTouchEvent() 方法,當觸控點的橫座標小於螢幕寬度十分之一時就不下發觸控事件:

 

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getX() < getMeasuredWidth() / 10) {
                    startSlide = true;
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onInterceptTouchEvent(event);
    }

 

目前效果如下:

 

對了,這裡還有一個 Bug,如果呼叫了 bindActivity() 再呼叫 unbindActivity() 方法,側滑操作仍會執行,關閉的時候就會由於 activity 為 null 而報 NullPointException,所以我們在攔截觸控事件和響應觸控事件的時候先進行非空判斷:

        if (activity == null) {
            return super.onInterceptTouchEvent(event);
        }
        if (activity == null) {
            return super.onTouchEvent(event);
        }

 

到目前為止,基本的側滑我們能實現了,對子 View 觸控事件的攔截我們也做了一些處理。

 

4 處理滑動衝突

接下來我們要處理一些控制元件的滑動衝突,左右滑動的控制元件最典型的也就是 ViewPager 了,目前公司使用的側滑關閉選單中也是隻處理了 ViewPager 的滑動衝突,如果不處理衝突,那麼在存在 ViewPager 的時候,無論 ViewPager 當前處於第幾個頁卡,只要我們在小於螢幕寬度十分之一的地方開始滑動,那就會響應側滑事件而不是 ViewPager 的滑動,我們希望的應該是在 ViewPager 處於非第一個頁卡時,先響應 ViewPager 的滑動,ViewPager 處於第一個頁卡時才響應側滑關閉事件。那麼首先需要一個容器來存放當前佈局中存在的 ViewPager,然後檢查到當前佈局中有 ViewPager 的時候就存到容器中,這樣我們在處理觸控事件的時候再根據有沒有 ViewPager 和 ViewPager 當前頁卡是第幾項來絕對如何處理觸控事件。

檢查當前佈局是否有 ViewPager 這個方法在繫結 Activity 的時候呼叫:

 

    private void checkHasViewPager(ViewGroup viewGroup) {
        int childCount = viewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            if (viewGroup.getChildAt(i) instanceof ViewPager) {
                viewPagerList.add((ViewPager) viewGroup.getChildAt(i));
            } else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                checkHasViewPager((ViewGroup) viewGroup.getChildAt(i));
            }
        }
    }

然後在 onInterceptTouchEvent() 方法中處理事件之前加入對 ViewPager 的處理:

        if (!viewPagerList.isEmpty()) {
            for (int i = 0; i < viewPagerList.size(); i++) {
                if (viewPagerList.get(i).getCurrentItem() != 0) {
                    return super.onInterceptTouchEvent(event);
                }
            }
        }

 

這樣處理過後就可以看到即使在小於螢幕寬度十分之一的地方滑動時也是先響應 ViewPager 的觸控事件,直到處於第一個頁卡才開始側滑關閉。細心的你會發現我又自作主張的在介面頂部加上了一個 SeekBar,而且當我想滑動 SeekBar 時並沒有成功,而是響應了側滑事件,其實這就是我為什麼想寫這個控制元件的起因。我希望我可以自定義某些控制元件也跟 ViewPager 一樣不被攔截觸控事件,所以我還加入了一個存放不想被攔截事件的容器。

然後給外部提供一個方法,可以新增希望不被攔截觸控事件的 View:

    public void addNotInterceptView(View view) {
        notInterceptViewList.add(view);
    }

onInterceptTouchEvent() 方法中處理完 ViewPager 就可以處理這些 View 了:

        if (!notInterceptViewList.isEmpty()) {
            for (int i = 0; i < notInterceptViewList.size(); i++) {
                View mView = notInterceptViewList.get(i);
                int[] location = new int[2];
                mView.getLocationOnScreen(location);
                if (event.getX() >= location[0] && event.getX() <= location[0] + mView.getWidth()
                        && event.getY() >= location[1] && event.getY() <= location[1] + mView.getHeight()) {
                    return super.onInterceptTouchEvent(event);
                }
            }
        }

當我們希望 SeekBar 的觸控事件不被攔截時,就可以呼叫 addNotInterceptView() 方法:

        SeekBar sbTest = (SeekBar) findViewById(R.id.sb_test);
        mSlideBackLayout.addNotInterceptView(sbTest);

現在效果如下:

 

5 總結

這個控制元件在使用時仍然和其他的控制元件一樣,必須設定 Activity 的主題為透明主題,繼承 Activity 時可以使用

@android:style/Theme.Translucent.NoTitleBar

繼承 AppCompat 時需自定義主題,然後在自定義主題中加入如下兩行:

        <!-- 透明背景 -->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!-- 設定是否透明 -->
        <item name="android:windowIsTranslucent">true</item>

這次的自定義側滑關閉 Activity 控制元件比起現在公司用的多了兩個功能,一是取消 Activity 的繫結,因為有的 Activity 並不希望有這個功能,二是可以新增自定義的希望不被攔截觸控事件的 View。當然我現在還沒有大量測試這個控制元件的穩定性,如果有發現問題,歡迎留言。

 

6 Github 傳送門

https://github.com/mrqinshou/SlideBackLayoutDemo