android 滚动

坐标系

原点在左上点 向右X 递增 向下Y 递增 (相对 和 绝对坐标)

getRawX() 获取触摸点到屏幕左边的距离

getRawY() 获取触摸点到屏幕的上边的距离

getX() 获取触摸点到父控件左边的距离

getY() 获取触摸点到父控件上边的距离

也可以使用 getLocationOnScreen(int localtion()) 获取

滑动的七种方法

layout 方法

这个中方法是因为ViewGroup 在布局时调用子控件的layout方法来布局


public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
    public ScrollTextView(Context context) {
        super(context);
    }

    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    int lastX;
    int lastY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;
                layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                lastX = rawX;
                lastY = rawY;
                break;


        }
        return true;
    }
}

offsetLeftAndRight 与offsetTopAndBottom

与layout类似


public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
    public ScrollTextView(Context context) {
        super(context);
    }

    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    int lastX;
    int lastY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;
                //layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
                lastX = rawX;
                lastY = rawY;
                break;


        }
        return true;
    }
}

LayoutParms

通过修改子view的layout参数 来实现scroll 会对别的子view 有一定的影响


public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
    public ScrollTextView(Context context) {
        super(context);
    }

    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ScrollTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    int lastX;
    int lastY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;

                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft() + offsetX;
                layoutParams.topMargin = getTop() + offsetY;
                setLayoutParams(layoutParams);

                lastX = rawX;
                lastY = rawY;
                break;


        }
        return true;
    }
}

scrollTo 与 scrollBy

scrollTo(绝对的位置) 与 scrollBy(相对的位置) 是对当前控件的内容的滚动

scrollTo 移动到哪就到哪

scrollBy 在当前位置上再移动多少

但方向与你计算的刚好是反的


public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
    public ScrollTextView(Context context) {
        super(context);
    }

    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    int lastX;
    int lastY;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;

               View pv = (View) getParent();
               pv.scrollBy(-offsetX,-offsetY);
                lastX = rawX;
                lastY = rawY;
                break;

        }
        return true;
    }


}

Scoller 类

scoller 用于计算移动的数值 但还是需要 scrollTo 与 scrollBy 来移动view 让移动更平滑


public class ScrollTextView extends android.support.v7.widget.AppCompatTextView {
    public ScrollTextView(Context context) {
        super(context);
        init(context);
    }

    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

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


    Scroller mScroller;

    void init(Context context) {
        mScroller = new Scroller(context);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断Scroller 是否执行完毕
        if (mScroller.computeScrollOffset()) {
            View pv = (View) getParent();
            pv.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

    int lastX;
    int lastY;

    public boolean onTouchEvent(MotionEvent event) {
        int rawX = (int) event.getRawX();
        int rawY = (int) event.getRawY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = rawX;
                lastY = rawY;
                break;
            case MotionEvent.ACTION_MOVE: {
                //计算偏移量
                int offsetX = rawX - lastX;
                int offsetY = rawY - lastY;

                View pv = (View) getParent();
                pv.scrollBy(-offsetX, -offsetY);
                lastX = rawX;
                lastY = rawY;
            }

            break;
            case MotionEvent.ACTION_UP: {
                View pv = (View) getParent();
                mScroller.startScroll(pv.getScrollX(), pv.getScrollY(), -pv.getScrollX(), -pv.getScrollY());
                invalidate();
            }

            break;
        }
        return true;
    }

}

属性动画

textView.animate().translationX(100).setDuration(100).start();

大boss ViewDragHelper

  1. 初始化 ViewDragHelper.create(this,callback)
  2. 拦截事件 自定义ViewGroup中的拦截事件交给它
  3. 处理computScroll()
  4. 处理回调Callback

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

public class DragViewGroup extends FrameLayout {
    public DragViewGroup( Context context) {
        super(context);
        init();
    }

    public DragViewGroup( Context context,  AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    ViewDragHelper mViewDragHelper;
    View mMenuView;
    View mMainView;
    int mWidth;
    void init(){
        mViewDragHelper = ViewDragHelper.create(this,callback);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMenuView = getChildAt(0);
        mMainView = getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenuView.getMeasuredWidth();

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        //交给ViewDragHelper 来处理
        return mViewDragHelper.shouldInterceptTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //交给ViewDragHelper 来处理
        mViewDragHelper.processTouchEvent(event);
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mViewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    ViewDragHelper.Callback callback = new ViewDragHelper.Callback(){

        //何时开始检测触摸事件
        @Override
        public boolean tryCaptureView(View view, int i) {
            //当前触摸的view 是mMainView 开始检测
            return mMainView == view;
        }

        //处理垂直滑动
        @Override
        public int clampViewPositionVertical(@NonNull View child, int top, int dy) {
            return super.clampViewPositionVertical(child, top, dy);
        }
        //处理水平滑动
        @Override
        public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) {
            super.clampViewPositionHorizontal(child, left, dx);
            return left;
        }
        //拖动结束后回调
        @Override
        public void onViewReleased(@NonNull View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);

            if (mMainView.getLeft()<500){
               //关闭菜单
                //相当于Scroller 的 startScroll 方法
                mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }else {
                //打开菜单
                mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
            }
        }

        //用户触摸到view 回调
        @Override
        public void onViewCaptured(@NonNull View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
        }

        //在拖曳状态改变时回调
        @Override
        public void onViewDragStateChanged(int state) {
            super.onViewDragStateChanged(state);
        }

        //位置改变时回调 常用于滑动时更改scale 进行缩放效果
        @Override
        public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
            super.onViewPositionChanged(changedView, left, top, dx, dy);
        }
    };
}

TouchSlop MotionEvent

ActionDown 手指按下

ActionMove 手指移动

ActionUp 手指抬起

TouchSlop:系统所能识别的滑动最小距离 是个常量

int scaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();

VelocityTracker

VelocityTracker 速度追踪 用于追踪手指在滑动过程中的速度 包括水平 和竖直方向的速度

 VelocityTracker velocityTracker = VelocityTracker.obtain();
 @Override
 public boolean onTouchEvent(MotionEvent event) {
        //添加事件
        velocityTracker.addMovement(event);

        //当我们想知道当前的滑动数度时可以使用如下方法
        //units
        velocityTracker.computeCurrentVelocity(1000);
        //在1000ms 内 手指在水平方向划过xV 从左到右为正
        int xV = (int) velocityTracker.getXVelocity();
        //在1000ms 内 手指在竖直方向划过yV 从上到下为正
        int yV = (int) velocityTracker.getYVelocity();
    }


@Override
protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        //当不需要的时候进行回收
        velocityTracker.clear();
        velocityTracker.recycle();
 }

嵌套滑动滑动冲突

常见的冲突场景

1,外部滑动方向和内部滑动方向不一致

2,外部滑动方向和内部滑动方向一致

3,两种情况的嵌套

真对三种情况如何处理

对于第一中情况: 我们可以判断用户是左右滑动还是上下滑动来进行事件的拦截

第二种情况:我们可以 当滑动到指定的位置后 再有下个view 进行滑动

解决方案:

​ 1,外部拦截法

​ 在onInterceptTouchEvent 中进行拦截

​ 2,内部拦截法

​ getParent().requestDisallowInterceptTouchEvent(true); 告诉父view 不要拦截让我来消费事件


public class HorizontalScrollViewEx extends ViewGroup {


    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

    public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

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

    VelocityTracker velocityTracker;
    Scroller scroller;

    void init(){
        velocityTracker = VelocityTracker.obtain();
        scroller = new Scroller(getContext());
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        boolean intercept = false;
        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                intercept = false;
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    intercept = true;
                }else {
                    intercept = false;
                }

                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        Log.e("vic-zhang",ev.getAction() + ">>>>intercept="+intercept);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercept;
    }

    int  mLastX,mLastY;
    int  mLastXIntercept,mLastYIntercept;
    int childIdex;
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        velocityTracker.addMovement(ev);
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()){
            case MotionEvent.ACTION_DOWN:
                Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                int  deltaX = x - mLastX;
                int  deltaY = y - mLastY;
                scrollBy(-deltaX,0);
                //Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                int  scrollX = getScrollX();
                velocityTracker.computeCurrentVelocity(1000);
                float xV = velocityTracker.getXVelocity();
                if (Math.abs(xV)>=50){
                    childIdex = xV>0?childIdex-1:childIdex+1;
                }else {
                    childIdex= (scrollX+mChildWidth/2)/mChildWidth;
                }
                childIdex = Math.max(0,Math.min(childIdex,getChildCount()-1));

                int dx = mChildWidth*childIdex - scrollX;
                scroller.startScroll(scrollX,0,dx,0);
                velocityTracker.clear();
                Log.e("vic-zhang",ev.getAction() + ">>>>ACTION_UP scrollX="+scrollX);
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
        //return super.onTouchEvent(ev);
    }


    @Override
    public void computeScroll() {
        if (scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),scroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        // super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //确定 view group 的大小
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        //setMeasuredDimension(getMaxChildWidth(),height);
        //我们使用固定的宽度高度 不需要 AT_MOST
        setMeasuredDimension(width,height);
        //测量子空间的宽度高度
        int count = getChildCount();
        for (int i = 0; i <count ; i++) {
            View childView = getChildAt(i);
            measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }
    }
    int mChildWidth;
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();

        int top = 0;
        int width = 0;
        for (int i = 0; i <count ; i++) {
            View child = getChildAt(i);
            int childH = child.getMeasuredHeight();
            int w = child.getMeasuredWidth();
            mChildWidth = w;
            //child.layout(l,top,r,top+childH);
            child.layout(width,t,w+width,b);
            top = top + childH;
            width+=w;
        }

    }
}

系统提供的嵌套滑动的机制

NestedScrollingParent和NestedScrollingChild

ScrollView 中嵌套 ViewPaer 导致 viewpager 中的内容不显示

如何解决:可以改用 NestedScrollView 和 RecyclerView PagerSnapHelper


NestedScrollingChild child;
NestedScrollingChild2 child2; //增强版 更好的处理Fling
NestedScrollingChildHelper helper;

NestedScrollingParent parent;
NestedScrollingParent2 parent2;//增强版 更好的处理Fling
NestedScrollingParentHelper parentHelper;

示例代码


import android.content.Context;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.v4.view.NestedScrollingParent2;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.NestedScrollView;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

public class DampLayout extends LinearLayout implements NestedScrollingParent2 {
    public DampLayout(Context context) {
        super(context);
        init();
    }

    public DampLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DampLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private static final int MAX_HEIGHT = 100;
    View headerView;
    View footerView;
    View childView;
    void init(){
        headerView = new View(getContext());
        footerView = new View(getContext());
    }

    //在setContentView之后、onMeasure之前调用的方法
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        childView = getChildAt(0);
        LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, MAX_HEIGHT);

        headerView.setBackgroundColor(Color.parseColor("#00FF00"));
        footerView.setBackgroundColor(Color.parseColor("#0000FF"));
        addView(headerView,0,layoutParams);
        addView(footerView,getChildCount(),layoutParams);
        //上移 隐藏 header
        scrollBy(0,MAX_HEIGHT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.LayoutParams params = childView.getLayoutParams();
        params.height = getMeasuredHeight();
    }



    /**
     *  当 子 view 调用 startNestedScroll  借助NestedScrollingChildHelper触发
     * @param child ViewParent包含触发嵌套滚动的view的对象
     * @param target 触发嵌套滚动的view  (在这里如果不涉及多层嵌套的话,child和target)是相同的
     * @param nestedScrollAxes  就是嵌套滚动的滚动方向了
     * @param type  TYPE_TOUCH 标识手指触碰 Scroll  TYPE_NON_TOUCH 标识非手指触碰 Fling
     * @return
     */
    @Override
    public boolean onStartNestedScroll(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {

        //返回true代表处理本次事件
        return target instanceof NestedScrollView;
    }


    /**
     * 在回调了 onStartNestedScroll 之后 如果onStartNestedScroll返回true的话,会立即回调
     * @param child
     * @param target
     * @param nestedScrollAxes
     * @param type
     */
    @Override
    public void onNestedScrollAccepted(@NonNull View child, @NonNull View target, int nestedScrollAxes, int type) {

    }


    /**
     * 当 子 view 调用 stopNestedScroll  借助NestedScrollingChildHelper触发
     * @param target
     * @param type
     */
    @Override
    public void onStopNestedScroll(@NonNull View target, int type) {

    }

    /**
     * child滑动以后回调的函数
     * @param target
     * @param dxConsumed 表示target已经消费的x方向的距离
     * @param dyConsumed 表示target已经消费的y方向的距离
     * @param dxUnconsumed 表示x方向剩下的滑动距离
     * @param dyUnconsumed 表示y方向剩下的滑动距离
     * @param type
     */
    @Override
    public void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
        getParent().requestDisallowInterceptTouchEvent(true);
        if (type == ViewCompat.TYPE_NON_TOUCH) {//非手指触发的滑动,即Filing
//            // dy>0向下fling dy<0向上fling
//            boolean showTop = dyUnconsumed < 0 && !target.canScrollVertically(-1);
//            boolean showBottom = dyUnconsumed > 0 && !target.canScrollVertically(1);
//            if (showTop || showBottom) {
//                scrollBy(0, dyUnconsumed);
//            }
//            adjust(dyUnconsumed, target);//调整错位
        }
    }

    /**
     * 在子view 滑动前调用
     * parent可以选择先于child进行滑动
     * @param target
     * @param dx  表示target本次滚动产生的x方向的滚动总距离
     * @param dy 表示target本次滚动产生的y方向的滚动总距离
     * @param consumed 表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
     * @param type
     */
    @Override
    public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
        //告诉 父View 事件交给我来处理
        getParent().requestDisallowInterceptTouchEvent(true);
       // if (type == ViewCompat.TYPE_TOUCH){
            scrollBy(0,dy);
           // consumed[1] = dy;
//            adjust(dy, target);//调整错位
       // }

    }

    private void adjust(int condition1, View condition2) {
        if (condition1 > 0 && getScrollY() > MAX_HEIGHT && !condition2.canScrollVertically(-1)) {
            scrollTo(0, MAX_HEIGHT);
        }
        if (condition1 < 0 && getScrollY() < MAX_HEIGHT && !condition2.canScrollVertically(1)) {
            scrollTo(0, MAX_HEIGHT);
        }
    }

    /**
     * 限制滑动 移动y轴不能超出最大范围
     */
    @Override
    public void scrollTo(int x, int y) {
        if (y < 0) {
            y = 0;
        } else if (y > MAX_HEIGHT * 2) {
            y = MAX_HEIGHT * 2;
        }
        super.scrollTo(x, y);
    }
}
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值