Android仿QQ侧滑菜单
先上效果圖:
GIF圖有點模糊,源碼已上傳Github:Android仿QQ側滑菜單
####整體思路:
自定義ItemView的根布局(SwipeMenuLayout extends LinearLayout),復寫onTouchEvent來處理滑動事件,注意這里的滑動是View里面內容的滑動而不是View的滑動,View里內容的滑動主要是通過scrollTo、scrollBy來實現,然后自定義SwipeRecycleView,復寫其中的onInterceptTouchEvent和onTouchEvent來處理滑動沖突。
####實現過程:
先來看每個ItemView的布局文件:
<?xml version="1.0" encoding="utf-8"?> <org.ninetripods.mq.study.recycle.swipe_menu.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/swipe_menu"android:layout_width="match_parent"android:layout_height="70dp"android:layout_centerInParent="true"android:background="@color/white"android:orientation="horizontal"app:content_id="@+id/ll_layout"app:right_id="@+id/ll_right_menu"><LinearLayoutandroid:id="@+id/ll_layout"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_content"android:layout_width="wrap_content"android:layout_height="match_parent"android:layout_marginLeft="20dp"android:gravity="center_vertical"android:text="HelloWorld"android:textSize="16sp" /><TextViewandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="right"android:layout_marginLeft="20dp"android:layout_marginRight="20dp"android:gravity="center_vertical|end"android:text="左滑←←←"android:textSize="16sp" /></LinearLayout><LinearLayoutandroid:id="@+id/ll_right_menu"android:layout_width="wrap_content"android:layout_height="match_parent"android:orientation="horizontal"><TextViewandroid:id="@+id/tv_to_top"android:layout_width="90dp"android:layout_height="match_parent"android:background="@color/gray_holo_light"android:gravity="center"android:text="置頂"android:textColor="@color/white"android:textSize="16sp" /><TextViewandroid:id="@+id/tv_to_unread"android:layout_width="90dp"android:layout_height="match_parent"android:background="@color/yellow"android:gravity="center"android:text="標為未讀"android:textColor="@color/white"android:textSize="16sp" /><TextViewandroid:id="@+id/tv_to_delete"android:layout_width="90dp"android:layout_height="match_parent"android:background="@color/red_f"android:gravity="center"android:text="刪除"android:textColor="@color/white"android:textSize="16sp" /></LinearLayout> </org.ninetripods.mq.study.recycle.swipe_menu.SwipeMenuLayout> 復制代碼android:id="@+id/ll_layout" 的LinearLayout寬度設置的match_parent,所以右邊的三個菜單按鈕默認我們是看不到的,根布局是SwipeMenuLayout,是個自定義ViewGroup,主要的滑動事件也是在這里面完成的。
RecycleView的布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"><includeandroid:id="@+id/toolbar"layout="@layout/m_toolbar" /><org.ninetripods.mq.study.recycle.swipe_menu.SwipeRecycleViewandroid:id="@+id/swipe_recycleview"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_below="@id/toolbar" /> </RelativeLayout> 復制代碼我們用到的SwipeRecycleView也是自定義RecycleView,主要是處理一些和SwipeMenuLayout的滑動沖突。
######先分析SwipeMenuLayout代碼:
public static final int STATE_CLOSED = 0;//關閉狀態 public static final int STATE_OPEN = 1;//打開狀態 public static final int STATE_MOVING_LEFT = 2;//左滑將要打開狀態 public static final int STATE_MOVING_RIGHT = 3;//右滑將要關閉狀態 復制代碼首先定義了SwipeMenuLayout的四種狀態: STATE_CLOSED 關閉狀態 STATE_OPEN 打開狀態 STATE_MOVING_LEFT 左滑將要打開狀態 STATE_MOVING_RIGHT 右滑將要關閉狀態
接著通過自定義屬性來獲得右側菜單根布局的id,然后通過findViewById()來得到根布局的View,進而獲得其寬度值。
//獲取右邊菜單id TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SwipeMenuLayout); mRightId = typedArray.getResourceId(R.styleable.SwipeMenuLayout_right_id, 0); typedArray.recycle(); 復制代碼相應的attr.xml文件:
<declare-styleable name="SwipeMenuLayout"><!-- format="reference"意為參考某一資源ID --><attr name="content_id" format="reference" /><attr name="right_id" format="reference" /></declare-styleable> 復制代碼@Overrideprotected void onFinishInflate() {super.onFinishInflate();if (mRightId != 0) {rightMenuView = findViewById(mRightId);}} 復制代碼接著來看onTouchEvent,先看ACTION_DOWN事件和ACTION_MOVE事件:
@Override public boolean onTouchEvent(MotionEvent event) {switch (event.getAction()) {case MotionEvent.ACTION_DOWN:mDownX = (int) event.getX();mDownY = (int) event.getY();mLastX = (int) event.getX();break;case MotionEvent.ACTION_MOVE:int dx = (int) (mDownX - event.getX());int dy = (int) (mDownY - event.getY());//如果Y軸偏移量大于X軸偏移量 不再滑動if (Math.abs(dy) > Math.abs(dx)) return false;int deltaX = (int) (mLastX - event.getX());if (deltaX > 0) {//向左滑動currentState = STATE_MOVING_LEFT;if (deltaX >= menuWidth || getScrollX() + deltaX >= menuWidth) {//右邊緣檢測scrollTo(menuWidth, 0);currentState = STATE_OPEN;break;}} else if (deltaX < 0) {//向右滑動currentState = STATE_MOVING_RIGHT;if (deltaX + getScrollX() <= 0) {//左邊緣檢測scrollTo(0, 0);currentState = STATE_CLOSED;break;}}scrollBy(deltaX, 0);mLastX = (int) event.getX();break;}return super.onTouchEvent(event); } 復制代碼在ACTION_MOVE事件中通過點擊所在坐標和上一次滑動記錄的坐標之差來判斷左右滑動,并進行左邊緣和右邊緣檢測,如果還未到左右內容的邊界,則通過scrollBy來實現滑動。 接著看ACTION_UP和ACTION_CANCEL事件:
case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:if (currentState == STATE_MOVING_LEFT) {//左滑打開mScroller.startScroll(getScrollX(), 0, menuWidth - getScrollX(), 0, 300);invalidate();} else if (currentState == STATE_MOVING_RIGHT || currentState == STATE_OPEN) {//右滑關閉smoothToCloseMenu();}//如果小于滑動距離并且菜單是關閉狀態 此時Item可以有點擊事件int deltx = (int) (mDownX - event.getX());return !(Math.abs(deltx) < mScaledTouchSlop && isMenuClosed()) || super.onTouchEvent(event);}return super.onTouchEvent(event); 復制代碼這里主要是當松開手時執行ACTION_UP事件,如果不處理,則會變成菜單顯示一部分然后卡在那里了,這當然是不行的,這里通過OverScroller.startScroll()來實現慣性滑動,然而當我們調用startScroll()之后還是不會實現慣性滑動的,這里還需要調用invalidate()去重繪,重繪時會執行computeScroll()方法:
@Override public void computeScroll() {if (mScroller.computeScrollOffset()) {// Get current x and y positionsint currX = mScroller.getCurrX();int currY = mScroller.getCurrY();scrollTo(currX, currY);postInvalidate();}if (isMenuOpen()) {currentState = STATE_OPEN;} else if (isMenuClosed()) {currentState = STATE_CLOSED;} } 復制代碼在computeScroll()方法中,我們通過Scroller.getCurrX()和scrollTo()來滑動到指定坐標位置,然后調用postInvalidate()又去重繪,不斷循環,直到滑動到邊界為止。
######再分析下SwipeRecycleView:
SwipeRecycleView是SwipeMenuLayout的父View,事件分發時,先到達的SwipeRecycleView,
@Override public boolean onInterceptTouchEvent(MotionEvent event) {boolean isIntercepted = super.onInterceptTouchEvent(event);switch (event.getAction()) {case MotionEvent.ACTION_DOWN:mLastX = (int) event.getX();mLastY = (int) event.getY();mDownX = (int) event.getX();mDownY = (int) event.getY();isIntercepted = false;//根據MotionEvent的X Y值得到子ViewView view = findChildViewUnder(mLastX, mLastY);if (view == null) return false;//點擊的子View所在的位置final int touchPos = getChildAdapterPosition(view);if (touchPos != mLastTouchPosition && mLastMenuLayout != null&& mLastMenuLayout.currentState != SwipeMenuLayout.STATE_CLOSED) {if (mLastMenuLayout.isMenuOpen()) {//如果之前的菜單欄處于打開狀態,則關閉它mLastMenuLayout.smoothToCloseMenu();}isIntercepted = true;} else {//根據點擊位置獲得相應的子ViewViewHolder holder = findViewHolderForAdapterPosition(touchPos);if (holder != null) {View childView = holder.itemView;if (childView != null && childView instanceof SwipeMenuLayout) {mLastMenuLayout = (SwipeMenuLayout) childView;mLastTouchPosition = touchPos;}}}break;case MotionEvent.ACTION_MOVE:case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:int dx = (int) (mDownX - event.getX());int dy = (int) (mDownY - event.getY());if (Math.abs(dx) > mScaleTouchSlop && Math.abs(dx) > Math.abs(dy)|| (mLastMenuLayout != null && mLastMenuLayout.currentState != SwipeMenuLayout.STATE_CLOSED)) {//如果X軸偏移量大于Y軸偏移量 或者上一個打開的菜單還沒有關閉 則禁止RecycleView滑動 RecycleView不去攔截事件return false;}break;}return isIntercepted; } 復制代碼通過findChildViewUnder()找到ItemView,進而通過getChildAdapterPosition(view)來獲得點擊位置,如果是第一次點擊,則會通過findViewHolderForAdapterPosition()找到對應的ViewHolder 并獲得子View;如果不是第一次點擊,和上次點擊不是同一個item并且前一個ItemView的菜單處于打開狀態,那么此時調用smoothToCloseMenu()關閉菜單。在ACTION_MOVE、ACTION_UP、ACTION_CANCEL事件中,如果X軸偏移量大于Y軸偏移量 或者上一個打開的菜單還沒有關閉 則禁止SwipeRecycleView滑動,SwipeRecycleView不去攔截事件,相應的將事件傳到SwipeMenuLayout中去。
@Overridepublic boolean onTouchEvent(MotionEvent e) {switch (e.getAction()) {case MotionEvent.ACTION_DOWN://若某個Item的菜單還沒有關閉,則RecycleView不能滑動if (!mLastMenuLayout.isMenuClosed()) {return false;}break;case MotionEvent.ACTION_MOVE:case MotionEvent.ACTION_UP:if (mLastMenuLayout != null && mLastMenuLayout.isMenuOpen()) {mLastMenuLayout.smoothToCloseMenu();}break;}return super.onTouchEvent(e);} 復制代碼在onTouchEvent的ACTION_DOWN事件中,如果某個Item的菜單還沒有關閉,則SwipeRecycleView不能滑動,在ACTION_MOVE、ACTION_UP事件中,如果前一個ItemView的菜單是打開狀態,則先關閉它。
####踩過的坑:
說起踩坑尼瑪真是一把鼻涕一把淚,因為水平有限遇到了很多坑,當時要不是趕緊看了一下銀行卡的余額不足,我差一點就把電腦砸了去買新的了~當時的心情是下面這樣的:
1、當在某個ItemView (SwipeMenuLayout) 保持按下操作,然后手勢從SwipeMenuLayout控件內部轉移到外部,然后菜單滑到一半就卡在那里了,在那里卡住了~那里卡住了~卡住了~住了~了~,當時有點不知所措,后來通過Debug發現SwipeMenuLayout的ACTION_UP已經不會執行了,想想也是,你都滑動外面了,人家憑啥還執行ACTION_UP方法,后來通過google發現SwipeMenuLayout不執行ACTION_UP但是會執行ACTION_CANCEL,ACTION_CANCEL是當前滑動手勢被打斷時調用,比如在某個控件保持按下操作,然后手勢從控件內部轉移到外部,此時控件手勢事件被打斷,會觸發ACTION_CANCEL,解決方法也就出來了,即ACTION_UP和ACTION_CANCEL都根據判斷條件去執行慣性滑動的邏輯。
2、假如某個ItemView (SwipeMenuLayout) 的右側菜單欄處于打開狀態,此時去上下滑動SwipeRecycleView,發現菜單欄關閉了,但同時SwipeRecycleView也跟著上下滑動了,這里的解決方法是在SwipeRecycleView的onTouchEvent中去判斷:
@Overridepublic boolean onTouchEvent(MotionEvent e) {switch (e.getAction()) {case MotionEvent.ACTION_DOWN://若某個Item的菜單還沒有關閉,則RecycleView不能滑動if (!mLastMenuLayout.isMenuClosed()) {return false;}................省略其他..................}return super.onTouchEvent(e);} 復制代碼通過判斷,若某個Item的菜單還沒有關閉,直接返回false,那么SwipeRecycleView就不會再消費此次事件,即SwipeRecycleView不會上下滑動了。
####后記: 本文主要運用的是View滑動的相關知識,如scrollTo、scrollBy、OverScroller等,水平有限,如果發現文章有誤,還請不吝賜教,不勝感激~最后再貼下源碼地址: Android仿QQ側滑菜單,如果對您有幫助,給個star吧,感謝老鐵~
下一篇:Android高仿QQ小紅點
轉載于:https://juejin.im/post/5a33e7cbf265da43310de175
總結
以上是生活随笔為你收集整理的Android仿QQ侧滑菜单的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络测试与分析工具简介
- 下一篇: 微信、陌陌等著名IM软件设计架构详解【转