之前的几篇博客,我测试了View事件分发机制中的一些知识点,我们理解事件分发机制的目的就是为了能够更好了知道View中事件的传递过程进而能够对于滑动冲突有针对性的解决措施,今天我们通过一个翻页实例来学习下滑动处理的方式之一-----外部拦截法;
因为要用到翻页,那么不可避免的要用到Scroller类,其实拿scrollBy和scrollTo也能做到翻页的效果,但不足是两者都是在瞬间完成对View内容的移动,用户体验度不好,注意这里是View内容的移动而不是View本身的移动,而Scroll类却能够进行平滑的移动,原因在于他将大的滑动根据设置的时间段分割成多个小的滑动,这样渐进式的移动体验度明显好于前者;
在正式讲解案例之前,我们有必要稍微了解下Scroller类:
我们平常使用Scroller的过程如下:
(1)创建Scroller实例;
(2)通过Scroller实例调用startScroll进行滑动事件开始之前的一些设置;
(3)调用startScroll之后需要调用invalidate来进行View重绘,而View重绘中的draw方法就会调用computeScroll方法,这个方法在View中是一个空实现,也就是说我们想要实现弹性滑动就需要重写computeScroll方法,在这个方法里面我们可以通过Scroller实例获取到当前滑动到的位置的scrollX以及scrollY,接着调用scrollTo来移动到这个位置即可,在scrollTo调用结束之后我们同样需要调用invalidate或者postInvalidate来进行View的重绘,原因很简单,因为你总不能移动一次就结束吧,后面的移动也需要重绘View的呢,那么很多人就在想你这里不也是使用的scrollT么?为什么你就能实现弹性滑动呢?第(4)点解释原因;
(4)其实在上面第(3)点中少说了一个判断条件,那就是当computeScrollOffset返回true的时候我们才会通过Scroller实例去获得scrollX以及scrollY,computeScrollOffset返回true表示我们的滑动还没有停止,还需要继续滑动,返回false表示滑动已经结束了,这时候当然不再需要调用invalidate或者postInvalidate来进行View的重绘了;
通过上面的讲述我们知道了在Scroller中最关键的两个方法是startScroll和computeScrollOffset以及View中没有实现的computeScroll方法,下面从源码角度分析下前两个方法,之后进入实例:
Scroller$startScroll
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / mDuration;
}
startScroll的四个参数startX和startY表示滑动起点,dx和dy表示滑动的距离,duration表示滑动的时间,可以看到我们把滑动模式设置为是SCROLL_MODE,设置mDuration这个事件默认是250ms,设置开始滑动时间mStartTime,并且将滑动起点、距离等进行相应的赋值操作;
computeScrollOffset
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished. loc will be altered to provide the
* new location.
*/
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
float x = timePassed * mDurationReciprocal;
if (mInterpolator == null)
x = viscousFluid(x);
else
x = mInterpolator.getInterpolation(x);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE[index];
final float d_sup = SPLINE[index + 1];
final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
刚开始判断滑动是否结束了,结束的话执行第8行返回false,这也就验证说明computeScrollOffset返回false的话表示滑动停止,否则的话会执行第13行,判断滑动事件是否在mDuration时间范围内,因为我们设定的mMode值是SCROLL_MODE,执行16--25行,设置mCurrX以及mCurrY,有了这两个值之后我们就可以在computeScroll中调用getCurrX以及getCurrY来获取到这两个值,并且通过scrollTo来移动到这个位置了;
好了,我们该开始实例部分了:
先来看看效果:
我们采用三个Fragment实现了三个切换的页面,其中第1个Fragment包含有一个ListView,因为ListView本身是上下滑动的,而我们这里引入了左右滑动,那么这种情况下可能就会出现滑动冲突了;第2和3个Fragment分别是显示一张图片,你叫简单,下面退出来各自的布局文件:
fragment1.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
</LinearLayout>
就仅仅是包含一个ListView而已;
fragment2.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/paoche1">
</ImageView>
</LinearLayout>
仅仅包含一个ImageView;
fragment3.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/paoche2">
</ImageView>
</LinearLayout>
同样仅仅包含一个ImageView;
接下来是三个Fragment的代码:
Fragment1.java
public class Fragment1 extends Fragment{
public ListView mListView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
String[] strArray = new String[20];
for(int i = 0;i < 20;i++)
strArray[i] = "Item"+i;
MyListAdapter adapter = new MyListAdapter(strArray, this.getActivity());
View view = inflater.inflate(R.layout.fragment1, container,false);
mListView = (ListView) view.findViewById(R.id.listView);
mListView.setAdapter(adapter);
return view;
}
}
因为我们的Fragment1里面有ListView,所以我们需要在他上面绑定数据,这些绑定操作是在Fragment1的onCreate方法里面进行的;
Fragment2.java
public class Fragment2 extends Fragment{
public ImageView mImageView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment2, container,false);
return view;
}
}
就是简单的返回fragment2对应的View;
Fragment3.kava
public class Fragment3 extends Fragment{
public ImageView mImageView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment3, container,false);
return view;
}
}
同样也只是简单的返回fragment3对应的View;
主界面的布局activity_main.xml:
<com.hzw.dealslideconflict.ScrollLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/fragment1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.hzw.dealslideconflict.Fragment1"/>
<fragment
android:id="@+id/fragment2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.hzw.dealslideconflict.Fragment2"/>
<fragment
android:id="@+id/fragment3"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.hzw.dealslideconflict.Fragment3"/>
</com.hzw.dealslideconflict.ScrollLayout>
可以看到主界面将三个Fragment添加进来,为了到达三个Fragment分别是三个页面的效果,每个Fragment的layout_width以及layout_height都设置成了match_parent,注意到最外面的布局是我们自己定义的,定义代码如下:
public class ScrollLayout extends ViewGroup{
//左边界值
public int leftBoard;
//右边界值
public int rightBoard;
//DOWN事件时x的位置
public float mXDown;
//上一次MOVE事件的x坐标
public float mXLastMove;
//当前MOVE事件的x坐标
public float mXMove;
//被认为是滑动的最短距离
public int mTouchSlop;
//获取弹性滑动对象
public Scroller mScroller;
public ScrollLayout(Context context) {
super(context);
}
public ScrollLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//获取被认为是最短的滑动距离,也就是说滑动超过mTouchSlop才算是滑动
ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
mScroller = new Scroller(getContext());
}
public ScrollLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取子View的个数
int childCount = getChildCount();
//对每个子View进行measure操作
for(int i = 0;i < childCount;i++)
{
View childView = getChildAt(i);
//测量每个子View
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//如果发生变化,则对他进行重新布局
if(changed)
{
int childCount = getChildCount();
for(int i = 0;i < childCount;i++)
{
View childView = getChildAt(i);
childView.layout(i*childView.getMeasuredWidth(), 0, (i+1)*childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
leftBoard = getChildAt(0).getLeft();//获取左边界值
rightBoard = getChildAt(childCount-1).getRight();//获取右边界值
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mXDown = ev.getRawX();//获取DOWN事件下相对于屏幕的x坐标
mXLastMove = mXDown;//初始化上一次MOVE事件的x坐标
break;
case MotionEvent.ACTION_MOVE:
mXMove = ev.getRawX();
float slideDistance = Math.abs(mXMove-mXLastMove);
mXLastMove = mXMove;
if(slideDistance > mTouchSlop)
{
//表示是翻页操作,拦截事件,拦截事件之后接下来的MOVE和UP事件都将由该View来处理
return true;
}
break;
default:
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_MOVE:
//实现具体的移动操作
mXMove = event.getRawX();
int xScrolled = (int)(mXLastMove - mXMove);
System.out.println(getScrollX()+xScrolled+getWidth());
//这里的getScrollX的值等于View的左边缘和View内容左边缘的距离
if(getScrollX()+xScrolled < leftBoard)
{
//表示超出了左边界,那么我们需要让其直接滑到左边界
scrollTo(leftBoard, 0);
return true;
}else if(getScrollX()+xScrolled+getWidth() > rightBoard)
{
//表示超出了右边界,那么我们需要让其直接滑到右边界
scrollTo(rightBoard-getWidth(), 0);
return true;
}
mXLastMove = mXMove;
scrollBy(xScrolled,0);
break;
case MotionEvent.ACTION_UP:
//在UP事件中要判断我们现在已经滑动到的位置,如果已经滑动超过一半的屏幕,那么应该翻到下一页,不到一半的话应该回退滑动
int index = (getScrollX()+getWidth()/2) / getWidth();
int distance = index*getWidth() - getScrollX();
mScroller.startScroll(getScrollX(), 0, distance, 0);
invalidate();//重新绘制
break;
default:
break;
}
return super.onTouchEvent(event);
}
@Override
public void computeScroll() {
//这个方法会在draw方法中调用
//判断滑动是否完成,返回true表示滑动没有完成
if(mScroller.computeScrollOffset())
{
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
}
来解释下这段代码的实现:
mTouchSlop指的是系统认为的被认为是滑动的最小距离,获方法就是25/26行代码;
接着我们重写了onMeasure方法,该方法是用来测量View的,第44行调用ViewGroup的measureChild方法进行测量;
接着重写onLayout方法来对View进行布局,因为我们的翻页操作是在水平方向进行的,所以可以看到第56行layout参数只有水平方向的左右有值;第58行和59行获取左右边界值,为了防止我们已经滑到最左边或者最右边了继续滑动还会出现页面的情况;
接着onInterceptTouchEvent的DOWN事件中主要进行的是mXDown以及mXLastMove的初始化操作;在MOVE事件中,首先获取到当前滑动到的位置mXMove,计算出当前位置与上次mXLastMove的绝对值差,第74行判断这个值是否大于默认的认为是滑动的最短距离,如果大于的话会直接返回true来拦截MOVE事件,这样MOVE事件就不会传递到当前View的子View上了,也就是说这时候进行的是水平滑动了,不再会进行ListView的上下滑动;
onInterceptTouchEvent拦截MOVE事件后就会执行当前View的onTouchEvent方法,我们来看看MOVE操作部分,首先当然是获取到当前位置,接着第97行是在判断有没有超出左边界,超出的话执行100行直接调用scrollTo让View滑到左边界处;第102行判断有没有超出右边界,有的话执行第105行直接执行scrollTo将当前View滑动到右边界处;如果没有超出左边界或者右边界的话,执行第109行,注意这里是scrollBy,因为我们这里的移动是相对于上一次的移动,当然你可以使用scrollTo,但是每次你需要计算出绝对移动的坐标;
invalidate转而会去执行draw方法,而draw会去执行computeScroll,也就是我们这里重写的computeScroll,该方法里面首先通过computeScrollOffset判断滑动没有停止的话,获取当前位置,调用scrollTo进行滑动,随后同样调用invalidate,这样间接的在递归调用computeScroll,直到滑动结束,达到了弹性滑动的效果;
注意一点的是:在onTouchEvent的DOWN下面要加return true这行代码;也就是第90行代码,原因在于,如果你的Fragment里面是ImageView或者TextView的话,默认情况下是没有为他们设置clickable以及longclickable属性的,这回导致当事件传递到他们上面的时候他们的onTouchEvent返回false,ScrollLayout作为他们的父View,他的onTouchEvent方法默认返回false,这会导致随后到来的MOVE事件将不再会由当前Fragment的父View执行,也就是你会发现你的程序将不再能滑动翻页;
最后就是MainActivity的代码了,他继承自FragmentActivity:
public class MainActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
这样,这个例子就讲解结束了;上面我们采用的解决滑动冲突的方法是外部拦截法,也就是说因为事件首先传递给父View,那么我们首先在父View上判断需不需要拦截这个事件,我们的这个例子很简单,只要我们在水平方向MOVE的距离大于了系统默认认为滑动的距离,我们就拦截事件,当然实际中可以通过速度、水平方向移动距离大于竖直方向等等来进行判断,我们实例中的ListView是上下移动的,而我们的页面切换是水平移动的,当出现水平移动的时候就让当前父View拦截了事件,而不会将他传递给ListView了,这也就解决了滑动冲突啦!