事件传递机制和ViewDragHelper

来源:互联网 发布:最小凸包面积算法 编辑:程序博客网 时间:2024/04/28 08:10

软件新版本迭代开发完毕,业务逻辑上没有太大的难度,这次有一难点就是自定义控件.接触到了事件传递机制和ViewDragHelper.


事件传递机制

首先说一下这次App开发中要用到的自定义控件.UI中大体分为两部分,一部分是个人信息展示,我使用了n个线性布局,第二部分是业务展示是ViewPager + Fragment实现左右分页滑动,Fragment中使用自定义的SwipeRefreshLayout实现上滑加载下滑刷新.而要实习的总体效果是向上滑动界面任意的地方时,个人信息部分收缩,个人业务部分向上滑动展示出来,滑动listview,如果向上滑动加载更多,向下滑动,如果滑动到顶端还在滑动将个人信息部分展示,业务部分收缩.

刚开始我自己的想法是使用事件传递机制,自定义个ScrollView和ViewPager.
ViewPager将事件拦截不往下分发则此时,listview无法滑动,这样ScrollView可以自由滑动,判断业务信息部分的头部是否到达顶端到达顶端了ViewPager向下分发事件,listview可以自由滑动.基本上就可以实现了效果.


首先聊一下事件分发机制.每一个ViewGroup都有以下三个方法
dispatchTouchEvent.
onInterceptTouchEvent.
onTouchEvent.
最小单元的View比如TextView只有onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev)
当触摸事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会从根元素依次往下传递直到最内层子元素或在中间某一元素中事件被拦截或者消费.
dispatchTouchEvent 的事件逻辑如下:
如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;这样该View的onTouchEvent事件也不会得到响应.
如果 return false,会将事件返回给父 View 的 onTouchEvent 进行消费。
如果返回系统默认的 super.dispatchTouchEvent(ev),事件会分发给当前 View 的 onInterceptTouchEvent 方法去进行处理。

public boolean onInterceptTouchEvent(MotionEvent ev)
在 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回 super.dispatchTouchEvent(ev) 事件会分发给当前 View 的 onInterceptTouchEvent 方法。
onInterceptTouchEvent 的事件逻辑如下:
如果 onInterceptTouchEvent 返回 true,则将事件进行拦截,并将拦截到的事件交由该 View 的 onTouchEvent 进行处理;
如果 onInterceptTouchEvent 返回 false,则将事件向子View传递,再由子 View 的 dispatchTouchEvent 来对这个事件处理;
如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev),事件会被拦截,并将事件交由该 View 的 onTouchEvent 进行处理。

public boolean onTouchEvent(MotionEvent ev)
在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情况下 onTouchEvent 会被调用。
onTouchEvent 的事件逻辑如下:
如果事件传递到该 View 的 onTouchEvent 方法,而该方法返回了 false,那么这个事件会从该 View 向父View传递,父 View 的 onTouchEvent 来接收,而且如果父View也是返回了了false那事件也会向向上传递由onTouchEvent接收处理.
如果返回了 true 则会接收并消费该事件。
如果返回 super.onTouchEvent(ev) 默认处理事件的逻辑和返回 false 时相同。

总结一下就是每一个View都是三个与事件拦截分发相关的三个事件,如果父View选择处理则子控件将得不到事件,如果父控件选择向下分发则子View进行处理.这是事件向下分发.事件还可以向上传递.即如果事件传到View的onTouchEvent,而该View的onTouchEvent返回了false或者super.onTouchEvent(ev)则该事件向上传递到父View的onTouchEvent,如果父View不选择处理将继续向上传递.
对于事件分发可以看看这篇博客
http://www.cnblogs.com/chenkailw/p/5113268.html

接下来说一下我的自定义ScrollView
自定义的ScroolView的java文件

public class CustomerScrollView extends ScrollView {    private OnScrollListener onScrollListener;    /**     * 主要是用在用户手指离开MyScrollView,MyScrollView还在继续滑动,我们用来保存Y的距离,然后做比较     */    private int lastScrollY;    ...    /**     * 设置滚动接口     * @param onScrollListener     */    public void setOnScrollListener(OnScrollListener onScrollListener){        this.onScrollListener = onScrollListener;    }    @Override    public boolean onTouchEvent(MotionEvent ev) {            return super.onTouchEvent(ev);    }    @Override    protected void onScrollChanged(int l, int t, int oldl, int oldt) {        super.onScrollChanged(l, t, oldl, oldt);    }    @Override    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {        super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);        if(onScrollListener != null){            onScrollListener.onScroll(lastScrollY = this.getScrollY());        }    }    /**     * 滚动的回调接口     */    public interface OnScrollListener{        /**         * 回调方法, 返回MyScrollView滑动的Y方向距离         */        public void onScroll(int scrollY);    }}

主Activity中

public class MainActivity extends AppCompatActivity implements CustomerScrollView.OnScrollListener {    private CustomerScrollView scrollview;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        scrollview = (CustomerScrollView) findViewById(R.id.scrollview);        scrollview.setOnScrollListener(this);    }    private int LayoutTop;    private int LayoutBottom;    @Override    public void onWindowFocusChanged(boolean hasFocus) {        super.onWindowFocusChanged(hasFocus);        if(hasFocus) {            //得到自己需要的值            //LayoutTop = mCustomerInfo.getBottom();            //LayoutBottom = Rly.getBottom();        }    }    @Override    public void onScroll(int scrollY) {        //判断什么时候该让子View获得事件.        if ((LayoutBottom) >= scrollY) {            //mBaoBeiViewPager.setEvent(false);        } else {            //mBaoBeiViewPager.setEvent(true);        }    }}

自定义的ViewPage不用全部给出代码了给出主要的一部分

public class CustomerViewPager extends ViewPager {    public boolean event = false;    ...    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        //return super.onInterceptTouchEvent(ev);        return childViewGetEvent() ? super.onInterceptTouchEvent(ev) : true;    }    public boolean childViewGetEvent() {        return event;    }    public void setEvent(boolean event) {        this.event = event;    }}

在mainActivity中结合回调函数判断滑动的高度是否到达预定高度,到达后将View事件分发给子View,这样子View处理事件.

这样基本上就实现了所要的结果.不过没有动画而且界面使用这种效果十分僵硬(个人认为很难看,产品却坚持要这样).


ViewDragHelper.

为了将界面做更加柔和美观一点于是就找一些动画和其他能实习该效果的方法.于是就涉及到了

Demo连接是https://github.com/SunnyTime/DragTopLayout.git,国外高人写的我只是修改了几个BUG.

DragTopLayout extends FrameLayout
构造方法不做介绍了.
DragTopLayout继承帧布局但是初看似乎更应该是线性布局垂直排布.在onLayout方法中进行了我们想要的排布.

 //onLayout会决定具体View的大小和位置    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        super.onLayout(changed, left, top, right, bottom);        dragRange = getHeight();        // In case of resetting the content top to target position before sliding.;        int contentTopTemp = contentTop;        resetTopViewHeight();        resetContentHeight();        //Math.min-->取最小值        //参数是左,上,右,下.        topView.layout(left, Math.min(topView.getPaddingTop(), contentTop - topViewHeight), right,                contentTop);        dragContentView.layout(left, contentTopTemp, right,                contentTopTemp + dragContentView.getHeight());    }

contentTop - topViewHeight = 0,在方法resetTopViewHeight中设置了这两者的值.

注意:
getMeasuredHeight()返回的是原始测量高度,与屏幕无关,getHeight()返回的是在屏幕上显示的高度。实际上在当屏幕可以包裹内容的时候,他们的值是相等的,只有当view超出屏幕后,才能看出他们的区别。当超出屏幕后,getMeasuredHeight()等于getHeight()加上屏幕之外没有显示的高度。这样展示出的布局就是TopView在上面,ContentView在下面.

重要的类ViewDragHelper,在这次代码中用到的方法:
public boolean tryCaptureView(View child, int pointerId)
判断哪一个View可以拖动.

public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)
在垂直方向上位置变化

//重新布局
requestLayout();

//计算比例,ratio = (top-collapseOffset) / (topViewHeight - collapseOffset),collapseOffset如果没有在xml中赋值就一直为0,top的值的范围是0~topViewHeight.
calculateRatio(contentTop);

//更新布局状态即展开收缩和滑动
updatePanelState();

public int getViewVerticalDragRange(View child)
在垂直方向上可拖动的范围

public int clampViewPositionVertical(View child, int top, int dy)
对child移动的边界进行控制
return Math.max(top, getPaddingTop() + collapseOffset)或
return Math.min(topViewHeight, Math.max(top, getPaddingTop() + collapseOffset)),为即将移动到的位置

public void onViewReleased(View releasedChild, float xvel, float yvel)
手指释放的时候回调,当子view不再被拖曳时调用.如果有需要,Fling的速度也会被提供.速度值会介于系统最小化和最大值之间.
注意:
如果mDragHelper.settleCapturedViewAt(left, top);方法去移动View,必须使用invalidate() / postInvalidate() 刷新View才有效果.

public void onViewDragStateChanged(int state)
当拖曳状态变更时回调该方法

还有其他一些常用的方法:

void onViewCaptured(View capturedChild, int activePointerId); //当子view被由于拖曳或被settle, 而被捕获时回调的方法.
void onEdgeTouched(int edgeFlags, int pointerId); //当父view其中一个被标记可拖曳的边缘被用户触摸, 同时父view里没有子view被捕获响应时回调该方法.
boolean onEdgeLock(int edgeFlags); //当原来可以拖曳的边缘被锁定不可拖曳时回调
void onEdgeDragStarted(int edgeFlags, int pointerId); //当用户开始从父view中”订阅的”(之前约定允许拖曳的)屏幕边缘拖曳,并且父view中没有子view响应时调用.

下面两个方法就是与事件分发拦截有关:
public boolean onTouchEvent(MotionEvent event).
public boolean onInterceptTouchEvent(MotionEvent ev),在这个方法中我加了个判断是否是在滑动.

switch (ev.getAction()) {      case MotionEvent.ACTION_DOWN:          touchDownY = ev.getY();          mScrolling = false;          break;      case MotionEvent.ACTION_MOVE:          if (Math.abs(touchDownY - ev.getY()) >= ViewConfiguration.get(                  getContext()).getScaledTouchSlop()) {              mScrolling = true;          } else {              mScrolling = false;          }          break;      case MotionEvent.ACTION_UP:          mScrolling = false;          break;  }

当如果TopView不是图片而是其他的控件且设置了点击事件这样会有冲突.点击事件不响应.

有些童靴想,如果将TopView收缩,那ContentView如何下滑动如何判断是否滑动到了顶端,并且把事情处理权上交.
AttachUtil.java文件中有相应的判断在你是用的ListView或者RecyclerView中监听滑动,使用EventBus将消息发送出去.
EventBus.getDefault().post(AttachUtil.isAdapterViewAttach(view));

这样就可以完成所要的交互结果.其中类ViewDragHelper还是一个难点,需要以后继续起摸索其中的回调方法.有兴趣的同学可以参考下面博客
http://blog.csdn.net/lmj623565791/article/details/46858663.
http://www.it165.net/pro/html/201505/40127.html
这两篇博客然后自己再写一些列子入门应该就可以,不过以后的实战还需要根据不同的需求来自己去设计.
文中有不足的地方请各位大虾多多指教.
Demo地址:https://github.com/SunnyTime/DragTopLayout.git

0 0
原创粉丝点击