最近PM2.5對側滑菜單比較感興趣,很多頁面上都用到了側滑菜單,之前也在網上看到了很多關于側滑,有自定義RecyclerView,也有自定義Item的,但是當自己真正去用的時候,發現有很多問題,所以打算自己參考網上的思路自己寫一個,果然,看花容易繡花難,寫的很艱辛,不過最后還是實現了,下面看看效果圖:
下面簡單分享下實現的思路:
自定義ViewGroup
這個其實沒什么太多要說的,主要是有幾點需要注意下:
- 需要復寫三個LayoutParams方法
generateDefaultLayoutParams
當動態向ViewGroup中添加沒有參數的child的時候,會自動調用這個方法,將其設置成為默認的參數
generateLayoutParams(AttributeSet attrs)
根據布局中的屬性來生成LayoutParams
generateLayoutParams(LayoutParams layoutParams)
代碼中動態添加參數
2.在復寫onMeasure方法的時候,需要對WrapContent這種情況進行特殊處理,因為很多時候item是包裹child的,高度并沒有固定死,所以需要特殊處理,不然會導致菜單欄的內容高度顯示不正確處理的方式就是以第一個child也就是內容區域為標準重新測量,代碼如下:
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
if (i == 0) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
childHeight = child.getMeasuredHeight();
} else {
int heightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
measureChild(child, widthMeasureSpec, heightSpec);
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
if (i > 0) {
mMaxDistance += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
}
}
3.onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int mLeftOffset = getPaddingLeft();
int topOffset = getPaddingTop();
for (int i = 0; i < childCount; i++) {
View mChild = getChildAt(i);
if (mChild.getVisibility() == GONE) {
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) mChild.getLayoutParams();
mLeftOffset += lp.leftMargin;
topOffset += lp.topMargin;
int measuredWidth = mChild.getMeasuredWidth();
int measuredHeight = mChild.getMeasuredHeight();
mChild.layout(mLeftOffset, topOffset, mLeftOffset + measuredWidth, topOffset + measuredHeight);
mLeftOffset += (measuredWidth + lp.rightMargin);
topOffset = getPaddingTop();
}
}
截止到這里,基本的measure跟layout就結束了,這個不是重點,重點在于解決滑動沖突。
View的滑動沖突
三個方法:
事件分發:public boolean dispatchTouchEvent(MotionEvent ev)
Touch 事件發生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由于某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由該 View 的 dispatchTouchEvent(MotionEvent ev)方法對事件進行分發。dispatchTouchEvent 的事件分發邏輯如下:
- 如果 return true,事件會分發給當前 View 并由 dispatchTouchEvent 方法進行消費,同時事件會停止向下傳遞;
- 如果 return false,事件分發分為兩種情況:
- 如果當前 View 獲取的事件直接來自 Activity,則會將事件返回給 Activity 的 onTouchEvent 進行消費;
- 如果當前 View 獲取的事件來自外層父控件,則會將事件返回給父 View 的 onTouchEvent 進行消費。
- 如果返回super.dispatchTouchEvent(ev),事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。
事件攔截:public boolean onInterceptTouchEvent(MotionEvent ev)
在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統默認的 super.dispatchTouchEvent(ev) 情況下,事件會自動的分發給當前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件攔截邏輯如下:
- 如果返回 true,則表示將事件進行攔截,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
- 如果返回 false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
- 如果返回 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 的 onTouchEvent 來接收,如果傳遞到上面的 onTouchEvent 也返回 false,這個事件就會“消失”,而且接收不到下一次事件。
● 如果返回了 true 則會接收并消費該事件。
● 如果返回 super.onTouchEvent(ev) 默認處理事件的邏輯和返回 false 時相同。
需要注意的是view是沒有onInterceptTouchEvent這個方法,只能分發,不存在攔截,只能分發,就跟view沒有layout方法是一樣的道理。
通過上面的分析,我們需要在onInterceptTouchEvent中進行攔截,然后在onToucheEvent中進行處理
onInterceptTouchEvent
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean consume = false;
acquireVelocityTracker(ev);
if (mInterPoint == null)
mInterPoint = new PointF();
if (mTouchPoint == null)
mTouchPoint = new PointF();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
consume = false;
mInterPoint.set(ev.getRawX(), ev.getRawY());
mTouchPoint.set(ev.getRawX(), ev.getRawY());
mPointerId = ev.getPointerId(0);
break;
case MotionEvent.ACTION_MOVE:
float abs = Math.abs(mInterPoint.x - ev.getRawX());
if (Math.abs(abs) > mTouchSlop) {
consume = true;
} else {
consume = isOpened;
}
break;
case MotionEvent.ACTION_UP:
if (isOpened && ev.getX() < getWidth() - getScrollX()) {
closeMenu();
consume = true;
}
break;
}
mInterPoint.set(ev.getRawX(), ev.getRawY());
mTouchPoint.set(ev.getRawX(), ev.getRawY());
return consume;
}
mInterPoint跟mTouchPoint是兩個PointF,用來記錄onInterceptTouchEvent跟onTouchEvent中的點坐標,isOpened是一個布爾值來記錄菜單是否打開,當菜單關閉的時候,點擊內容區域是不能進行攔截的,此時需要把點擊事件傳給child,當菜單打開的時候,此時需要group自己進行處理,需要關閉菜單,所以需要攔截此事件,自己進行處理,onInterceptTouchEvent事件的處理比較簡單,就是根據滑動的距離與當前菜單的顯示狀態比較來判斷是否攔截。
onTouchEvent
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (!mScroller.isFinished())
mScroller.abortAnimation();
int variationX = (int) (mTouchPoint.x - ev.getRawX());
int variationY = (int) (mTouchPoint.y - ev.getRawY());
if (Math.abs(variationX) < Math.abs(variationY)) {
getParent().requestDisallowInterceptTouchEvent(false);
} else {
getParent().requestDisallowInterceptTouchEvent(true);
scrollBy(variationX, 0);
int scrollX = getScrollX();
if (scrollX > mMaxDistance)
scrollTo(mMaxDistance, 0);
if (scrollX < 0)
scrollTo(0, 0);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
float velocityX = mVelocityTracker.getXVelocity(mPointerId);
if (Math.abs(velocityX) > 1000) {
if (velocityX < -1000)
openMenu();
else
closeMenu();
} else {
if (getScrollX() > mLimit)
openMenu();
else
closeMenu();
}
releaseVelocityTracker();
break;
}
mTouchPoint.set(ev.getRawX(), ev.getRawY());
return true;
}
onTouchEvent就顯得有些麻煩
- ACTION_DOWN
這里不需要記錄點坐標,只需要請求父容器不要攔截事件
- ACTION_DOWN
首先需要判斷此時的滑動方向,如果水平方向上的位移小于垂直方向上的位移,那么就把事件交給父容器處理,否則就自己進行處理
- ACTION_UP
通過兩種方式來確定菜單最終是打開還是關閉,一個是根據速度,一個是根據移動的距離,比較好理解
移動的方式
開啟菜單
private void openMenu() {
isOpened = true;
if (getScrollX() == mMaxDistance)
return;
mScroller.startScroll(getScrollX(), 0, mMaxDistance - getScrollX(), 0, 1000);
invalidate();
}
關閉菜單
private void closeMenu() {
isOpened = false;
if (getScrollX() == 0)
return;
mScroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 1000);
invalidate();
}
基本上實現了一個菜單的功能,上面只貼出了核心代碼,更多代碼可以下載Demo下來查看。