默認
打賞 發表評論 3
想開發IM:買成品怕坑?租第3方怕貴?找開源自已擼?盡量別走彎路了... 找站長給點建議
高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]
閱讀(15074) | 評論(3 收藏 淘帖2

前言


手機QQ首頁上的“消息”界面上,仿iOS的側滑菜單體驗很好,交互效果見下圖:

高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]_wechat136.jpg

本文分享的源碼高仿了手機QQ的這個效果,希望可以為有相同需求的IM開發者同行節省點擼碼時間。

高仿效果截圖


高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]_587163-da620814bd1017ec.gif

整體思路


自定義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">

    <LinearLayout
        android:id="@+id/ll_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <TextView
            android: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" />

        <TextView
            android: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>

    <LinearLayout
        android:id="@+id/ll_right_menu"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <TextView
            android: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" />

        <TextView
            android: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" />

        <TextView
            android: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">

    <include
        android:id="@+id/toolbar"
        layout="@layout/m_toolbar" />

    <org.ninetripods.mq.study.recycle.swipe_menu.SwipeRecycleView
        android: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>

@Override
 protected 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 positions
        int 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值得到子View
            View 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 {
                //根據點擊位置獲得相應的子View
                ViewHolder 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中去。
@Override
 public 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的菜單是打開狀態,則先關閉它。

踩過的坑


說起踩坑尼瑪真是一把鼻涕一把淚,因為水平有限遇到了很多坑,當時要不是趕緊看了一下銀行卡的余額不足,我差一點就把電腦砸了去買新的了~當時的心情是下面這樣的:
高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]_587163-dc6f96edbee65679.gif

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中去判斷:
@Override
 public 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不會上下滑動了。

Demo下載安裝


用手機掃描下面的二維碼安裝:
高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]_aaa.png

或者下載APK自行安裝:
android_market_yingyongbao_201707161729_v1.1_release.apk (2.22 MB , 下載次數: 0 )

源碼附件下載


高仿Android版手機QQ首頁側滑菜單源碼.zip (10.08 MB , 下載次數: 14 , 售價: 3 金幣)

(本源碼來自簡書博客_小馬快跑_,感謝原作者分享)

附錄:全站精品資源下載


[1] 精品源碼下載:
輕量級即時通訊框架MobileIMSDK的iOS源碼(開源版)[附件下載]
開源IM工程“蘑菇街TeamTalk”2015年5月前未刪減版完整代碼 [附件下載]
微信本地數據庫破解版(含iOS、Android),僅供學習研究 [附件下載]
NIO框架入門(四):Android與MINA2、Netty4的跨平臺UDP雙向通信實戰 [附件下載]
NIO框架入門(三):iOS與MINA2、Netty4的跨平臺UDP雙向通信實戰 [附件下載]
NIO框架入門(二):服務端基于MINA2的UDP雙向通信Demo演示 [附件下載]
NIO框架入門(一):服務端基于Netty4的UDP雙向通信Demo演示 [附件下載]
用于IM中圖片壓縮的Android工具類源碼,效果可媲美微信 [附件下載]
高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]
一個WebSocket實時聊天室Demo:基于node.js+socket.io [附件下載]
Android聊天界面源碼:實現了聊天氣泡、表情圖標(可翻頁) [附件下載]
高仿Android版手機QQ首頁側滑菜單源碼 [附件下載]
開源libco庫:單機千萬連接、支撐微信8億用戶的后臺框架基石 [源碼下載]
分享java AMR音頻文件合并源碼,全網最全
微信團隊原創Android資源混淆工具:AndResGuard [有源碼]
一個基于MQTT通信協議的完整Android推送Demo [附件下載]
Android版高仿微信聊天界面源碼 [附件下載]
仿微信的IM聊天時間顯示格式(含iOS/Android/Web實現)[圖文+源碼]

[2] 精品文檔和工具下載:
計算機網絡通訊協議關系圖(中文珍藏版)[附件下載]
史上最全即時通訊軟件簡史(精編大圖版)[附件下載]
基于RTMP協議的流媒體技術的原理與應用(技術論文)[附件下載]
獨家發布《TCP/IP詳解 卷1:協議》CHM版 [附件下載]
良心分享:WebRTC 零基礎開發者教程(中文)[附件下載]
MQTT協議手冊(中文翻譯版)[附件下載]
經典書籍《UNIX網絡編程》最全下載(卷1+卷2、中文版+英文版)[附件下載]
音視頻開發理論入門書籍之《視頻技術手冊(第5版)》[附件下載]
國際電聯H.264視頻編碼標準官方技術手冊(中文版)[附件下載]
Apache MINA2.0 開發指南(中文版)[附件下載]
網絡通訊數據抓包和分析工具 Wireshark 使用教程(中文) [附件下載]
最新收集NAT穿越(p2p打洞)免費STUN服務器列表 [附件下載]
高性能網絡編程經典:《The C10K problem(英文)》[附件下載]
即時通訊系統的原理、技術和應用(技術論文)[附件下載]
技術論文:微信對網絡影響的技術試驗及分析[附件下載]
華為內部3G網絡資料: WCDMA系統原理培訓手冊[附件下載]
網絡測試:Android版多路ping命令工具EnterprisePing[附件下載]
Android反編譯利器APKDB:沒有美工的日子里繼續堅強的擼
一款用于P2P開發的NAT類型檢測工具 [附件下載]
兩款增強型Ping工具:持續統計、圖形化展式網絡狀況 [附件下載]

[3] 精選視頻、演講PPT下載:
QQ空間移動端10億級視頻播放技術優化揭秘(視頻+PPT)[附件下載]
RTC實時互聯網2017年度大會精選演講PPT [附件下載]
微信分享開源IM網絡層組件庫Mars的技術實現(視頻+PPT)[附件下載]
微服務理念在微信海量用戶后臺架構中的實踐(視頻+PPT)[附件下載]
移動端IM開發和構建中的技術難點實踐分享(視頻+PPT)[附件下載]
網易云信的高品質即時通訊技術實踐之路(視頻+PPT)[附件下載]
騰訊音視頻實驗室:直面音視頻質量評估之痛(視頻+PPT)[附件下載]
騰訊QQ1.4億在線用戶的技術挑戰和架構演進之路PPT[附件下載]
微信朋友圈海量技術之道PPT[附件下載]
手機淘寶消息推送系統的架構與實踐(音頻+PPT)[附件下載]
如何進行實時音視頻的質量評估與監控(視頻+PPT)[附件下載]
Go語言構建高并發消息推送系統實踐PPT(來自360公司)[附件下載]
網易IM云千萬級并發消息處理能力的架構設計與實踐PPT [附件下載]
手機QQ的海量用戶移動化實踐分享(視頻+PPT)[附件下載]
釘釘——基于IM技術的新一代企業OA平臺的技術挑戰(視頻+PPT)[附件下載]
微信技術總監談架構:微信之道——大道至簡(PPT講稿)[附件下載]
Netty的架構剖析及應用案例介紹(視頻+PPT)[附件下載]
聲網架構師談實時音視頻云的實現難點(視頻采訪)
滴滴打車架構演變及應用實踐(PPT講稿)[附件下載]
微信海量用戶背后的后臺系統存儲架構(視頻+PPT)[附件下載]
在線音視頻直播室服務端架構最佳實踐(視頻+PPT)[附件下載]
從0到1:萬人在線的實時音視頻直播技術實踐分享(視頻+PPT)[附件下載]
微信移動端應對弱網絡情況的探索和實踐PPT[附件下載]
Android版微信從300KB到30MB的技術演進(PPT講稿)[附件下載]

即時通訊網 - 即時通訊開發者社區! 來源: - 即時通訊開發者社區!

標簽:聊天界面
上一篇:高仿Android版手機QQ可拖拽未讀數小氣泡源碼 [附件下載]下一篇:騰訊音視頻實驗室:直面音視頻質量評估之痛(視頻+PPT)[附件下載]

本帖已收錄至以下技術專輯

推薦方案
評論 3
我是來賺金幣的,能行不
牛逼牛逼
好資源,不要錯過。
簽名: 1234
打賞樓主 ×
使用微信打賞! 使用支付寶打賞!

返回頂部
曾氏料二肖中特