1. 程式人生 > >仿QQ實現側滑效果和刪除、置頂功能——ListView版

仿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())