Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義SwipeBackLayout控件實現右滑退出activity功能

自定義SwipeBackLayout控件實現右滑退出activity功能

編輯:關於Android編程

SwipeBackLayout

其實github上已經有這個開源庫了,我是個菜鳥,我喜歡用開源庫,同時也非常好奇它的實現原理。很多大神寫的代碼注釋都特別少,可能是他們覺得很簡單就懶得寫了,這點對新手來講就有點坑爹了;所以我只是借助大神的代碼向大家講解下這個的實現原理。介紹原理之前我先說下原創的問題,老實講我博客上講的東西以前絕對有人寫過,很多別人寫的很好,而我只是站在他們的肩膀上幫助下新手。我真的不喜歡那些很繞的代碼,我喜歡來的直一點的,寫出讓超新手都能看的懂的代碼,因為其中注釋代碼的程度到了令人發指的地步(大神可以無視)。

代碼

關鍵之處還是在於自定義控件SwipeBackLayout這裡。

public class SwipeBackLayout extends FrameLayout {
     /**
      * SwipeBackLayout的主布局
      */
     private View mContentView;
     /**
      * 是一個距離,表示滑動的時候手的移動要大於這個距離才開始移動    控件。如果小於這個距離就不觸發移動控件,
      * 如 viewpager就是用這個距離來判斷用戶是否翻頁,這個距離打印出來是16px
      */
     private int mTouchSlop;
     /**
      * 手指點擊屏幕時的Y坐標
      */
     private int downY;
     /**
      * 手指點擊屏幕時的X坐標
      */
     private int downX;
     /**
      * 手指點擊屏幕時,臨時的X坐標
      */
     private int tempX;
     /**
      * Android裡 Scroller類是為了實現View平滑滾動的一個Helper類
      */
     private Scroller mScroller;
     /**
      * 手機屏幕的寬度
      */
     private int viewWidth;
     /**
      * 表示屏幕是否正在滑動的標記
      */
     private boolean isSilding;
     /**
      * 表示是否finish掉當前的activity
      */
     private boolean isFinish;
     /**
      * 獲取系統資源的 drawable文件,帶有陰影的
      */
     private Drawable mShadowDrawable;
     /**
      * SwipeBackLayout依附的activity
      */
     private Activity mActivity;
     /**
      * 當前activity裡所存在的 viewpager的集合
      */
     private List mViewPagers = new LinkedList();// 創建一個空的 viewpager的集合
     /**
      * 是否對手勢進行攔截的設置,默認為true。若為false,則SwipeBackLayout這個 viewgroup對手勢不攔截不消費
      */
     private boolean mEnable = true;

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

     public SwipeBackLayout(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
            mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); // 是一個距離,表示滑動的時候手的移動要大於這個距離才開始移動控件。如果小於這個距離就不觸發移動控件,如 viewpager就是用這個距離來判斷用戶是否翻頁
           Log. d("xiao" , "mTouchSlop:" + mTouchSlop );
            mScroller = new Scroller(context); // Android裡Scroller 類是為了實現View平滑滾動的一個Helper類
            mShadowDrawable = getResources().getDrawable(R.drawable.shadow_left );// 獲取系統資源的 drawable文件
     }

     /**
      * 把swipeBackLayout附加到指定的activity中,放到 decor頂層窗口下,decorChild上
      * 這樣做的作用就是,讓SwipeBackLayout附加到任何activity時,就立於此activity主視圖之上
      *
      * @param activity
      */
     public void attachToActivity(Activity activity) {
            mActivity = activity; // 傳進來的activity
            int[] attrs = new int[] { android.R.attr.windowBackground };
            // 返回一個與主題Theme定義的 attrs數組對應的typedArray類型數組
           TypedArray a = activity.getTheme().obtainStyledAttributes(attrs);
            // 獲取typedArray數組中指定位置的資源id值
            int background = a.getResourceId(0, 0);
            // 回收TypedArray類型數組
           a.recycle();
            // 返回頂層窗口裝飾視圖
           ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
            // 返回裝飾視圖的指定位置的view,就是 decor的child,很形象
           ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
            // 給頂層窗口裝飾視圖的第一個子視圖設置背景資源,
            // background就是上面獲得的android.R.attr.windowBackground
           decorChild.setBackgroundResource(background);
            // decor頂層窗口裝飾視圖移除decorChild,殺了他的兒子
           decor.removeView(decorChild);
            // 這個應該是給SwipeBackLayout添加子view,因為SwipeBackLayout繼承自FrameLayout
            // 這時,SwipeBackLayout就是decorChild的父布局了
            this.addView(decorChild);
            // 把decorChild當成SwipeBackLayout的contentView進行設置
            this.setContentView(decorChild);
            // 然後給decor添加一個子view,這個this就是SwipeBackLayout
           decor.addView( this);
     }

     /**
      * 設置主布局視圖
      *
      * @param decorChild
      */
     private void setContentView(View decorChild) {
            mContentView = (View) decorChild.getParent(); // 返回decorChild的父布局,這個時候父布局就是SwipeBackLayout
     }

     /**
      * 設置手勢
      *
      * @param enable
      */
     public void setEnableGesture( boolean enable) {
            mEnable = enable;
     }

     /**
      * 事件攔截操作
      */
     @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (! mEnable) {
                 // false表示不攔截
                 return false;
           }

            // 處理ViewPager沖突問題
           ViewPager mViewPager = getTouchViewPager( mViewPagers, ev);// 獲取到觸摸地方的 viewpager

            if (mViewPager != null && mViewPager.getCurrentItem() != 0) {
                 // 當viewpager 不為空且viewpager不處在第一個item時,swipeBackLayout就不攔截
                 return super.onInterceptTouchEvent(ev); // 默認返回false,表示不攔截
           }

            switch (ev.getAction()) {
            case MotionEvent. ACTION_DOWN:
                 downX = tempX = ( int) ev.getRawX(); // 當點擊時候的X坐標
                 downY = ( int) ev.getRawY(); // 當點擊時候的Y坐標
                 break;
            case MotionEvent. ACTION_MOVE:
                 int moveX = ( int) ev.getRawX();
                 // 滿足此條件屏蔽SildingFinishLayout裡面子類的touch事件
                 if (moveX - downX > mTouchSlop
                           && Math. abs((int) ev.getRawY() - downY) < mTouchSlop) {
                      // 當水平移動的距離大於16px,且豎直方向的移動距離小於16px時,SwipeBackLayout攔截此次觸摸事件
                      // 就是說當手指move的時候,滿足上述條件時,觸摸事件就會被SwipeBackLayout的onInterceptTouchEvent方法攔截
                      // 繼而傳遞給SwipeBackLayout的onTouchEvent方法
                      return true;
                }
                 break;
           }

            return super.onInterceptTouchEvent(ev);
     }

     @Override
     public boolean onTouchEvent(MotionEvent event) {
            if (! mEnable) {
                 return false;
           }

            switch (event.getAction()) {
            case MotionEvent. ACTION_MOVE:
                 int moveX = ( int) event.getRawX(); // 移動後的X坐標
                 int deltaX = tempX - moveX; // 這個是點擊時和移動後,X坐標差值
                Log. d("xiao" , "deltaX:" + deltaX);// 右滑為負值
                 tempX = moveX; // 給tempX重新賦值
                 if (moveX - downX > mTouchSlop
                           && Math. abs((int) event.getRawY() - downY) < mTouchSlop) {
                      // 滿足上述條件時,觸摸事件由onInterceptTouchEvent傳遞至onTouchEvent方法中
                      isSilding = true; // 標記為正在滑動
                }

                 if (moveX - downX >= 0 && isSilding) {
                      // 當右滑且處於正在滑動的時候,主布局通過scrollBy整體移動,且通過打印deltaX為負值
                      mContentView.scrollBy(deltaX, 0);
                }
                 break;
            case MotionEvent. ACTION_UP:
                 isSilding = false; // 講滑動標記設置為false
                Log. d("xiaok" , "mContentView:" + mContentView.getScrollX());
                Log. d("xiaok" , "viewWidth:" + viewWidth );
                 if ( mContentView.getScrollX() <= - viewWidth / 2) {
                      // 當右滑距離超過屏幕寬度的一半時,標記isFinish為true表示滾動出界面,然後滾動出界面
                      isFinish = true;
                     scrollRight();
                } else {
                      // 否則界面回滾至原點,標記isFinish為false
                     scrollOrigin();
                      isFinish = false;
                }
                 break;
           }
            return true;
     }

     /**
      * 獲取SwipeBackLayout裡面的ViewPager的集合,這裡用到的好像是遞歸思想
      *
      * @param mViewPagers
      * @param parent
      */
     private void getAlLViewPager(List mViewPagers, ViewGroup parent) {
            int childCount = parent.getChildCount();
            for ( int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                 if (child instanceof ViewPager) {
                     mViewPagers.add((ViewPager) child);
                } else if (child instanceof ViewGroup) {
                     getAlLViewPager(mViewPagers, (ViewGroup) child);
                }
           }
     }

     /**
      * 返回我們touch的ViewPager
      *
      * @param mViewPagers
      * @param ev
      * @return
      */
     private ViewPager getTouchViewPager(List mViewPagers,
                MotionEvent ev) {
            if (mViewPagers == null || mViewPagers.size() == 0) {
                 // 如果mViewPagers集合為空,或者mViewPagers的size=0,那麼直接返回空
                 return null;
           }
           Rect mRect = new Rect(); // 創建一個新的空矩形。所有坐標被初始化為0。
            for (ViewPager v : mViewPagers) { // 遍歷mViewPagers集合,判斷我現在觸摸的地方是不是在某一個 viewpager范圍裡
                                                            // 如果在,那麼就返回這個 viewpager
                v.getHitRect(mRect); // 獲取每一個viewpager的矩形的坐標值,並賦值給mRect矩形

                 if (mRect.contains(( int) ev.getX(), ( int) ev.getY())) {
                      // 返回true,如果(x,y)坐標在mRect矩形的范圍內
                      return v; // 返回viewpager
                }
           }
            return null; // 否則返回空
     }

     @Override
     protected void onLayout( boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            // 不明白為什麼onLayout方法執行了兩次
           Log. d("xiao" , "changed:" + changed);
            if (changed) {
                 viewWidth = this.getWidth();
                Log. d("xiao" , "viewWidth:" + viewWidth );
                getAlLViewPager( mViewPagers, this); // 獲得SwipeBackLayout中的ViewPager的集合
           }
     }

     @Override
     protected void dispatchDraw(Canvas canvas) { // 當需要繪制子view的時候,才會調用此方法
            super.dispatchDraw(canvas);
            /**
            * 這個方法是用來繪制右滑退出時,SwipeBackLayout左側的那個陰影效果
            */
            if ( mShadowDrawable != null && mContentView != null) {

                 int left = mContentView.getLeft()
                           - mShadowDrawable.getIntrinsicWidth();
                 int right = left + mShadowDrawable.getIntrinsicWidth();
                 int top = mContentView.getTop();
                 int bottom = mContentView.getBottom();

                 mShadowDrawable.setBounds(left, top, right, bottom);
                 mShadowDrawable.draw(canvas);
           }

     }

     /**
      * 滾動出界面
      */
     private void scrollRight() {
            /**
            * 這裡解釋下getScrollX的意思:返回視圖左邊緣的X坐標,但是是反向的X軸,就是值是相反的
            */
            final int delta = ( viewWidth + mContentView.getScrollX());
            /**
            * 調用startScroll方法來設置一些滾動的參數,我們在computeScroll()方法中調用scrollTo來滾動item
            * 當 dx的值為正數時view向左滑動
            */
           Log. d("xiao" , "delta:" + delta);
            mScroller.startScroll( mContentView.getScrollX(), 0, -delta + 1, 0,
                     Math. abs(delta));
           postInvalidate();
     }

     /**
      * 滾動到起始位置
      */
     private void scrollOrigin() {

            int delta = mContentView.getScrollX();
           Log. d("xiao" , "1delta:" + delta);
            mScroller.startScroll( mContentView.getScrollX(), 0, -delta, 0,
                     Math. abs(delta));
           postInvalidate();
     }

     @Override
     public void computeScroll() {
            /**
            * 當我們執行 ontouch或invalidate()或postInvalidate()都會導致這個copmuteScroll方法的執行
            * 所以底下加一個判斷,computeSrcollOffset是在startScroll方法啟動時就會返回true
            */

            // 調用startScroll的時候scroller.computeScrollOffset()返回true,從文字上理解是計算偏移量
            if ( mScroller.computeScrollOffset()) {
                 mContentView.scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
                 postInvalidate();

                 if ( mScroller.isFinished() && isFinish) { //
                      // 當滑動結束,且當前view滑動出界面時,執行activity,finish的命令
                      mActivity.finish();
                }
           }
     }
}

說實話,稍微有點基礎的,認真看下代碼和注釋就已經看的懂了。為了幫助新手能看懂我再進行三點講解:
一、首先這個SwipeBackLayout繼承FrameLayout,注意attachToActivity方法。這個方法就是讓SwipeBackLayout包裹住我們XML裡寫的contentView這個步驟方便我們後面實現滑動finish的功能。圖示如下:
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
二、重寫SwipeBackLayout中的onInterceptTouchEvent和onTouchEvent方法,對手勢進行監聽。手勢監聽問題又涉及到了事件分發問題,這裡只是簡單說下。onInterceptTouchEvent方法表示父布局是否攔截當前事件,true表示攔截,false表示不攔截,且默認為不攔截。當手指向右滑動超過16px且上下滑動距離小於16px時讓onIntecpetTouchEvent方法返回true,表示攔截。攔截的意思是,SwipeBackLayout自身處理這個滑動事件,就不會傳遞給子view了。

然後再onTouchEvent方法的手指觸摸移動方法ACTION_MOVE中,根據不斷算出move移動的距離來對我們的SwipeBackLayout進行scrollBy方法。

在手指抬起ACTION_UP中,判斷是否右滑距離超過屏幕的一半,如果超過一半,則將SwipeBackLayout滾動出界面,finish掉當前activity,反之,則將SwipeBackLayout滾動回原點。
最後貼下scrollTo和scrollBy的區別,直接看源碼!
這裡寫圖片描述

三、注意橫向滑動的事件沖突,如viewpager的右滑。當viewpager的currentItem不等於0的時候,右滑事件應該是讓viewpager觸發的,這個時候SwipeBackLayout是不應該對滑動事件進行攔截的。也就是onInterceptTouchEvent方法這個時候返回false,表示不攔截,把事件交給子view中的viewpager進行處理。

if (mViewPager != null && mViewPager.getCurrentItem() != 0) {
                 // 當viewpager 不為空且viewpager不處在第一
                 //個item時,swipeBackLayout就不攔截
                 return super.onInterceptTouchEvent(ev); // 默認返回false,表示不攔截
           }
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved