view基础知识介绍(二)

view基础知识介绍

View的滑动

  • View的滑动可以通过三种方式来实现:

    1. 通过view本身提供的scrollTo和scrollBy方法
    2. 通过动画施加平移效果来实现
    3. 通过改变view的LayoutParams使得view重新布局来实现

      • scrollTo/scrollBy
        ①. 通过查看view的源码 我们可以发现 scrollBy方法其实也是调用了scrollTo方法来实现的

        scrollTo方法是基于所传递参数的绝对位置滑动 而scrollBy是根据所传递参数基于当前未知的滑动
        通过源码可知 这两个方法只能改变view内容的位置 而不能改变view在布局中的位置
        

        ②.使用动画

            如果使用动画来进行view的滑动 需要注意的是 动画是对view的影像所做的操作 并没有真正改变view的参数 如果希望动画过后的状态还可以保留的话 需要将fillAfter属性设置为true 否则动画结束之后影像会消失
            使用动画对view影像进行操作的话 会带来一个严重的问题 那就是view的影像无法响应onClick事件
            而view的真实位置依然可以响应onClick事件
            针对以上问题 解决方案为 我们可以在新位置蔚县创建一个和目标view一摸一样的view 连点击事件也相同
            在view完成平移动画之后 将目标view隐藏 将我们预先创建好的view显示出来
        

        ③.改变布局参数
        通过改变目标wiew的margin属性 可以使得view进行位移操作 或者我们可以在目标view的旁边设置一个宽高为0的view 在需要位移的时候更改这个view的宽度 自然可以达到 移动目标view的目的

        三种方式的对比

        scrollTo/scrollBy
            操作简单 适合对view内容的滑动
            是view提供的原生的实现滑动效果并且不影响内部点击事件的方法 但他只能滑动view内容 不能滑动view本身
        
        动画
            操作简单 主要适用于对外界没有交互的view和实现复杂的动画效果
        
        改变布局参数
            操作略微复杂 适合于有交互的view
        
      • 弹性滑动

        Scroller 弹性滑动对象
        用以实现view的弹性滑动 当我们使用view的scrollTo和scrollBy操作时 view的滑动是瞬间完成的 体验不好 所以我们需要使用scroller对象来实现view的弹性滑动操作 scroller本身无法实现view的弹性滑动 需要和 view的computeScroll方法配合使用

        代码如下

            /**
             * 自定义滑动View
             * 注意 该方式时机调用的还是view的scrollTo方法 滑动的只是view的内容 并不会改变view的实际位置
             */
            public class ScrollerView extends View{
        
                private Context mContext;
        
                //滑动总时间 默认为1000ms 可以设置
                private int mDuration = 1000;
        
                //构造一个scroller对象
                private Scroller mScroller = new Scroller(mContext);
        
                public ScrollerView(Context context) {
                    super(context);
                    mContext = context;
                }
        
                /**
                 * 缓慢的滑动到某个位置
                 * @param destX 要滑动到位置的X坐标
                 * @param destY 要滑动到位置的Y坐标
                 */
                public void smoothScrollTo(int destX,int destY){
                    //获取view当前的位置
                    int scrollX = getScrollX();
                    //计算view需要滑动的距离
                    int deltaX = destX - scrollX;
                    int scrollY = getScrollY();
                    int deltaY = destY - scrollY;
                    //在指定时间内滑向destX
                    //通过读取该方法的源码 我们可以发现 在该方法中只是将我们设置的数据进行了记录 并没有实质性的对view进行滑动
                    mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,mDuration);
                    //重点在这里 调用该方法 可以导致view的重绘 当view重绘时会调用draw方法 在draw方法中 又会调用computeScroll方法
                    //computeScroll方法在view中是一个空实现 在这里我们对其进行了重写
                    invalidate();
                }
        
                /**
                 * view 中的方法 在view的Draw方法中调用 但在view中是一个空实现 具体代码需要我们自己来补充
                 *
                 * scroller需要配合该方法才能实现弹性滑动 具体原因如下
                 * 我们在开始滑动的时候调用了invalidate方法 该方法会导致view重绘 view重绘时draw方法会调用computeScroll方法
                 * 由于我们对其进行了重写 调用我们这里的代码 在这里我们又向mScroller请求view当前的具体位置 通过scrollTo方法对其进行设置
                 * 接着又调用postInvalidate方法 再次导致view重绘 以此类推
                 *
                 */
                @Override
                public void computeScroll() {
                    //判断条件中的方法 通过阅读源码可知 其根据总执行时间以及当前时间 来确定滑动是否已经结束 如果还没有结束
                    //则返回true 同时 计算view当前应该滑动到的位置 当滑动没有结束时 我们去请求view当前应该滑动到的位置
                    //利用scrollTo方法对其进行设置 同时调用postInvalidate方法 使view重绘
                    if(mScroller.computeScrollOffset()){
                        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                        postInvalidate();
                    }
                }
        
                /**
                 * 设置view弹性滑动的总时间
                 * @param duration view弹性滑动的时间
                 */
                public void setmDuration(int duration){
                    mDuration = duration;
                }
        
            }
      • 动画
        动画可以很轻松的实现弹性滑动的效果 其原理类似于Scroller对象 同时 在动画中 我们可以通过设置addUpdateListener
        加上一些我们想要进行的其他操作

      • 使用延时策略

        使用延时策略的核心点依然是使用view的scrollTo方法 延时的具体实现可以通过handler或者view的postDelay Thread的sleep等

view的事件分发机制

  • 点击事件的传递规则
    点击事件的传递其实就是对MotionEvent事件的分发过程 该事件的分发过程主要由以下三个方法共同来完成

    public boolean dispatchTouchEvent(MotionEvent ev)
    
    该方法用来进行事件的分发 如果事件能够传递到当前view 那么此方法 一定会被调用
    返回结果受当前view的onTouchEvent方法和下级view的dispatchTouchEvent方法的影响 表示是否能消耗当前事件
    
    public boolean onInterceptTouchEvent(MotionEvent ev)
    
    该方法在dispatchTouchEvent方法内部调用 用来判断当前view是都拦截某个事件 如果当前view拦截了某个事件
    那么在同一事件序列中 此方法不会被再次调用 返回结果表示是否拦截当前事件
    
    public boolean onTouchEvent(MotionEvent ev)
    
    在dispatchTouchEvent方法中调用 用来处理点击事件 返回结果表示是否消耗当前事件 如果不消耗 则在同一事件序列中
    当前view无法再次接受到事件
    
    上述三个方法之间的关系可以用以下伪代码来表示
    
        public boolean dispatchTouchEvent(MotionEvent ev){
            boolean consume = false;
            if(onInterceptTouchEvent(ev)){
                consume = onTouchEvent(ev);
            }else{
                consume = child.dispatchTouchEvent(ev);
            }
            return consume;
        }
    

    对于一个根viewGrounp来说 点击事件产生之后 首先会传递给他 这时他的dispatchTouchEvent就会被调用 如果这个
    viewGrounp的onInterceptTouchEvent方法返回true 就表示这个viewGrounp要拦截当前事件 那么此时当前事件
    就会被交给viewGrounp的onTouchEvent方法来处理 如果这个viewGrounp的onInterceptTouchEvent方法返回false
    表示没有拦截这个事件 那么这个事件将会传递给viewGrounp的子元素来处理 接着子元素的dispatchTouchEvent方法就会被调用 如此反复 直到事件最终被处理为止。

    当一个view需要处理事件时 如果他设置了onTouchListener 那么onTouchListener会被优先调用 调用onTouchListener
    中的onTouch方法 这时事件的处理结果要看onTouch的返回结果 如果onTouch返回false 那么此时view的onTouchEvent方法才会被调用 用来处理该事件 当onTouchEvent处理事件时 如果此时我们设置了onClickListener的话 onClickListener会被调用 从这里我们可以看出来 平时我们经常使用的onClickListener优先级最低 处于事件传递机制的末端

    这几个方法的优先级高低如下所示
    onTouchListener–>onTouchEvent–>onClickListener

    当一个点击事件产生之后 他的传递过程遵循如下规则
    Activity–>window–>view
    事件总是先传递给Activity Activity再传递给window 最后window再分发给顶级view 顶级view接收到事件后 就会按照
    事件的分发机制去分发事件 此时如果末端的view在onTouchEvent中返回false 那么它的父容器的onTouchEvent方法将会被调用 以此类推 如果所有的元素都没有处理该事件 那么该事件最终会被返回给Activity处理 即Activity的onTouchEvent方法
    将会被调用 这个过程就类似于日常生活中任务的委派过程一样 上级委派下级去处理 下级委派给更下级去处理 如果下级不能处理该事件那么该事件最终还是要返回给上级去处理的

    关于事件传递机制 这里给出一些结论 根据这些结论可以更好地理解事件传递机制 如下所示

    1. 同一个事件序列 指的是从手指解除屏幕的那一刻开始 到手指离开屏幕结束 中间产生的一系列事件 这个事件序列以down事件开始 以up事件结束 中间包含数量不等的move事件

    2. 正常情况下 一个事件序列只能被一个view拦截并消耗 因为一个元素一旦拦截了这一事件 那么同一事件序列中的所有事件都会直接交给该元素来处理 因此同一事件序列中的事件不能交给两个view同时处理 但是通过特殊手段可以做到 比如一个view将本该自己处理的事件通过onTouchEvent强行传递给其他view处理

    3. 某个view一旦拦截某一个事件 那么这个事件序列都只能由他来处理 并且它的onInterceptTouchEvent不会再次被调用 因为这个事件序列中的所有事件都有他来处理 所以不需要再次询问他是否拦截了

    4. 某个view一旦开始处理某个事件 如果他没有将ACTION_DOWN消耗掉 返回了false 那么此时该事件将会重新交给他的父元素去处理 即父元素的onTouchEvent将会被重新调用 意思就是事件一旦交给一个view来处理 那么他就必须要将其消耗掉 至少要将ACTION_DOWN消耗掉 否则同一事件序列中的其他事件也就不再交给他处理了 这就好比上级交给你一件任务 你一开始就没有处理好 那么之后的任务也不回交给你了 同一个道理

    5. 如果view不消耗除了ACTION_DOWN以外的事件 那么这个点击事件会消失 此时父元素的onTouchEvent方法也不会被调用 并且当前view可以继续接受该事件序列中后续的事件 最终这些消失的事件会交给Activity来处理

    6. ViewGrounp默认不会拦截任何事件 在Android的源码当中 ViewGrounp的onInterceptTouchEvent方法默认返回为false

    7. view没有onInterceptTouchEvent方法 事件一旦传递给他 那么他的onTouchEvent方法就会被调用

    8. view的onTouchEvent方法默认都会消耗事件 返回true 除非他当前状态是不可点击的(clickable和longClickable同时为false)view的longClickable默认都为false clickable分情况 例如button为true textview为false

    9. view的enable属性不影响onTouchEvent的默认返回值 哪怕一个view当前处于不可用状态 事件传递给他之后 他的onTouchEvent方法依旧会返回true来消耗事件

    10. onClick会发生的前提是当前view是可点击的 并且他收到了down和up事件

    11. 事件传递过程是由外向内的 即事件总是先传递给父元素 然后再由父元素分发给子元素 通过requestDisallowInterceptTouchEvent
      方法可以在子元素中干预父元素的事件分发过程 请求父元素不要拦截该事件 该方法对ACTION_DOWN无效 因为通过阅读源码可知 该方法会改变
      父元素中的一个标记位 当事件为ACION_DOWN时 会reset这个标记位 所以该方法对ACTION_DOWN无效

view的滑动冲突

  • 冲突场景

    1. 外部滑动与内部滑动方向不一致
    2. 外部滑动方向与内部滑动方向一致
    3. 上述两种情况嵌套
  • 处理规则

    1. 对于场景1来说 当用户左右滑动时 需要让外界view拦截事件 当用户上下滑动时 需要让内部view拦截事件我们需要做的就是判断用户滑动的方向 具体来说 我们可以根据用户水平滑动和竖直滑动的距离差来判断用户滑动的方向
    2. 对于场景2来说 我们无法通过用户的滑动方向来确定到底需要哪个view来滑动 所以对于这种类型的滑动冲突 我们就需要在业务上来对两种滑动作出区分 例如 当处于某种状态之下 需要外部view来响应用户的滑动 另一种状态之下需要内部view来响应用户的滑动
    3. 对于场景3这种比较复杂的滑动冲突来说 单一的解决办法肯定是不行的 所以我们需要将上边两种方案结合起来使用 具体场景具体分析
  • 解决方式

    1. 对于场景1来说 具体的解决办法有两种 外部拦截法和内部拦截法
  • 外部拦截法
    指的是所有的点击事件都要经过父容器的拦截处理(不包括DOWN事件) 如果父容器需要此事件 就拦截 如果不需要就不进行拦截 外部拦截法需要重写父容器的onInterceptTouchEvent方法 在该方法中作出相应的判断处理 大体逻辑可以
    根据如下伪代码得出

        public boolean onInterceptTouchEvent(MotionEvent ev){
            boolean intercepted = false;
            int x = (int)ev.getX();
            int y = (int)ev.getY();
            switch(ev.getAction()){
                case MotionEvent.ACTION_DOWN:
                    intercepted = false;
                    break;
                case MotionEvetn.ACTION_MOVE:
                    if(父容器需要该事件){
                        intercepted = true;
                    }else{
                        intercepted = false;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    intercepted = false;
                    break;
                default:
                    break;
            }
            mLastXIntercept = x;
            mLastYIntercept = y;
    
            return intercepted;
        }
    

    上述代码是外部拦截法的典型逻辑 针对不同的滑动冲突处理 只需要修改父容器需要当前事件的条件即可 其他都不需要作出修改 在上述代码中 ACTION_DOWN事件 父容器必须返回false 因为一旦父容器拦截了这个事件 那么后边的事件都不能传递给子元素了 ACTION_MOVE事件中 我们根据需要来判断是否拦截该事件 这里是代码中的重点 ACTION_UP事件 我们也需要返回false 因为 如果我们在父容器的move事件中拦截了事件 那么该事件序列中后续的事件就会交给父容器来处理 我们无需关心up事件 如果我们 没有在父容器汇总拦截move事件的话 事件应该是交给子元素去处理的 但是我们在这里对up事件做了拦截 up事件就会交个父容器来处理 此时子元素将不能收到up事件 那么子元素就不能正确的响应点击事件了 所以在这里我们需要对ACTION_UP事件返回false

    总结起来其实就是我们只需要对ACTION_MOVE事件作出处理而已 ACTION_DOWN和ACTION_UP事件我们都只需要返回false即可

  • 内部拦截法

    内部拦截法的意思就是父容器在拦截阶段不做任何逻辑上的处理(并不是父元素不拦截 而是默认拦截除了ACTION_DOWN 以外的所有事件 子元素如果需要某个事件的话 就需要通过requestDisallowInterceptTouchEvent()方法来获取该事件) 所有的拦截逻辑处理都交给子元素去处理 子元素需要该事件就直接消耗掉 如果子元素不需要该事件的话 就交给父容器去处理 与外部拦截法相比 该方法会比较复杂 伪代码如下所示 我们需要重写的是 子元素 的dispatchTouchEvent方法

        public boolean dispatchTouchEvent(MotionEvent ev){
            int x = (int) ev.getX();
            int y = (int) ev.getY();
    
            switch(MotionEvent.getAction()){
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    if(子元素不需要该事件){
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                defalut:
                    break;
            }
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(ev);
        }
    

    上述代码是内部拦截法的典型代码 当面对不同的滑动策略时 只需要修改子元素中该方法中的判断条件即可 别的地方不需要做出改动 除了子元素需要重写该方法以外 父元素也需要作出一些处理 需要默认拦截除了ACTION_DOWN以外的所有事件(ACTION_DOWN事件不受requestDisallowTouchEvent()方法的控制 一旦父容器拦截了这个事件 那么所有的事件都不能传递到子元素中去了 内部拦截法也就无从谈起了) 这样当子元素认为不需要事件的时候 调用requestDisallowInterceptTouchEvent(false)时父容器才能继续拦截之后的事件 父容器的修改如下所示 为固定代码

        public boolean onInterceptTouchEvent(MotionEvent ev){
            int action = ev.getAction();
            if(action == MotionEvetn.ACTION_DOWN){
                return false;
            }else{
                return true;
            }
        }
    

    参考资料:Android开发艺术探索

你可能感兴趣的:(Android)