仿QQ对话列表滑动删除与置顶的原理及实现(一)

简介: 仿QQ对话列表滑动删除与置顶的原理及实现(一)

接下来,我们将完成QQ聊天界面的ListView滑动效果,大家可能都用过ListView,知道ListView是上下滑动的,并不会产生左右滑动的效果,如果想让ListView变成左右滑动的效果,必须对安卓源代码有所了解,如果你想了解源代码,请到http://blog.csdn.net/column/details/core-services.html 该专栏下了解详情。


我的思路就是:


所有的屏幕操作事件由ListView作做拦截,同时把事件传递给SlideView做滑动,这种实现的确可以达到效果,而且代码很简单,根本不需要处理什么复杂的滑动冲突。


QQ效果图与自己实现的效果图对比:


image.png

             


1.思路流程


首先我们需要实现自己的ListView来处理截获屏幕的事件,但不是由ListView处理,而是转发给自定义item控件处理,也就是实现的SlideView控件。根据处理手势设置item的状态,也就是说当已经滑动了,这个时候如果不获取item的状态,下次在滑动这个item的时候是不知道这个控件已经滑动了,不然就会有二次滑动,所以必须保存滑动状态。


下面用箭头详细说明今天代码流程:


当手指滑动某个item的时候——>ListView截获滑动事件——>在自定义ListView中实现onTouchEvent()方法,判断当前是哪个item——>然后将事件派发给item自己的SlideView去处理滑动——>处理完成后设置当前item的状态(防止二次滑动,也防止其他item滑动后,该item没有恢复到原来的模样)


2.定义Item容器


代码如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/view_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">
    </LinearLayout>
    <LinearLayout
        android:id="@+id/holder"
        android:layout_width="120dp"
        android:layout_height="match_parent"
        android:background="#C5C1AA"
        android:orientation="horizontal">
        <TextView
            android:id="@+id/top"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:gravity="center"
            android:background="#FFD700"
            android:text="@string/slide_holder_top"
            android:textSize="20sp"
            android:layout_weight="1"/>
        <TextView
            android:id="@+id/delete"
            android:layout_width="wrap_content"
            android:layout_height="50dp"
            android:gravity="center"
            android:text="@string/slide_holder_delete"
            android:textSize="20sp"
            android:layout_weight="1"/>
    </LinearLayout>
</merge>


这里需要说明的就是,merge节点在加载的时候会忽略掉视图的层级,也就是说加载是时候会自动忽略掉merge节点,直接加载内部的内容,这里就是两个LinearLayout了。



这里为什么要用这个,因为等会的SlideView会继承LinearLayout,如果用LinearLayout包裹这两个LinearLayout,就会多出一个本身没用LinearLayout,除了增加视力层级,没有任何效果。



第一个LinearLayout宽高都设置为match_parent,目的就是将后一个LinearLayout挤出屏幕之外。好达到有滑动出控件的效果。


3.实现SlideView


我们SlideView会继承自LinearLayout,当然你也可以继承其他的布局,只是LinearLayout考虑的相对简单点。


㈠定义item状态的接口

public interface OnSlideViewOnListener {
    //滑动的三个状态
    public static final int SLIDE_STATUS_OFF = 0;
    public static final int SLIDE_STATUS_START_SCROLL = 1;
    public static final int SLIDE_STATUS_ON = 2;
    /**
     * 更新滑动的状态
     *
     * @param view
     * @param status
     */
    public void Slide(View view, int status);
}

这个就不用多作解释了,一目了然。


㈡初始化SlideView

public void initView() {
    //获取上下文
    this.mContext = getContext();
    //初始化滑动对象
    this.mScroller = new Scroller(this.mContext);
    //设置该LinearLayout为横向坐标
    setOrientation(LinearLayout.HORIZONTAL);
    //加载布局
    View.inflate(this.mContext,R.layout.slide_holder,this);
    //获取容器
    this.mLinearLayout = (LinearLayout) findViewById(R.id.view_content);
    //将隐藏容器的宽度根据屏幕调节
    this.mHolderWidth = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.mHolderWidth, getResources().getDisplayMetrics()));
}


㈢处理ListView发送过来的事件

public void onRequiredToEvent(MotionEvent event) {
    //获取当前X,Y坐标
    int x = (int) event.getX();
    int y = (int) event.getY();
    Log.d(TAG, "x=" + x + " ,y=" + y);
    //getScrollX()获取的值则发生了变化:指调用的控件的水平移动的距离,当未移动的时候,获取的值为0. 当向右移动20,则获取值为 -20,再向右移动10,则获取-30
    int scrollX = getScrollX();
    //根据按键的方式处理相应的事件
    switch (event.getAction()) {
        //按下时候处理
        case MotionEvent.ACTION_DOWN: {
            if (!this.mScroller.isFinished()) {
                this.mScroller.abortAnimation();
            }
            if (this.mOnSlideViewOnListener != null) {
                this.mOnSlideViewOnListener.Slide(this, OnSlideViewOnListener.SLIDE_STATUS_START_SCROLL);
            }
            break;
        }
        //移动时候处理
        case MotionEvent.ACTION_MOVE: {
            //x,y各滑动了多少距离
            int deltaX = x - this.mLastX;
            int deltaY = y - this.mLastY;
            Log.d(TAG, "deltaX=" + deltaX + " ,deltaY=" + deltaY);
            if (Math.abs(deltaX) < Math.abs(deltaY) * 2) {
                //角度大于60度,不符合滑动的条件
                break;
            }
            int newScrollX = scrollX - deltaX;
            if (deltaX != 0) {
                if (newScrollX < 0) {
                    newScrollX = 0;
                } else if (newScrollX > this.mHolderWidth) {
                    newScrollX = this.mHolderWidth;
                }
                this.scrollTo(newScrollX, 0);
            }
            break;
        }
        //抬起时候处理
        case MotionEvent.ACTION_UP: {
            int newScrollX = 0;
            if (scrollX - this.mHolderWidth * 0.75 > 0) {
                newScrollX = this.mHolderWidth;
            }
            this.smoothScrollTo(newScrollX, 0);
            if (this.mOnSlideViewOnListener != null) {
                this.mOnSlideViewOnListener.Slide(this, newScrollX == 0 ? OnSlideViewOnListener.SLIDE_STATUS_OFF : OnSlideViewOnListener.SLIDE_STATUS_ON);
            }
            break;
        }
        default:
            break;
    }
    //将历史坐标更新
    this.mLastX = x;
    this.mLastY = y;
}


在开始的时候定义了一个Scroller对象,该对象是弹性滑动对象,滑动效果由他实现。


Math.abs(deltaX) < Math.abs(deltaY) * 2的解释:


deltaX为滑动的坐标变量,X向左坐标越小,X向右坐标越大,Y向下坐标越大,Y向上坐标越小,所以有可能相对于上一个坐标变量相减会是负值,所以用Math.abs()取他们的绝对值,因为我们只要保证横向滑动的时候,角度在60度以内就算是横向滑动,大于60度,那就算纵向滑动所以不处理滑动。而tan(63)约等于2,故对边除邻边要<2,所以把变更Y移过去就得到这个值。


当然图像更容易理解,如下所示:


21.png


大的直线箭头为X,Y轴,120度的箭头为滑动方向,我们在该图中为左,在矩形ListView的Item这个120度角度内都判断为向左滑动,如果手指超过这个角度,就不是横向滑动了。


getScrollX()指调用的控件的水平移动的距离,当未移动的时候,获取的值为0. 当向右移动20,则获取值为 -20,再向右移动10,则获取-30


this.mScroller.isFinished():


返回scroller是否已完成滚动。


返回值:停止滚动返回true,否则返回false


this.mScroller.abortAnimation():停止动画。Scroller滚动到最终x与y位置时中止动画。


这两句应该这样理解,当我按下某个item的时候,如果我在滑动这个item,抬起手后,他的动画还没有结束,这个时候,我又按下了刚才的item,就必须让他停止动画。又因为正在滑动,所以必须设置这个item的状态为滑动。


newScrollX = scrollX - deltaX;的目的是为了防止滑动越界,因为我可能手指一直向某个方向滑动,如果滑动超过了120dip,那么如果不处理越界,就会有多的空白部分,会导致滑动无止境,有可能滑动的屏幕上没有这个item了。所以当超过120dip的时候,就停止滑动if:newScrollX,else:newScrollX = this.mHolderWidth;当小于0后,也停止滑动:if:newScrollX < 0,else:newScrollX = 0;然后自执行滑动this.scrollTo(newScrollX, 0);


this.scrollTo(newScrollX, 0);是直接从某个坐标在X轴滑动newScrollX的距离,Y轴滑动0,X右为负,左为正,Y下为负,上为正。与坐标系相反。


最后解释一下,当手指抬起后,满足scrollX - this.mHolderWidth * 0.75 > 0,则滑动出隐藏控件。然后滑动。


滑动代码如下:

public void smoothScrollTo(int destX, int destY) {
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    this.mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);
    invalidate();//重绘
}


因为为横向滑动destY是没有意义的,这里这样是可能很容易扩展代码,首先获得手指滑动的距离,然后获得最终item需要滑动的距离delta,然后执行滑动。


public void startScroll (int startX, int startY, int dx, int dy) :以提供的起始点和将要滑动的距离开始滚动。滚动会使用缺省值250ms作为持续时间。


当调用startScroll方法后,scroller并不是直接包办一切,它只是帮助你计算每次移动的偏移量,你需要重写computeScroll方法,并在里面处理移动。代码如下:


//调用startScroll()是不会有滚动效果的,只有在computeScroll()获取滚动情况,做出滚动的响应
@Override
public void computeScroll() {
    if(this.mScroller.computeScrollOffset()){
        scrollTo(this.mScroller.getCurrX(),this.mScroller.getCurrY());
        postInvalidate();
    }
    super.computeScroll();
}


如果滑动出隐藏控件设置状态为ON,否则为OFF。


玩QQ的也应该知道,所有的item只有一个,也只能有一个滑动出现,如果要滑动出另一个控件必须将上个控件关闭。所以还需要将控件关闭的方法,代码如下 :


/**
 * 将当前状态置为关闭
 */
public void shrink(){
    if(getScrollX()!=0){
        this.smoothScrollTo(0,0);
    }
}

当滑动另一个控件的时候就调用上一个item控件的shrink()方法。


当然还需要将View加载进来,代码如下:

/**
 * 将View加载进来
 * @param view
 */
public void setContentView(View view){
    this.mLinearLayout.addView(view);
}

当然还要有设置回调函数的方法:


/**
 * 设置回调函数
 * @param onSlideViewOnListener
 */
public void setmOnSlideViewOnListener(OnSlideViewOnListener onSlideViewOnListener){
    this.mOnSlideViewOnListener=onSlideViewOnListener;
}

这样SlideView就基本实现了。

相关文章
|
6月前
|
XML Java Android开发
Android关于BottomNavigationView效果实现指南
本文详细介绍了Android中BottomNavigationView的实现与定制方法,涵盖颜色设置、图标修改、字体大小调整及多色图标处理等问题。通过XML和Java代码两种方式,解决图标颜色变化、点击效果等问题,并提供去除ActionBar的实现步骤。适合初学者及进阶开发者参考,助力打造更美观、功能丰富的底部导航栏。文末附源码,方便实践操作。
676 28
Android关于BottomNavigationView效果实现指南
|
JSON JavaScript IDE
JSON 数据格式化方法
JSON 数据格式化方法
647 3
|
运维 监控 安全
堡垒机是用来干什么的?堡垒机的好处有什么?
堡垒机能保障网络和数据不受来自外部和内部用户的入侵和破坏,运用各种技术手段实时收集和监控网络环境。
1705 1
|
API 开发工具 Android开发
Android Studio:解决AOSP自编译framework.jar引用不到的问题
在Android Studio中解决AOSP自编译framework.jar引用问题的几种方法,包括使用相对路径、绝对路径和通过`${project.rootDir}`动态获取路径的方法,以避免硬编码路径带来的配置问题。
1448 0
Android Studio:解决AOSP自编译framework.jar引用不到的问题
|
存储 JavaScript 前端开发
JavaScript进阶 - 浏览器存储:localStorage, sessionStorage, cookies
【7月更文挑战第8天】Web开发中的客户端存储技术,如`localStorage`, `sessionStorage`和`cookies`,用于保存用户设置和跟踪活动。`localStorage`持久化存储,`sessionStorage`随页面会话消失。两者提供基本的增删查改操作,但有大小限制和安全风险。`cookies`适合会话管理,可设置过期时间并能跨域。使用时注意存储量、安全性和跨域策略,选择适合场景的存储方式。
558 0
|
消息中间件 搜索推荐 UED
Elasticsearch 作为推荐系统后端的技术架构设计
【8月更文第28天】在现代互联网应用中,推荐系统已经成为提高用户体验和增加用户粘性的重要手段之一。Elasticsearch 作为一个高性能的搜索和分析引擎,不仅能够提供快速的全文检索能力,还可以通过其强大的数据处理和聚合功能来支持推荐系统的实现。本文将探讨如何利用 Elasticsearch 构建一个高效且可扩展的推荐系统后端架构,并提供一些具体的代码示例。
914 0
|
JavaScript 开发工具 开发者
vue3【提效】使用 VueUse 高效开发(工具库 @vueuse/core + 新增的组件库 @vueuse/components)
vue3【提效】使用 VueUse 高效开发(工具库 @vueuse/core + 新增的组件库 @vueuse/components)
969 1
|
Java Android开发 UED
安卓scheme_url调端:如果手机上多个app都注册了 http或者https 的 intent。 调端的时候,调起哪个app呢?
当多个Android应用注册了相同的URL Scheme(如http或https)时,系统会在尝试打开这类链接时展示一个选择对话框,让用户挑选偏好应用。若用户选择“始终”使用某个应用,则后续相同链接将直接由该应用处理,无需再次选择。本文以App A与App B为例,展示了如何在`AndroidManifest.xml`中配置对http与https的支持,并提供了从其他应用发起调用的示例代码。此外,还讨论了如何在系统设置中管理这些默认应用选择,以及建议开发者为避免冲突应注册更独特的Scheme。
2024年2月最新易支付系统全开源
2024年2月最新易支付系统全开源
370 3
|
Dubbo Java 应用服务中间件
微服务技术系列教程(29) - Dubbo-介绍&环境安装&入门案例
微服务技术系列教程(29) - Dubbo-介绍&环境安装&入门案例
249 0