软件新版本迭代开发完毕,业务逻辑上没有太大的难度,此次有一难点就是自定义控件.接触到了事件传递机制和ViewDragHelper.html
首先说一下此次App开发中要用到的自定义控件.UI中大致分为两部分,一部分是我的信息展现,我使用了n个线性布局,第二部分是业务展现是ViewPager + Fragment实现左右分页滑动,Fragment中使用自定义的SwipeRefreshLayout实现上滑加载下滑刷新.而要实习的整体效果是向上滑动界面任意的地方时,我的信息部分收缩,我的业务部分向上滑动展现出来,滑动listview,若是向上滑动加载更多,向下滑动,若是滑动到顶端还在滑动将我的信息部分展现,业务部分收缩.java
刚开始我本身的想法是使用事件传递机制,自定义个ScrollView和ViewPager.
ViewPager将事件拦截不往下分发则此时,listview没法滑动,这样ScrollView能够自由滑动,判断业务信息部分的头部是否到达顶端到达顶端了ViewPager向下分发事件,listview能够自由滑动.基本上就能够实现了效果.git
首先聊一下事件分发机制.每个ViewGroup都有如下三个方法
dispatchTouchEvent.
onInterceptTouchEvent.
onTouchEvent.
最小单元的View好比TextView只有onTouchEventgithub
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 方法去进行处理。ide
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.htmlpost
接下来讲一下个人自定义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中this
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处理事件.
这样基本上就实现了所要的结果.不过没有动画并且界面使用这种效果十分僵硬(我的认为很难看,产品却坚持要这样).
为了将界面作更加柔和美观一点因而就找一些动画和其余能实习该效果的方法.因而就涉及到了
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),在这个方法中我加了个判断是不是在滑动.
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 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