1. 程式人生 > >一分鐘輕鬆打造左滑刪除+下拉重新整理Listview控制元件

一分鐘輕鬆打造左滑刪除+下拉重新整理Listview控制元件

2017年8月21號更新內容:
最新程式碼已經上傳到github,部落格中原始碼解析中的程式碼不是最新的
github地址:https://github.com/guitarstar/SwipeLayout
如果僅僅關注如果使用,可以看如下直接使用的方法:

步驟 1. Add the JitPack repository to your build file

allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}
步驟 2. Add the dependency

dependencies {
        compile 'com.github.guitarstar:SwipeLayout:v1.0'
} 步驟 3. 列表的佈局layout編寫 <com.solo.library.SlideTouchView android:id="@+id/mSlideTouchView" android:layout_width="match_parent" android:layout_height="60dp"> <!-- 下層佈局 --> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent"
android:orientation="horizontal"> <Button android:id="@+id/btn_del" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="@android:color/holo_red_light" android:text="刪除"/> </LinearLayout> <!-- 上層佈局 --> <LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent" android:background="#fff"><!-- 這裡設個背景顏色將下層佈局遮掩 --> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center"/> </LinearLayout> </com.solo.library.SlideTouchView> 步驟 4. ListView的adapter繼承SlideBaseAdapter(使用recyclerview的adapter繼承SlideRecyclerViewBaseAdapter) 如果使用ListView的情況: public class LvAdapter extends SlideBaseAdapter { List list; public LvAdapter(List list) { this.list = list; } @Override public int[] getBindOnClickViewsIds() { return new int[]{R.id.btn_del}; //必須呼叫, 刪除按鈕或者其他你想監聽點選事件的View的id } @Override public int getCount() { return list.size(); } @Override public Object getItem(int position) { return list.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { MyViewHolder holder = new MyViewHolder(); if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_list_item, null); holder.tv = (TextView) convertView.findViewById(tv); holder.mSlideTouchView = (SlideTouchView) convertView.findViewById(R.id.mSlideTouchView); convertView.setTag(holder); bindSlideState(holder.mSlideTouchView); //必須呼叫 } else { holder = (MyViewHolder) convertView.getTag(); } bindSlidePosition(holder.mSlideTouchView, position);//必須呼叫 holder.tv.setText(String.valueOf(list.get(position))); return convertView; } 如果使用recyclerview的情況: public class RcyAdapter extends SlideRecyclerViewBaseAdapter { List<Integer> list; public RcyAdapter(List<Integer> list) { this.list = list; } @Override public int[] getBindOnClickViewsIds() { return new int[]{R.id.btn_del}; //必須呼叫, 刪除按鈕或者其他你想監聽點選事件的View的id } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_list_item, null); return new MyViewHolder(v); } @Override public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { MyViewHolder viewHolder = (MyViewHolder) holder; viewHolder.tv.setText(String.valueOf(list.get(position))); bindSlidePosition(viewHolder.mSlideTouchView, position);//必須呼叫 } @Override public int getItemCount() { return list.size(); } class MyViewHolder extends RecyclerView.ViewHolder { TextView tv; SlideTouchView mSlideTouchView; public MyViewHolder(View itemView) { super(itemView); tv = (TextView) itemView.findViewById(R.id.tv); mSlideTouchView = (SlideTouchView) itemView.findViewById(R.id.mSlideTouchView); bindSlideState(mSlideTouchView);//必須呼叫 } } } 步驟 5. 點選事件監聽 adapter.setupRecyclerView(mRecyclerView); //裡面的邏輯是監聽滾動關閉按鈕顯示 (ListView中呼叫adapter.setupListView(mListView);) adapter.setOnClickSlideItemListener(new OnClickSlideItemListener() { @Override public void onItemClick(ISlide iSlideView, View v, int position) { //點選item時會回撥此方法(onClick中也會回撥) Toast.makeText(v.getContext(), "click item position:" + position, Toast.LENGTH_SHORT).show(); } @Override public void onClick(ISlide iSlideView, View v, int position) { //控制元件的所有子控制元件的點選回撥都會回撥此方法 if (v.getId() == R.id.btn_del) { //對刪除按鈕的監聽(上面adapter的getBindOnClickViewsIds()中設定了R.id.btn_del) iSlideView.close(); //關閉當前的按鈕 list.remove(position); adapter.notifyDataSetChanged(); } } });

如下是原始碼解析內容:

左滑刪除最早應該是在iOS中出現的一個控制元件,後來相應地有了不少的android模仿者,其中一個就是我們偉大的android版騰訊QQ啦,它和iOS版的顯示效果又有點不同,具體不同看下圖:這裡寫圖片描述
大家可以用你的android手機和你的iPhone 6s開啟手機QQ對比一下效果~~ 什麼?沒有iPhone 6s %¥……*……@% 沒關係,有腎就好,哈哈哈
先看看最終實現的效果:
這裡寫圖片描述
下面我們開始考慮怎麼來實現。
首先,我們有兩個點需要思考的:

  1. 我們是通過繼承listview來實現呢,還是在listview載入相應的左滑刪除佈局。
  2. 怎麼樣來拖動這些佈局。

我是這樣考慮的:

  1. github上下拉重新整理的輪子多得數不清,如果用繼承listview的話不好整合。
  2. 可以用ViewDragHelper來實現。

好了,我們開始正式coding。
第一步:實現一個可以左右拖動的自定義View

package com.solo.charge.view;

import android.content.Context;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;

/**
 * Created by ling on 2015/9/9.
 */
public class DragView extends RelativeLayout implements View.OnClickListener{
    private View fgView , bgView;
    private ViewDragHelper mDrager;
    private DragStateListener mDragStateListener;
    private final int DRAG_LEFT = -1 , DRAG_RIGHT = 1;
    private int dragMode = DRAG_LEFT;
    private float minX , maxX;
    public DragView(Context context) {
        super(context);
    }

    public DragView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mDrager = ViewDragHelper.create(this, 5f, new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
                return child == fgView;
            }

            @Override
            public int getViewHorizontalDragRange(View child) {
                return bgView.getMeasuredWidth();
            }


            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return getPositionX(left);
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return 0;
            }

            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                if(Math.abs(fgView.getLeft()) != 0 || Math.abs(fgView.getLeft()) != bgView.getMeasuredWidth()){
                    float x = fgView.getLeft() + 0.1f * xvel;
                    mDrager.smoothSlideViewTo(fgView,
                            Math.abs(getPositionX(x)) > bgView.getMeasuredWidth() / 2 ? bgView.getMeasuredWidth() * dragMode : 0, 0);
                    postInvalidate();
                }

            }

            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
                if(changedView == fgView)
                    getParent().requestDisallowInterceptTouchEvent(fgView.getLeft() != 0 ? true : false);
                if(mDragStateListener != null){
                    if(left == 0){
                        mDragStateListener.onClosed(DragView.this);
                    }else if(Math.abs(left) == bgView.getMeasuredWidth()){
                        mDragStateListener.onOpened(DragView.this);
                    }
                }
            }
        });
    }

    public View getForegroundView() {
        return fgView;
    }

    public View getBackgroundView() {
        return bgView;
    }

    public void open(){
        fgView.offsetLeftAndRight(dragMode * (bgView.getMeasuredWidth() - fgView.getLeft()));
    }
    public void close(){
        fgView.offsetLeftAndRight(-fgView.getLeft());
    }
    public void openAnim(){
        mDrager.smoothSlideViewTo(fgView, bgView.getMeasuredWidth() * dragMode, 0);
        postInvalidate();
    }
    public void closeAnim(){
        mDrager.smoothSlideViewTo(fgView, 0, 0);
        postInvalidate();
    }

    public boolean isOpen(){
        return Math.abs(fgView.getLeft()) == bgView.getMeasuredWidth();
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //drag range
        if(dragMode == DRAG_LEFT){
            minX = -bgView.getMeasuredWidth();
            maxX = 0;
        }else{
            minX = 0;
            maxX = bgView.getMeasuredWidth();
        }
    }
    private int getPositionX(float x){
        if(x < minX) x = minX;
        if(x > maxX) x = maxX;
        return (int) x;
    }
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        if(getChildCount() != 2)
            throw new IllegalArgumentException("must contain only two child view");
        fgView = getChildAt(1);
        bgView = getChildAt(0);
        if(!(fgView instanceof ViewGroup && bgView instanceof ViewGroup))
            throw new IllegalArgumentException("ForegroundView and BackgoundView must be a subClass of ViewGroup");
        RelativeLayout.LayoutParams param = (RelativeLayout.LayoutParams)bgView.getLayoutParams();
        param.addRule(dragMode == DRAG_LEFT ? RelativeLayout.ALIGN_PARENT_RIGHT : RelativeLayout.ALIGN_PARENT_LEFT);
        param.width = LayoutParams.WRAP_CONTENT;

        //bind onClick Event
        fgView.setOnClickListener(this);
        int bgViewCount = ((ViewGroup)bgView).getChildCount();
        for (int i = 0; i < bgViewCount; i++) {
            View child = ((ViewGroup) bgView).getChildAt(i);
            if(child.isClickable()) child.setOnClickListener(this);
        }
    }

    @Override
    public void computeScroll() {
        if(mDrager.continueSettling(true)){
            postInvalidate();
        }
    }

    public void setOnDragStateListener(DragStateListener listener){
        mDragStateListener = listener;
    }

    @Override
    public void onClick(View v) {
        if(mDragStateListener != null) {
            if(v == fgView){
                if(isOpen()){
                    closeAnim();
                    return;
                }
                mDragStateListener.onForegroundViewClick(DragView.this , v);
            }else {
                mDragStateListener.onBackgroundViewClick(DragView.this , v);
            }
        }
    }

    public interface DragStateListener{
        void onOpened(DragView dragView);
        void onClosed(DragView dragView);
        void onForegroundViewClick(DragView dragView, View v);
        void onBackgroundViewClick(DragView dragView, View v);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return mDrager.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mDrager.processTouchEvent(event);
        return true;
    }
}

為了方便對這種上下層的關係來進行佈局免去自己計算處理的麻煩,DragView選擇繼承RelativeLayout來實現。
131~132行,分別取出上層佈局fgView和下層佈局bgView,然後就是使用ViewDragHelper(沒有使用過可以百度一下其用法,用起來還是很方便的)來對fgView控制拖動。
140~145行,處理點選事件,上層佈局fgView的點選事件來代替listview中的OnItemClickListener;而下層佈局bgView往往不僅僅只有一個刪除按鈕,也有可能有多個按鈕,還需要分別對這些按鈕來進行響應。
66行,在拖動的過程中不允許listview對其觸控事件攔截,防止拖動時和listview滾動發生衝突。

第二步,我們就可以開始用上面寫好的DragView了,下面是ListView的Item的佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.solo.charge.view.DragView
        android:id="@+id/drag_view"
        android:layout_width="match_parent"
        android:layout_height="60dp">
        <!-- 下層佈局 -->
        <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal">
            <Button
                android:id="@+id/btn_del"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
               android:background="@android:color/holo_red_light"
                android:text="刪除"/>
        </LinearLayout>
        <!-- 上層佈局 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#fff"><!-- 這裡設個背景顏色將下層佈局遮掩 -->
            <TextView
                android:id="@+id/tv"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"/>
        </LinearLayout>
    </com.solo.charge.view.DragView>
</LinearLayout>

第三步,在Acitivity中使用ListView來展現它,但這裡我們還要給ListView加下拉重新整理功能,這裡我們直接用輪子(https://github.com/chrisbanes/Android-PullToRefresh),為了方便測試,adaper也在Acitivity中來實現:

package com.solo.charge;

import android.os.Bundle;
import android.os.Handler;
import android.support.v7.app.ActionBarActivity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
import com.solo.charge.view.DragView;

import java.util.ArrayList;
import java.util.List;

public class TestActivity extends ActionBarActivity {
    private PullToRefreshListView mPullToRefreshListView;
    private List<String> list = new ArrayList<String>();
    private MyAdapter adapter = new MyAdapter();
    private Handler mHandler = new Handler();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        mPullToRefreshListView = (PullToRefreshListView) findViewById(R.id.listview);
        for (int i = 0 ; i < 20 ; i++){
            list.add(String.valueOf(i));
        }
        mPullToRefreshListView.setAdapter(adapter);
        mPullToRefreshListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {

            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                adapter.close();
            }
        });
        mPullToRefreshListView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener<ListView>() {
            @Override
            public void onRefresh(PullToRefreshBase<ListView> refreshView) {
                //模擬一下重新整理資料,額
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        list.clear();
                        for (int i = 0; i < 20; i++) {
                            list.add(String.valueOf(i));
                        }
                        adapter.notifyDataSetChanged();
                        mPullToRefreshListView.onRefreshComplete();
                    }
                }, 1000);
            }
        });
    }
    class MyAdapter extends BaseAdapter{
        List<DragView> views = new ArrayList<DragView>();
        @Override
        public int getCount() {
            return list.size();
        }

        @Override
        public Object getItem(int position) {
            return list.get(position);
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            ViewHolder holder = new ViewHolder();
            if(convertView == null){
                convertView = getLayoutInflater().inflate(R.layout.test_item , null);
                convertView.setTag(holder);
                DragView view = (DragView) convertView.findViewById(R.id.drag_view);
                views.add(view);
                view.setOnDragStateListener(new DragView.DragStateListener() {
                    @Override
                    public void onOpened(DragView dragView) {

                    }

                    @Override
                    public void onClosed(DragView dragView) {

                    }

                    @Override
                    public void onForegroundViewClick(DragView dragView , View v) {
                        int pos = (int) dragView.getTag();
                        Toast.makeText(TestActivity.this , "click item" + pos , Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onBackgroundViewClick(DragView dragView , View v) {
                        int pos = (int) dragView.getTag();

                        list.remove(pos);
                        adapter.notifyDataSetChanged();
                        Toast.makeText(TestActivity.this , ((Button)v).getText().toString() + pos , Toast.LENGTH_SHORT).show();
                    }
                });

            }else{
                holder = (ViewHolder) convertView.getTag();
            }
            holder.dv = (DragView) convertView.findViewById(R.id.drag_view);
            holder.dv.setTag(position);
            holder.dv.close();
            holder.tv = (TextView) convertView.findViewById(R.id.tv);
            holder.tv.setText((String) getItem(position));
            return convertView;
        }

        class ViewHolder{
            DragView dv;
            TextView tv;
        }

        public void close(){
            for (int i = 0; i < views.size(); i++) {
                if(views.get(i).isOpen())
                    views.get(i).closeAnim();
            }
        }
    }
}

在122行和104/110行,為了防止有ListView的快取機制造成資料混亂。這裡不同的position對應的點選事件也可以進行封裝一下(上面沒有處理),並在adaper裡提供OnItemClickListener介面來處理點選事件。

好了,也提供一下Activity對應的佈局檔案:

<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="com.solo.charge.TestActivity">
    <com.handmark.pulltorefresh.library.PullToRefreshListView
        android:id="@+id/listview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

大功告成!
demo.apk
第一次寫部落格,寫得不好、不對的地方還望大家指出。