自定义ViewPager

来源:互联网 发布:stm32和51编程一样吗 编辑:程序博客网 时间:2024/06/05 10:24

由于公司要求我做一个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;        }    });}@Overrideprotected 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();}@Overridepublic boolean onTouchEvent(MotionEvent event) {    super.onTouchEvent(event);    return true;}@Overridepublic 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);}@Overridepublic 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代码中也有注释。
computeScroll的实现
moveToPageIndex的实现

最后一个就是onInterceptTouchEvent,见名知意,该方法是用来拦截消息的,一旦拦截,那么后续分发的事件都拦截,我这边主要使用在这边,如下图:

这里写图片描述

就是在移动屏幕到其它页面时,如果手指是在该button上的话会同事触发该button的onclick事件,那么怎么才能阻止这种情况的发生?还记得我在第一篇博文中提到过完整的一次onclick是由down跟up组成的吧,那么问题就好办了,我们用手指在屏幕上滑动时我们就拦截move事件,move事件拦截后,则后续的事件(up)将不会再分发给各个子view控件,这样就不会同事触发button的onclick事件了。

还有就是注册一个回调接口监听界面的切换。

最后就是在xml布局文件中应用

这里写图片描述

以及在activity中注册界面切换的监听

这里写图片描述

好了!到这里基本上可以说是大功告成。

源码下载

1 1