自定义ViewPager

由于公司要求我做一个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代码中也有注释。
computeScroll的实现
moveToPageIndex的实现

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

这里写图片描述

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

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

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

这里写图片描述

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

这里写图片描述

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

源码下载

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值