Android 自定义ViewPager

项目地址

概述
  1. 处理滑动到左边界和右边界时,不允许滑动。
  2. 页面滑动一半回弹,滑动一半以上自动切换下一界面。
  3. 当页面内存在ScrollView这类子控件,事件要正常分发,不允许自定义ViewPager拦截事件。
  4. 回弹与切换动画处理。
源码分析
  1. 初始化
public ViewPagerY(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
    // Scroller设置的是一个匀速插值器
    myScroll = new Scroller(context, new LinearInterpolator());
    // 初始化ImageLoader
    ImageLoader.getInstance().init(ImageLoaderConfiguration.createDefault(mContext));
}
  1. 设置资源,如图片id,布局,图片链接等
/**
 * 为ViewPagerY设置资源集合
 *
 * @param res
 */
public void setRes(ArrayList<ResType> res) {
    for (ResType mResType : res) {
        if (mResType.getmType() == ResType.Type.IAMG) {
            // 如果是资源图片id,创建ImageView对象
            ImageView imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            imageView.setImageResource((Integer) mResType.getRes());
            // 将设置好的ImageView添加进ViewPagerY控件中
            this.addView(imageView);
        }
        if (mResType.getmType() == ResType.Type.URL) {
            // 如果资源是图片URL,创建ImageView对象.
            ImageView imageView = new ImageView(mContext);
            imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            // 使用ImageLoader将图片从网络获取设置到ImageView中.
            ImageLoader.getInstance().displayImage((String) mResType.getRes(), imageView);
            // 将设置好的ImageView添加进ViewPagerY控件中
            this.addView(imageView);
        }
        if (mResType.getmType() == ResType.Type.LAYOUT) {
            // 如果资源是自己写的布局文件,就获取该布局文件对应的View
            View view = LayoutInflater.from(mContext).inflate((Integer) mResType.getRes(), null);
            // 将获取到的View添加进ViewPagerY控件中
            this.addView(view);
        }
    }
}
// 资源类型类
public class ResType<T> {
    public enum Type {
        IAMG,
        LAYOUT,
        URL
    }
    private T mRes;
    private Type mType;
    public ResType(T res, Type mType) {
        this.mRes = res;
        this.mType = mType;
    }
    public T getRes() {
        return mRes;
    }

    public Type getmType() {
        return mType;
    }
}
  1. ViewPagerY对子View的测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 设置ViewPagerY尺寸
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 获取ViewPagerY宽高
    height = getMeasuredHeight();
    widht = getMeasuredWidth();
    // 创建子View的MeasureSpec, 创建MeasureSpec是有规律的,可以看这个笔记: https://blog.csdn.net/MoLiao2046/article/details/105708819
    int wMeasureSpec = MeasureSpec.makeMeasureSpec(widht, MeasureSpec.EXACTLY);
    int hMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    for (int i = 0; i < getChildCount(); i++) {
        // 遍历子View开始测量子View.
        getChildAt(i).measure(wMeasureSpec, hMeasureSpec);
    }
}
  1. ViewPagerY对子View进行布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        // 资源集合中每一个资源对应一个页面.
        // 分别设置这些View的左上角与右下角坐标.
        this.getChildAt(i).layout(i * widht, 0, i * widht + widht, height);
    }
}
  1. 判断手势,如果手势为水平滑动就拦截, 否则就正常将事件分发给子View处理.
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    // 默认交给ViewGroup拦截事件,ViewGroup一般是不会拦截事件.
    boolean interceptChildeEvent = super.onInterceptTouchEvent(event);
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN:
            // mLastX与mDownX是手指按下时候的坐标,这两个值会在ViewPagerY中onTouchEvent中用到,处理页面滑动用的.
            // onTouchEvent()中也能获取ACTION_DOWN事件,但是在onTouchEvent中获取手指按下坐标再进行相应移动处理会出现页面跳动的问题.
            mLastX = mDownX = interceptLastX = event.getX();
            interceptLastY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            // 获取当前手指坐标
            float moveX = event.getX();
            float moveY = event.getY();
            // 移动后,计算出滑动后与上一个坐标点之间的距离.
            float slopX = moveX - interceptLastX;
            float slopY = moveY - interceptLastY;
            // 得到手指滑动距离绝对值
            float slopAbsX = Math.abs(slopX);
            float slopAbsY = Math.abs(slopY);
            if ((slopAbsX > 0 || slopAbsY > 0) && (slopAbsX - slopAbsY) >= 6) {
                // 如果手指移动距离大于0,且横向移动距离减去纵向移动距离大于6像素
                // 那么ViewPagerY就将该事件拦截, 不分发给它的子View使用,留给自己使用了.
                // 这样会导致mFirstTouchTarget=null,之后子View就再也接收不到事件组的其他事件了.
                interceptChildeEvent = true;
            }
            interceptLastX = moveX;
            interceptLastY = moveY;
            break;
    }
    return interceptChildeEvent;
}
  1. 处理页面滑动回弹的逻辑
@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getActionMasked()) {
        case MotionEvent.ACTION_MOVE:
            float mMoveX = event.getX();
            // mLastX:是在onInterceptTouchEvent()中得到的值
            float mDiffX = mMoveX - mLastX;
            mLastX = mMoveX;
            if (event.getPointerId(event.getActionIndex()) == 0 && event.getPointerCount() == 1) {// 这个条件可以控制只追踪屏幕中的一个手指的滑动.
                int scrollX = getScrollX();
                if (currentIndex == 0) {
                    // 第一页
                    if (mDiffX < 0) {
                        // 如果向左滑动
                        ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                    } else {
                        // 处理先向左滑动,然后又向右滑动.
                        float mDiffMargin = scrollX - mDiffX;
                        if (mDiffMargin >= 0) {
                            // 内容左边距离控件左边的距离减去向右滑动距离,如果大于0,说明内容左边距离控件左边还有间隔距离,滑动距离取手指移动距离.
                            ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                        } else {
                            // 内容左边距离控件左边的距离减去向右滑动距离,如果小于0,说明内容左边与控件左边需要重合,滑动距离取getScrollX().
                            ViewPagerY.this.scrollBy(-scrollX, 0);
                        }
                    }
                }
                if (currentIndex == getChildCount() - 1) {
                    // 最后一页
                    if (mDiffX > 0) {
                        // 如果向右滑动
                        ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                    } else {
                        // 处理先向右滑动,然后又向左滑动.
                        // (((getChildCount() - 1) * widht) - scrollX): 表示内容右边距离控件右边的距离
                        float mDiffMargin = (((getChildCount() - 1) * widht) - scrollX) + mDiffX;
                        if (mDiffMargin >= 0) {
                            // 说明内容右边与控件右边还有距离,滑动距离取手指移动距离.
                            ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                        } else {
                            // 说明内容右边与控件右边需要重合,滑动距离取内容右边与控件右边的距离.
                            ViewPagerY.this.scrollBy(-(((getChildCount() - 1) * widht) - scrollX), 0);
                        }
                    }
                }
                if (currentIndex != 0 && currentIndex != getChildCount() - 1) {
                    // scrollBy总是和移动的相反
                    ViewPagerY.this.scrollBy((int) -mDiffX, 0);
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            float mUpX = event.getX();
            // mDownX:是在onInterceptTouchEvent()中得到的值
            if (mUpX - mDownX > getWidth() / 2) {
                // 移动到上一个
                moveTo(currentIndex - 1);
            } else if (mUpX - mDownX < -getWidth() / 2) {
                // 移动到下一个
                moveTo(currentIndex + 1);
            } else {
                // 移动到当前页面
                moveTo(currentIndex);
            }
            break;
    }
    return true;
}
  1. 页面切换逻辑
/**
 * 页面切换
 * @param index
 */
public void moveTo(int index) {
    int duration = 0;
    if (index < 0) {
        index = 0;
    } else if (index > getChildCount() - 1) {
        index = getChildCount() - 1;
    }
    // 中间经历几个界面, 每个页面切换时长是固定的.
    int count = currentIndex - index;
    if (count != 0) {
        duration = mDuration * count;
    } else {
        duration = mDuration;
    }
    currentIndex = index;
    if (onPageChangeListener != null) {
        // 页面切换的监听.
        onPageChangeListener.onPageSelect(currentIndex);
    }
    // getScrollX(): 内容左边与ViewPager控件左边距离
    // currentIndex * getWidth(): 切换到currentIndex界面时的getScrollX()值.
    // 得到需要移动的距离.
    int distanceX = currentIndex * getWidth() - getScrollX();
    //给MyScroll 计算的类赋初始值
    myScroll.startScroll(getScrollX(), 0, distanceX, 0, Math.abs(duration));
    invalidate();
}
// 想要缓慢滑动这个也很重要.
@Override
public void computeScroll() {
    //如果为true说明移动还没结束
    if (myScroll.computeScrollOffset()) {
        //得到计算的位置,然后移动
        float currX = myScroll.getCurrX();
        scrollTo((int) currX, 0);
        invalidate();
    }
}
效果图

效果图

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值