在上两篇文章中,我们已经实现了基本的界面的布局和移动效果,但是mImgShotViewmContentView却不能响应事件,而事件的响应就需要我们手动进行事件分发!

android 仿当乐游戏详情页面(一)
android 仿当乐游戏详情页面(二)
android 仿当乐游戏详情页面(三)

事件分发分析

在前面第二篇中,我们是通过手势来实现布局的移动,为了让系统能响应手势,在onTouchEvent(MotionEvent event)方法里面,调用了mDetector.onTouchEvent(event);将系统的焦点传递给了手势,因此,当滑动mImgShotView这个ViewPager时,会出现焦点丢失,截图不能进行切换的问题。

因此,解决这个问题最好的方法就是重写dispatchTouchEvent(MotionEvent ev)方法,对事件分发进行处理。

在上一篇文章中,已经介绍了,在仿当乐的游戏详情页面中,mContentView有三种不同的状态:

  1. 顶部状态时,ToolBarmContentView将获取到焦点。
  2. 中间状态时,ToolBarmImgShotView将获取到焦点。
  3. 底部状态时,上一篇文章中已经介绍了,这个状态,mImgShotView的参数将发生改变,因此,事件焦点的分发便需要进行改变。

事件分发的实现

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
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mRawY <= mTopL) {
mCurrentState = STATE_TOP;
} else if (mTopL < mRawY && mRawY <= mCenterL + (mBarH >> 1)) {
mCurrentState = STATE_CENTER;
} else if (mCenterL + (mBarH >> 1) <= mRawY && mRawY < mBottomL + mBarH) {
mCurrentState = STATE_BOTTOM;
} else {
mCurrentState = STATE_OTHER;
}
boolean isTop = mRawY == mTopL;
// 处理横向滑动的事件
if (Math.abs(ev.getX() - mOldX) >= 0 && Math.abs(ev.getY() - mOldY) < 300 && isTop) {
mOldX = ev.getX();
return super.dispatchTouchEvent(ev);
}

float t = Math.abs(ev.getY() - mOldY);
//处于顶部时的事件过滤区域
if (isTop && (ev.getY() < mTopL || t < 10)) {
return super.dispatchTouchEvent(ev);
}

//处于中间时的事件过滤区域
if (mCurrentState == STATE_CENTER && ev.getY() < (mCenterL + mBarH) && mRawY >= mCenterL) {
return super.dispatchTouchEvent(ev);
}

//处于底部时的事件过滤区域
if (mCurrentState == STATE_BOTTOM && ev.getY() < (mBottomL + mBarH) && mRawY >= mBottomL) {
return super.dispatchTouchEvent(ev);
}

boolean isUp = ev.getY() - mOldY < 0;

if (isTop && mCurrentState == STATE_TOP) {
mOldY = (int) ev.getY();
if (isScrollTop && !isUp) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return super.dispatchTouchEvent(ev);
}
return onTouchEvent(ev);
} else {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return super.dispatchTouchEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
}

return onTouchEvent(ev);
}

以上便是事件分发的全部代码

我们从最简单的地方开始分析代码,在上一篇文章中,定义了几个变量mTopL、mCenterL、mBottomL用来确定布局移动的基准位置,mRawY用来确定布局的当前的Y坐标。

因此,便可以使用这几个变量来确认当前布局所处的状态:

1
2
3
1. mRawY <= mTopL                                                 ==>   布局处于顶部状态
2. mTopL < mRawY && mRawY <= mCenterL + (mBarH >> 1) ==> 布局处于中间状态。
3. mCenterL + (mBarH >> 1) <= mRawY && mRawY < mBottomL + mBarH ==> 布局处于底部状态

在2、3中,mBarH表示的是ToolBar的高度常量,+ mBarH,表示往下移动的偏移常量。

处理普通的事件过滤

继续看代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float t = Math.abs(ev.getY() - mOldY);
//处于顶部时的事件过滤区域
if (isTop && (ev.getY() < mTopL || t < 10)) {
return super.dispatchTouchEvent(ev);
}

//处于中间时的事件过滤区域
if (mCurrentState == STATE_CENTER && ev.getY() < (mCenterL + mBarH) && mRawY >= mCenterL) {
return super.dispatchTouchEvent(ev);
}

//处于底部时的事件过滤区域
if (mCurrentState == STATE_BOTTOM && ev.getY() < (mBottomL + mBarH) && mRawY >= mBottomL) {
return super.dispatchTouchEvent(ev);
}

  1. 第一个if语句,是用来处理顶部状态的事件过滤的,但是由于布局处于顶部状态时,mContentView需要获取事件,而mContentView是一个ViewPager,如果ViewPager加载的Fragment有滑动控件,将是一个很复杂的分发过程,而Fragment滑动控件的处理我们是必须考虑的。

    因此,第一个if语句里面,只处理mContentView的处于顶部状态时的点击事件,只要t小于10,就判断当前的事件为点击事件。而为了让焦点从手势交还给系统,只需要return super.dispatchTouchEvent(ev);便能将事件焦点拦截交还给系统。
  2. 第二个if语句就比较简单了,当处于中间状态时,只要事件的Y坐标小于mCenterL + mBarH坐标时,统统将事件焦点交还给系统, mRawY >= mCenterL,有这个判断时,系统才能确定是中间状态时的事件过滤,不会导致晚上移动的情况下,莫名奇妙事件就交给了系统。
  3. 第三个if语句和第二个if语句差不多,mRawY >= mBottomL这句代码同上所示,只有这个判断时,才能确认是底部事件的过滤,如果没有这句话,mContentView在中间状态时,就会处理,底部事件的过滤,将导致,mContentView处于中间状态时,将失去对手势的控制。

接下来便是处理mContentView的滑动控件的事件处理了!

在当乐的游戏详情界面中,mContentView处于顶部时,里面的ListView或者ScrollView,只有滑动到最顶部时,再外下滑动,mContentView才能将焦点交还给系统。

处理Fragment 中滑动事件过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
boolean isUp = ev.getY() - mOldY < 0;

if (isTop && mCurrentState == STATE_TOP) {
mOldY = (int) ev.getY();
if (isScrollTop && !isUp) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return super.dispatchTouchEvent(ev);
}
return onTouchEvent(ev);
} else {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return super.dispatchTouchEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
}

如上的代码所示,如果,当mContentView中的Fragment的滑动控件滑动到顶部,并且mContentView处于顶部,并且手势向上则将事件焦点交给手势,否则,交还给系统。

这里需要注意以下两行代码:

1
2
3
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return super.dispatchTouchEvent(ev);
}

这两行代码是整个滑动事件分发的灵魂!!!,这两句代码是告诉系统,当焦点进行交换时,告诉当前掌控焦点的服务(系统、手势)交出焦点,重新进行分配!!如果没有这两句代码,焦点的切换将很有可能失败(就为了写出两行代码,工期延期了整整一个星期,说多了都是泪!!!!!!!)。

处理横向事件过滤

1
2
3
4
if (Math.abs(ev.getX() - mOldX) >= 0 && Math.abs(ev.getY() - mOldY) < 300 && isTop) {
mOldX = ev.getX();
return super.dispatchTouchEvent(ev);
}

以上代码是实现mContentViewfragment的切换的,这代码很简单,只要是横向手势,并且X的偏差小于300就认为其是横向事件。

最终效果

最终效果

写在最后

来来回回一个多月,这个页面的blog算是写完了(整个功能实现也就7、8天,写着blog写了快两个月了,懒癌晚期伤不起)。现在的效果已经和当乐的差不多了,但是还是有点差别,比如,我这没有底部栏,比如,我这中间状态不能对mContentView进行切换,其实这些都很容易实现(其实我是懒癌晚期,不想写了..)

最后还是说个思路吧:

  1. 底部导航栏那个,在布局里面写FrameLayout,然后编写自定义View,在主界面的代码里面,给mContentView设置addOnPageChangeListener事件监听,在滑动的过程中,FrameLayout动态添加你的自定义的导航栏View。(我在公司的APP里面采用的是这个思路)
  2. mContentView中间状态的切换,这个只需要在下面的语句中添加横向状态的添加横向手势移动的判断方法便可以了,需要注意下事件分发的范围
    1
    2
    3
    if (mCurrentState == STATE_CENTER && ev.getY() < (mCenterL + mBarH) && mRawY >= mCenterL) {
    return super.dispatchTouchEvent(ev);
    }

源代码

其实上面说的全部都是废话,真正重要的还是源代码!!

点击我获取源代码,最后跪求star和issues