美文网首页
Android Scroll分析

Android Scroll分析

作者: 玄策 | 来源:发表于2017-06-20 17:32 被阅读42次

    参考资料

    郭霖 Scroller完全解析
    鸿洋 ViewDragHelper完全解析
    鸿洋 ViewDragHelper实战 自己打造Drawerlayout


    -目录

    • 1)layout
    • 2)offsetLeftAndRight() offsetTopAndBottom()
    • 3)LayoutParams()
    • 4)scrollTo() scrollBy()
    • 5)Scroller
    • 6)属性动画
    • 7)ViewDragHelper

    -实现滑动的7种方法

    public class DragView extends View {
        private static final String TAG = "DragView";
        private int lastX, lastY;
        private Scroller scroller;
    
        public DragView(Context context, AttributeSet attrs) {
            super(context, attrs);
            scroller = new Scroller(context);
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
    
                    //方法一
    //                layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
                    //方法二
    //                offsetLeftAndRight(offsetX);
    //                offsetTopAndBottom(offsetY);
                    //方法三
    //                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
    //                layoutParams.leftMargin = getLeft()+offsetX;
    //                layoutParams.topMargin = getTop()+offsetY;
    //                setLayoutParams(layoutParams);
                    //方法四
                    ((View)getParent()).scrollBy(-offsetX,-offsetY);
    
                    break;
                case MotionEvent.ACTION_UP:
                    View view =  (View)getParent();
                    Log.i(TAG, "getScrollX: "+view.getScrollX());
                    Log.i(TAG, "getScrollY: "+view.getScrollY());
                    scroller.startScroll(view.getScrollX(),view.getScrollY(),-view.getScrollX(),-view.getScrollY());
                    invalidate();
                    break;
            }
            return true;
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (scroller.computeScrollOffset()){
                Log.i(TAG, "getCurrX: "+scroller.getCurrX());
                Log.i(TAG, "getCurrY: "+scroller.getCurrY());
                ((ViewGroup)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
                invalidate();
            }
        }
    }
    

    1) layout


    2) offsetLeftAndRight() offsetTopAndBottom()


    3) LayoutParams()

    //使用MarginLayoutParams更加方便还不用考虑父布局是LinearLayout还是RelativeLayout
    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
    layoutParams.leftMargin = getLeft()+offsetX;
    layoutParams.rightMargin = getRight()+offsetY;
    setLayoutParams(layoutParams);
    

    4) scrollTo() scrollBy()

    任何一个控件都是可以滚动的,因为View类中有scrollTo()和scrollBy()两个方法,scrollBy()是让View相对于当前位置滚动某段距离,scrollTo()是让View相对于初始位置滚动某段距离。

    scrollTo,scrollBy方法移动的是View的内容,如果ViewGroup中使用scrollTo,scrollBy,那么移动的将是所有子View。

    protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            layout = (LinearLayout) findViewById(R.id.layout);
            scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
            scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
            scrollToBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    layout.scrollTo(-60, -100); //注意此处是layout的scrollTo()
                }
            });
            scrollByBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    layout.scrollBy(-60, -100);//注意此处是layout的scrollBy()
                }
            });
        }
    

    下图中为什么scrollBy(-60, -100),按钮确是向手机坐标系的x和y轴正向移动呢?
    答:可以想象屏幕是一个放大镜,而下面是一个巨大的画布,使用scrollBy方法,将layout向X轴负方向(左)平移60,向Y轴负方向(上)平移100,则layout内的子view相当于向X轴和Y轴的正方向上移动了。

    20160110164232041.gif

    5) Scroller

    使用Scroller模仿ViewPager的例子

    startScroll(int startX,int startY,int dx, int dy,int duration)
    startScroll(int startX,int startY,int dx, int dy)
    
    20160114230048304.gif
    /**
     * Created by 涂高峰 on 2017/6/21.
     */
    public class ScrollerLayout extends ViewGroup {
        private static final String TAG = "ScrollerLayout";
        private Scroller mScroller;
        private int mDownX,mMoveX;
        private int leftBorder,rightBorder;
        private int mTouchSlop;
        public ScrollerLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            mScroller = new Scroller(context);
            //大于这个距离,系统认为是移动
            mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int count = getChildCount();
            for (int i=0; i<count; i++){
                View child = getChildAt(i);
                measureChild(child,widthMeasureSpec,heightMeasureSpec);
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            int count = getChildCount();
            for (int i=0; i<count; i++){
                View child = getChildAt(i);
                child.layout(i*child.getMeasuredWidth(), 0, (i+1)*child.getMeasuredWidth(), child.getMeasuredHeight());
            }
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount()-1).getRight();
            Log.i(TAG, "leftBorder: "+leftBorder);
            Log.i(TAG, "rightBorder: "+rightBorder);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            int x = (int) ev.getRawX();
            switch (ev.getAction()){
                case  MotionEvent.ACTION_DOWN:
                    mDownX = x;
                    mMoveX = x;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //按下的坐标与当前移动坐标绝对值 大于 系统默认的移动距离
                    //拦截此移动事件,不向子view传递,进入自身的onTouchEvent
                    if (Math.abs(mDownX - x)>mTouchSlop){
                        return true;
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getRawX();
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    //如果子控件为Button之类的clickable控件,则会由button消费掉down事件,当viewgroup滑动时,会拦截move事件并处理
                    //但是若子控件为TextView之类的非clickable控件,则viewgroup和textview都不会消费掉down事件.
                    //由于没有任何view消费down事件,后续事件将由上层消费,而不会往下传递给viewgroup.所以此处需要将down事件消费掉,从而能继续接收后续事件
                    return true;
                case MotionEvent.ACTION_MOVE:
                    //偏移量
                    int offsetX = mMoveX-x;
                    //左边界处理
                    if (getScrollX()+offsetX < leftBorder){
                        scrollTo(leftBorder,0);
                        return true;
                    }
                    //右边界处理
                    if (getScrollX()+offsetX + getWidth()> rightBorder){
                        scrollTo(rightBorder-getWidth(),0);
                        return true;
                    }
                    //滑动处理
                    scrollBy(offsetX,0);
                    mMoveX = x;
                    break;
                case MotionEvent.ACTION_UP:
                    //手指抬起,判断是哪个子控件的index
                    //小于第一个子控件的一半宽度则认为是第一个子控件
                    //大于第一个子控件的一半宽度则认为是下一个子控件
                    int index = (getScrollX()+getWidth()/2)/getWidth();
                    Log.i(TAG, "index: "+index); //结果为  0  1  2
                    //根据子空间index计算偏移量
                    int dy = index * getWidth() - getScrollX();
                    Log.i(TAG, "dy: "+dy);
                    mScroller.startScroll(getScrollX(),0,dy,0);
                    invalidate();
                    break;
            }
            return super.onTouchEvent(event);
        }
    
        //重绘会调用此方法,此方法中的invalidate又会触发重绘,从而循环实现弹性滑动
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()){
                scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                invalidate();
            }
        }
    }
    

    6) 属性动画(动画中讲解)


    7) ViewDragHelper

    在自定义ViewGroup中,很多效果都包含用户手指去拖动其内部的某个View(eg:侧滑菜单等),针对具体的需要去写好onInterceptTouchEvent和onTouchEvent这两个方法是一件很不容易的事,需要自己去处理:多手指的处理、加速度检测等等。
    好在官方在v4的支持包中提供了ViewDragHelper这样一个类来帮助我们方便的编写自定义ViewGroup

    1)ViewDragHelper类相关的API:

    方法 说明
    create(ViewGroup forParent, ViewDragHelper.Callback cb) 创建viewDragHelper
    captureChildView(View childView, int activePointerId) 捕获子视图
    checkTouchSlop(int directions, int pointerId) 检查移动是否为最小的滑动速度
    findTopChildUnder(int x, int y) 返回指定位置上的顶部子视图
    flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) 解决捕获视图自由滑动的位置
    getActivePointerId() 获取活动的子视图的id
    getCapturedView() 获取捕获的视图
    getEdgeSize() 获取边界的大小
    getMinVelocity() 获取最小的速度
    getTouchSlop() 获取最小的滑动速度
    getViewDragState() 获取视图的拖动状态
    isCapturedViewUnder(int x, int y) 判断该位置是否为捕获的视图
    isEdgeTouched(int edges) 判断是否为边界触碰
    setEdgeTrackingEnabled(int edgeFlags) 设置边界跟踪
    settleCapturedViewAt(int finalLeft, int finalTop) 设置捕获的视图到指定的位置
    smoothSlideViewTo(View child, int finalLeft, int finalTop) 滑动侧边栏到指定的位置
    shouldInterceptTouchEvent(MotionEvent ev) 处理父容器是否拦截事件
    processTouchEvent(MotionEvent ev) 处理父容器拦截的事件

    2)ViewDragHelper.Callback相关API:

    方法 说明
    clampViewPositionHorizontal(View child, int left, int dx) 控制横轴的移动距离
    clampViewPositionVertical(View child, int top, int dy) 控制纵轴的移动距离
    getViewHorizontalDragRange(View child) 获取视图在横轴移动的距离
    getViewVerticalDragRange(View child) 获取视图在纵轴的移动距离
    onEdgeDragStarted(int edgeFlags, int pointerId) 处理当用户触碰边界移动开始的回调
    onEdgeLock(int edgeFlags) 处理边界被锁定时的回调
    onEdgeTouched(int edgeFlags, int pointerId) 处理边界被触碰时的回调
    onViewCaptured(View capturedChild, int activePointerId) 当视图被捕获时的回调
    onViewDragStateChanged(int state) 当视图的拖动状态改变的时候的回调
    onViewPositionChanged(View changedView, int left, int top, int dx, int dy) 当捕获的视图位置发生改变的时候的回调
    onViewReleased(View releasedChild, float xvel, float yvel) 当视图的拖动被释放的时候的回调
    tryCaptureView(View child, int pointerId) 判断此时的视图是否为想要捕获的视图时会调用
    getOrderedChildIndex(int index) 获取子视图的Z值
    //方法的大致的回调顺序:
    
    1)shouldInterceptTouchEvent:
    
    DOWN:
        getOrderedChildIndex(findTopChildUnder)
        ->onEdgeTouched
    
    MOVE:
        getOrderedChildIndex(findTopChildUnder)
        ->getViewHorizontalDragRange & 
          getViewVerticalDragRange(checkTouchSlop)(MOVE中可能不止一次)
        ->clampViewPositionHorizontal&
          clampViewPositionVertical
        ->onEdgeDragStarted
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged
    
    2)processTouchEvent:
    
    DOWN:
        getOrderedChildIndex(findTopChildUnder)
        ->tryCaptureView
        ->onViewCaptured
        ->onViewDragStateChanged
        ->onEdgeTouched
    MOVE:
        ->STATE==DRAGGING:dragTo
        ->STATE!=DRAGGING:
            onEdgeDragStarted
            ->getOrderedChildIndex(findTopChildUnder)
            ->getViewHorizontalDragRange&
              getViewVerticalDragRange(checkTouchSlop)
            ->tryCaptureView
            ->onViewCaptured
            ->onViewDragStateChanged
    

    例子
    1)任意移动
    2)移动完毕后回到原位
    3)边界移动时对View进行捕获(未成功。。)

    20150713095339390.gif
    public class VDHDemo extends LinearLayout {
        private static final String TAG = "VDHDemo";
        private ViewDragHelper mDragger;
    
        private View mDragView;
        private View mAutoBackView;
        private Point mAutoBackOriPos = new Point();
    
        public VDHDemo(Context context, AttributeSet attrs) {
            super(context, attrs);
            //第二个参数为敏感度(sensitivity),敏感度越大mTouchSlop就越小
            //mTouchSlop为系统认为是移动的最小距离,即ViewConfiguration.get(context).getScaledPagingTouchSlop()
            mDragger = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() {
                @Override
                public boolean tryCaptureView(View child, int pointerId) {
                    //返回true表示可以捕获该view,可根据第一个参数决定捕获哪个view
                    //如: return xxView == child;
                    return mDragView==child || mAutoBackView==child;
    //                return true;
                }
    
                //边界控制
                @Override
                public int clampViewPositionHorizontal(View child, int left, int dx) {
                    final int leftBound = getPaddingLeft(); //左边界为viewgroup的paddingleft
                    final int rightBound = getWidth() - leftBound - getPaddingRight() - 200; //200为子view的宽度
    
                    final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
                    return newLeft;
                }
    
                //边界控制
                @Override
                public int clampViewPositionVertical(View child, int top, int dy) {
                    return top;
                }
    
                //手指释放时回调
                @Override
                public void onViewReleased(View releasedChild, float xvel, float yvel) {
    //                super.onViewReleased(releasedChild, xvel, yvel);
                    //若为mAutoBackView,则回到初始位置,调用settleCapturedViewAt()
                    //其内部为mScroller.startScroll(),别忘了invalidate和computeScroll
                    //注意你拖动的越快,返回的越快
                    if (releasedChild == mAutoBackView){
                        mDragger.settleCapturedViewAt(mAutoBackOriPos.x,mAutoBackOriPos.y);
                        invalidate();
                    }
                }
                //如果子View不消耗事件,那么整个手势(DOWN-MOVE*-UP)都是直接进入onTouchEvent,
                // 在onTouchEvent的DOWN的时候就确定了captureView
    
                //如果消耗事件,那么就会先走onInterceptTouchEvent方法,判断是否可以捕获,
                // 而在判断的过程中会去判断另外两个回调的方法:getViewHorizontalDragRange和getViewVerticalDragRange,
                // 只有这两个方法返回大于0的值才能正常的捕获。
                @Override
                public int getViewHorizontalDragRange(View child)
                {
                    return getMeasuredWidth()-child.getMeasuredWidth();
                }
    
                @Override
                public int getViewVerticalDragRange(View child)
                {
                    return getMeasuredHeight()-child.getMeasuredHeight();
                }
            });
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return mDragger.shouldInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            mDragger.processTouchEvent(event);
            return true;
        }
    
        @Override
        protected void onFinishInflate() {
            super.onFinishInflate();
            mDragView = getChildAt(0);
            mAutoBackView = getChildAt(1);
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            //onLayout结束后将mAutoBackView的返回原点设置为其初始的点
            mAutoBackOriPos.x = mAutoBackView.getLeft();
            mAutoBackOriPos.y = mAutoBackView.getTop();
        }
    
        @Override
        public void computeScroll() {
            if (mDragger.continueSettling(true)){
                invalidate();
            }
        }
    }
    

    相关文章

      网友评论

          本文标题:Android Scroll分析

          本文链接:https://www.haomeiwen.com/subject/gmqaqxtx.html