💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[TOC] # 1. 前言 也就是使用ListView来做一个类似QQ的每个条目可以侧滑出菜单的效果。实现步骤大致为: * 定义好Item的布局,也就是主Item填充整个屏幕的宽度,而对应的侧滑出来的Menu就默认不显示,而放置在屏幕之外; * 为了做到上面的效果,这里需要使用自定义ViewGroup,然后复写onlayout方法,对Menu进行放置。 * 当然,因为要响应手指的侧滑事件,所以这里需要复写onTouchEvent方法对滑动事件进行处理,使用scrollTo来进行移动。 * 在移动的过程中进行边界判断,也就是屏蔽非法值; * 然后为移动添加动态效果,也就是当移动距离大于这个Menu的宽度的一般的时候,就默认打开或者关闭; * 打开或者关闭的效果使用Scroller来进行动态计算,请求重新绘制,然后在computeScroll中进行scrollTo方法的调用。 * 在使用Scroller的时候,注意到移动的距离为目标值减去当前X/Y轴的位置。 * 由于我们是在ListView中进行Item的侧滑,故而这里要处理一下滑动冲突问题,也就是如果是横滑,就请求父控件放行; * 然后需要处理当item的menu展开,有其余非当前item的事件的时候,关闭当前的item。 # 2. 实现 ## 2.1 布局 ``` <com.weizu.sideslip.MyContainer xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dp"> <!--主要显示的内容--> <TextView android:id="@+id/content_title" android:layout_width="match_parent" android:layout_height="60dp" android:background="#33000000" android:text="AAA" android:textSize="24sp" android:gravity="center_vertical" android:paddingLeft="10dp" /> <!--右边的menu--> <LinearLayout android:layout_width="wrap_content" android:layout_height="60dp" android:orientation="horizontal"> <TextView android:layout_width="match_parent" android:layout_height="60dp" android:background="@color/white" android:textColor="@color/red" android:text="Delete" android:textSize="24sp" android:gravity="center_vertical" android:paddingLeft="10dp" android:paddingRight="10dp" /> </LinearLayout> </com.weizu.sideslip.MyContainer> ``` 而MyContainer也就是这里需要进行复写onlayout的自定义ViewGroup,这里直接继承自FrameLayout,因为不必关onmeasure,只需要简单的进行onLayout即可。当然,因为要处理手指触摸事件,这里需要复写onTouchEvent,并在其中判断横向或者竖向滑动,如果是横向滑动,就请求父控件发行。对应的使用Scroller来动态计算移动的位置,即: ~~~ class MyContainer(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { var itemWidth = 0 var scroller: Scroller = Scroller(context) override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, left, top, right, bottom) val childView = getChildAt(1) itemWidth = childView.width childView.layout(width, 0, width + itemWidth, childView.height) } var startX = 0f var downX = 0f var downY = 0f override fun onTouchEvent(event: MotionEvent?): Boolean { super.onTouchEvent(event) when(event?.action){ MotionEvent.ACTION_DOWN -> { startX = event.x downX = event.x downY = event.y } MotionEvent.ACTION_MOVE -> { var xAxis = (scrollX - ( event.x - startX)).toInt() if(xAxis > itemWidth) { xAxis = itemWidth } else if(xAxis < 0) { xAxis = 0 } scrollTo(xAxis, 0) startX = event.x // 因为外层ListView可以竖直滑动,而这里的Item可以横向滑动,所以这里也要处理一下事件 val distanceX = abs(event.x - downX) val distanceY = abs(event.y - downY) if(distanceX > distanceY && distanceX > 8){ parent.requestDisallowInterceptTouchEvent(true) } } MotionEvent.ACTION_UP -> { // 抬起手指就根据位置动画 if (scrollX > itemWidth / 2) { // 开启菜单 openMenu() } else { closeMenu() } } } return true } fun openMenu(){ // 目标 - scrollX val dx = itemWidth - scrollX scroller.startScroll(scrollX, scrollY, dx, scrollY) invalidate() } fun closeMenu(){ // 目标 - scrollX val dx = 0 - scrollX scroller.startScroll(scrollX, scrollY, dx, scrollY) invalidate() } override fun computeScroll() { super.computeScroll() if(scroller.computeScrollOffset()){ scrollTo(scroller.currX, scroller.currY) invalidate() } } } ~~~ 首先在上面的类中增加监听接口,然后处理一下Item的点击事件,这里需要注意,因为还是会有事件冲突需要处理,因为这里需要对事件拦截,故而复写onInterceptTouchEvent方法。 完整代码,首先是自定义VIewGroup: ~~~ class MyContainer(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { var itemWidth = 0 var scroller: Scroller = Scroller(context) override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, left, top, right, bottom) val childView = getChildAt(1) itemWidth = childView.width childView.layout(width, 0, width + itemWidth, childView.height) } // 放行点击事件,因为Item的点击事件是其子View需要响应 override fun onInterceptTouchEvent(event: MotionEvent?): Boolean { var intercept = false when(event?.action){ MotionEvent.ACTION_DOWN -> { startX = event.x downX = event.x downY = event.y } MotionEvent.ACTION_MOVE -> { val xAxis = (scrollX - ( event.x - startX)).toInt() if(xAxis > 8) { intercept = true // 横向滑动需要拦截 } } } return intercept } var startX = 0f var downX = 0f var downY = 0f override fun onTouchEvent(event: MotionEvent?): Boolean { super.onTouchEvent(event) when(event?.action){ MotionEvent.ACTION_DOWN -> { startX = event.x downX = event.x downY = event.y mListener?.onDown(this) } MotionEvent.ACTION_MOVE -> { var xAxis = (scrollX - ( event.x - startX)).toInt() if(xAxis > itemWidth) { xAxis = itemWidth } else if(xAxis < 0) { xAxis = 0 } scrollTo(xAxis, 0) startX = event.x // 因为外层ListView可以竖直滑动,而这里的Item可以横向滑动,所以这里也要处理一下事件 val distanceX = abs(event.x - downX) val distanceY = abs(event.y - downY) if(distanceX > distanceY && distanceX > 8){ parent.requestDisallowInterceptTouchEvent(true) } } MotionEvent.ACTION_UP -> { // 抬起手指就根据位置动画 if (scrollX > itemWidth / 2) { // 开启菜单 openMenu() } else { closeMenu() } } } return true } fun openMenu(){ // 目标 - scrollX val dx = itemWidth - scrollX scroller.startScroll(scrollX, scrollY, dx, scrollY) invalidate() mListener?.onOpen(this) } fun closeMenu(){ // 目标 - scrollX val dx = 0 - scrollX scroller.startScroll(scrollX, scrollY, dx, scrollY) invalidate() mListener?.onClose(this) } override fun computeScroll() { super.computeScroll() if(scroller.computeScrollOffset()){ scrollTo(scroller.currX, scroller.currY) invalidate() } } interface OnItemMenuStateChangeListener{ fun onClose(view: MyContainer) fun onDown(view: MyContainer) fun onOpen(view: MyContainer) } private var mListener: OnItemMenuStateChangeListener? = null fun setOnItemChangeListener(l: OnItemMenuStateChangeListener){ mListener = l } } ~~~ 然后是ManActivity: ~~~ /** * 侧滑菜单 */ class MainActivity : AppCompatActivity() { val listView by lazy { findViewById<ListView>(R.id.listView) } val datas = mutableListOf<String>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) initDatas() listView.adapter = MyAdapter() } fun initDatas() { for (i in 0 until 30) { datas.add("Content ${i}") } } inner class MyAdapter : BaseAdapter() { override fun getCount() = datas.size override fun getItem(position: Int) = position override fun getItemId(position: Int) = position.toLong() override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { var view: View? = convertView var myViewHolder: MyViewHolder? = null if (convertView == null) { myViewHolder = MyViewHolder() view = View.inflate(this@MainActivity, R.layout.item_layout, null) myViewHolder.textView = view.findViewById<TextView>(R.id.content_title) myViewHolder.delete = view.findViewById<TextView>(R.id.delete) view.setTag(myViewHolder) } else { myViewHolder = convertView.getTag() as MyViewHolder? } myViewHolder?.apply { myViewHolder.textView?.text = datas.get(position) (view as MyContainer).setOnItemChangeListener(MyItemMenuChangeListener()) myViewHolder.textView?.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View?) { Toast.makeText( this@MainActivity, "点击了:${datas.get(position)}", android.widget.Toast.LENGTH_LONG ).show() } }) // 删除数据,更新对应的ListView myViewHolder.delete?.setOnClickListener(object : View.OnClickListener { override fun onClick(v: View?) { // 由于ListView中Item的复用机制,会导致当前打开的Item用来显示下个数据,而实际上我们所 // 期望的是更新的时候,没有Item的Menu被打开,故而需要调用一次closeMenu ((v?.parent?.parent) as MyContainer).closeMenu() datas.remove(datas.get(position)) notifyDataSetChanged() } }) } return view!! } } inner class MyViewHolder { var textView: TextView? = null var delete: TextView? = null } // 在ListView中上一轮打开的Item var lastMyContainer: MyContainer? = null // inner class MyItemMenuChangeListener : MyContainer.OnItemMenuStateChangeListener { override fun onClose(view: MyContainer) { Log.e("TAG", "onClose: ") if (lastMyContainer == view) { lastMyContainer = null } } override fun onDown(view: MyContainer) { Log.e("TAG", "onDown: ") // 判断是否是本轮自己的MyContainer,否则就是上轮打开的MyContainer,也就是上轮的Item if (view != lastMyContainer) { lastMyContainer?.closeMenu() } } override fun onOpen(view: MyContainer) { Log.e("TAG", "onOpen: ") // 更新本轮MyContainer的值 lastMyContainer = view } } } ~~~ 至于主布局,也就是一个Linearlayout包一个ListView: ~~~ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> ~~~ 效果: ![](https://img.kancloud.cn/21/24/2124c9d8bcc77492e9d5b154ff2677b0_284x428.png) * 可响应点击delete删除该条目; * Content的点击Toast; * 滑动到一半的自动滚动,关闭或者打开;