SlidingMenu原始碼分析(二)
SlidingMenu原始碼分析-附自定義底部滑出選單控制元件
本帖最後由 boredream 於 2014-11-26 19:17 編輯
本章算是自定義控制元件系列的一個例項分析,SlidingMenu算是自定義控制元件裡比較優秀的一個了,所以選了這個研究
看之前最好先簡單看下我文章裡的自定義控制元件教程以及ScrollView原理分析~可以點開個人資料檢視相關文章~
< Scroll效果研究-系統ScrollView原始碼分析>
http://www.eoeandroid.com/thread-553375-1-1.html
自定義控制元件教程第一篇
http://www.eoeandroid.com/thread-548644-1-1.html
SlidingMenu設計思路三個主要的ViewGroup, 主ViewGroup裡面包含兩個重疊的ViewGroup,蓋在上面的就是顯示內容,而下面的就是選單
上面的側滑以後露出後面的View~
主ViewGroup作為一個自定義控制元件,裡面的內容和選單利用自定義屬性設定
SlidingMenu(繼承RelativeLayout),就是這個主ViewGroup,
其中又包含兩個,內容CustomViewAbove和選單CustomViewBehind(都繼承ViewGroup),
SlidingMenu提供兩個自定義屬性,供使用者傳入兩個佈局id,對應主體內容和選單佈局,
此外還有其他一些屬性供開發者設定選單效果
下面是程式碼分析
----------------------------------------------------------------------------------------
建構函式中主要程式碼如下:
- LayoutParams behindParams = new LayoutParams(LayoutParams. MATCH_PARENT, LayoutParams.MATCH_PARENT);
- mViewBehind = new CustomViewBehind(context);
- addView(mViewBehind , behindParams);
- LayoutParams aboveParams = new LayoutParams(LayoutParams. MATCH_PARENT, LayoutParams.MATCH_PARENT);
- mViewAbove = new CustomViewAbove(context);
- addView(mViewAbove , aboveParams);
- // register the CustomViewBehind with the CustomViewAbove
- mViewAbove.setCustomViewBehind(mViewBehind );
- mViewBehind.setCustomViewAbove(mViewAbove );
- mViewAbove.setOnPageChangeListener(new OnPageChangeListener() {
- public static final int POSITION_OPEN = 0;
- public static final int POSITION_CLOSE = 1;
- public static final int POSITION_SECONDARY_OPEN = 2;
- public void onPageScrolled( int position, float positionOffset,
- int positionOffsetPixels) { }
- public void onPageSelected( int position) {
- if (position == POSITION_OPEN && mOpenListener != null) {
- mOpenListener.onOpen();
- } else if (position == POSITION_CLOSE && mCloseListener != null) {
- mCloseListener.onClose();
- } else if (position == POSITION_SECONDARY_OPEN && mSecondaryOpenListner != null ) {
- mSecondaryOpenListner.onOpen();
- }
- }
- });
- // now style everything!
- TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlidingMenu);
- // set the above and behind views if defined in xml
- int mode = ta.getInt(R.styleable.SlidingMenu_mode, LEFT);
- setMode(mode);
- int viewAbove = ta.getResourceId(R.styleable. SlidingMenu_viewAbove, -1);
- if (viewAbove != -1) {
- setContent(viewAbove );
- } else {
- setContent( new FrameLayout(context));
- }
- int viewBehind = ta.getResourceId(R.styleable.SlidingMenu_viewBehind, -1);
- if (viewBehind != -1) {
- setMenu(viewBehind);
- } else {
- setMenu(new FrameLayout(context));
- }
新建兩個自定義控制元件CustomViewAbove/Behind將其addView到SlidingMenu中,
然後在通過屬性獲取到view的id後會在setContent/Menu方法中設定對應佈局
下面是content處理相關程式碼,menu同理
- /**
- * Set the above view content to the given View.
- *
- * @param view The desired content to display.
- */
- public void setContent(View view) {
- mViewAbove.setContent(view);
- showContent();
- }
- /**
- * Set the behind view (menu) content to the given View.
- *
- * @param view The desired content to display.
- */
- public void setMenu(View v) {
- mViewBehind.setContent(v);
- }
下面分別介紹Above和Behind兩部分
-----------------------------------------------------------------------------------------
CustViewAbove類,包含主要內容的類,即顯示在SlidingMenu上面/前端的部分
由於SlidingMenu實現效果重點在於滑動,滑動前端顯示的內容然後展現出下面的選單部分,
所以重中之重自然就是這個CustViewAbove類裡的onTouchEvent裡的處理了
以下是核心方法ouTouchEvent中的分析,請先看完教程Scroll效果研究-系統ScrollView原始碼分析
SlidingMenu的CustomViewAbove和ScrollView處理沒什麼區別,看註釋就知道很多都是直接拷貝過去的
同理分按下,滑動,抬起幾部分看
1.按下
- case MotionEvent.ACTION_DOWN :
- /*
- * If being flinged and user touches, stop the fling. isFinished
- * will be false if being flinged.
- */
- completeScroll();
- // Remember where the motion event started
- int index = MotionEventCompat. getActionIndex(ev);
- mActivePointerId = MotionEventCompat. getPointerId(ev, index);
- mLastMotionX = mInitialMotionX = ev.getX();
如果還在滾動則停止~
記住開始在哪裡點下的,這裡只記錄了x軸座標,因為側滑只關注橫線移動~
mActivityPointerId是記錄多點觸碰的activity第一個點的id,總之是用來處理多點除控時拖動的穩定性
2.滑動
- case MotionEvent.ACTION_MOVE :
- if (!mIsBeingDragged) {
- determineDrag(ev);
- if ( mIsUnableToDrag)
- return false;
- }
- if (mIsBeingDragged) {
- // Scroll to follow the motion event
- final int activePointerIndex = getPointerIndex(ev, mActivePointerId);
- if ( mActivePointerId == INVALID_POINTER)
- break;
- final float x = MotionEventCompat. getX(ev, activePointerIndex);
- final float deltaX = mLastMotionX - x;
- mLastMotionX = x;
- float oldScrollX = getScrollX();
- float scrollX = oldScrollX + deltaX;
- final float leftBound = getLeftBound();
- final float rightBound = getRightBound();
- if (scrollX < leftBound) {
- scrollX = leftBound;
- } else if (scrollX > rightBound) {
- scrollX = rightBound;
- }
- // Don't lose the rounded component
- mLastMotionX += scrollX - ( int) scrollX;
- scrollTo(( int) scrollX, getScrollY());
- pageScrolled(( int) scrollX);
- }
- break;
挑重點介紹了
首先是determineDrag方法,即確定當前動作是否算是一個我們需要的拖動事件,是否要消費處理之~
方法如下
- private void determineDrag(MotionEvent ev) {
- final int activePointerId = mActivePointerId;
- final int pointerIndex = getPointerIndex(ev, activePointerId);
- if (activePointerId == INVALID_POINTER || pointerIndex == INVALID_POINTER)
- return;
- final float x = MotionEventCompat. getX(ev, pointerIndex);
- final float dx = x - mLastMotionX;
- final float xDiff = Math.abs(dx);
- final float y = MotionEventCompat. getY(ev, pointerIndex);
- final float dy = y - mLastMotionY;
- final float yDiff = Math.abs(dy);
- if (xDiff > (isMenuOpen()? mTouchSlop/2: mTouchSlop) && xDiff > yDiff && thisSlideAllowed(dx)) {
- startDrag();
- mLastMotionX = x;
- mLastMotionY = y;
- setScrollingCacheEnabled( true);
- // TODO add back in touch slop check
- } else if (xDiff > mTouchSlop) {
- mIsUnableToDrag = true;
- }
- }
最主要是地方在於if 對diff的判斷語句
其中x/yDiff是x和y軸的移動距離長度,
mTouchSlop是系統對於拖動的最低長度判斷,即至少移動多少多少距離,才算是一個拖動的動作~
所以判斷的條件就是:
當選單沒開啟時,xDiff大於mTouchSlop時才視為一個我們所需要的拖動,開啟時則只要一半即可,
並且要同時滿足xDiff>yDiff,即拖動是一個橫向大於45度的
thisSlideAllowed可以理解為是判斷是否是在選單開啟時touch到了選單部分的位置
總結起來就是
如果拖動距離達到最小閥值且大於橫向45度角度,且是touch到主體部分,則為我們需要的觸控事件
然後記錄x,y的賦值給mLastMotionX/Y
當determineDrag判斷這是一個我們需要的拖動事件了以後,就開始讓aboveView隨著動作滾動了
首先是最後一行pageScrolled((int ) scrollX);設定監聽資料的,先無視
最重要是scrollTo方法,在系統的scrollTo基礎上又加了點其他處理,如下
- @Override
- public void scrollTo(int x, int y) {
- super.scrollTo(x, y);
- mScrollX = x;
- mViewBehind.scrollBehindTo( mContent, x, y);
- ((SlidingMenu)getParent()).manageLayers(getPercentOpen());
- }
下面程式碼中manageLayers是提高效能用的,不深入研究了
super.scrollTo就是讓aboveView隨著手勢拖動滾動,這個比較簡單
此外,
使用SlidingMenu的時候可以注意到,滾動上面主體內容展示後面選單時,選單也有一個滾動展開效果
這個 mViewBehind .scrollBehindTo( mContent , x, y);就是處理這個的
這裡先知道作用,分析完above部分後再詳細分析behind部分,會介紹這個作用
然後是抬起方法,對於SlidingMenu,如果使用過肯定會有一定了解,以下是一些效果需求
如果選單開啟到一定位置,則抬起時選單會完全開啟
如果選單開啟部分太小,則抬起時選單會收回去
還要有甩的處理,隨著手勢甩開選單,甩關閉選單~
下面是程式碼部分
- case MotionEvent.ACTION_UP :
- if (mIsBeingDragged) {
- final VelocityTracker velocityTracker = mVelocityTracker;
- velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
- int initialVelocity = ( int) VelocityTrackerCompat.getXVelocity(
- velocityTracker, mActivePointerId);
- final int scrollX = getScrollX();
- final float pageOffset = ( float) (scrollX - getDestScrollX(mCurItem)) / getBehindWidth();
- final int activePointerIndex = getPointerIndex(ev, mActivePointerId);
- if ( mActivePointerId != INVALID_POINTER) {
- final float x = MotionEventCompat. getX(ev, activePointerIndex);
- final int totalDelta = ( int) (x - mInitialMotionX);
- int nextPage = determineTargetPage(pageOffset, initialVelocity, totalDelta);
- setCurrentItemInternal(nextPage, true, true, initialVelocity);
- } else {
- setCurrentItemInternal( mCurItem, true, true, initialVelocity);
- }
- mActivePointerId = INVALID_POINTER;
- endDrag();
- } else if (mQuickReturn && mViewBehind.menuTouchInQuickReturn( mContent, mCurItem, ev.getX() + mScrollX)) {
- // close the menu
- setCurrentItem(1);
- endDrag();
- }
- break;
同樣,簡單的不介紹了,參考之前的ScrollView原始碼分析教程
直接定位到SlidingMenu中特殊處理的部分
pageOffset為選單當前開啟比例,是一個0~1的值,比如開啟一半了比例就是0.5,
該值可以是正可以是負,用於表示方向,向右為正向左為負~
計算方法為
- final float pageOffset = ( float) (scrollX - getDestScrollX(mCurItem )) / getBehindWidth();
0代表左邊的選單開啟狀態,1是沒有選單開啟,2是右邊選單開啟
計算出頁pageOffset後再根據速度和移動距離最終算出目標頁位置,方法如下
- private int determineTargetPage (float pageOffset, int velocity, int deltaX) {
- int targetPage = mCurItem;
- if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) {
- if (velocity > 0 && deltaX > 0) {
- targetPage -= 1;
- } else if (velocity < 0 && deltaX < 0){
- targetPage += 1;
- }
- } else {
- targetPage = ( int) Math. round(mCurItem + pageOffset);
- }
- return targetPage;
- }
當達不到if條件時,則計算當前頁索引mCurItem與選單開啟狀態比例值pageOffset和的四捨五入值~
舉個else處理的例子
比如當前頁為1即未開啟選單狀態,此時左移了選單寬度十分之三的距離,即pageOffset=-0.3
那結果就是 1 - 0.3 = 0.7 取四捨五入就是1~ 當前頁還是1,即速度不夠時移動三分之一抬起選單還會收回去
還是剛才的條件,左移換成了十分之八的距離,即pageOffset=-0.8,
結果就是1 - 0.8 = 0.2 四捨五入就是0~ 即當前頁應該是0左選單了
以上簡單總結就是,速度距離足夠後,根據速度控制左右滑動來顯示選單或者內容
如果速度不夠,則移動超過選單一半距離時就切換選單開啟關閉狀態,不然就收回去
注意,determineTargetPage方法只是獲取目標頁位置,實際跳轉工作是下面的方法
- void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
- if (!always && mCurItem == item) {
- setScrollingCacheEnabled( false);
- return;
- }
- item = mViewBehind.getMenuPage(item);
- final boolean dispatchSelected = mCurItem != item;
- mCurItem = item;
- final int destX = getDestScrollX(mCurItem );
- if (dispatchSelected && mOnPageChangeListener != null) {
- mOnPageChangeListener.onPageSelected(item);
- }
- if (dispatchSelected && mInternalPageChangeListener != null ) {
- mInternalPageChangeListener.onPageSelected(item);
- }
- if (smoothScroll) {
- smoothScrollTo(destX, 0, velocity);
- } else {
- completeScroll();
- scrollTo(destX, 0);
- }
- }
監聽設定無視,核心方法是smoothScrollTo方法,如下
- /**
- * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
- *
- * @param x the number of pixels to scroll by on the X axis
- * @param y the number of pixels to scroll by on the Y axis
- * @param velocity the velocity associated with a fling, if applicable. (0 otherwise)
- */
- void smoothScrollTo(int x, int y, int velocity) {
- if (getChildCount() == 0) {
- // Nothing to do.
- setScrollingCacheEnabled( false);
- return;
- }
- int sx = getScrollX();
- int sy = getScrollY();
- int dx = x - sx;
- int dy = y - sy;
- if (dx == 0 && dy == 0) {
- completeScroll();
- if (isMenuOpen()) {
- if ( mOpenedListener != null)
- mOpenedListener.onOpened();
- } else {
- if ( mClosedListener != null)
- mClosedListener.onClosed();
- }
- return;
- }
- setScrollingCacheEnabled( true);
- mScrolling = true;
- final int width = getBehindWidth();
- final int halfWidth = width / 2;
- final float distanceRatio = Math. min(1f, 1.0f * Math.abs(dx) / width);
- final float distance = halfWidth + halfWidth *
- distanceInfluenceForSnapDuration(distanceRatio);
- int duration = 0;
- velocity = Math.abs(velocity);
- if (velocity > 0) {
- duration = 4 * Math. round(1000 * Math.abs (distance / velocity));
- } else {
- final float pageDelta = ( float) Math. abs(dx) / width;
- duration = ( int) ((pageDelta + 1) * 100);
- duration = MAX_SETTLE_DURATION;
- }
- duration = Math.min(duration, MAX_SETTLE_DURATION);
- mScroller.startScroll(sx, sy, dx, dy, duration);
- invalidate();
- }
最後發現內部實現最終還是Scroller類的startScroll方法~讓它繼續完成開啟/關閉選單的 剩下動畫
至於duration滑動持續時間的計算演算法就不研究了~Scroller用法也不介紹了,之前ScrollView教程裡有
------------------------------------------------------------------------------------------------------
以上部分其實看懂ScrollView原理以後看完全沒壓力理解,但還有一個沒介紹的知識點,touch事件分發問題~
問題場景:
在選單開啟的時候要進行判斷,
如果點選在主體部分,則自動收回選單,即讓主體部分消費這個touch事件,
如果此時點選在選單部分,那above中應該不消費此事件,要將其傳給behind部分,
然後讓behind中的listview或者button等自定義設定的控制元件去獲取處理touch事件~
事件分發可以參考
http://www.eoeandroid.com/thread-277371-1-1.html
此外還有選單狀態開啟時點主體部分則直接關閉選單,或者back鍵直接關閉選單等,原理上面都介紹了,
這些邏輯方面的處理還有其他一些優化部分就不介紹了~
------------------------------------------------------------------------------------------------------
選單部分CustomViewBehind中內容不多,主要有兩大塊內容
1.滾動
研究主體內容部分的時候已經提到過,當前端內容部分拖動顯示/關閉選單時,選單也有一個隨之滾動的效果~
2.效果繪製
包括陰影,淡入淡出效果等
繪製部分比較複雜需要以後有時間專門介紹,先介紹滾動相關的部分即1
相關方法如下,該方法在CustomViewAbove的scrollTo中呼叫
- public void scrollBehindTo(View content, int x, int y) {
- int vis = View. VISIBLE;
- if (mMode == SlidingMenu. LEFT) {
- if (x >= content.getLeft()) vis = View. INVISIBLE;
- scrollTo(( int)((x + getBehindWidth())* mScrollScale), y);
- } else if (mMode == SlidingMenu. RIGHT) {
- if (x <= content.getLeft()) vis = View. INVISIBLE;
- scrollTo(( int)(getBehindWidth() - getWidth() +
- (x-getBehindWidth())* mScrollScale), y);
- } else if (mMode == SlidingMenu. LEFT_RIGHT) {
- mContent.setVisibility(x >= content.getLeft() ? View.INVISIBLE : View.VISIBLE );
- mSecondaryContent.setVisibility(x <= content.getLeft() ? View.INVISIBLE : View.VISIBLE );
- vis = x == 0 ? View. INVISIBLE : View. VISIBLE;
- if (x <= content.getLeft()) {
- scrollTo(( int)((x + getBehindWidth())* mScrollScale), y);
- } else {
- scrollTo(( int)(getBehindWidth() - getWidth() +
- (x-getBehindWidth())* mScrollScale), y);
- }
- }
- if (vis == View. INVISIBLE)
- Log. v(TAG, "behind INVISIBLE" );
- setVisibility(vis);
- }
只分析LEFT的情況(請他情況同理)
比如選單的width即getBehindWidth的寬度為300
那從選單關閉狀態一直滾到選單完全開啟狀態,above主體內容x軸上的scroll變化就是0 ~ -300
此時如果mScrollScale即滾動比例為0.3
那按照上面scrollBehindTo中的演算法, behind選單部分x軸上的scroll變化就是
(0+300)*0.3 ~ (-300+300)*0.3即100~0
如果比例換成0.6,那behind的x變化就是180~0
極端情況下
1.mScrollScale=0, behind的x變化為 0~0, 此時會發現behind選單部分在開啟關閉的過程中無任何滾動
2.mScrollScale=1,behind的換標為300~0, 此時的效果就是主體和選單緊挨著以同一個速度滾動
注意,這裡的x為滾動的偏移量,即scrollTo使用的引數,
比如scrollView中,scrollTo(0, 100)會往下滾動到y偏移量100的位置,
此時scrollView裡面的內容視覺上看其實是向上移了~
這裡同理
雖然朝右拖動的時候view看上去是朝右運動,但x軸上滾動偏移量是減少的
------------------------------------------------------------------------------------------------------
最後是demo,自定義底部滑出選單控制元件
根據SlidingMenu修改的,刪除了SlidingMenu中大量程式碼,只保留了最核心的部分方便理解~
缺陷
限定死了behind選單部分的高度和底部運動的比例值等在SlidingMenu中是可以設定的資訊
老樣子,回覆可以免積分
遊客,如果您要檢視本帖隱藏內容請回復
相關文章
- SlidingMenu原始碼分析原始碼
- MyBatis原始碼分析(二)MyBatis原始碼
- 原始碼分析二:LeakCanary原始碼
- Backbone原始碼分析(二)原始碼
- YYCache 原始碼分析(二)原始碼
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- redis原始碼分析(二)、redis原始碼分析之sds字串Redis原始碼字串
- LinkedList原始碼分析(二)原始碼
- Volley原始碼分析(二)原始碼
- Retrofit原始碼分析二 代理模式原始碼模式
- Tinker接入及原始碼分析(二)原始碼
- Spring原始碼系列(二)--bean元件的原始碼分析Spring原始碼Bean元件
- Vue原始碼分析系列二:$mount()方法Vue原始碼
- 窺探React-原始碼分析(二)React原始碼
- Zookeeper原始碼分析(二) —– zookeeper日誌原始碼
- Netty原始碼分析--Reactor模型(二)Netty原始碼React模型
- Kafka原始碼分析(二) - 生產者Kafka原始碼
- Spring原始碼分析之IoC(二)Spring原始碼
- Kubernetes Deployment 原始碼分析(二)原始碼
- Zookeeper原始碼分析(二) ----- zookeeper日誌原始碼
- Android Loader原始碼分析(二)Android原始碼
- MPTCP 原始碼分析(二) 建立子路徑TCP原始碼
- JUnit原始碼分析(二)——觀察者模式原始碼模式
- Go 互斥鎖 Mutex 原始碼分析(二)GoMutex原始碼
- 5.2 Spring5原始碼--Spring AOP原始碼分析二Spring原始碼
- Android 原始碼分析(二)handler 機制Android原始碼
- OkHttp 原始碼分析(二)—— 快取機制HTTP原始碼快取
- Java併發之AQS原始碼分析(二)JavaAQS原始碼
- gson-plugin基礎原始碼分析(二)Plugin原始碼
- OkHttpClient原始碼分析(二) —— RetryAndFollowUpInterceptor和BridgeInterceptorHTTPclient原始碼
- RecyclerView 原始碼分析(二) —— 快取機制View原始碼快取
- TMCache原始碼分析(二)---TMDiskCache磁碟快取原始碼快取
- ecshop 二次開發,原始碼分析原始碼
- Apache DolphinScheduler-1.3.9原始碼分析(二)Apache原始碼
- RxJava2原始碼分析(二):操作符原理分析RxJava原始碼
- SpringBoot2.0原始碼分析(二):整合ActiveMQ分析Spring Boot原始碼MQ
- Retrofit原始碼分析三 原始碼分析原始碼
- netty原始碼分析之pipeline(二)Netty原始碼