ViewPager 分析(章节二)

接上一篇博客,这边博客说下实战情况。总不能一直纸上谈兵。这里我们重点围绕实现网易云云村(viewpager嵌套scrollview上下滑动冲突解决)效果来谈关于viewpager的问题。

提出问题:

  • viewpager怎样进行布局的
  • viewpager怎样滚动的
  • viewpager里面嵌套了scrollview怎样解决掉滑动冲突(效果要与网易云音乐的云村一样)
  • viewpager里面的Adapger有什么用
  • viewpager动画问题
  • viewpager怎么换成竖直方向
    等等

正文

  • viewpager动画问题
  • viewpager怎么换成竖直方向
    把俩个问题结合在一起说,为什么呢,因为解决完这俩个问题就实现了我们viewpager的上下滚动,先说实现原理,在viewpager源码中,也是通过我们的坐标在判断滑动,当然标准的是通过我们的x坐标来计算,直接重写所有判断吗?当然不是这样做虽然可以但是工作量大,所以我们不采用,我们这里用另一种想法,我们取坐标值都是通过一个MotionEvent对象来,MotionEvent对象中封装了 X Y的信息,假如我们把里面的xy调换一下不就可以了吗,简直nice。
    我们只需要在onInterceptTouchEvent() onTouchEvent()俩个方法中调用这个方法替换就可以了。这俩个方法中都要使用。
 private MotionEvent swapEvent(MotionEvent event) {
        //获取宽高
        float width = getWidth();
        float height = getHeight();
        //将Y轴的移动距离转变成X轴的移动距离
        float swappedX = (event.getY() / height) * width;
        //将X轴的移动距离转变成Y轴的移动距离
        float swappedY = (event.getX() / width) * height;
        //重设event的位置
        event.setLocation(swappedX, swappedY);
        return event;
    }

那么替换完了会是一个怎样的效果,效果会是你上下滑动没错,确实切换的视图,但是视图确是从左边出来,所以这里我们还需要更换一下视图动画,我们自定义一个上下切换的动画,然后在构造方法中替换原来的就可以。
那么如果没有了解过PageTransformer动画的可能看不太明白,这里就说一下方法先简单理解一下,下期有时间再来说PageTransformer相关的内容,setAlpha() 这个方法是关于透明度的。setTranslationX这个方法是关于X轴移动的。

 setPageTransformer(true, new DefaultVerticalTransformer());
public class DefaultVerticalTransformer implements ViewPager.PageTransformer {

    @Override
    public void transformPage(View view, float position) {

        float alpha = 0;
        if (0 <= position && position <= 1) {
            alpha = 1 - position;
        } else if (-1 < position && position < 0) {
            alpha = position + 1;
        }
        view.setAlpha(alpha);
        float transX = view.getWidth() * -position;
        view.setTranslationX(transX);
        float transY = position * view.getHeight();
        view.setTranslationY(transY);
    }
}
  • viewpager里面嵌套了scrollview怎样解决掉滑动冲突
    现在我们在实现了上下滚动的viewpager以后,我们应该来关系下事件冲突的问题,为什么会有事件冲突,因为我们的viewpager是上下滚动而我们的scrollview也是上下滚动,如果viewpager嵌套了scrollview那么什么时候该scrollview滚动什么时候该viewpager滚动,这是一个问题,如果我们希望的效果是,当scrollview到达顶部或者底部的时候viewpager滚动,否则scrollview滚动的效果的时候,那么默认的事件处理肯定满足不了我们。这个时候我们就需要重写一下viewpager的事件分发。
    上代码:
public class VerticalViewPager extends ViewPager {
 //记录当前滚动状态 0向上 1向下
    private int moveSta = 0;
    //按下坐标
    private int downY = 0;
    private boolean boo = false;

    public VerticalViewPager(Context context) {
        this(context, null);
    }

	 public VerticalViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        //设置viewpage的切换动画,这里设置才能真正实现垂直滑动的viewpager
        setPageTransformer(true, new DefaultVerticalTransformer());
    }

	  @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //修改标志位
        requestDisallowInterceptTouchEvent(false);
        int action = ev.getAction() & MotionEvent.ACTION_MASK;
        boo = false;
        MyScrollView scrollView = (MyScrollView) getChildAt(getCurrentItem());
        switch (action){
            case MotionEvent.ACTION_DOWN:
                downY = (int) ev.getY(0);
                break;
            case MotionEvent.ACTION_MOVE:
                if (getHeight() >= scrollView.getChildAt(0).getHeight() && (ev.getY(0) - downY < 0 || ev.getY(0) - downY > 1)){
                    boo = true;
                }
                if (ev.getY(0) - downY > 0){
                    moveSta = 0;
                }else{
                    moveSta = 1;
                }
                if (moveSta == 0 && scrollView!=null && scrollView.isScrolledToTop()) {
                    boo = true;
                } else if (moveSta == 1 && scrollView!=null && scrollView.isScrolledToBottom()) {
                    boo = true;
                }
                break;
            case MotionEvent.ACTION_UP:
                if (ev.getY(0) - downY >= 0 && ev.getY(0) - downY <= 1){
                    boo = false;
                }
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
	
	
    /**
     * 拦截touch事件
     *
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(swapEvent(ev));
            swapEvent(ev);
        }
        if (boo){
            super.onInterceptTouchEvent(swapEvent(ev));
            swapEvent(ev);
        }
        return boo;
    }
	
	   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return super.onTouchEvent(swapEvent(ev));
    }

    private MotionEvent swapEvent(MotionEvent event) {
        //获取宽高
        float width = getWidth();
        float height = getHeight();
        //将Y轴的移动距离转变成X轴的移动距离
        float swappedX = (event.getY() / height) * width;
        //将X轴的移动距离转变成Y轴的移动距离
        float swappedY = (event.getX() / width) * height;
        //重设event的位置
        event.setLocation(swappedX, swappedY);
        return event;
    }
}

这里可能你刚看代码会觉得比较奇怪,为什么我会在分发的方法中进行了判断,而不再拦截方法中。我为什么又在分发的方法中调用requestDisallowInterceptTouchEvent(false);现在我来为你一一解答,首先先假设不调用requestDisallowInterceptTouchEvent(false);方法而且把判断直接些在拦截方法中会发生什么,上下滑动没有问题,但是你会发现你在拦截帆帆发的move中,只能监听到俩三次move的动作,然后就没然后了,这样会导致当你把scrollview滑动顶部的时候会不能滑动viewpager,只有当你再次触发一次事件才能正常滑动viewpager,为什么会这样我们来看一下viewgroup的源码

  // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
					//重点
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

就看其中这一段判断是否拦截的代码,原因就出现在上面的标注重点的地方,标注位问题,move过多会直接返回true,不会去调用拦截方法进行判断了,所以我们也就接收不到了。所以这里要想接收到我们需要去修改这个标志位,那么就调用requestDisallowInterceptTouchEvent(false);修改就可以实现,那么调用以后的效果就如下所说:
当scrollview滑动底部接着往下滑动的时候可以正常滑动viewpager,但是这里会有一个问题,就是惯性滚动,如果你在scrollview滑动速度很快,导致滚动滚动滑倒底部,然后接着下滑,但是此时的事件没有交给viewpager,所以不会调用回弹等方法,就会出现下一个viewpager界面卡出来一点,就类似于viewpager是一个大的scrollview效果,你把下一个视图滑动出来了但是没有回滚回去。这样的情况就比较尴尬了,所以我们为了解决这个惯性滚动的问题,是不是应该这样处理,当我们手指离开屏幕以后就把事件交给viewpager处理。那么思路如下,当我们拦截的时候就调用viewpager的拦截里面回弹等方法,如果不则不调用回弹等方法直接把事件交给scrollview就可以,但是我们不能直接把super.onInterceptTouchEvent(swapEvent(ev));写道MOVE中去,为什么因为它里面有DOWN的判断,所以我们应该在拦截方法之前就判断出是否需要拦截,然后告诉拦截方法就可以了,所以我们应该在分发方法钟就做好判断传递。这样最后的代码就是上面的了,效果还是很nice的。
最后说一下我们还需要自定义一下Scrollview,在里面写一些判断是否到底或者顶部的方法供viewpager调用。并且我们判断到底部或者顶部的时候还需要判断一下当前的滑动方法,比如是否正在下滑或者上滑,不然会出现第一个界面一直在顶部的情况,而且还有当scrollview内容较少的时候,应该是不需要scrollview滚动所以应该把事件直接交给viewpager来处理。

public class MyScrollView extends ScrollView {

    public MyScrollView(Context context) {
        this(context, null);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private boolean isScrolledToTop = false;// 初始化的时候设置一下值
    private boolean isScrolledToBottom = false;
    private boolean isTouchLetOut = true; //是否触摸
    

    private ISmartScrollChangedListener mSmartScrollChangedListener;

    /**
     * 定义监听接口
     */
    public interface ISmartScrollChangedListener {
        void onScrolledToBottom();

        void onScrolledToTop();
    }

    public void setScanScrollChangedListener(ISmartScrollChangedListener smartScrollChangedListener) {
        mSmartScrollChangedListener = smartScrollChangedListener;
    }
    

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isTouchLetOut = true;
                break;
            case MotionEvent.ACTION_MOVE:
                //这里使用move移动判断准确些
                isTouchLetOut = true;
                if (getScrollY() == 0) {
                    isScrolledToTop = true;
                    isScrolledToBottom = false;
                } else if (getScrollY() + getHeight() - getPaddingTop() - getPaddingBottom() == getChildAt(0).getHeight()) {
                    isScrolledToBottom = true;
                    isScrolledToTop = false;
                } else {
                    isScrolledToTop = false;
                    isScrolledToBottom = false;
                }
                notifyScrollChangedListeners();
                break;
            case MotionEvent.ACTION_UP:
                isTouchLetOut = false;
                break;
        }
        return super.onTouchEvent(ev);
    }

    private void notifyScrollChangedListeners() {
        if (isScrolledToTop) {
            if (mSmartScrollChangedListener != null) {
                mSmartScrollChangedListener.onScrolledToTop();
            }
        } else if (isScrolledToBottom) {
            if (mSmartScrollChangedListener != null) {
                mSmartScrollChangedListener.onScrolledToBottom();
            }
        }
    }

    public boolean isScrolledToTop() {
        return isScrolledToTop;
    }

    public boolean isScrolledToBottom() {
        return isScrolledToBottom;
    }

     //阻尼:1000为将惯性滚动速度缩小1000倍,近似drag操作。
    @Override
    public void fling(int velocity) {
        super.fling(velocity / 2);
    }

}

到此,我们的实战viewpager就完成了,那么到这一步,相信你对viewpager的事件分发应该有了一定的理解,想在viewpager实现轮播图切换framgent等问题都不是啥麻烦了。下一次我们再来一起说一说viewpager适配器相关的内容以及相关动画的实现。

原创文章 29 获赞 5 访问量 4313

猜你喜欢

转载自blog.csdn.net/weixin_41078100/article/details/104738557
今日推荐