由于公司要求我做一个luancher项目并分为3页。考虑到以后要应对需求的多样性,那么就不考虑去用SDK原生的了,也就只能自己写一个出来。那么 今天将跟大家分享 如何自定义一个简陋的ViewPager。功能上还有一些不足,以后再慢慢优化,希望大家多多指教。下面开始为大家带来自定义ViewPager的实现。
好了,首先我们自定义一个MyViewPager继承于ViewGroup,由于luancher各个界面中要放很多一些子控件,所以这边肯定要继承于ViewGroup了,下面来说说所要实现的一些相关的方法。还是先上代码,然后逐一击破吧。
package com.example.myviewpager;
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class MyViewPager extends ViewGroup {
private Context mContext;
/**
* 滑动工具类
*/
private Scroller myScroller;
/**
* 手势识别的工具类
*/
private GestureDetector detector;
/**
* 是否为快速滑动事件
*/
private boolean isFling;
/**
* 当前的ID值 显示在屏幕上的子View的下标
*/
private int currId = 0;
/**
* down事件时的x坐标
*/
private int firstX = 0;
/**
* 界面发生改变时的回调接口
*/
private MyPageChangedListener pageChangedListener;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.mContext = context;
initData();
}
private void initData() {
myScroller = new Scroller(mContext);
detector = new GestureDetector(mContext, new OnGestureListener() {
@Override
/**
* 手指离开触摸屏的那一刹那时触发
*/
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
@Override
/**
* 如果是按下的时间超过瞬间,而且在按下的时候没有松开或者是拖动时触发
*/
public void onShowPress(MotionEvent e) {
}
@Override
/**
* 手指在触摸屏上滑动时触发
*/
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
/**
* 移动当前view内容 移动一段距离 disX X方向移的距离 为正是,view向左移动,为负时,图片向右移动 disY
* Y方向移动的距离
*/
scrollBy((int) distanceX, 0);
return false;
}
@Override
/**
* 手指长按触摸屏,并且没有松开时触发
*/
public void onLongPress(MotionEvent e) {
}
@Override
/**
* 手指在触摸屏上迅速移动,并松开的动作,即快速滑动时触发
*/
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
isFling = true;
if (velocityX > 0 && currId > 0) { // 快速向右滑动
currId--;
} else if (velocityX < 0 && currId < getChildCount() - 1) { // 快速向左滑动
currId++;
}
moveToDest(currId);
return false;
}
@Override
/**
* 用户按下屏幕时触发
*/
public boolean onDown(MotionEvent e) {
return false;
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// int size = MeasureSpec.getSize(widthMeasureSpec);
// int mode = MeasureSpec.getMode(widthMeasureSpec);
// 设置子view的测量大小
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
v.measure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
/**
* 对子view进行布局,确定子view的具体位置,changed 若为true ,说明布局发生了变化
*
* l\t\r\b\ 则是指当前viewgroup 在其父view中的位置
*
* 另外父view 会根据子view的需求,和自身的情况,来综合确定子view的位置
*/
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
// 取得下标为I的子view
View view = getChildAt(i);
// 指定子view的位置 , 左,上,右,下,是指在viewGround坐标系中的位置
// view.getWidth() 得到view的真实的大小。
// view.getMeasuredWidth() 得到view的测量的大小
view.layout(0 + i * getWidth(), 0, getWidth() + i * getWidth(), getHeight());
}
}
@Override
/**
* view挂载到窗体时触发
*/
protected void onAttachedToWindow() {
super.onAttachedToWindow();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
return true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
detector.onTouchEvent(ev);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = (int) ev.getX();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (!isFling) {
int nextId = 0;
if (ev.getX() - firstX > getWidth() / 2) {
nextId = currId - 1;
} else if (firstX - ev.getX() > getWidth() / 2) {
nextId = currId + 1;
} else {
nextId = currId;
}
moveToDest(nextId);
}
isFling = false;
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
// break;
return true;
case MotionEvent.ACTION_UP:
break;
}
return super.onInterceptTouchEvent(ev);
}
/**
* 移动到指定的界面
*
* @param <br/>
* nextId 界面的下标
*/
public void moveToDest(int nextId) {
/*
* 对 nextId 进行判断 ,确保 是在合理的范围 即 nextId >=0 && next <=getChildCount()-1
*/
currId = (nextId > 0) ? nextId : 0;
currId = (currId <= getChildCount() - 1) ? currId : (getChildCount() - 1);
// 瞬间移动
// scrollTo(currId*getWidth(), 0);
// 设置回调
if (pageChangedListener != null) {
pageChangedListener.moveToDest(currId);
}
// 最终的位置 - 现在的位置 = 要移动的距离
int distance = currId * getWidth() - getScrollX();
// 设置滑动这段距离所运行的时间,开始缓慢滑动效果
myScroller.startScroll(getScrollX(), 0, distance, 0, Math.abs(distance));
/*
* 刷新当前view onDraw()方法 的执行
*/
invalidate();
}
@Override
/**
* invalidate() 会导致 computeScroll()这个方法的执行
*/
public void computeScroll() {
if (myScroller.computeScrollOffset()) {
// 获取滑动过后的当前X坐标
int newX = (int) myScroller.getCurrX();
scrollTo(newX, 0);
invalidate();
}
}
public void setOnPageChangedListener(MyPageChangedListener pageChangedListener) {
this.pageChangedListener = pageChangedListener;
}
/**
* 页面改时时的监听接口
*/
public interface MyPageChangedListener {
void moveToDest(int currid);
}
}
这个类就这么多东西了,没其他的。
那么首先是重写构造函数,带两个参数的构造函数,这样即可在XML中应用,如果是实现只带一个参数的构造则要代码new出来而不能直接在XML中应用,在构造方法中先获取上下文context,然后初始化一些数据,这边用到了2个工具类:一个是Scroller滑动辅助工具类,另外一个则是GestureDetector手势识别的工具类,GestureDetector这个估计很多人都用过,具体该方法在什么时候回调在代码中已注释,Scroller则是在后面滑动的时候要用到的。
然后就是onMeasure方法对子view进行大小的测量
还有一点作为父view控件,需要对子view进行布局layout,设置子view的位置,否则将子view无法显示出来
上面的都是最基本的了。
好了,下面进入重点:
那么体验SDK Viewpager时是可以左右滑动,并且快速滑动时可以进入到下一界面。根据这一点,我这边重写了几个方法
dispatchTouchEventon(),TouchEvent(),onInterceptTouchEvent(),computeScroll()。就这4个重要的函数了!另外还有一个moveToPageIndex()是移动当前屏幕到第几页的。
首先用户手指按下(down),移动(move),抬起(up)这些事件我们都要捕获到拿来用,同时也要分发给各个子view,既然这样,那我这边是这样来做的。
在TouchEvent中返回true,表示后续事件统统拿到(这边如果对于Android事件分发机制不是很熟悉的同学请去查阅相关资料)。
dispatchTouchEventon中(用户操作时,最先执行的就是这个方法,消息分发)我们将事件分发到GestureDetector这个类,一部分事件在GestureDetector中去处理,同时记得要return super.dispatchTouchEvent(ev); 将事件交由父类分发到各个子view,这里不能return true(如果return true表示后续不在分发接下来的事件)否则子View将无法获取分发的事件。
然后对于滑动屏幕这个事件就交由GestureDetector去处理,那么dispatchTouchEventon我们要做的就是处理up事件,当用户手指离开屏幕时, 如果不是快速滑动的情况下,我们要将屏幕移动到第几页面中,而且还要判断页面的ID,不能超过页面总数,也不能小于0。
moveToPageIndex()则是要与computeScroll()结合起来讲解,首先moveToPageIndex中我们用到了Scroller工具类,目的就要实现一种带有动画效果的滑动,如果我们在moveToPageIndex中直接用scrollTo函数就会产生不好的用户体验,界面切换的时候,总是瞬时的,我们要让它有一定的切换时间。具体API代码中也有注释。
最后一个就是onInterceptTouchEvent,见名知意,该方法是用来拦截消息的,一旦拦截,那么后续分发的事件都拦截,我这边主要使用在这边,如下图:
就是在移动屏幕到其它页面时,如果手指是在该button上的话会同事触发该button的onclick事件,那么怎么才能阻止这种情况的发生?还记得我在第一篇博文中提到过完整的一次onclick是由down跟up组成的吧,那么问题就好办了,我们用手指在屏幕上滑动时我们就拦截move事件,move事件拦截后,则后续的事件(up)将不会再分发给各个子view控件,这样就不会同事触发button的onclick事件了。
还有就是注册一个回调接口监听界面的切换。
最后就是在xml布局文件中应用
以及在activity中注册界面切换的监听
好了!到这里基本上可以说是大功告成。