自定义可水平滚动的View

ViewPager是我们比较常用的一个控件,最常用的就是经典的ViewPage+fragment模式了,这里不讲这个模式,主要讲的是滑动冲突,由于在ViewPage中已经在内部源码里面解决了滑动冲突,所以我们可以使用于多种滑动规则。下面我就来实现一个简单的ViewPage;

我们让HorizontalScrollViewEx 继承自ViewGroup ,因为ViewGroup本身是没有解决滑动冲突的,这时候加入我们要在HorizontalScrollViewEx 里面添加一个ListView的时候,就会出现滑动冲突了;

public class HorizontalScrollViewEx extends ViewGroup {
    private int lastX;                  //最近一次点击屏幕时的x,y坐标(不包括当前这次)
    private int lastY;
    private int lastInteceptX;          //上次拦截时的xy坐标
    private int lastInteceptY;
    private Scroller mScroller;         //Scroller工具类,用来实现弹性滑动
    private VelocityTracker mVelocityTracker;
    private int curChildIndex;          //当前子View的下标
    private int childSize;              //子View的个数
    private int mChi
</pre><pre name="code" class="java">//初始化<span style="font-family: Arial, Helvetica, sans-serif;">Scroller 和</span><span style="font-family: Arial, Helvetica, sans-serif;">VelocityTracker,后面用来实现弹性滑动和检测手指移动的速度</span><pre name="code" class="java">    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

ldWidth; //子View的宽度
 解决滑动冲突的方法主要是重写父类的onInterceptTouchEvent方法 

一般可分为两种套路:

1,外部拦截法:

    //伪代码描述如下
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                    isIntercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (满足条件)) {//如果满足条件就拦截
                    isIntercept = true;
                } else {
                    isIntercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                break;
        }
        return isIntercept;
    }
为了能让子类能拦截到, MotionEvent.ACTION_DOWN和 MotionEvent.ACTION_UP:必须返回false,具体请参考android的事件分发机制

然后在 MotionEvent.ACTION_MOVE:中判断是否满足父类的拦截条件,如果满足的话就返回true,这样同一个事件序列就不会只交给父类处理,返回false的话就分发给子类。

下面看一下我们实行的代码

    //拦截事件,采用外部拦截的方法,如果X轴的滑动距离大于Y轴的滑动距离就拦截。
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean isIntercept = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    isIntercept = true;
                    mScroller.abortAnimation();         //增加滑动效果
                } else
                    isIntercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - lastX;
                int deltaY = y - lastY;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {//如果满足条件就拦截
                    isIntercept = true;
                } else {
                    isIntercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                isIntercept = false;
                break;
        }
        lastInteceptX = x;
        lastInteceptY = y;
        lastX = x;
        lastY = y;
        return isIntercept;
    }
要判断是 HorizontalScrollViewEx 水平滑动 还是HorizontalScrollViewEx 的子View(ListView)的上下滚动,我们采用的根据手指移动的水平距离和垂直距离的大小之间的关系来判别的(当然,这里其实有很多方法,比如可以根据速度比较,具体按需实现)。当水平方向移动的距离大于垂直移动的距离的时候HorizontalScrollViewEx 就拦截点击事件。这时候就会执行onTouchEvent方法,所以在这里为了实现水平滑动,我们还必须重写父类的onTouchEvent方法。
</pre><pre name="code" class="java">    //点击事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        mChildWidth = getChildAt(0).getWidth();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation(); //增加滑动效果
                }
                break;
            case MotionEvent.ACTION_MOVE: {
                int delataX = x - lastX;
                int delataY = x - lastY;
                scrollBy(-delataX, 0);          //从右向左滑delataX<0,从左向右滑delataX>0
            }
            break;
            case MotionEvent.ACTION_UP: {
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocityX = (int) mVelocityTracker.getXVelocity();
                if (Math.abs(velocityX) >= 50) {
                    curChildIndex = velocityX > 0 ? curChildIndex - 1 : curChildIndex + 1;
                } else {
                    curChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                curChildIndex = Math.max(0, Math.min(curChildIndex, childSize - 1));
                int dx = curChildIndex * mChildWidth - scrollX;   //在这里总结一下,比较容易理解:在MotionEvent.ACTION_MOVE
                smoothScrollTo(dx, 0);                            //里面调用scrollBy,知道手指离开屏幕,这时候就调用MotionEvent.ACTION_UP
                mVelocityTracker.clear();                         //事件,这里我们考虑一种情况,加入我们没有完全移动到下一个ListView,
                break;                                            //这时候就会卡在这两个子View之间,所以我们根据离开时手指的速度来判断是否
            }                                                     //大于我们指定的值,如果满足的话就到滚动到下一个子View,否则回滚到当前子View
            default:
                break;
        }
        lastY = y;
        lastX = x;
        return true;
    }
这样就解决了滑动冲突。一般建议采用外部拦截法,相对比内部拦截方便而且也适用于大部分情况

接着贴上其余的代码

    //测量自己的宽度高度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        childSize = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        View childView = getChildAt(0);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(heightMeasureSpec);
        if (childSize == 0)
            setMeasuredDimension(0, 0);
        if (heightMode == MeasureSpec.AT_MOST && widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(childView.getWidth() * childSize, childView.getHeight());
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, childView.getHeight());
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(childView.getWidth() * childSize, heightsize);
        }
    }

    //确定子View的位置
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int mChildCount = childSize;
        int childLeft = 0;
        for (int i = 0; i < mChildCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                child.layout(childLeft, 0, childLeft + child.getMeasuredWidth(), child.getMeasuredHeight());
                childLeft += child.getMeasuredWidth();
            }
        }

    }

    //和Scroller类配合使用,完成弹性滑动的效果
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    //缓慢滑动到指定位置
    public void smoothScrollTo(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 800);
        invalidate();
    }

    //当当前view不可见的时候调用,释放资源
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mVelocityTracker.recycle();
    }

具体的代码可以参考自:https://github.com/JoeYoungFang/CustomView

参考自《android开发艺术探索》




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值