(本篇文章已授權(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