ViewPager控件可以让我们做出很多漂亮的界面,例如导航, 页面菜单等. 那么我们自己能否去实现ViewPager的效果呢? 本文将介绍如何使用ViewGroup + scrollTo + scroller实现ViewPager控件, 并且会简单地实现一个自己的scroller, 来了解学习系统提供的scroller类滑屏功能的实现思想.
首先看一下实现的效果:
- 定义自己的ViewPager类–MyViewPager继承ViewGroup
public class MyViewPager extends ViewGroup {
private Context context;
//手势识别工具类
private GestureDetector detector;
public MyViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
initView();
}
private void initView() {
detector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
return false;
}
@Override
public void onShowPress(MotionEvent e) {
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
return false;
}
//处理移动事件
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
//将当前视图内容偏移(x , y)个单位,可视区域也跟着偏移(x,y)个单位
//也就是说让视图跟着鼠标移动, distanceX为鼠标在屏幕上移动的距离
scrollBy((int) distanceX, 0);
return false;
}
@Override
public void onLongPress(MotionEvent e) {
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
return false;
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置每个子view的位置
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.layout(i * getWidth(), 0, (i + 1) * getWidth(), getHeight());
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);
return true;
}
}
(1) 在MyViewPager中, 重写OnLayout方法, 在OnLayout方法中去确定子view在ViewPager中的位置:
通过getChildAt获得所有的子view, 调用子view的layout方法设置每个子view的位置, layout接收4个参数(就是左上角坐标与右下角坐标), 来确定view的大小, 如上图可以分析出每个view在MyViewPager中的位置, 例如第2个view的位置是:
[getWidth(), 0, 2 * getWidth(), getHeight()] , getWidth 与 getHeight为MyViewPager的宽高. 我们可以看到变化的只有横坐标.
(2) 重写onTouchEvent方法, 此方法处理屏幕触摸事件, 在这里使用用户手势识别工具类GestureDetector来处理action.move事件, 在OnGestureListener的onScroll中处理move事件, onScroll方法的参数distanceX, 就是工具类帮我们计算好的手指在屏幕上x轴方向移动的距离, 然后就可以很方便的使用此参数, 调用scrollBy(x, y)方法移动视图, 让视图偏移(x, y). 这样就实现了MyViewPager随手指移动而移动.
2.使用MyViewPager. 并给MyViewPager设置子view
public class MainActivity extends AppCompatActivity {
private MyViewPager myViewPager;
private int[] imgIds = new int[] {
R.mipmap.a, R.mipmap.b, R.mipmap.c,
R.mipmap.d, R.mipmap.e, R.mipmap.f
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
myViewPager = (MyViewPager) findViewById(R.id.my_viewpager);
for (int i = 0; i < imgIds.length; i++) {
ImageView imageView = new ImageView(this);
imageView.setImageResource(imgIds[i]);
myViewPager.addView(imageView);
}
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.myviewpager.MainActivity">
<com.myviewpager.MyViewPager
android:id="@+id/my_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
给MyViewPager设置6张图片. 现在效果图如下:
3.现在图片可以跟随手指滑动而滑动, 但是还不能自动地切换, 图片只能停在你移动到的地方, 如果想实现切换的效果, 还需要我们自己去处理touch事件, 在onTouchEvent中继续添加代码:
//标记当前显示在屏幕上的图片
private int currPos = 0;
//记录按下时的横坐标
private int firstX = 0;
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
detector.onTouchEvent(event);
//添加下面的代码
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
firstX = getScrollX(); //获得按下去时的横坐标
break;
case MotionEvent.ACTION_UP: //判断显示哪个子view, 如果滑动大于父控件的一半切换子view
int tmpPos = currPos;
if ((getScrollX() - firstX) > getWidth() / 2) { //向左滑动
tmpPos++;
} else if ((firstX - getScrollX()) > getWidth() / 2) { //向右滑动
tmpPos--;
}
MoveToDest(tmpPos); //切换到指定的图片
break;
default:
break;
}
return true;
}
public void MoveToDest(int tmpPos) {
//确定currPos的值, 保证currPos的范围在[0, getChildCount() - 1]
currPos = tmpPos > 0 ? tmpPos : 0;
currPos = currPos < getChildCount() - 1 ? currPos : (getChildCount() - 1);
//将视图内容偏移至(x , y)坐标处,可视区域位于(x , y)坐标处
scrollTo(currPos * getWidth(), 0);
}
定义两个成员变量, firstX, currPos来记录点击屏幕时的点与当前显示在屏幕上的子view. 在touch_up事件中处理: 当在屏幕上滑动的距离大于屏幕的一半切换视图, 否则留在当前视图. 在MoveToDest方法中调用scrollTo来实现.
现在的效果为:
4.在ViewPager中, 我们去切换视图时, 并不是瞬间完成, 而是有个过程.
我们实现MyScroller类来实现滑动过程,新建MyScroller类:
public class MyScroller {
private Context context;
private int disX;
private int startY;
private int startX;
private int disY;
private long startTime; //开始动画时间
private boolean isFinish; //标志是否结束动画
//默认运行时间,500ms
private int duration = 500;
//当前绘制所在的X位置
private long currX;
//当前绘制所在的Y位置
private long currY;
public MyScroller(Context context) {
this.context = context;
}
public long getCurrY() {
return currY;
}
public void setCurrY(long currY) {
this.currY = currY;
}
public long getCurrX() {
return currX;
}
public void setCurrX(long currX) {
this.currX = currX;
}
/**
* 开始移动
* @param startX 开始时的x坐标
* @param startY 开始时的y坐标
* @param disX x方向要移动的距离
* @param disY y方向要移动的距离
*/
public void startScroll(int startX, int startY, int disX, int disY) {
this.startX = startX;
this.startY = startY;
this.disX = disX;
this.disY = disY;
this.startTime = SystemClock.uptimeMillis();
this.isFinish = false;
}
/**
* 计算当前的运行状况
* @return
* true 还在运行
* false 运行结束
*/
public boolean computeScrollOffset() {
if (isFinish) {
return false;
}
//获得绘制时的时间
long passTime = SystemClock.uptimeMillis() - startTime;
//如果时间还在允许的范围内
if (passTime <= duration) {
currX = startX + disX * passTime / duration;
currY = startY + disY * passTime / duration;
} else { //绘制运行结束
currX = startX + disX;
currY = startY + disY;
isFinish = true;
}
return true;
}
}
在startScroll方法中记录下当前滑动点坐标与目标点坐标, 并记录下当前时间. 然后在computeScrollOffset中开始去更新当前的滑动的坐标.
接下来使用MyScroller来实现滑动过程,在MyViewPager类中定义MyScroller类成员变量并去使用它来实现滑动过程:
private MyScroller myScroller;
//在initView中初始化
private void initView() {
...............
myScroller = new MyScroller(context);
...............
}
然后修改moveToDest方法,不去使用ScrollTo来实现移动,使用MyScroller来实现:
public void moveToDest(int nextId) {
..................
//不使用scrollTo来实现移动
// scrollTo(currId * getWidth(), 0);
//获得移动的距离,移动距离等于最终位置-当前位置
int distance = currId * getWidth() - getScrollX();
myScroller.startScroll(getScrollX(), 0, distance, 0);
//会导致computeScroll方法执行
invalidate();
}
@Override
public void computeScroll() {
//计算当前绘制状况,进行绘制
if (myScroller.computeScrollOffset()) {
int nowX = (int) myScroller.getCurrX();
scrollTo(nowX, 0);
invalidate();
}
}
现在的效果你会看到视图切换时就会有个过程而不是很快就完成了:
现在视图的切换是匀速的,如果想达到ViewPager那种加速效果,将MyScroller改成系统提供的Scroller类即可,只需要将private MyScroller myScroller; 改为private Scroller myScroller; 其他都不需要动就可以实现加速效果, 因为我们的MyScroller类的接口和系统Scroller接口一样。
5.现在MyViewPager中的View全为ImageView, 那么向MyViewPager中添加一个布局(包括一个button和listView)来看一下是否同样的支持滑动效果.
(1)布局文件list_view.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:id="@+id/list_layout"
android:orientation="vertical" >
<Button
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_margin="10dp"
android:text="button"/>
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"/>
</LinearLayout>
(2)我们将布局文件加载到MyViewPager的第四个view
private LinearLayout listLayout;
private ListView listview;
private String[] datas = { "Apple", "Banana", "Orange", "Watermelon",
"Pear", "Grape", "Pineapple", "Strawberry", "Cherry", "Mango" };
@Override
protected void onCreate(Bundle savedInstanceState) {
............
//添加下面代码
listLayout = (LinearLayout) LayoutInflater.from(getApplicationContext())
.inflate(R.layout.list_view, null);
listview = (ListView) listLayout.findViewById(R.id.listview);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this, android.R.layout.simple_list_item_1, datas);
listview.setAdapter(adapter);
for (int i = 0; i < ids.length; i++) {
if (i == 3) { //将布局文件添加到第四个位置
msv.addView(listLayout);
} else {
ImageView imageView = new ImageView(this);
imageView.setBackgroundResource(imgIds[i]);
msv.addView(imageView);
}
}
}
这个时候你运行会发现这个布局不会显示出来, 因为这个时候我们没有在MyViewPager中去调用布局控件的OnMeasure方法, 让其去测量子View的大小, 导致布局view没有显示出来,所以我们需要在MyViewPager中去重载OnMeasure方法,去调用每个子view的measure,绘制子view的大小。
//绘制每个子控件的尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(widthMeasureSpec, heightMeasureSpec);
}
}
现在的效果为:
现在我们发现,当滑动到布局文件中,在listview上可以上下滑,但是此时左右滑动失效了,不能左右滑动。
原因因为Touch事件的传递导致, 我们知道touch事件是从父view到子view一层一层传递,当我们左右滑动时,此事件由MyViewPager一层层传递到listview,但是listview并不支持(消费)此事件,所以导致左右滑动不起作用了, 现在我们重写onInterceptTouchEvent,来去判断touch事件,如果是左右滑动事件那么就去中断此事件,不让其再往下传递,我们去处理消费它。
/**
* 返回true, 中断事件,执行自己的onTouchEvent方法
* 返回false, 默认处理,不中断事件, 也不会执行自己的onTouchEvent方法
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result = false;
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
//解决点击图片时跳动的bug
detector.onTouchEvent(ev);
firstX = (int) ev.getX();
firstY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
int disx = (int) Math.abs(ev.getX() - firstX); //不管是左右移,只判断是否左右移动
int disy = (int) Math.abs(ev.getY() - firstY); //竖直方向移动距离
//判断是否为水平方向移动,disx > 10 防止手指按住屏幕抖动
if(disx > disy && disx > 10) {
result = true;
} else {
result = false;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
return result;
}
现在效果如下:
此时还缺少一点就是开放一个接口让外部来使用, 比如导航界面,点击某一个点,会直接跳到那个点指定的页面。
在MyViewPager中添加接口:
//监听器对象
private MyPagerChangedListener listener;
public MyPagerChangedListener getListener() {
return listener;
}
public void setListener(MyPagerChangedListener listener) {
this.listener = listener;
}
//页面改变时的监听接口
public interface MyPagerChangedListener{
void moveToDest(int currId);
}
然后在moveToDest方法中去判断此监听器是否为空,不为空就调用其接口。
//移动到指定的子控件上
public void moveToDest(int nextId) {
..................
//触发listener事件
if (listener != null) {
listener.moveToDest(currId);
}
..............
}
至此, 使用ViewGroup实现ViewPager效果完成, 如有问题可以留言。
源码下载地址:
http://download.csdn.net/detail/lbcab/9536961