美文网首页Android资源收录Android开发Android自定义View
Android 自定义View学习(十四)——View滑动学习

Android 自定义View学习(十四)——View滑动学习

作者: 英勇青铜5 | 来源:发表于2016-10-05 19:12 被阅读1427次

    学习资料:

    • Android群英传
    • Android艺术探索

    滑动效果就是实现动态修改一个View的坐标。
    实现滑动效果的基本思想:
    手指落在屏幕触控屏幕时,系统记下当前的触摸点坐标;手指在屏幕移动时,系统记下移动后的触摸点坐标,获取到每一次相对前一次触摸点坐标的偏移量,通过偏移量来修改View的坐标,不断重复,实现整个滑动过程

    1.系统辅助类 <p>

    MotionEvent一样,滑动事件系统还提供了另外一些类


    1.1 TouchSlop最小距离 <p>

    TouchSlop是系统识别最小的滑动距离,是一个常量值。当手指在屏幕滑动距离小于这个值时,系统不会将动作视为滑动。这个常量值的具体大小和设备也有关,不同的屏幕分辨率,可能会不一样

    获得方式:

    ViewContfiguration.get(getConetxt()).getScaledTouchSlop()
    

    利用这个临界值,可以将一些不想要的手指操作给过滤掉


    1.2 VelocityTracker 速度追踪 <p>

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

    使用过程:

    • 第1步,在View.onToucheEvent()获取VelocityTracker对象
    • 第2步,使用拿到的VelocityTracker对象来计算x,y轴方向的速度
    • 第3步,在比较恰当及时的时机,将VelocityTracker对象释放掉,回收内存

    代码:

    public class ScrollerActivity extends AppCompatActivity {
        private VelocityTracker velocityTracker;
        private final String TAG = "ScrollerActivity";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_scroller);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            //获取VelocityTracker
            velocityTracker = VelocityTracker.obtain();
            velocityTracker.addMovement(event);
            //计算滑动速度
            velocityTracker.computeCurrentVelocity(1000);//计算速度
            float xVelocity = velocityTracker.getXVelocity();
            float yVelocity = velocityTracker.getYVelocity();
            Log.e(TAG,"&&&-->x = "+xVelocity+"---> y = "+yVelocity);
            return super.onTouchEvent(event);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            if (null != velocityTracker){
                velocityTracker.clear();//重置
                velocityTracker.recycle();//回收内存
            }
        }
    }
    
    

    直接在Acvitity测试,获取速度的结果,

    • x轴速度,从左向右滑动时,速度为正,从右向左滑动为负;
    • x轴速度,从上向下滑动时,速度为正,从下向上滑动为负;

    正负值就是要看滑动的方向和x,y轴方向是否一致


    注意:
    在使用velocityTracker.getXVelocity(),velocityTracker.getYVelocity()获取速度之前,要先根据设置的单位时间来计算速度。计算公式v = (终点- 起点) /t。计算出来的速度是相对于设置的时间的。

    计算出来的速度指的是一段时间内滑过的像素数。

    velocityTracker.computeCurrentVelocity(t)

    t = 1000,在1000ms内,假设匀速水平滑过了1000px,水平速度就是1000,也就是1000px/1000ms
    t = 100,在100ms内,假设匀速水平滑过了100px,水平速度就是100,也就是100px/100ms


    1.3 GestureDetector 手势检测 <p>

    用于辅助检测单击、滑动、长按、双击

    使用步骤:

    • 第1步:创建GestureDetector对象,并实现OnGestureListener接口。
    • 第2步:接管目标ViewonTouhEvent方法

    GestureDetector.setOnDoubleTapListener(onDoubleTapListener)可以实现双击

    Activicty为目标View代码:

    public class ScrollerActivity extends AppCompatActivity {
        private Toast toast;
        private GestureDetector mGestureDetector;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_scroller);
            initGestureDetector();
        }
    
        /**
         * 初始化 GestureDetector
         */
        private void initGestureDetector() {
            mGestureDetector = new GestureDetector(ScrollerActivity.this,onGestureListener );
            //解决屏幕长按后无法拖动
            mGestureDetector.setIsLongpressEnabled(false);
        }
    
        private GestureDetector.OnGestureListener onGestureListener = new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {//手指轻触屏幕的一瞬间,由一个ACTION_DOWN触发
                showToast("轻触一下");
                return true;
            }
    
            @Override
            public void onShowPress(MotionEvent e) {//手指轻触屏幕,尚未松开或拖动,由一个ACTION_DOWN触发
                showToast("轻触未松开");
            }
    
            @Override
            public boolean onSingleTapUp(MotionEvent e) {//手指离开屏幕,伴随一个ACTION_UP触发,单击行为
                showToast("单击");
                return true;
            }
    
            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {//手指按下屏幕并拖动
                // 由一个由一个ACTION_DOWN,多个ACTION_MOVE触发,是拖动行为
                showToast("拖动");
                return false;
            }
    
            @Override
            public void onLongPress(MotionEvent e) {//长按
                showToast("长按");
            }
    
            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                //按下屏幕,快速滑动后松开,由一个由一个ACTION_DOWN,多个ACTION_MOVE,一个ACTION_UP触发
                showToast("快速滑动");
                return false;
            }
        };
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            boolean consume = mGestureDetector.onTouchEvent(event);
            return consume;
        }
    
        /**
         * Toast
         */
        private void showToast(String str) {
            if (null == toast) {
                toast = Toast.makeText(ScrollerActivity.this, str, Toast.LENGTH_LONG);
            } else {
                toast.setText(str);
            }
            toast.show();
        }
    }
    

    OnGestureListeneronDown(),onSingleTapUp(),onScroll(),onFling()方法都有一个boolean类型的返回值,这个值表示是否消费事件


    1.4 Scroller 弹性滑动对象 <p>

    用于实现View的弹性滑动。Scroller本身无法实现弹性滑动,需要配合ViewcomputeScroll()方法

    Scroller使用有个固定的3步走模式:

    1. 初始化Scroller对象
    2. 重写ViewcomputeScroll()方法
    3. 调用mScroller.startScroll()方法

    简单使用:

    public class ScrollerView extends LinearLayout {
        private Scroller mScroller;
    
        public ScrollerView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initScroller();
        }
    
        /**
         * 初始化Scroller
         */
        private void initScroller() {
            mScroller = new Scroller(getContext());
        }
    
        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {//判断Scroller是否执行完毕
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                postInvalidate();
            }
        }
    
        public void smoothScrollTo(int destX, int destY) {
            //计算相对于左上角的偏移量
            final int deltaX = getScrollX() - destX;
            final int deltaY = getScrollY() - destY;
            //在1000ms内滑向destX destY
            mScroller.startScroll(0, 0, deltaX, deltaY, 1000);
            invalidate();
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return true;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    smoothScrollTo((int) event.getX(), (int) event.getY());
                    break;
                case MotionEvent.ACTION_UP://恢复左上角
                    mScroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY(), 1000);
                    invalidate();
                    break;
            }
            return true;
        }
    }
    
    

    效果便是手指点在屏幕哪里,一秒内,ScrollerView内的所有子控件便会滑动到手指落的点的位置

    关于Scroller这里先了解一点点,打算之后再单独来学习


    2.实现滑动的7种方法 <p>

    Android群英传中,徐医生给出7种滑动方法:

    1. layout方法
    2. offsetLetAndRight()和offsetTopAndBottom()
    3. LayoutParams
    4. scrollTo和scrollBy
    5. Scroller
    6. 属性动画
    7. ViewDragHelper

    5上面刚刚有了解,以后还会继续补充学习,6Android动画基础知识学习(下)学习了解过。1234在本篇会学习了解,这几个方法感觉效果都不是很好,滑动效果很突兀,最重要的便是方法7,下篇单独来学习


    2.1ayout方法

    View进行绘制时,会调用onLayout()方法来设置显示的位置

    代码:

    public class ScrollerView extends LinearLayout {
    
        private float lastX, lastY;
    
        public ScrollerView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return true;
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            float x = event.getX();
            float y = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    lastX = x;
                    lastY = y;
                    break;
                case MotionEvent.ACTION_MOVE:
                    //计算偏移量
                    float offsetX = x - lastX;
                    float offsetY = y - lastY;
                    //计算四个顶点的位置
                    int left = (int) (getLeft() + offsetX);
                    int top = (int) (getTop() + offsetY);
                    int right = (int) (getRight() + offsetX);
                    int bottom = (int) (getBottom() + offsetY);
                    //布局回调
                    layout(left, right, top, bottom);
                    break;
            }
            return true;
        }
    }
    

    不晓得是我代码有问题还是这个思路本身有问题,体验非常不好,childView在滑动过程中,大小会发生改变


    2.2 offsetLeftAndRight和offsetTopAndBottom <p>

    系统提供的对左右上下移动的API的封装,效果和使用与layout方法类似

    layout方法代码简单修改:

    case MotionEvent.ACTION_MOVE:
        //计算偏移量
        float offsetX = x - lastX;
        float offsetY = y - lastY;
        Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
        offsetLeftAndRight((int)offsetX);
        offsetTopAndBottom((int)offsetY);
    break;
    

    子控件会随着手指在屏幕滑动而滑动

    offset有效区域

    这个方法遇到个问题,有些区域无效,只有黄色边框内才有效


    2.3 LayoutParams 布局参数 <p>

    LayoutParams保存了一个View的布局参数。可以通过LayoutParams来动态地修改一个布局的位置参数。

    简单的修改代码:

     case MotionEvent.ACTION_MOVE:
        //计算偏移量
        float offsetX = x - lastX;
        float offsetY = y - lastY;
        Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
        MarginLayoutParams layoutParams = (MarginLayoutParams) getLayoutParams();
        layoutParams.leftMargin = (int) (getLeft() + offsetX);
        layoutParams.topMargin = (int) (getTop() + offsetY);
        setLayoutParams(layoutParams);
    break;
    

    同样有一个有效区域的问题


    2.4 使用scrollTo或者scrollBy <p>

    两个方法区别: scrollBy()相对移动,scrollTo()绝对移动

    View内部有两个属性mScrllXmScrollY,分别可以通过getScrollX()getScrollY()方法得到

    在滑动过程中,mScrollX总是等于View左边缘和View内容左边缘在水平方方向的距离;mScrollY总是等于View上边缘和View中内容上边缘在竖直方向的距离。View边缘是指View的位置也就是View的四个顶点到父容器的距离,View内边缘是内容距离View四边的距离。

    无论是scrollTo()还是scrollBy()都只能改变View内容的位置而不能改变View在布局中的位置

    mScrollX/Y单位为像素px。当View左边缘在View内容左边缘右边时,mScrollX为正值,反之为负值;同理,当View上边缘在View内容上边缘下边时,mScrollX为正值,反之为负值。也就是说,View从左向右滑动,mScrollX为负值,反之为正值;从上往下滑动,mScrollY为负值,反之为正值

    mScrollX和mScrollY变换规律

    白色为View原始位置,紫色矩形为内容


    简单使用:

     case MotionEvent.ACTION_MOVE:
        //计算偏移量
        float offsetX = x - lastX;
        float offsetY = y - lastY;
        Log.e("offset", "&&&--" + offsetX + "-->" + offsetY);
        //scrollBy((int)-offsetX,(int)-offsetY);
        scrollTo((int)-offsetX,(int)-offsetY);
        break;
    

    根据规律图,手势和实际移动方向相反,在设置参数时,设置为了-offsetX


    几种方式,简单总结

    • scrollTo/By:操作简单,适合对View内容的滑动
    • 属性动画:操作简单,适用于没交互View和实现复杂的动画效果
    • 改变布局参数:操作复杂,适用于有交互的View

    3. 滑动冲突

    滑动冲突常见的场景:

    1. 外部滑动方向和内部滑动方向不一致
    2. 外部滑动方向和内部滑动方向一致
    3. 上面两种情况嵌套
    滑动冲突

    解决方式有两种:外部拦截,内部拦截


    3.1 外部拦截 <p>

    外部拦截思路:
    点击事件都会先经过父容器的拦截处理,如果父容器需要处理此事就拦截,否则就不进行拦截。重写父容器的onInterceptTouchEvent()方法

    伪码:

    public boolean onInterceptTouchEvent(MotionEvent event){
           boolean intercepted = false;
           int x = (int) event.getX();
           int y = (int) event.getY();
           switch(event.getAction()){
                 case MotionEvent.ACTION_MOVE:
                      intercepted = true;
                 break;
                 case MotionEvent.Move:
                      if(父容器需要当前点击事件){
                         intercepted = true;
                      }else{
                         intercepted = false; 
                      }
                 break;
                 case MotionEvent.ACTION_UP:
                      intercepted = false;
                 break;
          }
          mLastXIntercept = x;
          mLastYIntercept = y;
          return intercepted;
    }
    

    处理思路代码基本都是固定的。
    首先,在ACTION_DOWN中,父容器必须返回false,不拦截ACTION_DOWN事件。因为一旦拦截了ACTION_DOWN后续的ACTION_MOVEACTION_UP都会又父容器来处理,这样事件就无法传递给childView
    其次,在ACTION_MOVE中,可以根据需要来进行拦截,需要就返回true,否则就false
    最后,在ACTION_UP中,返回false


    注意:
    如果父容器在ACTION_UP中,返回了truechildView就不会再收到ACTION_UP事件,childViewonClick事件就不会触发。父容器比较特殊,一旦开始拦截某个事件,之后的序列事件都是交给父容器来处理,包括ACTION_UP,即使在ACTION_UP中返回falseACTION_UP还是由父容器处理


    3.2 内部拦截 <p>

    内部拦截法指的是父容器不拦截任何事件,所有的事件都传递给childView,根据需要,childView来选择是否消费,需要配合requestDisallowInterceptTouchEvent()方法。重写childViewdispatchTouchEvent()方法

    伪码:

    public boolean dispatchTouchEvent(MotionEvent event){
           int x = (int) event.getX();
           int y = (int) event.getY();
           switch(event.getAction()){
              case MotionEvent.ACTION_DOWN:
                   parent.requestDisallowInterceptTouchEvent(true);
              break;
              case MotionEvent.ACTION_MOVE:
                   int deltaX = x - mLastX;
                   int deltaY = y - mLastY;
                   if(父容器需要此类点击事件){
                       parent.requestDisallowInterceptTopuchEvent(false);
                   }
              break;
              case MotionEvent.ACTION_UP:
                   break;
              break;
           }
           mLastX = x ;
           mLastY = y ;
           return super.dispatchTouchEvent(event);
    }
    

    使用稍微比外部麻烦。

    ACTION_DOWN中,使用parent.requestDisallowInterceptTouchEvent(true),让父容器不拦截ACTION_DOWN事件,ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT标记位控制


    4.最后 <p>

    国庆放假在家的效率有些低,事有点多。农村娃,还下地干了会活,哈哈。打算将自定义系列结束呢,完不成不计划了。按照学习计划,还剩下2篇学习内容

    本人很菜,有错误请指出

    共勉 :)

    相关文章

      网友评论

      • 19cdd9982f8a:大神 你好 我的为什么没效果啊
        @Override
        public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        firstX= x;
        firstY =y;
        break;
        case MotionEvent.ACTION_MOVE:
        float offsetX = x - firstX;
        float offsetY = y - firstY;
        Log.e("offset","&&&--"+offsetX+"-->"+offsetY);
        scrollTo((int)offsetX,(int)offsetY);
        break;
        }
        return true;
        }
      • dodo_lihao:向你学习!
        英勇青铜5:@dodo_lihao :scream::scream::scream:多谢打赏,互相学习

      本文标题:Android 自定义View学习(十四)——View滑动学习

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