軟件新版本迭代開發完畢,業務邏輯上沒有太大的難度,此次有一難點就是自定義控件.接觸到了事件傳遞機制和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