仿QQ實現側滑效果和刪除、置頂功能——ListView版
我儘量不打錯別字,用詞準確,不造成閱讀障礙
高仿QQ側滑效果,實現置頂、刪除功能,完美適用於ListView,至於RecyclerView正在研究,效果有些問題。
本側滑很簡單,只有右側的側滑,並沒有其他酷炫的功能,希望給大家一個提示思路,如果需求簡單的話可以自己照著寫,不需要加入第三方庫。本文是一步步完善功能的,最後會有完整程式碼。
原理
自定義ViewGroup,繼承自FrameLayout,將“刪除”、“置頂”兩個按鈕寫到螢幕外面,然後通過監聽手勢滑動,呼叫ScroollTo()方法或ScrollBy()方法實現位移,實現側滑效果,最後解決滑動衝突和其它Bug,完成刪除、置頂功能。
程式碼
item的佈局:
<com.teststudy.longl.myapplication3.MyRecyclerView2.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RelativeLayout
android: id="@+id/ll_content_view"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:visibility="visible">
<ImageView
android:id ="@+id/iv_avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_test2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@id/iv_avatar"
android:text="好友名稱"
android:textColor="#000000"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_test3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_test2"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@id/iv_avatar"
android:maxLines="1"
android:text="內容展示,隨便寫一些東西測試一下就好" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="10dp"
android:layout_marginTop="15dp"
android:text="昨天" />
</RelativeLayout>
<LinearLayout
android:layout_width="200dp"
android:layout_height="70dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_toFirst"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:gravity="center"
android:text="置頂"
android:textColor="@android:color/white"
android:textSize="22sp" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/holo_red_light"
android:gravity="center"
android:text="刪除"
android:textColor="@android:color/white"
android:textSize="22sp" />
</LinearLayout>
</com.teststudy.longl.myapplication3.MyRecyclerView2.SlideLayout>
“置頂”、“刪除”正常是看不見的,只有側滑時才看得見;SlideLayout就是我們要自定義的ViewGroup,繼承FrameLayout。
SlideLayout程式碼:
public class SlideLayout extends FrameLayout {
private View mMenuView;
private int mMenuWidth;
private int mMenuHeight;
private int mContentWidth;
private Scroller mScroller;
private float startX;
private float downX;
private float downY;
private onSlideChangeListener mOnSlideChangeListener;
public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = getMeasuredWidth();
mMenuWidth = mMenuView.getMeasuredWidth();
mMenuHeight = mMenuView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//將menu佈局到右側不可見
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
break;
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
}
scrollTo(Math.min(disX, mMenuWidth), getScrollY());
startX = x;
break;
case MotionEvent.ACTION_UP:
if (getScrollX() < mMenuWidth / 2) {
closeMenu();
} else {
openMenu();
}
break;
}
return true;
}
//攔截事件不傳遞給子view
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
@Override
public void computeScroll() {
super.computeScroll();
//當動畫執行完成以後,執行新的動畫
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
}
}
程式碼很簡單,就不做過多介紹了,到這裡會有許多Bug;
Bug1:滑動衝突
ListView的上下滑動和SlideLayout的左右滑動會有衝突,解決思路有兩個:1.自定義ListView; 2.子View告知父View不要攔截。此處我們採用第二個方案。在OnTouchEvent() 方法中的ACTION_MOVE事件中做判斷:
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
}
scrollTo(Math.min(disX, mMenuWidth), getScrollY());
final float moveX = Math.abs(x - downX);
final float moveY = Math.abs(y - downY);
if (moveX > moveY && moveX > 10f) {
//父佈局不要攔截子view的touch事件
getParent().requestDisallowInterceptTouchEvent(true);
}
startX = x;
break;
Bug2:點選事件
如果我此時在Adapter裡面給整個item中ContentView部分(除刪除、置頂以外的部分即左邊部分)新增點選事件,側滑的效果就不見了,這是因為父佈局(SlideLayout)預設是不攔截子view的點選事件的,事件會由子View消費掉,不會返回給父View處理,所以我們需要攔截子View的點選事件,在onInterceptTouchEvent() 方法中的ACTION_MOVE情況中做處理:
case MotionEvent.ACTION_MOVE:
final float moveX = Math.abs(x - downX);
if (moveX > 10f) { //對touch事件進行攔截
intercept = true;
}
break;
這樣會根據intercept來靈活判斷是否攔截Touch事件。
Bug3:多個Item同時出現置頂、刪除效果
QQ中只有一個Item會出現置頂、刪除效果,不允許多個Item同時出現,任何操作都應該側滑回位,如何實現?我們可以新增監聽方法:
private onSlideChangeListener mOnSlideChangeListener;
public interface onSlideChangeListener {
void onMenuOpen(SlideLayout slideLayout);
void onMenuClose(SlideLayout slideLayout);
void onClick(SlideLayout slideLayout);
}
public void setOnSlideChangeListener(onSlideChangeListener onSlideChangeListener1) {
this.mOnSlideChangeListener = onSlideChangeListener1;
}
然後在onInterceptTouchEvent() 方法中ACTION_DOWN和openMenu()、closeMenu()中新增監聽:
//... 省略部分
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onClick(this);
}
break;
//...省略部分
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuOpen(this);
}
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuClose(this);
}
}
Adapter中可以設定監聽了:
mSlideLayout.setOnSlideChangeListener(new SlideLayout.onSlideChangeListener() {
@Override
public void onMenuOpen(SlideLayout slideLayout) {
mSlideLayout = slideLayout;
}
@Override
public void onMenuClose(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout = null;
}
}
@Override
public void onClick(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout.closeMenu();
}
}
});
這樣在openMenu時會賦值slideLayout,在任何click時會觸發onInterceptTouchEvent() 的DOWN事件,進而執行closeMenu()方法。所以總的SliderLayout是這樣的:
private View mMenuView;
private int mMenuWidth;
private int mMenuHeight;
private int mContentWidth;
private Scroller mScroller;
private float startX;
private float downX;
private float downY;
private onSlideChangeListener mOnSlideChangeListener;
public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = getMeasuredWidth();
mMenuWidth = mMenuView.getMeasuredWidth();
mMenuHeight = mMenuView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//將menu佈局到右側不可見
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction())