默认
打赏 发表评论 3
想开发IM:买成品怕坑?租第3方怕贵?找开源自已撸?尽量别走弯路了... 找站长给点建议
高仿Android版手机QQ首页侧滑菜单源码 [附件下载]
阅读(74047) | 评论(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 , 下载次数: 6 )

源码附件下载


高仿Android版手机QQ首页侧滑菜单源码.zip (10.08 MB , 下载次数: 38 , 售价: 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
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

返回顶部