如何利用RecyclerView打造炫酷滑動卡片

(本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布)

前言

前段時間一直在B站追《黑鏡》第三季,相比前幾季,這季很良心的拍了六集,??著實過了一把癮。由于看的是字幕組貢獻的版本,每集開頭都插了一個app的廣告,叫“人人美劇”,一向喜歡看美劇的我便掃了一下二維碼,安裝了試一試。我打開app,匆匆滑動了一下首頁的美劇列表,然后便隨手切換到了訂閱頁面,然后,我就被訂閱頁面的動畫效果吸引住了。

沒錯,就是上面這玩意兒,是不是很炫酷,本著發揚一名碼農的職業精神,我心里便癢癢的想實現這種效果,當然因為長期的fork compile,第一時間我還是上網搜了搜,有木有哪位好心人已經開源了類似的控件。借助強大的Google,我馬上搜到了一個項目 SwipeCards,是仿照探探的老父親Tinder的app動畫效果打造的,果然程序員都一個操行,看到好看的就想動手實現,不過人家的成績讓我可望而不可及~

他實現的效果是這樣的:

嗯,還不錯,為了進行思想上的碰撞,我就download了一下他的源碼,稍稍read了一下_

作為一個有思想,有抱負的程序員,怎么能滿足于compile別人的庫呢?必須得自己動手,豐衣足食啊!

正式開工

思考

一般這種View都是自定義的,然后重寫onLayout,但是有木有更簡單的方法呢?由于項目里一直使用RecyclerView,那么能不能用RecyclerView來實現這種效果呢?能,當然能啊!得力于RecyclerView優雅的擴展性,我們完全可以自定義一個LayoutManager來實現嘛。

布局實現

RecyclerView可以通過自定義LayoutManager來實現各種布局,官方自己提供了LinearLayoutManager、GridLayoutManager,相比于ListView,可謂是方便了不少。同樣,我們也可以通過自定義LayoutManager,實現這種View一層層疊加的效果。

自定義LayoutManager,最重要的是要重寫onLayoutChildren()

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    detachAndScrapAttachedViews(recycler);
    for (int i = 0; i < getItemCount(); i++) {
        View child = recycler.getViewForPosition(i);
        measureChildWithMargins(child, 0, 0);
        addView(child);
        int width = getDecoratedMeasuredWidth(child);
        int height = getDecoratedMeasuredHeight(child);
        layoutDecorated(child, 0, 0, width, height);
        if (i < getItemCount() - 1) {
            child.setScaleX(0.8f);
            child.setScaleY(0.8f);
        }
    }
}

這種布局實現起來其實相當簡單,因為每個child的left和top都一樣,直接設置為0就可以了,這樣child就依次疊加在一起了,至于最后兩句,主要是為了使頂部Child之下的childs有一種縮放的效果。

動畫實現

下面到了最重要的地方了,主要分為以下幾個部分。

(1)手勢追蹤

當手指按下時,我們需要取到RecyclerView的頂部Child,并讓其跟隨手指滑動。

public boolean onTouchEvent(MotionEvent e) {
    if (getChildCount() == 0) {
        return super.onTouchEvent(e);
    }
    View topView = getChildAt(getChildCount() - 1);
    float touchX = e.getX();
    float touchY = e.getY();
    switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTopViewX = topView.getX();
            mTopViewY = topView.getY();
            mTouchDownX = touchX;
            mTouchDownY = touchY;
            break;
        case MotionEvent.ACTION_MOVE:
            float dx = touchX - mTouchDownX;
            float dy = touchY - mTouchDownY;
            topView.setX(mTopViewX + dx);
            topView.setY(mTopViewY + dy);
            updateNextItem(Math.abs(topView.getX() - mTopViewX) * 0.2 / mBorder + 0.8);
            break;
        case MotionEvent.ACTION_UP:
            mTouchDownX = 0;
            mTouchDownY = 0;
            touchUp(topView);
            break;
    }
    return super.onTouchEvent(e);
}

手指按下的時候,記錄topChildView的位置,移動的時候,根據偏移量,動態調整topChildView的位置,就實現了基本效果。但是這樣還不夠,記得我們在實現布局時,對其他子View進行了縮放嗎?那時候的縮放是為現在做準備的。當手指在屏幕上滑動時,我們同樣會調用updateNextItem(),對topChildView下面的子view進行縮放。

private void updateNextItem(double factor) {
    if (getChildCount() < 2) {
        return;
    }
    if (factor > 1) {
        factor = 1;
    }
    View nextView = getChildAt(getChildCount() - 2);
    nextView.setScaleX((float) factor);
    nextView.setScaleY((float) factor);
}

這里的factor計算很簡單,只要當topChildView滑動到設置的邊界時,nextView剛好縮放到原本大小,即factor=1,就可以了。因為nextView一開始縮放為0.8,所以可計算出:

factor=Math.abs(topView.getX() - mTopViewX) * 0.2 / mBorder + 0.8

(2)抬起手指

手指抬起后,我們要進行狀態判斷

1.滑動未超過邊界

此時我們需要對topChildView進行歸位。

2.超過邊界

此時我們需要根據滑動方向,使topChildView飛離屏幕。

對于這兩種情況,我們都是通過計算view的終點坐標,然后利用動畫實現的。對于第一種,很簡單,targetX和targetY直接就是topChildView的原始坐標。但是對于第二種,需要根據topChildView的原始坐標和目前坐標,計算出線性表達式,然后再根據targetX來計算targetY,至于targetX,往右飛targetX就可以賦為getScreenWidth,而往左就直接為0-view.width,只要終點在屏幕外就可以。具體代碼如下。

private void touchUp(final View view) {
    float targetX = 0;
    float targetY = 0;
    boolean del = false;
    if (Math.abs(view.getX() - mTopViewX) < mBorder) {
        targetX = mTopViewX;
        targetY = mTopViewY;
    } else if (view.getX() - mTopViewX > mBorder) {
        del = true;
        targetX = getScreenWidth()*2;
        mRemovedListener.onRightRemoved();
    } else {
        del = true;
        targetX = -view.getWidth()-getScreenWidth();
        mRemovedListener.onLeftRemoved();
    }
    View animView = view;
    TimeInterpolator interpolator = null;
    if (del) {
        animView = getMirrorView(view);
        float offsetX = getX() - mDecorView.getX();
        float offsetY = getY() - mDecorView.getY();
        targetY = caculateExitY(mTopViewX + offsetX, mTopViewY + offsetY, animView.getX(), animView.getY(), targetX);
        interpolator = new LinearInterpolator();
    } else {
        interpolator = new OvershootInterpolator();
    }
    final boolean finalDel = del;
    animView.animate()
            .setDuration(500)
            .x(targetX)
            .y(targetY)
            .setInterpolator(interpolator)
            .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    if (!finalDel) {
                        updateNextItem(Math.abs(view.getX() - mTopViewX) * 0.2 / mBorder + 0.8);
                    }
                }
            });

}

對于第二種情況,如果直接啟動動畫,并在動畫結束時通知adapter刪除item,在連續操作時,會導致數據錯亂。但是如果在動畫啟動時直接移除item,又會失去動畫效果。所以我在這里采用了另一種辦法,在動畫開始前創建一個與topChildView一模一樣的鏡像View,添加到DecorView上,并隱藏刪除掉topChildView,然后利用鏡像View來展示動畫。添加鏡像View的代碼如下:

private ImageView getMirrorView(View view) {
    view.destroyDrawingCache();
    view.setDrawingCacheEnabled(true);
    final ImageView mirrorView = new ImageView(getContext());
    Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
    mirrorView.setImageBitmap(bitmap);
    view.setDrawingCacheEnabled(false);
    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight());
    int[] locations = new int[2];
    view.getLocationOnScreen(locations);

    mirrorView.setAlpha(view.getAlpha());
    view.setVisibility(GONE);
    ((SwipeCardAdapter) getAdapter()).delTopItem();
    mirrorView.setX(locations[0] - mDecorViewLocation[0]);
    mirrorView.setY(locations[1] - mDecorViewLocation[1]);
    mDecorView.addView(mirrorView, params);
    return mirrorView;
}

因為鏡像View是添加在DecorView上的,topChildView父容器是RecyclerVIew,而View的x、y是相對于父容器而言的,所以鏡像View的targetX和targetY需要加上一定偏移量。

好了到這里,一切就準備就緒了,下面讓我們看看動畫效果如何。

總結

效果是不是還不錯,項目地址在這里: https://github.com/HalfStackDeveloper/SwipeCardRecyclerView,歡迎大家fork AND star!也希望大家在使用app,看到一些酷炫效果的時候,也自己去動手實現,誰讓我們是有著職業精神的碼農呢!

(轉載請標明ID:半棧工程師,個人博客:https://halfstackdeveloper.github.io)

歡迎關注我的知乎專欄:https://zhuanlan.zhihu.com/halfstack

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,710評論 25 708
  • 內容抽屜菜單ListViewWebViewSwitchButton按鈕點贊按鈕進度條TabLayout圖標下拉刷新...
    皇小弟閱讀 46,859評論 22 665
  • 簡介: 提供一個讓有限的窗口變成一個大數據集的靈活視圖。 術語表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,199評論 0 16
  • 第一次去面試。 第一次面試就是去阿里uc 準備當然是不夠充分的。電視劇里白領刷卡進門,保安巡邏的陣勢出現在眼前,竟...
    y小賢閱讀 157評論 0 0
  • 今天在家里,似乎也沒那么興奮,原先想做的事,好像也沒特別想去做,就這樣各種攤! 爸媽全都在家陪著,好像有點小尷尬,...
    ypguy閱讀 177評論 0 0