Activity之SwipeBack原理解析

最近在項目中遇到了使用SwipeBackLayout來模擬ios中右滑退出當前界面的效果(萬惡的模仿IOS),頗感神奇,然后大致研究了下其代碼實現(xiàn)的原理,接下來就一些主要的原理做一些講解。

原理概括:

通過使用SwipeBackLayout作為咱們設置contentView的Parent,之后右滑的操作,則會由咱們的最外層容器SwipeBackLayout來處理,右滑中移動的距離,則將SwipeBackLayout的childView向右移動相應的距離。移動之后左邊的間隙,則在draw方法來繪制置透明色,來顯示下層的界面(必須在主題中指定windowIsTranslucent為true,這樣咱們才可以看到下層的activity)。

原理解析:

為了驗證咱們的原理是否準確,咱們通過一下幾個方面進行驗證:

  • SwipeBackLayout是如何設置最外層container的呢?
    在SwipeBackActivity中的onPostCreate的回調(diào)中,可以發(fā)現(xiàn)通過SwipeBackActivityHelper的onPostCreate來執(zhí)行SwipeBackLayout的attachToActivity方法。在此方法中,通過拿到decorView的子view,使用貍貓換太子,把咱們SwipeBackLayout作為根view。具體的代碼如下:
    public void attachToActivity(Activity activity) {
        mActivity = activity;
        TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{
                android.R.attr.windowBackground
        });
        int background = a.getResourceId(0, 0);
        a.recycle();

        ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
        ViewGroup decorChild = (ViewGroup) decor.getChildAt(0);
        decorChild.setBackgroundResource(background);
        decor.removeView(decorChild);
        addView(decorChild);
        setContentView(decorChild);
        decor.addView(this);
    }

  • SwipeBackLayout的右滑操作是否是進行自己的右移操作實現(xiàn)?
    像這種滑動的過程中,移動的view的效果,肯定都是在OnTouchEvent中的move方法,判斷坐標的改變值,之后進行view的操作來實現(xiàn)的。接下來,咱們找一下代碼進行驗證一下,通過代碼查找,發(fā)現(xiàn)它將onTouchEvent的操作邏輯放置在
    ViewDragHelper中進行:
       case MotionEvent.ACTION_MOVE: {
          if (mDragState == STATE_DRAGGING) {
              final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
              final float x = MotionEventCompat.getX(ev, index);
              final float y = MotionEventCompat.getY(ev, index);
              final int idx = (int) (x - mLastMotionX[mActivePointerId]);
              final int idy = (int) (y - mLastMotionY[mActivePointerId]);

              dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);

              saveLastMotion(ev);
          } else {
              // Check to see if any pointer is now over a draggable view.
              final int pointerCount = MotionEventCompat.getPointerCount(ev);
              for (int i = 0; i < pointerCount; i++) {
                  final int pointerId = MotionEventCompat.getPointerId(ev, i);
                  final float x = MotionEventCompat.getX(ev, i);
                  final float y = MotionEventCompat.getY(ev, i);
                  final float dx = x - mInitialMotionX[pointerId];
                  final float dy = y - mInitialMotionY[pointerId];

                  reportNewEdgeDrags(dx, dy, pointerId);
                  if (mDragState == STATE_DRAGGING) {
                      // Callback might have started an edge drag.
                      break;
                  }

                  final View toCapture = findTopChildUnder((int) x, (int) y);
                  if (checkTouchSlop(toCapture, dx, dy)
                          && tryCaptureViewForDrag(toCapture, pointerId)) {
                      break;
                  }
              }
              saveLastMotion(ev);
          }
          break;
      }

在以上的代碼中,發(fā)現(xiàn)獲取了移動的距離idx和idy,之后調(diào)用了dragTo的方法。跳轉到dragTo方法:

    private void dragTo(int left, int top, int dx, int dy) {
        int clampedX = left;
        int clampedY = top;
        final int oldLeft = mCapturedView.getLeft();
        final int oldTop = mCapturedView.getTop();
        if (dx != 0) {
            clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
            mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
        }
        if (dy != 0) {
            clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
            mCapturedView.offsetTopAndBottom(clampedY - oldTop);
        }

        if (dx != 0 || dy != 0) {
            final int clampedDx = clampedX - oldLeft;
            final int clampedDy = clampedY - oldTop;
            mCallback
                    .onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);
        }
    }

可以看出它是通過調(diào)用了offsetLeftAndRight跟offsetTopAndBottom來改變相應view的位置。

  • 在SwipeBackLayout右滑的過程中,左邊的透明部分是如何處理的?
    這部分的邏輯,在SwipeBackLayout的drawChild的方法中,可以看出一些端倪。通過childView的移動之后的具體,在移動之后的間隙出,繪制透明色跟過度的圖片。看代碼:
    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        final boolean drawContent = child == mContentView;

        boolean ret = super.drawChild(canvas, child, drawingTime);
        if (mScrimOpacity > 0 && drawContent
                && mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE) {
            drawShadow(canvas, child);
            drawScrim(canvas, child);
        }
        return ret;
    }

    private void drawScrim(Canvas canvas, View child) {
        final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
        final int alpha = (int) (baseAlpha * mScrimOpacity);
        final int color = alpha << 24 | (mScrimColor & 0xffffff);

        if ((mTrackingEdge & EDGE_LEFT) != 0) {
            canvas.clipRect(0, 0, child.getLeft(), getHeight());
        } else if ((mTrackingEdge & EDGE_RIGHT) != 0) {
            canvas.clipRect(child.getRight(), 0, getRight(), getHeight());
        } else if ((mTrackingEdge & EDGE_BOTTOM) != 0) {
            canvas.clipRect(child.getLeft(), child.getBottom(), getRight(), getHeight());
        }
        canvas.drawColor(color);
    }

    private void drawShadow(Canvas canvas, View child) {
        final Rect childRect = mTmpRect;
        child.getHitRect(childRect);

        if ((mEdgeFlag & EDGE_LEFT) != 0) {
            mShadowLeft.setBounds(childRect.left - mShadowLeft.getIntrinsicWidth(), childRect.top,
                    childRect.left, childRect.bottom);
            mShadowLeft.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
            mShadowLeft.draw(canvas);
        }

        if ((mEdgeFlag & EDGE_RIGHT) != 0) {
            mShadowRight.setBounds(childRect.right, childRect.top,
                    childRect.right + mShadowRight.getIntrinsicWidth(), childRect.bottom);
            mShadowRight.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
            mShadowRight.draw(canvas);
        }

        if ((mEdgeFlag & EDGE_BOTTOM) != 0) {
            mShadowBottom.setBounds(childRect.left, childRect.bottom, childRect.right,
                    childRect.bottom + mShadowBottom.getIntrinsicHeight());
            mShadowBottom.setAlpha((int) (mScrimOpacity * FULL_ALPHA));
            mShadowBottom.draw(canvas);
        }
    }

可以明確的看出,他正是根據(jù)不同的移動方向,來繪制咱們所需要的那塊透明的過度區(qū)域。在drawScrim繪制底色,在drawShadow中繪制過渡的圖片,來達到咱們所需要的效果。

不足之處:

我們從代碼中可以發(fā)現(xiàn),它是在decorview中給第一個子view來添加一個父view(SwipeBackLayout)來實現(xiàn)view滑動之后,結束當前activity的效果。注意問題來了,decorView的第一個childView是不包含狀態(tài)欄的,這樣在5.0上就會出現(xiàn)一個視覺的bug。界面在右滑的過程中,狀態(tài)欄是不改變的,在確定滑動過程結束之后,才會執(zhí)行activity的finish的方法,這樣狀態(tài)欄次啊會消失。因為在5.0上的,嗯,我的大魅族就是5.0的,親測這個bug,5.0上系統(tǒng)會根據(jù)界面的頭部,動態(tài)來設置狀態(tài)欄的顏色,(當然代碼也是可以設置的),這個視覺的bug就比較明顯了。

Demo實現(xiàn):

現(xiàn)在,樓主感覺自己掌握了這個原理,就來驗證是否可行,咱們通過簡單的demo來實現(xiàn)。Demo地址,主要的細節(jié)點如下:

  • 在主題中設置添加<item name="android:windowIsTranslucent">true</item>
  • onIntercept跟onTouchEvent事件的重寫,進行指定的view的移動操作。
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容