[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;
* 滑动到一半的自动滚动,关闭或者打开;
- 介绍
- UI
- MaterialButton
- MaterialButtonToggleGroup
- 字体相关设置
- Material Design
- Toolbar
- 下拉刷新
- 可折叠式标题栏
- 悬浮按钮
- 滑动菜单DrawerLayout
- NavigationView
- 可交互提示
- CoordinatorLayout
- 卡片式布局
- 搜索框SearchView
- 自定义View
- 简单封装单选
- RecyclerView
- xml设置点击样式
- adb
- 连接真机
- 小技巧
- 通过字符串ID获取资源
- 自定义View组件
- 使用系统控件重新组合
- 旋转菜单
- 轮播图
- 下拉输入框
- 自定义VIew
- 图片组合的开关按钮
- 自定义ViewPager
- 联系人快速索引案例
- 使用ListView定义侧滑菜单
- 下拉粘黏效果
- 滑动冲突
- 滑动冲突之非同向冲突
- onMeasure
- 绘制字体
- 设置画笔Paint
- 贝赛尔曲线
- Invalidate和PostInvalidate
- super.onTouchEvent(event)?
- setShadowLayer与阴影效果
- Shader
- ImageView的scaleType属性
- 渐变
- LinearGradient
- 图像混合模式
- PorterDuffXfermode
- 橡皮擦效果
- Matrix
- 离屏绘制
- Canvas和图层
- Canvas简介
- Canvas中常用操作总结
- Shape
- 圆角属性
- Android常见动画
- Android动画简介
- View动画
- 自定义View动画
- View动画的特殊使用场景
- LayoutAnimation
- Activity的切换转场效果
- 属性动画
- 帧动画
- 属性动画监听
- 插值器和估值器
- 工具
- dp和px的转换
- 获取屏幕宽高
- JNI
- javah命令
- C和Java相互调用
- WebView
- Android Studio快捷键
- Bitmap和Drawable图像
- Bitmap简要介绍
- 图片缩放和裁剪效果
- 创建指定颜色的Bitmap图像
- Gradle本地仓库
- Gradle小技巧
- RxJava+Okhttp+Retrofit构建网络模块
- 服务器相关配置
- node环境配置
- 3D特效