[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>
~~~
效果:

* 可響應點擊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特效