其实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,表示不拦截
}
http://blog.csdn.net/xiechengfa/article/details/45317503
最关键和难的部分我已经讲解完了,其他部分看看就好了。我注释的代码是在这里下载的。
其实原不原创对我来说无所谓,只要能帮助到大家就好了,我也不求你点赞,也不会像很多人那样直接一个字不改就复制粘贴,然后放到github上还骗人star。就这样吧。