最近在项目中遇到了使用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的移动操作。