仿微信消息列表

2024-03-21 10:20
文章标签 列表 消息 仿微信

本文主要是介绍仿微信消息列表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

仿微信消息列表

文章目录

  • 仿微信消息列表
  • 前言
  • 一、典型的事件类型
  • 二、Scroller
  • 三、View的滑动
    • 1.scrollTo/scrollBy
    • 2.修改布局参数
    • 3.动画
  • 四、使用步骤
    • 1.布局文件
    • 2.自定义View-ScrollerLinearLayout
  • 五、问题
  • 总结


前言

最近自己在利用空闲时间开发一个APP,目的是为了巩固所学的知识并扩展新知,加强对代码的理解扩展能力。消息模块是参照微信做的,一开始并没有准备做滑动删除的功能,觉得删除嘛,后面加个长按的监听不就行了,但是对于有些强迫症的我来说,是不大满意这种解决方法的,但由于我对自定义view的了解还是比较少,而且之前也没有做过,所以就作罢。上周看了任玉刚老师的《Android开发艺术探索》中的View事件体系章节,提起了兴趣,就想着试一试吧,反正弄不成功也没关系。最后弄成了,但还是有些小瑕疵(在五、问题中),希望大佬能够指教一二。话不多说,放上一张动图演示下:
在这里插入图片描述


提示:以下是本篇文章正文内容,下面案例可供参考

一、典型的事件类型

在附上源码之前,想先向大家介绍下事件类型,在手指接触屏幕后所产生的一系列事件中,典型的事件类型有如下几种:

  • ACTION_DOWN ---- 手指刚接触屏幕
  • ACTION_MOVE ---- 手指在屏幕上移动
  • ACTION_UP ---- 手指刚离开屏幕

正常情况下、一次手指触摸屏幕的行为会触发一系列点击事件:

  • 点击屏幕后松开,事件序列为DOWN -> UP
  • 点击屏幕滑动后松开,事件序列为DOWN -> MOVE -> … -> MOVE -> UP

二、Scroller

Scroller - 弹性滑动对象,用于实现View的弹性滑动。
当使用View的scrollTo/scrollBy方法来实现滑动时,其过程是在瞬间完成的,这个过程没有过渡效果,用户体验感较差,这个时候就可以使用Scroller来实现有过渡效果的滑动,其过程不是瞬间完成的,而是在一定时间间隔内完成的。

三、View的滑动

Android手机由于屏幕较小,为了给用户呈现更多的内容,就需要使用滑动来显示和隐藏一些内容,不管滑动效果多么绚丽,它们都是由不同的滑动外加特效实现的。View的滑动可以通过三种方式实现:

  • scrollTo/scrollBy:操作简单,适合对View内容的滑动。
  • 修改布局参数:操作稍微复杂,适合有交互的View。
  • 动画:操作简单,适合没有交互的View和实现复杂的动画效果。

1.scrollTo/scrollBy

为了实现View的滑动,View提供了专门的方法来实现这一功能,也就是scrollTo/scrollBy。是基于所传参数的绝对滑动。

2.修改布局参数

即改变LayoutParams,比如想把一个布局向右平移100px,只需要将该布局LayoutParams中的marginLeft参数值增加100px即可。或者在该布局左边放入一个默认宽度为0px的空View,当需要向右平移时,重新设置空View的宽度就OK了,

3.动画

动画和Scroller一样具有过渡效果,View动画是对View的影像做操作,并不能真正改变View的位置,单击新位置无法触发onClick事件,在这篇文章中并没有使用到,所以不再赘叙了。

四、使用步骤

1.布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"xmlns:widget="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><com.example.myapplication.view.ScrollerLinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><RelativeLayoutandroid:id="@+id/friend_item"android:layout_width="match_parent"android:layout_height="wrap_content"android:paddingHorizontal="16dp"android:paddingVertical="10dp"><LinearLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="horizontal"><com.makeramen.roundedimageview.RoundedImageViewandroid:id="@+id/friend_icon"android:layout_width="45dp"android:layout_height="45dp"android:src="@mipmap/touxiang"app:riv_corner_radius="5dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:layout_gravity="center"android:layout_marginLeft="12dp"android:gravity="center_vertical"android:orientation="vertical"><TextViewandroid:id="@+id/friend_name"android:layout_width="wrap_content"android:layout_height="wrap_content"android:singleLine="true"android:textColor="@color/black"android:textSize="15dp"tools:text="好友名" /><TextViewandroid:id="@+id/friend_last_mess"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="3dp"android:layout_marginEnd="18dp"android:singleLine="true"android:textColor="@color/color_dbdbdb"android:textSize="12dp"tools:text="最后一条信息内容" /></LinearLayout></LinearLayout><TextViewandroid:id="@+id/last_mess_time"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentEnd="true"android:layout_marginTop="5dp"android:singleLine="true"android:textColor="@color/color_dbdbdb"android:textSize="11dp"tools:text="时间" /></RelativeLayout><LinearLayoutandroid:layout_width="240dp"android:layout_height="match_parent"android:orientation="horizontal"><Buttonandroid:id="@+id/unread_item"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_weight="1"android:clickable="true"android:background="@color/color_theme"android:gravity="center"android:text="标为未读"android:textColor="@color/color_FFFFFF" /><Buttonandroid:id="@+id/top_item"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_weight="1"android:clickable="true"android:background="@color/color_orange"android:gravity="center"android:text="置顶"android:textColor="@color/color_FFFFFF" /><Buttonandroid:id="@+id/delete_item"android:layout_width="match_parent"android:layout_height="match_parent"android:layout_weight="1"android:clickable="true"android:background="@color/color_red"android:gravity="center"android:text="删除"android:textColor="@color/color_FFFFFF" /></LinearLayout></com.example.myapplication.view.ScrollerLinearLayout><Viewandroid:layout_width="match_parent"android:layout_height="1px"android:layout_alignParentBottom="true"android:layout_marginLeft="60dp"android:layout_marginRight="3dp"android:background="@color/color_e7e7e7" /></LinearLayout>

提示:ScrollerLinearLayout布局最多包含两个子布局(默认是这样,后面可能还会修改成自定义),一个是展示在用户面前充满屏幕宽度的布局,一个是待展开的布局,在该xml布局中,ScrollerLinearLayout布局包含了一个RelativeLayout和一个LinearLayoutLinearLayout中包含了三个按钮,分别是删除、置顶、标为未读。

2.自定义View-ScrollerLinearLayout

代码如下:

/*** @Copyright : China Telecom Quantum Technology Co.,Ltd* @ProjectName : My Application* @Package : com.example.myapplication.view* @ClassName : ScrollerLinearLayout* @Description : 文件描述* @Author : yulu* @CreateDate : 2023/8/17 17:05* @UpdateUser : yulu* @UpdateDate : 2023/8/17 17:05* @UpdateRemark : 更新说明*/
class ScrollerLinearLayout @JvmOverloads constructor(context: Context,attrs: AttributeSet? = null,defStyleAttr: Int = 0
) :LinearLayout(context, attrs, defStyleAttr) {private val mScroller = Scroller(context)  // 用于实现View的弹性滑动private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlopprivate var mVelocityTracker: VelocityTracker? = null   // 速度追踪private var intercept = false   // 拦截状态 初始值为不拦截private var lastX: Float = 0fprivate var lastY: Float = 0f  // 用来记录手指按下的初始坐标var expandWidth = 720   // View待展开的布局宽度 需要手动设置 3*dpprivate var expandState = false   // View的展开状态private val displayWidth =context.applicationContext.resources.displayMetrics.widthPixels  // 屏幕宽度private var state = trueoverride fun onTouchEvent(event: MotionEvent): Boolean {Log.e(TAG, "onTouchEvent $event")when (event.action) {MotionEvent.ACTION_DOWN -> {if (!expandState) {state = false}}else -> {state = true}}return state}override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "onInterceptTouchEvent Result : ${onInterceptTouchEvent(ev)}")Log.e(TAG, "dispatchTouchEvent : $ev")mVelocityTracker = VelocityTracker.obtain()mVelocityTracker!!.addMovement(ev)return super.dispatchTouchEvent(ev)}override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {Log.e(TAG, "onInterceptTouchEvent $ev")when (ev?.action) {MotionEvent.ACTION_DOWN -> {lastX = ev.rawXlastY = ev.rawY// 处于展开状态且点击的位置不在扩展布局中 拦截点击事件intercept = expandState && ev.x < (displayWidth - expandWidth)}MotionEvent.ACTION_MOVE -> {// 当滑动的距离超过10 拦截点击事件intercept = lastX - ev.x > 10moveWithFinger(ev)}MotionEvent.ACTION_UP -> {// 判断滑动距离是否超过布局的1/2chargeToRightPlace(ev)intercept = false}MotionEvent.ACTION_CANCEL -> {chargeToRightPlace(ev)intercept = false}else -> intercept = false}return intercept}/*** 将布局修正到正确的位置*/private fun chargeToRightPlace(ev: MotionEvent) {val eventX = ev.x - lastXLog.e(TAG, "该事件滑动的水平距离 $eventX")if (eventX < -(expandWidth / 4)) {smoothScrollTo(expandWidth, 0)expandState = trueinvalidate()} else {expandState = falsesmoothScrollTo(0, 0)invalidate()}// 回收内存mVelocityTracker?.apply {clear()recycle()}//清除状态lastX = 0finvalidate()}/*** 跟随手指移动*/private fun moveWithFinger(event: MotionEvent) {//获得手指在水平方向上的坐标变化// 需要滑动的像素val mX = lastX - event.xif (mX > 0 && mX < expandWidth) {scrollTo(mX.toInt(), 0)}// 获取当前水平方向的滑动速度mVelocityTracker!!.computeCurrentVelocity(500)val xVelocity = mVelocityTracker!!.xVelocity.toInt()invalidate()}/*** 缓慢滚动到指定位置*/private fun smoothScrollTo(destX: Int, destY: Int) {val delta = destX - scrollX// 在多少ms内滑向destXmScroller.startScroll(scrollX, 0, delta, 0, 600)invalidate()translationY = 0f}// 流畅地滑动override fun computeScroll() {if (mScroller.computeScrollOffset()) {scrollTo(mScroller.currX, mScroller.currY);postInvalidate()}}override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {expandWidth = childViewWidth()invalidate()super.onLayout(changed, l, t, r, b)}/*** 最多只允许有两个子布局*/private fun childViewWidth(): Int {Log.e(TAG, "childCount ${this.childCount}")return if (this.childCount > 1) {val expandChild = this.getChildAt(1) as LinearLayoutif (expandChild.measuredWidth != 0){expandWidth = expandChild.measuredWidth}Log.e(TAG, "expandWidth $expandWidth")expandWidth} else0}companion object {const val TAG = "ScrollerLinearLayout_YOLO"}
}

自定义View的思路比较简单,就是在ACTION_DOWN时记录初始的横坐标,在ACTION_MOVE中判断是否需要拦截该事件,当滑动的距离超过10,拦截该点击事件,并且View跟随手指移动。在ACTION_UPACTION_CANCEL中将布局修正到正确的位置,主要是根据滑动的距离来判断是否要展开并记录展开的状态。在ACTION_DOWN中判断是否处于展开状态,如果在展开状态且点击的位置不在扩展布局中,拦截点击事件。

提示:拦截点击事件是为了防止不必要的点击。

五、问题

自定义布局中的expandWidth参数在childViewWidth()方法和onLayout()方法中都赋值了一次,在onLayout()方法中查看日志expandWidth是有值的,可是在moveWithFinger()方法中打日志查看得到的expandWidth参数值仍然是0,导致无法正常滑动。去到其他的页面再返回到消息界面就可以正常滑动了,再次查看日志也有值了。

提示:这个问题不知道如何解决,所以需要手动设置expandWidth的值。


总结

初步的和自定义View认识了,小试牛刀,自己还是很满意这个学习成果的。希望在接下来的学习中不要因为没有接触过而放弃学习,勇于迈出第一步。文章若出现错误,欢迎各位批评指正,写文不易,转载请注明出处谢谢。

这篇关于仿微信消息列表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832415

相关文章

C++ RabbitMq消息队列组件详解

《C++RabbitMq消息队列组件详解》:本文主要介绍C++RabbitMq消息队列组件的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. RabbitMq介绍2. 安装RabbitMQ3. 安装 RabbitMQ 的 C++客户端库4. A

Python中合并列表(list)的六种方法小结

《Python中合并列表(list)的六种方法小结》本文主要介绍了Python中合并列表(list)的六种方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋... 目录一、直接用 + 合并列表二、用 extend() js方法三、用 zip() 函数交叉合并四、用

Spring Boot中的YML配置列表及应用小结

《SpringBoot中的YML配置列表及应用小结》在SpringBoot中使用YAML进行列表的配置不仅简洁明了,还能提高代码的可读性和可维护性,:本文主要介绍SpringBoot中的YML配... 目录YAML列表的基础语法在Spring Boot中的应用从YAML读取列表列表中的复杂对象其他注意事项总

SpringCloud整合MQ实现消息总线服务方式

《SpringCloud整合MQ实现消息总线服务方式》:本文主要介绍SpringCloud整合MQ实现消息总线服务方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录一、背景介绍二、方案实践三、升级版总结一、背景介绍每当修改配置文件内容,如果需要客户端也同步更新,

C++类和对象之初始化列表的使用方式

《C++类和对象之初始化列表的使用方式》:本文主要介绍C++类和对象之初始化列表的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C++初始化列表详解:性能优化与正确实践什么是初始化列表?初始化列表的三大核心作用1. 性能优化:避免不必要的赋值操作2. 强

一文带你搞懂Redis Stream的6种消息处理模式

《一文带你搞懂RedisStream的6种消息处理模式》Redis5.0版本引入的Stream数据类型,为Redis生态带来了强大而灵活的消息队列功能,本文将为大家详细介绍RedisStream的6... 目录1. 简单消费模式(Simple Consumption)基本概念核心命令实现示例使用场景优缺点2

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

Python列表去重的4种核心方法与实战指南详解

《Python列表去重的4种核心方法与实战指南详解》在Python开发中,处理列表数据时经常需要去除重复元素,本文将详细介绍4种最实用的列表去重方法,有需要的小伙伴可以根据自己的需要进行选择... 目录方法1:集合(set)去重法(最快速)方法2:顺序遍历法(保持顺序)方法3:副本删除法(原地修改)方法4:

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

SpringKafka消息发布之KafkaTemplate与事务支持功能

《SpringKafka消息发布之KafkaTemplate与事务支持功能》通过本文介绍的基本用法、序列化选项、事务支持、错误处理和性能优化技术,开发者可以构建高效可靠的Kafka消息发布系统,事务支... 目录引言一、KafkaTemplate基础二、消息序列化三、事务支持机制四、错误处理与重试五、性能优