如何利用RecyclerView打造炫酷滑動(dòng)卡片

(本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布)

前言

前段時(shí)間一直在B站追《黑鏡》第三季,相比前幾季谱醇,這季很良心的拍了六集泽论,??著實(shí)過(guò)了一把癮。由于看的是字幕組貢獻(xiàn)的版本该面,每集開(kāi)頭都插了一個(gè)app的廣告夭苗,叫“人人美劇”,一向喜歡看美劇的我便掃了一下二維碼隔缀,安裝了試一試题造。我打開(kāi)app,匆匆滑動(dòng)了一下首頁(yè)的美劇列表猾瘸,然后便隨手切換到了訂閱頁(yè)面晌梨,然后,我就被訂閱頁(yè)面的動(dòng)畫(huà)效果吸引住了须妻。

沒(méi)錯(cuò)仔蝌,就是上面這玩意兒,是不是很炫酷荒吏,本著發(fā)揚(yáng)一名碼農(nóng)的職業(yè)精神敛惊,我心里便癢癢的想實(shí)現(xiàn)這種效果,當(dāng)然因?yàn)殚L(zhǎng)期的fork compile绰更,第一時(shí)間我還是上網(wǎng)搜了搜瞧挤,有木有哪位好心人已經(jīng)開(kāi)源了類似的控件锡宋。借助強(qiáng)大的Google,我馬上搜到了一個(gè)項(xiàng)目 SwipeCards特恬,是仿照探探的老父親Tinder的app動(dòng)畫(huà)效果打造的执俩,果然程序員都一個(gè)操行,看到好看的就想動(dòng)手實(shí)現(xiàn),不過(guò)人家的成績(jī)讓我可望而不可及~

他實(shí)現(xiàn)的效果是這樣的:

嗯癌刽,還不錯(cuò)役首,為了進(jìn)行思想上的碰撞,我就download了一下他的源碼显拜,稍稍read了一下_

作為一個(gè)有思想衡奥,有抱負(fù)的程序員,怎么能滿足于compile別人的庫(kù)呢远荠?必須得自己動(dòng)手矮固,豐衣足食啊譬淳!

正式開(kāi)工

思考

一般這種View都是自定義的档址,然后重寫(xiě)onLayout,但是有木有更簡(jiǎn)單的方法呢邻梆?由于項(xiàng)目里一直使用RecyclerView辰晕,那么能不能用RecyclerView來(lái)實(shí)現(xiàn)這種效果呢?能确虱,當(dāng)然能昂选!得力于RecyclerView優(yōu)雅的擴(kuò)展性校辩,我們完全可以自定義一個(gè)LayoutManager來(lái)實(shí)現(xiàn)嘛窘问。

布局實(shí)現(xiàn)

RecyclerView可以通過(guò)自定義LayoutManager來(lái)實(shí)現(xiàn)各種布局,官方自己提供了LinearLayoutManager宜咒、GridLayoutManager惠赫,相比于ListView,可謂是方便了不少故黑。同樣儿咱,我們也可以通過(guò)自定義LayoutManager,實(shí)現(xiàn)這種View一層層疊加的效果场晶。

自定義LayoutManager混埠,最重要的是要重寫(xiě)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);
        }
    }
}

這種布局實(shí)現(xiàn)起來(lái)其實(shí)相當(dāng)簡(jiǎn)單,因?yàn)槊總€(gè)child的left和top都一樣诗轻,直接設(shè)置為0就可以了钳宪,這樣child就依次疊加在一起了,至于最后兩句,主要是為了使頂部Child之下的childs有一種縮放的效果吏颖。

動(dòng)畫(huà)實(shí)現(xiàn)

下面到了最重要的地方了搔体,主要分為以下幾個(gè)部分。

(1)手勢(shì)追蹤

當(dāng)手指按下時(shí)半醉,我們需要取到RecyclerView的頂部Child疚俱,并讓其跟隨手指滑動(dòng)。

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);
}

手指按下的時(shí)候缩多,記錄topChildView的位置呆奕,移動(dòng)的時(shí)候,根據(jù)偏移量瞧壮,動(dòng)態(tài)調(diào)整topChildView的位置,就實(shí)現(xiàn)了基本效果匙握。但是這樣還不夠咆槽,記得我們?cè)趯?shí)現(xiàn)布局時(shí),對(duì)其他子View進(jìn)行了縮放嗎圈纺?那時(shí)候的縮放是為現(xiàn)在做準(zhǔn)備的秦忿。當(dāng)手指在屏幕上滑動(dòng)時(shí),我們同樣會(huì)調(diào)用updateNextItem()蛾娶,對(duì)topChildView下面的子view進(jìn)行縮放灯谣。

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計(jì)算很簡(jiǎn)單,只要當(dāng)topChildView滑動(dòng)到設(shè)置的邊界時(shí)蛔琅,nextView剛好縮放到原本大小胎许,即factor=1,就可以了罗售。因?yàn)閚extView一開(kāi)始縮放為0.8辜窑,所以可計(jì)算出:

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

(2)抬起手指

手指抬起后,我們要進(jìn)行狀態(tài)判斷

1.滑動(dòng)未超過(guò)邊界

此時(shí)我們需要對(duì)topChildView進(jìn)行歸位寨躁。

2.超過(guò)邊界

此時(shí)我們需要根據(jù)滑動(dòng)方向穆碎,使topChildView飛離屏幕。

對(duì)于這兩種情況职恳,我們都是通過(guò)計(jì)算view的終點(diǎn)坐標(biāo)所禀,然后利用動(dòng)畫(huà)實(shí)現(xiàn)的。對(duì)于第一種放钦,很簡(jiǎn)單色徘,targetX和targetY直接就是topChildView的原始坐標(biāo)。但是對(duì)于第二種操禀,需要根據(jù)topChildView的原始坐標(biāo)和目前坐標(biāo)贺氓,計(jì)算出線性表達(dá)式,然后再根據(jù)targetX來(lái)計(jì)算targetY,至于targetX辙培,往右飛targetX就可以賦為getScreenWidth蔑水,而往左就直接為0-view.width,只要終點(diǎn)在屏幕外就可以扬蕊。具體代碼如下搀别。

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);
                    }
                }
            });

}

對(duì)于第二種情況,如果直接啟動(dòng)動(dòng)畫(huà)尾抑,并在動(dòng)畫(huà)結(jié)束時(shí)通知adapter刪除item歇父,在連續(xù)操作時(shí),會(huì)導(dǎo)致數(shù)據(jù)錯(cuò)亂再愈。但是如果在動(dòng)畫(huà)啟動(dòng)時(shí)直接移除item榜苫,又會(huì)失去動(dòng)畫(huà)效果。所以我在這里采用了另一種辦法翎冲,在動(dòng)畫(huà)開(kāi)始前創(chuàng)建一個(gè)與topChildView一模一樣的鏡像View垂睬,添加到DecorView上,并隱藏刪除掉topChildView抗悍,然后利用鏡像View來(lái)展示動(dòng)畫(huà)驹饺。添加鏡像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;
}

因?yàn)殓R像View是添加在DecorView上的,topChildView父容器是RecyclerVIew缴渊,而View的x赏壹、y是相對(duì)于父容器而言的,所以鏡像View的targetX和targetY需要加上一定偏移量衔沼。

好了到這里蝌借,一切就準(zhǔn)備就緒了,下面讓我們看看動(dòng)畫(huà)效果如何指蚁。

總結(jié)

效果是不是還不錯(cuò)骨望,項(xiàng)目地址在這里: https://github.com/HalfStackDeveloper/SwipeCardRecyclerView,歡迎大家fork AND star欣舵!也希望大家在使用app擎鸠,看到一些酷炫效果的時(shí)候,也自己去動(dòng)手實(shí)現(xiàn)缘圈,誰(shuí)讓我們是有著職業(yè)精神的碼農(nóng)呢劣光!

(轉(zhuǎn)載請(qǐng)標(biāo)明ID:半棧工程師,個(gè)人博客:https://halfstackdeveloper.github.io)

歡迎關(guān)注我的知乎專欄:https://zhuanlan.zhihu.com/halfstack

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末糟把,一起剝皮案震驚了整個(gè)濱河市绢涡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌遣疯,老刑警劉巖雄可,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡数苫,警方通過(guò)查閱死者的電腦和手機(jī)聪舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)虐急,“玉大人箱残,你說(shuō)我怎么就攤上這事≈褂酰” “怎么了被辑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)敬惦。 經(jīng)常有香客問(wèn)我盼理,道長(zhǎng),這世上最難降的妖魔是什么俄删? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任宏怔,我火速辦了婚禮,結(jié)果婚禮上抗蠢,老公的妹妹穿的比我還像新娘举哟。我一直安慰自己思劳,他們只是感情好迅矛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著潜叛,像睡著了一般秽褒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上威兜,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天销斟,我揣著相機(jī)與錄音,去河邊找鬼椒舵。 笑死蚂踊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笔宿。 我是一名探鬼主播犁钟,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泼橘!你這毒婦竟也來(lái)了涝动?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炬灭,失蹤者是張志新(化名)和其女友劉穎醋粟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡米愿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年厦凤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吗货。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泳唠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宙搬,到底是詐尸還是另有隱情笨腥,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布勇垛,位于F島的核電站脖母,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏闲孤。R本人自食惡果不足惜谆级,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望讼积。 院中可真熱鬧肥照,春花似錦、人聲如沸勤众。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)们颜。三九已至吕朵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窥突,已是汗流浹背努溃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阻问,地道東北人梧税。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像称近,于是被迫代替她去往敵國(guó)和親第队。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評(píng)論 25 707
  • 內(nèi)容抽屜菜單ListViewWebViewSwitchButton按鈕點(diǎn)贊按鈕進(jìn)度條TabLayout圖標(biāo)下拉刷新...
    皇小弟閱讀 46,708評(píng)論 22 664
  • 簡(jiǎn)介: 提供一個(gè)讓有限的窗口變成一個(gè)大數(shù)據(jù)集的靈活視圖煌茬。 術(shù)語(yǔ)表: Adapter:RecyclerView的子類...
    酷泡泡閱讀 5,140評(píng)論 0 16
  • 第一次去面試斥铺。 第一次面試就是去阿里uc 準(zhǔn)備當(dāng)然是不夠充分的。電視劇里白領(lǐng)刷卡進(jìn)門(mén)坛善,保安巡邏的陣勢(shì)出現(xiàn)在眼前晾蜘,竟...
    y小賢閱讀 153評(píng)論 0 0
  • 今天在家里邻眷,似乎也沒(méi)那么興奮,原先想做的事剔交,好像也沒(méi)特別想去做肆饶,就這樣各種攤! 爸媽全都在家陪著岖常,好像有點(diǎn)小尷尬驯镊,...
    ypguy閱讀 175評(píng)論 0 0