1. 程式人生 > >自定義控制元件:側拉刪除

自定義控制元件:側拉刪除

SwipeLayout 側拉刪除

  • 掌握ViewDragHelper 的用法
  • 掌握平滑動畫的原理及狀態更新事件回撥

應用場景:QQ 聊天記錄,郵件管理,需要對條目進行功能擴充套件的場景,效果圖:

ViewDragHelper 初始化

建立自定義控制元件SwipeLayout 繼承FrameLayout

public class SwipeLayout extends FrameLayout {
    private ViewDragHelper mHelper;
    public SwipeLayout(Context context) {
        this
(context,null); } public SwipeLayout(Context context, AttributeSet attrs) { this(context, attrs,0); } public SwipeLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); //1.建立ViewDragHelper mHelper = ViewDragHelper.create(this
, mCallback); } //2.轉交觸控事件,攔截判斷,處理觸控事件 @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mHelper.shouldInterceptTouchEvent(ev); }; @Override public boolean onTouchEvent(MotionEvent event) { try { //多點觸控有一些小bug,最好catch 一下 mHelper.processTouchEvent(event); } catch
(Exception e) { } //消費事件,返回true return true; }; //3.處理回撥事件 ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() { @Override public boolean tryCaptureView(View child, int pointerId) { return false; } }; }

第3-8 行通過構造方法互調,將三個構造方法串連起來,這樣初始化程式碼只需要寫在第三個構造方法中即可
第11-37 行ViewDragHelper 使用三步曲

介面初始化

將SwipeLayout 佈局到activity_main.xml 中

<RelativeLayout 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" >
    <com.example.swipe.SwipeLayout
        android:layout_width="match_parent"
        android:layout_height="60dp" >
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_gravity="right"
            android:layout_height="match_parent" >
            <TextView
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:background="#666666"
                android:gravity="center"
                android:text="Call"
                android:textColor="#FFFFFF" />
            <TextView
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:background="#FF0000"
                android:gravity="center"
                android:text="Delete"
                android:textColor="#FFFFFF" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#33000000"
            android:gravity="center_vertical" >
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_marginLeft="10dp"
                android:src="@drawable/head_1" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="宋江" />
        </LinearLayout>
    </com.example.swipe.SwipeLayout>
</RelativeLayout>

重寫SwipeLayout 中mCallback 方法,實現簡單的拖拽

//3.處理回撥事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
    //返回值決定了child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        //child 被使用者拖拽的孩子
        return true;
    }
    //返回值決定將要移動到的位置,此時還沒有發生真正的移動
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        //left 建議移動到的位置
        return left;
    }
};

拖拽事件的傳遞

限定拖拽範圍

第一個子view 命名為後佈局,第二個子view 命名為前佈局

private View mBackView;
private View mFrontView;
//此方法中查詢控制元件
@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    mBackView = getChildAt(0);
    mFrontView = getChildAt(1);
};

獲取控制元件寬高及拖拽範圍

private int mRange;
private int mWidth;
private int mHeight;
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //mBackView 的寬度就是mFrontView 的拖拽範圍
    mRange = mBackView.getMeasuredWidth();
    //控制元件的寬
    mWidth = getMeasuredWidth();
    //控制元件的高
    mHeight = getMeasuredHeight();
}

重寫mCallback 回撥方法

ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
    //返回值決定了child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }
    //返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
    @Override
    public int getViewHorizontalDragRange(View child) {
        return mRange;
    }
    //返回值決定將要移動到的位置,此時還沒有發生真正的移動
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        // left 建議移動到位置
        if (child == mFrontView) {
            //限定前佈局的拖拽範圍
            if (left < -mRange) {
                //前佈局最小的左邊位置不能小於-mRange
                left = -mRange;
            } else if (left > 0) {
                //前佈局最大的左邊位置不能大於0
                left = 0;
            }
        } else if (child == mBackView) {
            //限定後佈局的拖拽範圍
            if (left < mWidth - mRange) {
                //後佈局最小左邊位置不能小於mWidth - mRange
                left = mWidth - mRange;
            } else if (left > mWidth) {
                //後佈局最大的左邊位置不能大於mWidth
                left = mWidth;
            }
        }
        return left;
    }
};

第7-11 行需要返回一個大於0 的拖拽範圍
第14-37 行通過mRange 分別計算前後佈局的拖拽範圍

傳遞拖拽事件

初始化前後佈局的位置,重寫SwipeLayout 的onLayout()方法

@Override
protected void onLayout(boolean changed, int left, int top, int right,
                        int bottom) {
    super.onLayout(changed, left, top, right, bottom);
    //預設是關閉狀態
    layoutContent(false);
};
private void layoutContent(boolean isOpen) {
    //設定前佈局位置
    Rect rect = computeFrontRect(isOpen);
    mFrontView.layout(rect.left, rect.top, rect.right, rect.bottom);
    //根據前佈局位置計算後佈局位置
    Rect backRect = computeBackRectViaFront(rect);
    mBackView.layout(backRect.left, backRect.top, backRect.right, backRect.bottom);
}

private Rect computeBackRectViaFront(Rect rect) {
    int left = rect.right;
    return new Rect(left, 0, left + mRange, mHeight);
}
/**
 * 計算佈局所在矩形區域
 * @param isOpen
 * @return
 */
private Rect computeFrontRect(boolean isOpen) {
    int left = 0;
    if(isOpen){
        left = -mRange;
    }
    return new Rect(left, 0, left + mWidth, mHeight);
}

第2-7 行重新擺放子view 的位置
第8-15 行由於後佈局是連線在前佈局後面一起滑動的,所以可以通過前佈局的位置計算後佈局的位置
前後佈局在拖拽過程中互相傳遞變化量

ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
    // 返回值決定了child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }
    // 返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
    @Override
    public int getViewHorizontalDragRange(View child) {
        return mRange;
    }
    // 返回值決定將要移動到的位置,此時還沒有發生真正的移動
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        // left 建議移動到位置
        if (child == mFrontView) {
            // 限定前佈局的拖拽範圍
            if (left < -mRange) {
                // 前佈局最小的左邊位置不能小於-mRange
                left = -mRange;
            } else if (left > 0) {
                // 前佈局最大的左邊位置不能大於0
                left = 0;
            }
        } else if (child == mBackView) {
            // 限定後佈局的拖拽範圍
            if (left < mWidth - mRange) {
                // 後佈局最小左邊位置不能小於mWidth - mRange
                left = mWidth - mRange;
            } else if (left > mWidth) {
                // 後佈局最大的左邊位置不能大於mWidth
                left = mWidth;
            }
        }
        return left;
    }
    //位置發生改變時,前後佈局的變化量互相傳遞
    @Override
    public void onViewPositionChanged(View changedView, int left, int top,
                                      int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
        //left 最新的水平位置
        //dx 剛剛發生的水平變化量
        //位置變化時,把水平變化量傳遞給另一個佈局
        if(changedView == mFrontView){
            //拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
            mBackView.offsetLeftAndRight(dx);
        }else if(changedView == mBackView){
            //拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
            mFrontView.offsetLeftAndRight(dx);
        }
        //相容低版本,重繪一次介面
        invalidate();
    }
};

第38-54 行拖拽前佈局時,將前佈局的變化量傳遞給後佈局,拖拽後佈局時,把後佈局的變化量傳遞給前佈局,這樣前後佈局就可以連動起來

結束動畫

跳轉動畫

// 3.處理回撥事件
ViewDragHelper.Callback mCallback = new ViewDragHelper.Callback() {
    // 返回值決定了child 是否可以被拖拽
    @Override
    public boolean tryCaptureView(View child, int pointerId) {
        return true;
    }
    // 返回拖拽的範圍,返回一個大於0 的值,計算動畫執行的時長,水平方向是否可以被滑開
    @Override
    public int getViewHorizontalDragRange(View child) {
        return mRange;
    }
    // 返回值決定將要移動到的位置,此時還沒有發生真正的移動
    @Override
    public int clampViewPositionHorizontal(View child, int left, int dx) {
        // left 建議移動到位置
        if (child == mFrontView) {
            // 限定前佈局的拖拽範圍
            if (left < -mRange) {
                // 前佈局最小的左邊位置不能小於-mRange
                left = -mRange;
            } else if (left > 0) {
                // 前佈局最大的左邊位置不能大於0
                left = 0;
            }
        } else if (child == mBackView) {
            // 限定後佈局的拖拽範圍
            if (left < mWidth - mRange) {
                // 後佈局最小左邊位置不能小於mWidth - mRange
                left = mWidth - mRange;
            } else if (left > mWidth) {
                // 後佈局最大的左邊位置不能大於mWidth
                left = mWidth;
            }
        }
        return left;
    }
    @Override
    public void onViewPositionChanged(View changedView, int left, int top,
                                      int dx, int dy) {
        super.onViewPositionChanged(changedView, left, top, dx, dy);
        //left 最新的水平位置
        //dx 剛剛發生的水平變化量
        //位置變化時,把水平變化量傳遞給另一個佈局
        if(changedView == mFrontView){
            //拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
            mBackView.offsetLeftAndRight(dx);
        }else if(changedView == mBackView){
            //拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
            mFrontView.offsetLeftAndRight(dx);
        }
        //相容低版本,重繪一次介面
        invalidate();
    }
    //鬆手時會被呼叫
    @Override
    public void onViewReleased(View releasedChild, float xvel, float yvel) {
        super.onViewReleased(releasedChild, xvel, yvel);
        //xvel 水平方向上的速度,向左為-,向右為+
        if(xvel == 0 && mFrontView.getLeft() < -mRange * 0.5f){
            //xvel 變0 時,並且前佈局的左邊位置小於-mRange 的一半
            open();
        }else if (xvel < 0){
            //xvel 為-時,開啟
            open();
        }else{
            //其它情況為關閉
            close();
        }
    }
};
public void close() {
    //呼叫之前佈局子view 的方法直接跳轉到關閉位置
    layoutContent(false);
}

public void open() {
    //呼叫之前佈局子view 的方法直接跳轉到開啟位置
    layoutContent(true);
}

第55-70 行重寫Callback 的onViewReleased()方法,該方法在鬆手後被呼叫,結束動畫需要在此處做

平滑動畫

public void close() {
    close(true);
}
public void open() {
    open(true);
}
public void close(boolean isSmooth) {
    int finalLeft = 0;
    if(isSmooth){
        if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
            ViewCompat.postInvalidateOnAnimation(this);
        };
    }else{
        layoutContent(false);
    }
}
public void open(boolean isSmooth) {
    int finalLeft = -mRange;
    if (isSmooth) {
        //mHelper.smoothSlideViewTo(child, finalLeft, finalTop)開啟一個平滑動畫將child
        //移動到finalLeft,finalTop 的位置上。此方法返回true 說明當前位置不是最終位置需要重繪
        if(mHelper.smoothSlideViewTo(mFrontView, finalLeft, 0)){
            //呼叫重繪方法
            //invalidate();可能會丟幀,此處推薦使用ViewCompat.postInvalidateOnAnimation()
            //引數一定要傳child 所在的容器,因為只有容器才知道child 應該擺放在什麼位置
            ViewCompat.postInvalidateOnAnimation(this);
        };
    } else {
        layoutContent(true);
    }
}
//重繪時computeScroll()方法會被呼叫
@Override
public void computeScroll() {
    super.computeScroll();
    //mHelper.continueSettling(deferCallbacks)維持動畫的繼續,返回true 表示還需要重繪
    if(mHelper.continueSettling(true)){
        ViewCompat.postInvalidateOnAnimation(this);
    }
}

第1-31 行過載open(),close()方法,保留跳轉動畫,新增平滑動畫
第32-39 行重寫computeScroll()方法維持動畫的繼續,此處必須重寫,否則沒有動畫效果

監聽回撥

定義回撥介面

在SwipeLayout 中定義公開的介面

//控制元件有三種狀態
public enum Status{
    Open,Close,Swiping
}
//初始狀態為關閉
private Status status = Status.Close;
public Status getStatus() {
    return status;
}

public void setStatus(Status status) {
    this.status = status;
}

public interface OnSwipeListener{
    //通知外界已經開啟
    public void onOpen();
    //通知外界已經關閉
    public void onClose();
    //通知外界將要開啟
    public void onStartOpen();
    //通知外界將要關閉
    public void onStartClose();
}
private OnSwipeListener onSwipeListener;
public OnSwipeListener getOnSwipeListener() {
    return onSwipeListener;
}
public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
    this.onSwipeListener = onSwipeListener;
}

第20-23 行SwipeLayout 做為ListView 的item 時將要開啟或關閉時需要通知其它item 做相應的處理,所以增加這兩個方法

更新狀態及回撥監聽

修改Callback 的onViewPositionChanged()方法

@Override
public void onViewPositionChanged(View changedView, int left, int top,
                                  int dx, int dy) {
    super.onViewPositionChanged(changedView, left, top, dx, dy);
    //left 最新的水平位置
    //dx 剛剛發生的水平變化量
    //位置變化時,把水平變化量傳遞給另一個佈局
    if(changedView == mFrontView){
        //拖拽的是前佈局,把剛剛發生的變化量dx 傳遞給後佈局
        mBackView.offsetLeftAndRight(dx);
    }else if(changedView == mBackView){
        //拖拽的是後佈局,把剛剛發生的變化量dx 傳遞給前佈局
        mFrontView.offsetLeftAndRight(dx);
    }
    //更新狀態及呼叫監聽
    dispatchDragEvent();
    //相容低版本,重繪一次介面
    invalidate();
}

第15-16 行呼叫更新狀態及回撥監聽的方法
dispatchDragEvent()方法

/**
 * 更新狀態回撥監聽
 */
protected void dispatchDragEvent() {
    //需要記錄一下上次的狀態,對比當前狀態和上次狀態,在狀態改變時呼叫監聽
    Status lastStatus = status;
    //獲取更新狀態
    status = updateStatus();
    //在狀態改變時呼叫監聽
    if(lastStatus != status && onSwipeListener != null){
        if(status == Status.Open){
            onSwipeListener.onOpen();
        }else if(status == Status.Close){
            onSwipeListener.onClose();
        }else if(status == Status.Swiping){
            if(lastStatus == Status.Close){
                //如果上一次狀態為關閉,現在是拖拽狀態,說明正在開啟
                onSwipeListener.onStartOpen();
            }else if(lastStatus == Status.Open){
                //如果上一次狀態為開啟,現在是拖拽狀態,說明正在關閉
                onSwipeListener.onStartClose();
            }
        }
    }
}
private Status updateStatus() {
    //通過前佈局左邊的位置可以判斷當前的狀態
    int left = mFrontView.getLeft();
    if(left == 0){
        return Status.Close;
    }else if(left == -mRange){
        return Status.Open;
    }
    return Status.Swiping;
}

修改activity_main.xml,給SwipeLayout 加上id

<RelativeLayout 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" >
    <com.example.swipe.SwipeLayout
        android:id="@+id/sl"
        android:layout_width="match_parent"
        android:layout_height="60dp" >

        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_gravity="right"
            android:layout_height="match_parent" >
            <TextView
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:background="#666666"
                android:gravity="center"
                android:text="Call"
                android:textColor="#FFFFFF" />
            <TextView
                android:layout_width="60dp"
                android:layout_height="match_parent"
                android:background="#FF0000"
                android:gravity="center"
                android:text="Delete"
                android:textColor="#FFFFFF" />
        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#33000000"
            android:gravity="center_vertical" >
            <ImageView
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_marginLeft="10dp"
                android:src="@drawable/head_1" />
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="10dp"
                android:text="宋江" />
        </LinearLayout>
    </com.example.swipe.SwipeLayout>
</RelativeLayout>

MainActivity 中設定監聽回撥

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SwipeLayout swipeLayout = (SwipeLayout) findViewById(R.id.sl);
        swipeLayout.setOnSwipeListener(new OnSwipeListener() {

            @Override
            public void onStartOpen() {
                Utils.showToast(getApplicationContext(), "要去打開了");
            }

            @Override
            public void onStartClose() {
                Utils.showToast(getApplicationContext(), "要去關閉了");
            }

            @Override
            public void onOpen() {
                Utils.showToast(getApplicationContext(), "已經打開了");
            }

            @Override
            public void onClose() {
                Utils.showToast(getApplicationContext(), "已經關閉了");
            }
        });
    }
}

Utils 提供單例Toast 方法

public class Utils {
    private static Toast toast;
    public static void showToast(Context context, String msg) {
        if (toast == null) {
            toast = Toast.makeText(context, "", Toast.LENGTH_SHORT);
        }
        toast.setText(msg);
        toast.show();
    }
}

整合到ListView

修改activity_main.xml

<RelativeLayout 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/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </ListView>
</RelativeLayout>

ListView 需要的資料

public class Cheeses {
    public static final String[] NAMES = new String[]{"宋江", "盧俊義", "吳用",
            "公孫勝", "關勝", "林沖", "秦明", "呼延灼", "花榮", "柴進", "李應", "朱仝", "魯智 深",
            "武松", "董平", "張清", "楊志", "徐寧", "索超", "戴宗", "劉唐", "李逵", "史進", " 穆弘",
            "雷橫", "李俊", "阮小二", "張橫", "阮小五", " 張順", "阮小七", "楊雄", "石秀", " 解珍",
            " 解寶", "燕青", "朱武", "黃信", "孫立", "宣贊", "郝思文", "韓滔", "彭玘", "單廷珪 ",
            "魏定國", "蕭讓", "裴宣", "歐鵬", "鄧飛", " 燕順", "楊林", "凌振", "蔣敬", "呂方 ",
            "郭盛", "安道全", "皇甫端", "王英", "扈三娘", "鮑旭", "樊瑞", "孔明", "孔亮", " 項充",
            "李袞", "金大堅", "馬麟", "童威", "童猛", "孟康", "侯健", "陳達", "楊春", "鄭天壽 ",
            "陶宗旺", "宋清", "樂和", "龔旺", "丁得孫", "穆春", "曹正", "宋萬", "杜遷", "薛永 ", "施恩",
            "周通", "李忠", "杜興", "湯隆", "鄒淵", "鄒潤", "朱富", "朱貴", "蔡福", "蔡慶", " 李立",
            "李雲", "焦挺", "石勇", "孫新", "顧大嫂", "張青", "孫二孃", " 王定六", "鬱保四", " 白勝",
            "時遷", "段景柱"};
}

修改SwipeLayout 的OnSwipeListener 介面,在回撥介面方法時把自己傳出去

public interface OnSwipeListener{
    //通知外界已經開啟
    public void onOpen(SwipeLayout swipeLayout);
    //通知外界已經關閉
    public void onClose(SwipeLayout swipeLayout);
    //通知外界將要開啟
    public void onStartOpen(SwipeLayout swipeLayout);
    //通知外界將要關閉
    public void onStartClose(SwipeLayout swipeLayout);
}

ListView 的Adapter

public class MyAdapter extends BaseAdapter {
    private Context     context;
    //記錄上一次被開啟item
    private SwipeLayout lastOpenedSwipeLayout;
    public MyAdapter(Context context) {
        super();
        this.context = context;
    }
    @Override
    public int getCount() {
        return Cheeses.NAMES.length;
    }
    @Override
    public Object getItem(int position) {
        return Cheeses.NAMES[position];
    }
    @Override
    public long getItemId(int position) {
        return position;
    }
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if(convertView == null){
            convertView = View.inflate(context, R.layout.list_item, null);
        }
        TextView name = (TextView) convertView.findViewById(R.id.name);
        name.setText(Cheeses.NAMES[position]);
        SwipeLayout swipeLayout = (SwipeLayout) convertView;
        swipeLayout.setOnSwipeListener(new OnSwipeListener() {
            @Override
            public void onOpen(SwipeLayout swipeLayout) {
                //當前item 被開啟時,記錄下此item
                lastOpenedSwipeLayout = swipeLayout;
            }
            @Override
            public void onClose(SwipeLayout swipeLayout) {
            }
            @Override
            public void onStartOpen(SwipeLayout swipeLayout) {
                //當前item 將要開啟時關閉上一次開啟的item
                if(lastOpenedSwipeLayout != null){
                    lastOpenedSwipeLayout.close();
                }
            }
            @Override
            public void onStartClose(SwipeLayout swipeLayout) {
            }
        });
        return convertView;
    }
}