仿 iOS 列表的編輯功能 – 刪除篇

SouthernBox發表於2019-02-28

在 iOS 的設定裡面,有一種編輯的效果,進入編輯狀態後,列表左邊推出圓形的刪除按鈕,點選後再出現右邊確認刪除按鈕,相當於給使用者二次確認。看下在 Android 上如何實現。

iOS 的效果如下:

仿 iOS 列表的編輯功能 – 刪除篇

仿 iOS 列表的編輯功能 – 刪除篇

我實現的效果是這樣的:

仿 iOS 列表的編輯功能 – 刪除篇

下面說說我是怎麼做的吧。

EditLayout

我們自定義了一個 EditLayout 繼承 FrameLayout。
可以看出,這個控制元件由左中右三部分組成,對應的,我在 EditLsyout 裡建立了以下成員變數:

private View mContentView;  //內容部分
private View mLeftView;     //左邊圓形刪除按鍵
private View mRightView;    //右邊刪除按鍵
private int mWidth;         //內容部分寬度
private int mHeight;        //內容部分高度
private int mLeftWidth;     //左邊部分寬度
private int mRightWidth;    //右邊部分寬度複製程式碼

獲取控制元件及寬高

當 View 中所有的子控制元件 均被對映成 xml 後,會觸發 onFinishInflate 方法,當 view 的大小發生變化時,會觸發 onSizeChanged 方法,所以我們可以這樣賦值:

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mLeftView = getChildAt(0);
    mContentView = getChildAt(1);
    mRightView = getChildAt(2);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = w;
    mHeight = h;
    mRightWidth = mRightView.getMeasuredWidth();
    mLeftWidth = mLeftView.getMeasuredWidth();
}複製程式碼

擺放控制元件位置

獲取到控制元件和寬高,我們就可以擺放它們的位置了。我們知道,View 是通過 onLayout 方法來擺放控制元件位置的。這裡有兩種擺放方式,編輯狀態和非編輯狀態,程式碼如下:

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    //判斷是否為編輯模式,擺放每個子View的位置
    if (EditAdapter.isEdit) {
        mContentView.layout(mLeftWidth, 0, mLeftWidth + mWidth, mHeight);
        mRightView.layout(mWidth + mLeftWidth, 0, mRightWidth + mWidth + LeftWidth, mHeight);
        mLeftView.layout(0, 0, mLeftWidth, mHeight);
    } else {
        mContentView.layout(0, 0, mWidth, mHeight);
        mRightView.layout(mWidth, 0, mRightWidth + mWidth, mHeight);
        mLeftView.layout(-mLeftWidth, 0, 0, mHeight);
    }
}複製程式碼

滑動效果

滑動效果,我交給了 ViewDragHelper 處理。要使用 ViewDragHelper ,需要實現一個 ViewDragHelper.Callback,這是一個抽象類,我們這裡只關注它的三個方法:

//返回值決定 child 是否可拖拽
public boolean tryCaptureView(View child, int pointerId)
//限定移動範圍,返回值為對應控制元件的左邊位置
public int clampViewPositionHorizontal(View child, int left, int dx)
//當 changedView 發生移動時的回撥(可以用來更新其他子 View 的位置)
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy)複製程式碼

我實現的 Callback 程式碼如下:

ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {

    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return false;
    }

    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        if (child == mContentView) {
            if (left < -mRightWidth) {
                left = -mRightWidth;
            } else if (left > mLeftWidth) {
                left = mLeftWidth;
            }
        } else if (child == mRightView) {
            if (left < mWidth - mRightWidth) {
                left = mWidth - mRightWidth;
            } else if (left > mWidth) {
                left = mWidth;
            }
        } else if (child == mLeftView) {
            if (left < mWidth - mRightWidth) {
                left = mWidth - mRightWidth;
            } else if (left > -mLeftWidth) {
                left = 0 - mLeftWidth;
            }
        }
        return left;
    }

    @Override
    public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
        if (changedView == mContentView) {
            mRightView.offsetLeftAndRight(dx);
            mLeftView.offsetLeftAndRight(dx);
        } else if (changedView == mRightView) {
            mContentView.offsetLeftAndRight(dx);
            mLeftView.offsetLeftAndRight(dx);
        } else if (changedView == mLeftView) {
            mContentView.offsetLeftAndRight(dx);
            mRightView.offsetLeftAndRight(dx);
        }
        invalidate();
    }
};

mDragHelper = ViewDragHelper.create(this, mCallback);複製程式碼

對了,實現滑動還需要重寫 computeScroll 方法:

@Override
public void computeScroll() {
    super.computeScroll();
    if (mDragHelper.continueSettling(true)) {
        ViewCompat.postInvalidateOnAnimation(this);
    }
}複製程式碼

三種狀態

我們這個控制元件存在三種狀態,分別是左邊展開,右邊展開,還有關閉。相應的,我們定義三個方法,用於滑動到不同的狀態:

/**
 * 展開左側
 */
public void openLeft() {
    if (mOnStateChangeListener != null) {
        mOnStateChangeListener.onLeftOpen(this);
    }
    mDragHelper.smoothSlideViewTo(mContentView, mLeftWidth, 0);
    invalidate();
}

/**
 * 展開右側
 */
public void openRight() {
    if (mOnStateChangeListener != null) {
        mOnStateChangeListener.onRightOpen(this);
    }
    mDragHelper.smoothSlideViewTo(mContentView, -mRightWidth, 0);
    invalidate();
}

/**
 * 關閉
 */
public void close() {
    if (mOnStateChangeListener != null) {
        mOnStateChangeListener.onClose(this);
    }
    mDragHelper.smoothSlideViewTo(mContentView, 0, 0);
    invalidate();
}複製程式碼

mOnStateChangeListener 是一個監聽器,會在 EditLayout 狀態改變的時候呼叫。我在回撥方法裡儲存了當前向右展開的 EditLayout。

到這裡,EditLayout 就完成了。

EditAdapter

接下來看下介面卡 EditAdapter。

item 佈局

item 的 xml 檔案裡面,最外層用我們的 EditLayout 包裹,然後裡面的三個子佈局,按順序,對應我們左中右三個部分。

切換編輯模式

這裡需要定義一個 EditLayout 的集合 allItems,在 onBindViewHolder 的時候將佈局新增進去。
然後我們定義兩個公開方法,用於切換所有 item 的狀態,在切換編輯模式的時候呼叫:

/**
 * 關閉所有 item
 */
public void closeAll() {
    for (EditLayout layout : allItems) {
        editLayout.close();
    }
}

/**
 * 將所有 item 向左展開
 */
public void openLeftAll() {
    for (EditLayout layout : allItems) {
        editLayout.openLeft();
    }
}複製程式碼

EditRecyclerView

當列表有某一項是右邊展開了,我希望在滑動列表的時候能將它關閉,變回向左展開的狀態,所以我自定義了一個 RecyclerView。

可以重寫了 onTouchEvent 方法,實現上面說的效果:

@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (getAdapter() instanceof EditAdapter) {
                rightOpenItem = ((EditAdapter) getAdapter()).getRightOpenItem();
            }
            if (EditAdapter.isEdit && rightOpenItem != null) {
                rightOpenItem.openLeft();
            }
    }
    return super.onTouchEvent(e);
}複製程式碼

當滑動列表的時候,先判斷是否有向右展開項,有的話就將它變回向左展開。

這樣就完成啦,妥妥的。

原始碼地址

相關文章