本篇内容
第一部分:自定义ViewGroup的使用,手势识别器和Scroller滑动
第二部分:处理滑动监听,处理滑动冲突,增加ViewPager的指示器
基础概念
常见的滑动冲突:外部滑动方向和内部滑动方向不一致、外部滑动方向和内部滑动方向一致。
我们自定义的ViewPager如果在其中一页中存在ListView,那么就需要解决滑动冲突的问题。
由于系统自带ViewPager中,自己已经解决了滑动冲突。
第一部分
1、创建一个类,继承ViewGroup,由于ViewPager里面包含多个子View,所以继承这个类,实现onLayout方法
onLayout:这个方法是对我们该View的一个位置摆放,这里可以看到onLayout这(int l,int t,int r,int b)这四个参数,分别代表着这个ViewPager的左上右下的位置,由于你引用ViewPager是match_parent,所以l和t为0,r和b为宽和高的距离
public class MyViewPager extends ViewGroup{
public MyViewPager(Context context) {
super(context);
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
2、我们在自己的Activity中
引用自定义的这个组件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.handsome.app2.View.Custom.MyViewPager
android:layout_width="match_parent"
android:id="@+id/vp_my"
android:layout_height="match_parent"/>
</RelativeLayout>
3、复制几张
图片作为演示,并为他们创建Id数组
private int[] image_id = {R.drawable.guide_map1,R.drawable.guide_map2,R.drawable.guide_map3,R.drawable.guide_map4};
4、接着我们需要对图片进行
初始化,并加入到ViewPager中,我们写个初始化方法,并将他们放在构造方法中
public MyViewPager(Context context) {
super(context);
initView();
}
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView(){
for (int i=0;i<image_id.length;i++){
ImageView iv = new ImageView(getContext());
iv.setBackgroundResource(image_id[i]);
this.addView(iv);
}
}
5、这个时候,启动程序,
你是看不到有图片出现的,因为你还没有对这几张图片进行位置的摆放,所以需要在onLayout中进行位置处理
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < image_id.length; i++) {
this.getChildAt(i).layout(i * getWidth(), t, (i + 1) * getWidth(), b);
}
}
这样处理的好处就是将图片
一字排开
6、现在已经排好了图片,接着我们就来处理滑动事件了,我们通过一个手势识别器自动帮我们识别滑动事件
private GestureDetector mDetector ;
mDetector = new GestureDetector(new SimpleOnGestureListener(){
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//scrollBy:相对滑动,相对我们当前的控件多少距离,就滑动多少距离
//distanceX是我们手滑动的距离,即我们的手相对控件滑动了多少,所以X轴滑动这个距离,Y轴滑动0
scrollBy((int)distanceX,0);
return super.onScroll(e1, e2, distanceX, distanceY);
}
});
通过onTouchEvent
委托给手势识别器
,并且
返回true,让这个控件消耗这个事件
@Override
public boolean onTouchEvent(MotionEvent event) {
mDetector.onTouchEvent(event);
return true;
}
这个时候我们就可以看下效果图
7、我们看到跟ViewPager还差一点,就是滑到第几张就自动复原和不能超出头和尾部的图片,这时就要处理滑动事件了
@Override
public boolean onTouchEvent(MotionEvent event) {
mDetector.onTouchEvent(event);
//触摸事件处理
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
//你滑动的距离加上屏幕的一半,除以屏幕宽度,如果你滑动距离超过了屏幕的一半,这个pos就加1
int pos = (scrollX + getWidth() / 2) / getWidth();
//滑到最后一张的时候,不能出边界
if (pos >= image_id.length) {
pos = image_id.length - 1;
}
//绝对滑动,直接滑到指定的x值
scrollTo(pos * getWidth(), 0);
break;
}
return true;
}
效果图
8、基本效果已经出来了,就是没有很自然的滑动过去,那么这个时候就要用到scroller了
private Scroller mScroller;
可以将ScrollTo替换掉了,让它自然滑动mScroller = new Scroller(getContext());
使用invalidate这个方法会有执行一个回调方法computeScroll,我们来重写这个方法//绝对滑动,直接滑到指定的x值 //scrollTo(pos * getWidth(), 0); //自然滑动,从手滑到的地方开始,滑动距离是页面宽度减去滑到的距离,时间由路程的大小来决定 mScroller.startScroll(scrollX, 0, pos * getWidth() - scrollX, 0, Math.abs(pos * getWidth())); invalidate(); break;
其实Scroller的原理就是用ScrollTo来一段一段的进行,最后看上去跟自然的一样,必须使用postInvalidate,这样才会一直回调computeScroll这个方法,直到滑动结束。基本上ViewPager的效果就出来了,看下效果图:@Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), 0); postInvalidate(); } }
以下是这一部分的整个类的源码
public class MyViewPager extends ViewGroup { private int[] image_id = {R.drawable.guide_map1, R.drawable.guide_map2, R.drawable.guide_map3, R.drawable.guide_map4}; private GestureDetector mDetector; private Scroller mScroller; public MyViewPager(Context context) { super(context); initView(); } public MyViewPager(Context context, AttributeSet attrs) { super(context, attrs); initView(); } public MyViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { for (int i = 0; i < image_id.length; i++) { ImageView iv = new ImageView(getContext()); iv.setBackgroundResource(image_id[i]); this.addView(iv); } mDetector = new GestureDetector(new SimpleOnGestureListener() { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //scrollBy:相对滑动,相对我们当前的控件多少距离,就滑动多少距离 //distanceX是我们手滑动的距离,即我们的手相对控件滑动了多少,所以X轴滑动这个距离,Y轴滑动0 scrollBy((int) distanceX, 0); return super.onScroll(e1, e2, distanceX, distanceY); } }); mScroller = new Scroller(getContext()); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { for (int i = 0; i < image_id.length; i++) { this.getChildAt(i).layout(i * getWidth(), t, (i + 1) * getWidth(), b); } } @Override public boolean onTouchEvent(MotionEvent event) { mDetector.onTouchEvent(event); //触摸事件处理 switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: int scrollX = getScrollX(); //你滑动的距离加上屏幕的一半,除以屏幕宽度,如果你滑动距离超过了屏幕的一半,这个pos就加1 int pos = (scrollX + getWidth() / 2) / getWidth(); //滑到最后一张的时候,不能出边界 if (pos >= image_id.length) { pos = image_id.length - 1; } //绝对滑动,直接滑到指定的x值 //scrollTo(pos * getWidth(), 0); //自然滑动,从手滑到的地方开始,滑动距离是页面宽度减去滑到的距离,时间由路程的大小来决定 mScroller.startScroll(scrollX, 0, pos * getWidth() - scrollX, 0, Math.abs(pos * getWidth())); invalidate(); break; } return true; } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { scrollTo(mScroller.getCurrX(), 0); postInvalidate(); } } }
第二部分
1、接下来介绍指示器的完成,指示器用RadioButton来实现,在xml中编写RadioGroup
2、在主页面中对RadioButton初始化<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RadioGroup android:id="@+id/rg" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" /> <com.handsome.app2.View.Custom.MyViewPager android:id="@+id/vp_my" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
3、回到我们的MyViewPager类,创建ViewPager的监听事件接口final RadioGroup rg = (RadioGroup) findViewById(R.id.rg); for (int i = 0; i < 4; i++) { RadioButton rb = new RadioButton(this); //这里用使用0,1,2,3为id,对应ViewPager的pos值 rb.setId(i); rg.addView(rb); //默认第一个选中 if (i == 0) { rb.setChecked(true); } }
4、创建ViewPager设置页面的方法,并调用接口的onPagerChange方法private OnPagerChangeListener listener; public interface OnPagerChangeListener { void onPagerChange(int pos); } public void setOnPagerChangeListener(OnPagerChangeListener listener) { this.listener = listener; }
这个时候,滑动事件处理就可以用setCurrentItem方法来替代/** * 设置当前页面 * @param pos */ public void setCurrentItem(int pos) { mScroller.startScroll(getScrollX(), 0, pos * getWidth() - getScrollX(), 0, Math.abs(pos * getWidth())); invalidate(); //页面切换接口回调 if (listener != null) { listener.onPagerChange(pos); } }
5、回到Activity中进行绑定事件case MotionEvent.ACTION_UP: int scrollX = getScrollX(); //你滑动的距离加上屏幕的一半,除以屏幕宽度,如果你滑动距离超过了屏幕的一半,这个pos就加1 int pos = (scrollX + getWidth() / 2) / getWidth(); //滑到最后一张的时候,不能出边界 if (pos >= image_id.length) { pos = image_id.length - 1; } //绝对滑动,直接滑到指定的x值 //scrollTo(pos * getWidth(), 0); //自然滑动,从手滑到的地方开始,滑动距离是页面宽度减去滑到的距离,时间由路程的大小来决定 // mScroller.startScroll(scrollX, 0, pos * getWidth() - scrollX, 0, Math.abs(pos * getWidth())); // invalidate(); setCurrentItem(pos); break;
这样就完成了切换效果,看效果图final MyViewPager myViewPager = (MyViewPager) findViewById(R.id.vp_my); //按钮点击事件 rg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { int pos = checkedId; myViewPager.setCurrentItem(pos); } }); //ViewPager切换事件 myViewPager.setOnPagerChangeListener(new MyViewPager.OnPagerChangeListener() { @Override public void onPagerChange(int pos) { rg.check(pos); } });
滑动冲突的处理
1、我们在ViewPager中嵌套一个ScrollView作为它的子View,这样ViewPager是左右滑动,ScrollView是上下滑动,那么就造成了滑动冲突
创建一个ScrollView的xml文件
2、在主界面中添加这个页面到ViewPager中,同时增加一个RadioButton<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hensen_的博客" android:textSize="60dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="http://blog.csdn.net/qq_30379689" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hensen_的博客" android:textSize="60dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="http://blog.csdn.net/qq_30379689" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hensen_的博客" android:textSize="60dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="http://blog.csdn.net/qq_30379689" /> </LinearLayout> </ScrollView>
这个时候你可以看到增加了一个页面,但是页面是空白的:由于ViewGroup只遍历它的一个子View(即在这里的ScrollView),并不会去遍历ScrollView里面的内容,所以必须重写它的onMeasure方法,对ScrollView的子View进行遍历// 添加ScrollView页面 View testView = View.inflate(this, R.layout.view_scroller, null); myViewPager.addView(testView, 1);
查看效果图@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec); } }
这个时候你会发现这个页面只能上下滑动不能左右滑动,所以需要处理滑动事件冲突3、重写父控件的onInterceptTouchEvent方法,如果是左右滑动,我们的父控件就把滑动事件拦截下来
这个时候还需要把将ACTION_DOWN传递给手势识别器,因为拦截了MOVE的事件后,DOWN的事件也要给拦截给手势识别器,否则会丢失事件int startX; int startY; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 如果左右滑动, 就需要拦截, 上下滑动,不需要拦截 switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: startX = (int) ev.getX(); startY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: int endX = (int) ev.getX(); int endY = (int) ev.getY(); int dx = endX - startX; int dy = endY - startY; if (Math.abs(dx) > Math.abs(dy)) { // 左右滑动 return true;// 中断事件传递, 不允许孩子响应事件了, 由父控件处理 } break; default: break; } return false;// 不拦截事件,优先传递给孩子处理 }
到现在,ViewPager就完成了滑动冲突的处理,既能上下滑动和左右滑动了switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mDetector.onTouchEvent(ev);// 将ACTION_DOWN传递给手势识别器, 避免事件丢失 startX = (int) ev.getX(); startY = (int) ev.getY(); break;