【Android開源項目解析】RecyclerView側(cè)滑刪除粒子效果實現(xiàn)——初探Android開源粒子庫 Leonids

前兩天在微博上看到了這個側(cè)滑刪除的粒子效果,但是只有IOS的,所以心血來潮,寫了個玩玩,下面簡單介紹下實現(xiàn)的思路

項目簡介

先不廢話,上效果圖

項目地址:https://github.com/ZhaoKaiQiang/ParticleLayout

實現(xiàn)原理解析

其實看了那么多的關于側(cè)滑刪除的項目,再來思考這個問題,就so easy了!

咱們先分析下需求:

  • 側(cè)滑手勢檢測
  • 粒子跟手效果
  • 刪除狀態(tài)判斷
  • 數(shù)據(jù)源刷新

ok,知道需求了,咱們看對策

  • 手勢檢測可以重寫onTouch,判斷移動方向和距離
  • 粒子效果使用第三方的開源項目leonids,跟手效果就是簡單的觸摸位置的更新
  • 假定滑動距離超過item的寬度一半,就代表刪除
  • 添加回調(diào)接口,完成數(shù)據(jù)源刷新

代碼實現(xiàn)

知道了咱們的需求,并且每一個需求都有了解決方案,那么剩下的問題其實就是如何寫代碼的問題了。

下面這部分,最好參考這個項目的源碼進行閱讀~

首先,這肯定是屬于自定義控件,那么咱們繼承誰呢?我選擇繼承FrameLayout,為啥呢?因為在FrameLayout里面咱們可以控制遮罩效果。

其實完成遮罩效果,也有兩種方案,

  1. 在FrameLayout中放置一個和背景色相同的布局,然后再onTouch中控制寬度,來模擬遮罩效果
  2. 直接重寫dispatchDraw(Canvas canvas) ,使用Canvas.clipRect()控制繪制區(qū)域,模擬遮罩效果

其實這兩種效果我都做過,在第一個版本中使用的是方案一,可以完成這個效果,但是不知道怎么回事,在5.0以上系統(tǒng)中,遮罩層的層級關系和5.0以下不一致,因此導致在5.0以上不能使用。除此之外, 使用第一種效果需要多一層布局,效率低,而且通用性不好,所以在這里我選擇第二種方案。

咱們開始看代碼~

public ParticleLayout(Context context) {
        this(context, null);
    }

    public ParticleLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ParticleLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        backLayoutRect = new Rect();
        backLocation = new int[2];
    }

構(gòu)造函數(shù)非常簡單,在三個參數(shù)構(gòu)造函數(shù)中,初始化兩個變量,backLayoutRect用于控制內(nèi)容區(qū)域,用于后面的觸摸邊界檢測,backLocation則用于存儲布局位置,粒子效果需要用坐標。

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (getChildCount() != 1) {
            throw new IllegalArgumentException("the count of child view must be one !");
        }

        backLayout = (ViewGroup) getChildAt(0);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        backLayout.getLocationInWindow(backLocation);
        backLayoutRect.set(backLocation[0], backLocation[1],
                backLocation[0] + backLayout.getMeasuredWidth(),
                backLocation[1] + backLayout.getMeasuredHeight());
    }

上面的代碼很好理解,在onSizeChange()里面對子View數(shù)量進行強制規(guī)定,必須為一個,方便獲取到內(nèi)容區(qū)域,而在onLayout()則在測量、布局之后,獲取到布局所在位置和初始化內(nèi)容區(qū)域backLayoutRect。

到現(xiàn)在位置,就初始化完畢了,下面其實就是觸摸事件的寫法。

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getX() > backLayoutRect.width() * 4 / 5) {
                    isSwape = true;
                    startX = event.getX();

                    if (bitmapArrays == null || bitmapArrays.length == 0) {
                        particleSystem = new ParticleSystem((Activity) getContext(), COUNT_OF_PARTICAL_BITMAP, DEFAULT_PARTICLE_BITMAP, TIME_TO_LIVE);
                    } else {
                        Random random = new Random();
                        int resId = bitmapArrays[random.nextInt(bitmapArrays.length)];
                        particleSystem = new ParticleSystem((Activity) getContext(), COUNT_OF_PARTICAL_BITMAP, resId, TIME_TO_LIVE);
                    }

                    particleSystem.setAcceleration(0.00013f, 90)
                            .setSpeedByComponentsRange(0f, 0.3f, 0.05f, 0.3f)
                            .setFadeOut(TIME_TO_FADE_OUT, new AccelerateInterpolator())
                            .emitWithGravity(backLayout, Gravity.RIGHT, COUNT_OF_PARTICAL_BITMAP);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                clipWidth = (int) (startX - event.getX());
                if (isSwape && clipWidth > 0) {
                    requestLayout();
                    particleSystem.updateEmitVerticalLine(backLayoutRect.right - clipWidth, backLayoutRect.top - getStatuBarHeight(), backLayoutRect.bottom - getStatuBarHeight());
                } else {
                    particleSystem.stopEmitting();
                }
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                startX = 0;
                clipWidth = 0;
                invalidate();
                isSwape = false;
                particleSystem.stopEmitting();
                getParent().requestDisallowInterceptTouchEvent(false);

                if (event.getX() >= getWidth() / 2) {
                    isDelete = false;
                } else {
                    isDelete = true;
                }

                if (isDelete) {
                    if (mDeleteListener != null) {
                        mDeleteListener.onDelete();
                    }
                }
                break;
        }

        if (isSwape) {
            return true;
        }

        return super.onTouchEvent(event);
    }

雖然代碼長了點,但是也不難啊,onInterceptTouchEvent返回true,就是為了這個區(qū)域內(nèi)的任何觸摸事件,我都要攔截一下,至于到底處不處理,看爺心情~

在onTouchEvent()里面實現(xiàn)了核心的代碼邏輯,咱們一起看一下~

首先ACTION_DOWN的時候,如果觸摸的位置是寬度的4/5處,則認為想要側(cè)滑啦,記下開始的X坐標startX,然后在下面初始化了ParticleSystem對象。這個ParticleSystem對象是粒子庫leonids里面的主要業(yè)務類,在這里實現(xiàn)了諸如粒子圖片、存活時間、加速插值器、漸變消失時間等等參數(shù)。

到了ACTION_MOVE,如果x的移動距離是正數(shù),也就是往左滑,并且isSwape為true,就 requestLayout()一下,這個是為啥呢?這是因為如果不 requestLayout(),那么布局的位置就還是初始化時候的位置,從而導致粒子效果位置不對,所以重新計算一下現(xiàn)在的位置,然后調(diào)用

 particleSystem.updateEmitVerticalLine(backLayoutRect.right - clipWidth, backLayoutRect.top - getStatuBarHeight(), backLayoutRect.bottom - getStatuBarHeight());

當然,這個方法在原先的粒子庫是沒有的,我自己添加上去的,就是為了能順著一條豎線往外發(fā)送粒子,感興趣的可以去看源碼。

下面這句代碼是為了只要處于觸摸模式,那么外面的父控件,也就是RecyclerView就不會劫持觸摸事件了。

getParent().requestDisallowInterceptTouchEvent(true);

最后,ACTION_UP和ACTION_CANCEL,在這里完成數(shù)據(jù)的初始化和刪除狀態(tài)的判斷,并且如果有監(jiān)聽器,則調(diào)用。

當然,如果isSwape為true,那么觸摸時間就被消耗了,否則,默認處理即可。

這個時候跟手粒子已經(jīng)實現(xiàn)了,那么遮罩咋辦?

簡單~

 @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.clipRect(0, 0, backLayoutRect.right - clipWidth, getHeight());
        super.dispatchDraw(canvas);
    }

在畫子View之前,clipRect一下,這樣,就只會繪制范圍內(nèi)的布局,遮罩效果也就算是實現(xiàn)了。

如何使用

使用也非常簡單,首先看布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.socks.library.ParticleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/root_layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@android:color/white">

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="2dp"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginTop="2dp"
        app:cardBackgroundColor="@android:color/white"
        app:cardCornerRadius="4dp">

        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:gravity="center"
            android:text="測試"
            android:textColor="@android:color/primary_text_light"
            android:textSize="20sp" />
    </android.support.v7.widget.CardView>

</com.socks.library.ParticleLayout>

就和平常的FrameLayout一樣使用即可,再看下MainActivity

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private ParticleAdapter mAdapter;
    private LinearLayoutManager layoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        mAdapter = new ParticleAdapter();
        recyclerView.setAdapter(mAdapter);
    }


    private class ParticleAdapter extends RecyclerView.Adapter<ParticleAdapter.ParticleViewHolder> {

        private ArrayList<String> strings;

        ParticleAdapter() {
            strings = new ArrayList<>();
            for (int i = 0; i < 20; i++) {
                strings.add("POSITION = " + i);
            }
        }

        @Override
        public ParticleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = getLayoutInflater().inflate(R.layout.item_layout, parent, false);
            return new ParticleViewHolder(view);
        }

        @Override
        public void onBindViewHolder(final ParticleViewHolder holder, final int position) {
            holder.tv.setText(strings.get(position));
            holder.root_layout.setDeleteListener(new ParticleLayout.DeleteListener() {
                @Override
                public void onDelete() {
                    Toast.makeText(MainActivity.this, "DETELE", Toast.LENGTH_SHORT).show();
                    strings.remove(position);
                    mAdapter.notifyItemRemoved(position);
                    mAdapter.notifyItemRangeChanged(position, strings.size() - position);
                }
            });
            holder.root_layout.setBitmapArrays(R.drawable.ic_star, R.drawable.ic_partical, R.drawable.ic_boom);
        }

        @Override
        public int getItemCount() {
            return strings.size();
        }

        class ParticleViewHolder extends RecyclerView.ViewHolder {

            TextView tv;
            ParticleLayout root_layout;

            public ParticleViewHolder(View itemView) {
                super(itemView);
                tv = (TextView) itemView.findViewById(R.id.tv);
                root_layout = (ParticleLayout) itemView.findViewById(R.id.root_layout);
            }
        }
    }
}

代碼非常簡單,不需要我再說了吧?

唯一需要注意的是,在刪除之后,需要刷新數(shù)據(jù)源,否則就會刪除錯誤位置的數(shù)據(jù),

下面的代碼是為了添加粒子圖片,隨機顯示。

 holder.root_layout.setBitmapArrays(R.drawable.ic_star, R.drawable.ic_partical, R.drawable.ic_boom);

是不是很簡單?so easy~

拜拜~

更多參考

Android開源粒子庫 Leonids


尊重原創(chuàng),轉(zhuǎn)載請注明:From 凱子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵權(quán)必究!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,316評論 25 708
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,523評論 0 17
  • 有那樣一個瞬間 我突然 愛上了你 愛上那風 那陽光 和你俏皮的影子 即使我們從不了解 即使 我不敢有一點點的期許 ...
    銀古的蟲閱讀 264評論 0 2
  • 1、類中什么時候可以定義類方法: 如果你不想每次使用方法都需要創(chuàng)建對象開辟存儲空間 并且如果該方法中沒有使用到屬性...
    山中石頭閱讀 655評論 0 1
  • 環(huán)境,方向 1.一旦環(huán)境脫離,所有毛病立馬顯現(xiàn)! 2.沉淀,反思,自己為什么出發(fā),自己要到哪兒去!方向,,,,,,...
    橘子俠閱讀 179評論 0 0