美文网首页
Android Scroll分析

Android Scroll分析

作者: 张文靖同学 | 来源:发表于2017-09-02 09:44 被阅读62次

    前言

    本篇谈论Android Scroll的应用以及如何在应用中添加滑动效果。
    你可以学到:

    • 发生滑动效果的原因
    • 如何处理、实现滑动效果

    1.1 滑动是如何产生的

    滑动一个View的本质其实就是移动一个View,改变其当钱所在的位置,他的原理和动画效果十分的相似,就是通过不断的改变View的坐标来实现这一效果,动态且不断的改变View的坐标,从而实现View跟随用户触摸滑动而滑动
    但是在讲解滑动效果之前,需要先了解一下android中窗口坐标体系和屏幕的触控事件——MotionEven

    首先看一下Android坐标系


    image.png

    系统提供了getLocationOnScreen(intlocation[])来获取Android坐标中的位置,即该视图左上角Android的坐标,另外,在触摸事件中使用getRawX(),getRawY()方法来获取坐标同样是Android坐标系中的坐标。

    视图坐标系中触控事件通过getX,getY来获取的坐标就是视图坐标中的坐标。

    这些方法可以分成两个类别

    View提供的获取坐标方法
    getTop():获取到的是View自身的顶部到其父布局顶部的距离
    getLeft():获取到的是View自身的左边到其父布局左边的距离
    getRight():获取到的是View自身的右边到其父布局右边的距离
    getBottom():获取到的是View自身的底部到其父布局底部的距离

    MotionEvent提供的方法
    getX():获取点击事件距离控件左边的距离,即视图坐标
    getY():获取点击事件距离控件顶部的距离,即视图坐标
    getRawX:获取点击事件整个屏幕左边的距离,即绝对坐标
    getRawY:获取点击事件整个屏幕顶部的距离,即绝对坐标

    2、实现滑动的七种方法

    1.layout方法

    我们都知道,在View的绘制上,会调用onLayout()方法来设置显示的位置,同样可以修改View的left,top,right,bottom四个属性来控制View的坐标,与前面提供的模板代码一样,每次调用onTouchEvent()的时候,我们来获取点的坐标

    public class DragView1 extends View {
    
        private int lastX;
        private int lastY;
    
        public DragView1(Context context) {
            super(context);
            ininView();
        }
    
        public DragView1(Context context, AttributeSet attrs) {
            super(context, attrs);
            ininView();
        }
    
        public DragView1(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            ininView();
        }
    
        private void ininView() {
            // 给View设置背景颜色,便于观察
            setBackgroundColor(Color.BLUE);
        }
    
        // 视图坐标方式
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            Log.e("x","x:"+x);
            Log.e("y","y:"+y);
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 记录触摸点坐标
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    // 在当前left、top、right、bottom的基础上加上偏移量
                    layout(getLeft() + offsetX,
                            getTop() + offsetY,
                            getRight() + offsetX,
                            getBottom() + offsetY);
    //                        offsetLeftAndRight(offsetX);
    //                        offsetTopAndBottom(offsetY);
                    break;
            }
            return true;
        }
    }
    

    2.offsetLeftAndRight()与offsetTopAndBottom()

    这个方法相当于系统提供的一个对左右,上下移动的封装,当计算出偏移量的时候,只需要使用如下的代码就可以完成View的重新布局,效果和使用Layout()方法是一样的

    //同时对左右偏移
    offsetLeftAndRight(officeX);
    //同时对上下偏移
    offsetTopAndBottom(officeY);
    

    3.LayoutParams

    LayoutParams保留了一个View的布局参数,因此可以在程序中,通过改变LayoutParams来动态改变一个布局的位置参数,从而改变View位置的效果,我们可以很方便的在程序中使用getLayoutParams()来获取一个View的LayoutParams,当然,在计算偏移量的方法和Layout方法中计算offset是一样的,当获取到偏移量之后,可以通过setLayoutParams来改变LayoutParams

      public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // 记录触摸点坐标
                    lastX = (int) event.getX();
                    lastY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    // 计算偏移量
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
    //                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                    layoutParams.leftMargin = getLeft() + offsetX;
                    layoutParams.topMargin = getTop() + offsetY;
                    setLayoutParams(layoutParams);
                    break;
            }
            return true;
        }
    

    4.scrollTo与scrollBy

    在一个View当中,系统提供了scrollTo与scrollBy这两种方式来实现移动一个View的位置,这两种方法的区别也很好理解,to和by, scrollTo(x,y);表示移动到一个具体的 点,scrollBy(dx,dy);表示移动的增量

    我们就该View所在的ViewGroup中使用scrollBy方法来移动这个view
    ((View)getParent()).scrollBy(-officeX,-officeY);

    int officeX = rawX - lastX;
    int officeY = rawY - lastY;
    scrollBy(-officeX,-officeY);
    

    5.Scroller

    既然提到scrollTo与scrollBy,那就不得不提一下Scroller类了,Scroller和scrollTo与scrollBy十分的相似,有着千丝万缕的关系,那么他们有什么具体的区别尼?要解答这个问题,首先我们来看一个小栗子,假设要完成这样的一个效果:点击button,让一个viewgroup的子View移动100像素,问题看似很简单,只要使用scrollBy的方法就可以,的确,用这个方法确实可以,可是那都是一瞬间完成的事情,很突兀,而Scroller就可以实现平滑的效果,而不再是一瞬间的事情.

    说道Scroller的原理,其实他与前面使用scrollTo与scrollBy的方法原理是一样的,下面我们通过一个小栗子来演示一下

     private void ininView(Context context) {
            setBackgroundColor(Color.BLUE);
            // 初始化Scroller
            mScroller = new Scroller(context);
        }
    //重写computeScroll这个方法,他是使用Scroller的核心,
    //系统在绘制View的同时,会在onDraw()方法中调用这个方法,
    //这个方法实际上就是使用了ScrollTo()方法再结合Scroller对象,
    //帮助获取到当前的滚动值
    //我们可以通过不断的瞬息移动一个小的距离来实现整体上的平滑移动效果
        @Override
        public void computeScroll() {
            super.computeScroll();
            // 判断Scroller是否执行完毕
            if (mScroller.computeScrollOffset()) {
                ((View) getParent()).scrollTo(
                        mScroller.getCurrX(),
                        mScroller.getCurrY());
                // 通过重绘来不断调用computeScroll
                invalidate();
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = (int) event.getX();
                    lastY = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    int offsetX = x - lastX;
                    int offsetY = y - lastY;
                    ((View) getParent()).scrollBy(-offsetX, -offsetY);
                    break;
                case MotionEvent.ACTION_UP:
                    // 手指离开时,执行滑动过程
                    View viewGroup = ((View) getParent());
                    mScroller.startScroll(
                            viewGroup.getScrollX(),
                            viewGroup.getScrollY(),
                            -viewGroup.getScrollX(),
                            -viewGroup.getScrollY(),5000);
                    invalidate();
                    break;
            }
            return true;
        }
    

    startScroll开启模拟过程

    public void startScroll(int startX,int startY,int dx,int dy)
    public void startScroll(int startX,int startY,int dx,int dy)
    

    6.属性动画

    查看我的博客动画部分

    7.ViewDragHelper

    这就是今天的好菜了,Google在其support库中为我们提供了一个DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现策划效果,这两个布局,大大的方便了我们自己创建自己的滑动布局,然而,这两个强大的布局背后,却隐藏着一个鲜为人知,却功能强大的类——ViewDragHelper,通过ViewDragHelper,基本可以实现各种不同的侧滑,拖放需求,因此这个方法也是各种滑动解决方案的终极绝招

    ViewDragHelper虽然很强大,但是使用也是本章节最复杂 的,我们需要了解ViewDragHelper的基本使用方法的基础上,通过不断的练习去掌握它,我们这里就实现一个 QQ滑动侧边栏的布局,我么来看看具体怎么实现的

    第一步:初始化ViewDragHelper

    首先,自认是初始化ViewDragHelper,ViewDragHelper通常定义在一个ViewGroup中,通过其静态方法初始化

     mViewDragHelper = ViewDragHelper.create(this,callback);
    

    他的第一个参数是要监听的View,第二个参数是一个Callback回调,,这个回调是整个业务的核心,
    第二步:拦截事件
    重写拦截事件,传递给ViewDragHelper处理

     @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return mViewDragHelper.shouldInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
             mViewDragHelper.processTouchEvent(event);
            return true;
        }
    

    第三步:处理computeScroll
    这里的代码是模板代码,就不详细解释了

     @Override
        public void computeScroll() {
            //以下是模板代码
            if (mViewDragHelper.continueSettling(true)){
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    

    第四步:处理回调 !!

    //侧滑回调
        private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
            //何时开始触摸
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                //如果当前触摸的child是mMainView开始检测
                return mMainView == child;
            }
    
            //处理水平滑动 top 代表垂直方向上child移动的距离
            //dy代表比较前一次的增量
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return left;
            }
    
            //处理垂直滑动
            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return 0;
            }
    
            //拖动结束后调用
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                //手指抬起后缓慢的移动到指定位置
                if(mMainView.getLeft() <500){
                    //关闭菜单
                    mViewDragHelper.smoothSlideViewTo(mMainView,0,0);
                    ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                }else{
                    //打开菜单
                    mViewDragHelper.smoothSlideViewTo(mMainView,300,0);
                    ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
                }
            }
        };
      //XML加载组建后回调
        @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();
        }
    

    更多

    在Cakkback中系统给我们提供了很多的方法来监听
    onViewCaptured

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

    onViewDragStateChanged

            //拖拽状态改变时,比如idle,dragging
            @Override
            public void onViewDragStateChanged(int state) {
                super.onViewDragStateChanged(state);
            }
    

    onViewPositionChanged
    //位置发生改变,常用语滑动scale效果

            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
            }
    

    代码托管

    此内容的代码已经提交到了GitOSC上了
    地址:https://git.oschina.net/EverZc/AndroidHero

    相关文章

      网友评论

          本文标题:Android Scroll分析

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