alighters

程序、写作、人生

Activity之SwipeBack原理解析

| Comments

最近在项目中遇到了使用SwipeBackLayout来模拟ios中右滑退出当前界面的效果(万恶的模仿IOS),颇感神奇,然后大致研究了下其代码实现的原理,接下来就一些主要的原理做一些讲解。

原理概括:

通过使用SwipeBackLayout作为咱们设置contentView的Parent,之后右滑的操作,则会由咱们的最外层容器SwipeBackLayout来处理,右滑中移动的距离,则将SwipeBackLayout的childView向右移动相应的距离。移动之后左边的间隙,则在draw方法来绘制置透明色,来显示下层的界面(必须在主题中指定windowIsTranslucent为true,这样咱们才可以看到下层的activity)。

原理解析:

为了验证咱们的原理是否准确,咱们通过一下几个方面进行验证: + SwipeBackLayout是如何设置最外层container的呢? 在SwipeBackActivity中的onPostCreate的回调中,可以发现通过SwipeBackActivityHelper的onPostCreate来执行SwipeBackLayout的attachToActivity方法。在此方法中,通过拿到decorView的子view,使用狸猫换太子,把咱们SwipeBackLayout作为根view。具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的右滑操作是否是进行自己的右移操作实现? 像这种滑动的过程中,移动的view的效果,肯定都是在OnTouchEvent中的move方法,判断坐标的改变值,之后进行view的操作来实现的。接下来,咱们找一下代码进行验证一下,通过代码查找,发现它将onTouchEvent的操作逻辑放置在 ViewDragHelper中进行:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
   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;
  }

在以上的代码中,发现获取了移动的距离idx和idy,之后调用了dragTo的方法。跳转到dragTo方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}

可以看出它是通过调用了offsetLeftAndRight跟offsetTopAndBottom来改变相应view的位置。

  • 在SwipeBackLayout右滑的过程中,左边的透明部分是如何处理的? 这部分的逻辑,在SwipeBackLayout的drawChild的方法中,可以看出一些端倪。通过childView的移动之后的具体,在移动之后的间隙出,绘制透明色跟过度的图片。看代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@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);
    }
}

可以明确的看出,他正是根据不同的移动方向,来绘制咱们所需要的那块透明的过度区域。在drawScrim绘制底色,在drawShadow中绘制过渡的图片,来达到咱们所需要的效果。

不足之处:

我们从代码中可以发现,它是在decorview中给第一个子view来添加一个父view(SwipeBackLayout)来实现view滑动之后,结束当前activity的效果。注意问题来了,decorView的第一个childView是不包含状态栏的,这样在5.0上就会出现一个视觉的bug。界面在右滑的过程中,状态栏是不改变的,在确定滑动过程结束之后,才会执行activity的finish的方法,这样状态栏次啊会消失。因为在5.0上的,嗯,我的大魅族就是5.0的,亲测这个bug,5.0上系统会根据界面的头部,动态来设置状态栏的颜色,(当然代码也是可以设置的),这个视觉的bug就比较明显了。

Demo实现:

现在,楼主感觉自己掌握了这个原理,就来验证是否可行,咱们通过简单的demo来实现。Demo地址,主要的细节点如下: + 在主题中设置windowIsTransluceni为true + onIntercept跟onTouchEvent事件的重写,进行指定的view的移动操作。

版权归作者所有,转载请注明原文链接:/blog/2015/11/30/Activity-SwipeBack-Analysize/

给 Ta 个打赏吧...

Comments