酷炫開源專案cardsui-for-android-超詳細原始碼分析,詳解所用特效是如何實現的
以下是我擷取的2個圖片,可以自定義成Card形式的View,佈局可以自己設定。點選露出來的部分可以使點選的Card滑落到下面,也可以左右滑動刪除Card。效果非常好
這篇文章主要寫下通過原始碼分析一下幾個地方是怎麼實現的。
Card的View和佈局
相互疊層的card
點選翻轉下滑
左右移動刪除條目
可以根據需求自行決定觀看,哈哈
從git下載匯入專案中後,一共有3個檔案,一個是Library檔案,另外兩個是作者寫的Demo,第一個很簡單,第二個demo是可以用調色盤動態建立Card,所以需要另外下載ColorPicker和actionbarsherlock開源專案。
這篇文章只分析原始碼,所以就不看Demo了,只需要看Library檔案就行,下面正式開始:
CardsUILib檔案截圖
如圖一共只有3個包,10多個類。
這裡我們只關心這幾個類Card, CardStack, StackAdapter, SwipDismissTouchListener, CardUI,下面我們來一個一個分析
定義Card的view和佈局詳解:
Card
該類是一個抽象類並且繼承至AbstractCard抽象類,會重寫該抽象類的getView方法,請看我加的中文註解
Card類就是未來我們自定義Card類的父類,裡面定義了一些規範當然他也從AbstractCard中繼承了一些規範。@Override public View getView(Context context) { //getCardLayout(),這個方法返回一個佈局,該佈局只有一個FrameLayout,可以理解這句就是建立一個乾淨的空佈局 View view = LayoutInflater.from(context).inflate(getCardLayout(), null); mCardLayout = view; try { //這個是通過getCardContent()方法獲取View加到之前的空佈局上 //getCardContent()方法是一個抽象類,具體由子類實現,其實就是為了把子類的View加到父View的佈局上 ((FrameLayout) view.findViewById(R.id.cardContent)) .addView(getCardContent(context)); } catch (NullPointerException e) { e.printStackTrace(); } //為這個佈局設定寬高,和Margin,最後返回這個佈局,這個佈局就是Card的根View了,具體的‘長相’就看子類的實現啦 LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); int bottom = Utils.convertDpToPixelInt(context, 12); lp.setMargins(0, 0, 0, bottom); view.setLayoutParams(lp); return view; }
Card類裡還有一些其他的方法,比如設定了setOnClickListener(), getOnLongClickListener() 還有一個重要的介面OnCardSwiped 裡面有個onCardSwiped方法,這些都是讓自定義子類去看需求來實現的,過多的細節就不囉嗦
CardStack
該類的作用看名字就可以猜到,他是一個Card堆,或者說是Card的一個集合,專門存放Card。
講到這裡,需要跳一下,暫時把這個類放下先去看另一個類CardUI。一會回來再接著看他
CardUI
該類繼承自FrameLayout。哈哈,說到這裡大家是不是已經知道怎麼回事了。
沒錯,CardUI, CardStack, Card這3個類是一一包含的關係。CardStack裝入各種Card,最後再把他放到最終父佈局CardUI中。
我截個圖大家理解更清晰
整個所有是一個CardUI
所以CardUI中一定有addCard方法 和addStack方法
public void addCard(Card card, boolean refresh) {
CardStack stack = new CardStack();
stack.add(card);
mStacks.add(stack);
if (refresh)
refresh();
}
public void addStack(CardStack stack, boolean refresh) {
mStacks.add(stack);
if (refresh)
refresh();
}
每次add之後都會呼叫一下refresh()方法,原因當然就是要重新來調整佈局的大小啦
看refresh方法前先看一下這個類初始化的一些程式碼
//該方法在構造方法中呼叫,目的就是初始化該自定義佈局用到的
private void initData(Context context) {
mContext = context;
LayoutInflater inflater = LayoutInflater.from(context);
//所有CardStack的集合
mStacks = new ArrayList<AbstractCard>();
//inflate a different layout, depending on the number of columns
//自定義一個listview作為容器
if (mColumnNumber == 1) {
inflater.inflate(R.layout.cards_view, this);
// init observable scrollview
mListView = (QuickReturnListView) findViewById(R.id.listView);
} else {
//initialize the mulitcolumn view
//使用TableLayout作為容器,其原理和使用listview基本相同,不過效果沒有listview好,所以基本不用它
inflater.inflate(R.layout.cards_view_multicolumn, this);
mTableLayout = (TableLayout) findViewById(R.id.tableLayout);
}
// mListView.setCallbacks(this);
mHeader = inflater.inflate(R.layout.header, null);
mQuickReturnView = (ViewGroup) findViewById(R.id.sticky);
mPlaceholderView = mHeader.findViewById(R.id.placeholder);
}
作者的原始碼預設就是用自定義Listview(QuickReturnListView)作為父View放在自定義FrameLayout(CardUI)佈局中,我用過一次TableLayout,結果效果非常不好,我覺得原因可能是因為listview用adapter賦值的原因。
另外這裡我們其實不需要定義一個自定義的Listview,用系統帶的就行,作者這裡用了一個自定義的listview-QuickReturnListView,這個自定義Listview目的是為了新增一個隨著拖拽一起移動的header用的,這部分程式碼可以去看CardUI的setHeader()方法。我覺得這個header沒必要這麼做,而且作者在給的demo中也沒有這方面的例子,所以這裡就不管它了。直接看refresh方法
CardUI-refresh該方法主要作用就是用新的資料給父view容器賦值
public void refresh() {
if (mAdapter == null) {
//StackAdapter繼承自BaseAdapter,用於過listview都知道幹什麼的,這裡就把他當listview用法一樣想就對了
mAdapter = new StackAdapter(mContext, mStacks, mSwipeable);
//這個判斷是用mListview做父view還是mTableLayout做父view
if (mListView != null) {
//如果是listview做父view就可以這樣想象:這個自定義的FrameLayout中有一個Listview,這個Listview中的每個Item就是一個CardStack
mListView.setAdapter(mAdapter);
} else if (mTableLayout != null) {
//如果是TableLayout做父View就可以想象成:這個自定義的FrameLayout中有一個TableRow,每一個Row裡面放一個CardStack,原理和Listview相同,因為基本不用TableRow,所以關於TableRow的程式碼就省略了
.....
}
上面程式碼的目的就是例項化一個StackAdapter介面卡為Listvew賦值,所以直接來到StackAdapter
StackAdapter
只看getView方法就夠了
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//獲取當前的CardStack
final CardStack stack = getItem(position);
stack.setAdapter(this);
stack.setPosition(position);
// the CardStack can decide whether to use convertView or not
//呼叫stack的getView,就是為了把存放在Stack中的Card的view都取出來,放在convertView中返回給Listview作為item的View
convertView = stack.getView(mContext, convertView, mSwipeable);
return convertView;
}
看到這個stack.getView了吧,轉了一大圈終於轉回來了。下面主要來講這個stack中重寫的getView方法
Card相互疊層樣式詳解
Card疊層的效果其實就是通過多個view的覆蓋來實現的,具體看下面程式碼
CardStack--getView
這個getView方法就是把所有的Card繪製出來,並且設定一些card的監聽
public View getView(Context context, View convertView, boolean swipable) {
mContext = context;
.....
//這個是CardStack的根view了(也就是每個listview的item的view),佈局檔案可以自己定義,想要什麼樣的自己改就好了
final View view = LayoutInflater.from(context).inflate(
R.layout.item_stack, null);
.....
//這裡開始就通過迴圈將CardStack中每個Card的view加入父view容器中,每個Card的具體處理的程式碼也都是在這裡,所以這是本文最重點的地方了
Card card;
View cardView;
//迴圈遍歷每一個Card
for (int i = 0; i < cardsArraySize; i++) {
card = cards.get(i);
//初始化佈局大小
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.WRAP_CONTENT);
int topPx = 0;
// handle the view
if (i == 0) { //第一個,也就是最下面的一個
cardView = card.getViewFirst(context);
}
else if (i == lastCardPosition) { //最後一個,也就是最上面的一個
cardView = card.getViewLast(context);
}
else { //中間的
cardView = card.getView(context);
}
// handle the listener
//最後一個Card,也就是最上面的一個,直接和我們互動的一個Card
//設定它的點選監聽,這裡的實現寫在具體的子類裡面,因為不同的Card的點選操作各不相同
if (i == lastCardPosition) {
cardView.setOnClickListener(card.getClickListener());
cardView.setOnLongClickListener(card.getOnLongClickListener());
}
//非最後一個Card,也就是不是最上面的card,是被蓋住的。
//這裡的監聽主要是為了點選他漏出來的部分可以讓他翻轉覆蓋到最上面,所以這個監聽的具體實現可以寫在本類中,因為這個是共通的
else {
cardView.setOnClickListener(getClickListener(this, container, i));
cardView.setOnLongClickListener(getOnLongClickListener(this, container, i));
}
//*這裡打個星號,這段程式碼就是控制Card檢視疊層效果的,原理就是從第二個card開始,向下移動特定的位置,這個位置取決與你的CardStack的佈局檔案,這裡是45f和最後減去12f的調整。
if (i > 0) {
float dp = (_45F * i) - _12F;
topPx = Utils.convertDpToPixelInt(context, dp);
}
//計算出第二個和第一個的偏移量以後,就設定它的margin,就會實現出一個疊一個並且露出頭部的效果了
lp.setMargins(0, topPx, 0, 0);
cardView.setLayoutParams(lp);
//判斷是否可以左右滑動
if (swipable) {
//為cardview設定觸控監聽器,監聽器的實現是一個自定義的SwipeDismissTouchListener,
//這裡有個自定義的Callback回撥介面--OnDismissCallback,裡面有一個onDismiss回撥方法
//先不急看他,先進入到SwipeDismissTouchListener監聽器中去看看
cardView.setOnTouchListener(new SwipeDismissTouchListener(
cardView, card, new OnDismissCallback() {
@Override
public void onDismiss(View view, Object token) {
//含簡單,就是刪除這個Card,並通知adapter
Card c = (Card) token;
// call onCardSwiped() listener
c.OnSwipeCard();
cards.remove(c);
mAdapter.setItems(mStack, getPosition());
// refresh();
mAdapter.notifyDataSetChanged();
}
}));
}
//將cardView放在View容器中
container.addView(cardView);
}
return view;
}
左右滑動刪除card詳解:
SwipeDismissListener
這個類繼承View.OnTouchListener介面,所以需要重寫onTouch方法,去監聽使用者的觸控操作
在switch中判斷觸控操作,這裡有3個DOWN,UP,MOVE分別是按下,擡起,滑動
執行順序是按下,滑動,擡起 或者是按下,擡起
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
// offset because the view is translated during swipe
motionEvent.offsetLocation(mTranslationX, 0);
if (mViewWidth < 2) {
//取得view的寬度
mViewWidth = mView.getWidth();
}
switch (motionEvent.getActionMasked()) {
//按下操作
case MotionEvent.ACTION_DOWN: {
//記錄按下點的X座標
mDownX = motionEvent.getRawX();
//一個速率記錄器,就是記錄操作速度用的,具體的去google吧
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(motionEvent);
//如果返回false,表明處理沒有完成,可以交給onClick,和onLongClick接著處理
//如果返回true,則表明已經對該事件做了處理,不會繼續傳遞下去了
//這裡我們還需要onClick,onLongClick處理,所以返回false
return false;
}
//擡起操作,建議先看下面的滑動操作後再來看擡起操作,因為這是一個完整的操作順序
case MotionEvent.ACTION_UP: {
if (mVelocityTracker == null) {
break;
}
//deltaX表示當前點減去按下時的點的差,就是X軸上移動的距離
float deltaX = motionEvent.getRawX() - mDownX;
//下面4句就是得到滑動時X軸的速度,和Y軸的速度
mVelocityTracker.addMovement(motionEvent);
mVelocityTracker.computeCurrentVelocity(1000);
float velocityX = Math.abs(mVelocityTracker.getXVelocity());
float velocityY = Math.abs(mVelocityTracker.getYVelocity());
//dismiss這個變數記錄的此次滑動是否成功,成功則刪除,不成功復原
boolean dismiss = false;
boolean dismissRight = false;
//成功條件1,滑動的距離大於當前view的一半寬
if (Math.abs(deltaX) > mViewWidth / 2) {
dismiss = true;
//記錄,判斷滑動時向左還是向右
dismissRight = deltaX > 0;
}
//成功條件2,X軸滑動速度大於設定的一個最小速度,小於設定的最大速度,並且大於Y軸的速度
else if (mMinFlingVelocity <= velocityX
&& velocityX <= mMaxFlingVelocity && velocityY < velocityX) {
dismiss = true;
//記錄,判斷滑動時向左還是向右
dismissRight = mVelocityTracker.getXVelocity() > 0;
}
//上面2個條件滿足任何一個都可以執行滑動刪除
if (dismiss) {
//執行動畫,先判斷是左滑還是右滑,之後朝著這個方向移動整個view長度,並且透明度減到0,在動畫結束時呼叫performDismiss()方法
animate(mView)
.translationX(dismissRight ? mViewWidth : -mViewWidth)
.alpha(0).setDuration(mAnimationTime)
.setListener(new AnimatorListener() {
//動畫結束時呼叫
@Override
public void onAnimationEnd(Animator arg0) {
performDismiss();
}
});
} else {
..........
}
//手指滑動操作
case MotionEvent.ACTION_MOVE: {
if (mVelocityTracker == null) {
break;
}
mVelocityTracker.addMovement(motionEvent);
//deltaX表示當前點減去按下時的點的差,就是X軸上移動的距離
float deltaX = motionEvent.getRawX() - mDownX;
//這個判斷可以理解為手指滑動幅度,超過規定距離後,可以執行滑動刪除操作
if (Math.abs(deltaX) > mSlop) {
//是否可以滑動標記,標記為ture
mSwiping = true;
//標記為true以後,意思可以理解為onTouchEvent事件不會再繼續傳遞
mView.getParent().requestDisallowInterceptTouchEvent(true);
// Cancel listview's touch
//這裡已經完成任務,所以new一個cancelEvent取消該次觸控操作
MotionEvent cancelEvent = MotionEvent.obtain(motionEvent);
cancelEvent
.setAction(MotionEvent.ACTION_CANCEL
| (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
mView.onTouchEvent(cancelEvent);
cancelEvent.recycle();
}
//判斷能否執行滑動刪除
if (mSwiping) {
//這個是剛才得到的滑動操作在X軸上的位移量
mTranslationX = deltaX;
//這裡使用animator動畫的setTranslationX方法,將當前的view在X軸上移動deltaX個偏移量
//這裡要說一下animator動畫,他是3.0以後加入的新動畫,他和animation的區別我理解就是animation只是View的繪製效果的改變,而真正View屬性不變
//animator不僅是效果,他的屬性,位置和大小都會跟著變化。
ViewHelper.setTranslationX(mView, deltaX);
// TODO: use an ease-out interpolator or such
//這個動畫的作用是根據滑動的距離來改變view的透明度,演算法大家可以仔細看看,不難理解,而且設計的很巧妙
setAlpha(mView,Math.max(0f,Math.min(1f, 1f - 2f * Math.abs(deltaX)/ mViewWidth)));
return true;
}
break;
}
}
return false;
}
在動畫結束的時候呼叫這個方法,來真正刪除card
private void performDismiss() {
//要刪除view的佈局引數
final ViewGroup.LayoutParams lp = mView.getLayoutParams();
//要刪除view的高度
final int originalHeight = mView.getHeight();
//這個動畫作用是要將origalHeight的值在mAniamtionTime中縮減到1
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, 1)
.setDuration(mAnimationTime);
animator.addListener(new AnimatorListenerAdapter() {
@Override
//這個在所有動畫結束後呼叫
public void onAnimationEnd(Animator animation) {
//這句會呼叫實現了這個callback介面的onDismiss方法,這個方法我們是在CardStack方法中重寫的,其作用就是真正的刪除這個Card
mCallback.onDismiss(mView, mToken);
//復原動畫層,這段我也有點不是太懂,為什么要給他復原會去,我刪除過這段程式碼,效果沒有什麼變化,哪位朋友知道希望可以告訴我一下。。。
setAlpha(mView, 1f);
ViewHelper.setTranslationX(mView, 0);
lp.height = originalHeight;
mView.setLayoutParams(lp);
}
});
//這段程式碼會更新動畫改編的值,所以可以在update監聽函式中執行需要處理改值的操作
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
lp.height = (Integer) valueAnimator.getAnimatedValue();//就是要減少他的高度,一直到1為止
mView.setLayoutParams(lp);
}
});
//開始動畫
animator.start();
}
在動畫結束的時候會呼叫callback介面的onDismiss方法,去重寫這個方法的CardStack中看看
CardStack getView
cardView.setOnTouchListener(new SwipeDismissTouchListener(
cardView, card, new OnDismissCallback() {
@Override
public void onDismiss(View view, Object token) {
//很簡單,就是刪除這個Card,並通知adapter
Card c = (Card) token;
// call onCardSwiped() listener
c.OnSwipeCard();
cards.remove(c);
mAdapter.setItems(mStack, getPosition());
// 通知重新整理;
mAdapter.notifyDataSetChanged();
}
}));
點選被覆蓋的Card翻轉到最前面詳解:
滑動刪除正式結束,這個開源元件還有另外一個特色,就是點選後面的Card,會翻轉到前面來,接著滑落到最下,其他的則依次上升。
接下來就看這個是怎麼實現的
還記得在CardStack中會給card設定onclick的監聽嗎?
// handle the listener
//最後一個Card,也就是最上面的一個,直接和我們互動的一個Card
//設定它的點選監聽,這裡的實現寫在具體的子類裡面,因為不同的Card的點選操作各不相同
if (i == lastCardPosition) {
cardView.setOnClickListener(card.getClickListener());
cardView.setOnLongClickListener(card.getOnLongClickListener());
}
//非最後一個Card,也就是不是最上面的card,是被蓋住的。
//這裡的監聽主要是為了點選他漏出來的部分可以讓他翻轉覆蓋到最上面,所以這個監聽的具體實現可以寫在本類中,因為這個是共通的
else {
cardView.setOnClickListener(getClickListener(this, container, i));
cardView.setOnLongClickListener(getOnLongClickListener(this, container, i));
}
就是這裡了,我們點選被覆蓋的card的監聽就是寫在getOnLongClickListener(this, container, i)這個方法中
看程式碼前先看下截圖,這樣可能會容易理解一些
開始看程式碼,這裡是一個點選監聽,之後根據點選card的位置不同調用不同的處理方法。有兩種情況:
1.點選的是被覆蓋在最後面的card則呼叫onClickFirstCard(cardStack, container, index, views);
2.不是則呼叫onClickOtherCard(cardStack, container, index, views,last);
private OnClickListener getClickListener(final CardStack cardStack,
final RelativeLayout container, final int index) {
return new OnClickListener() {
@Override
public void onClick(View v) {
View[] views = new View[container.getChildCount()];
for (int i = 0; i < views.length; i++) {
views[i] = container.getChildAt(i);
}
//last是view陣列中card的最後一個,就是在最外面露出來的那個card
int last = views.length - 1;
if (index != last) {
//點選的是最裡面的一個card
if (index == 0) {
onClickFirstCard(cardStack, container, index, views);
}
//點選的是夾在中間的card
else if (index < last) {
onClickOtherCard(cardStack, container, index, views, last);
}
}
}
}
下面我們來分別看看這兩個方法是如何處理的 //點選的是覆蓋在最裡面的card
public void onClickFirstCard(final CardStack cardStack,
final RelativeLayout frameLayout, final int index,
View[] views) {
// run through all the cards
for (int i = 0; i < views.length; i++) {
ObjectAnimator anim = null;
//先處理最裡面的view,需要讓它從最裡面一直滑落到最下面
if (i == 0) {
//滑落的單個距離,這個距離就是兩個card之間的距離
float downFactor = 0;
//下面的45F就是兩個card之間差的高度,這個高度是根據CardStack的佈局計算出來的,如果用自定義的佈局話需要重新計算這個差值
if (views.length > 2) {
//有幾個card就滑落相應個數*每個card相差的高度(這裡是45F),就計算出card需要下滑的距離了
downFactor = convertDpToPixel((_45F)
* (views.length - 1) - 1);
} else {
downFactor = convertDpToPixel(_45F);
}
//使用animator動畫,在讓view在Y軸上移動剛才計算出需要下滑的高度,這樣就改變了card的位置了
anim = ObjectAnimator.ofFloat(views[i],
NINE_OLD_TRANSLATION_Y, 0, downFactor);
//當然只改變位置還不夠,還要有別的處理,所以這裡給動畫加了監聽器
anim.addListener(getAnimationListener(cardStack,
frameLayout, index, views[index]));
}
//這個是倒數第二個view,如果最裡面的滑落到最下面,則他需要一升一個位置
else if (i == 1) {
//這裡為什麼是17f,不是45f。因為他有一個最上面的彈起的效果,先上升一些,最後的時候再全升上去
float upFactor = convertDpToPixel(-17f);
anim = ObjectAnimator.ofFloat(views[i],
NINE_OLD_TRANSLATION_Y, 0, upFactor);
}
//其餘的一次上升一個card間相差的單位
else {
float upFactor = convertDpToPixel(-1 * _45F);
anim = ObjectAnimator.ofFloat(views[i],
NINE_OLD_TRANSLATION_Y, 0, upFactor);
}
if (anim != null)
//開始動畫
anim.start();
}
}
這裡看這個動畫監聽器裡的實現,看看移動完card的位置後還需要什麼操作 private AnimatorListener getAnimationListener(final CardStack cardStack,
final RelativeLayout frameLayout, final int index,
final View clickedCard) {
return new AnimatorListener() {
//動畫開始時先觸發這個監聽回撥方法
@Override
public void onAnimationStart(Animator animation) {
//如果idex=0,則表示移動的是最裡面的card
//如果移動最裡面的card的話,之前倒數第二個就會升上去變成第一個,這裡要對倒數第二個card做一些佈局上的操作
if (index == 0) {
View newFirstCard = frameLayout.getChildAt(1);
newFirstCard.setBackgroundResource(com.fima.cardsui.R.drawable.card_background);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT , WRAP_CONTENT);
int top = 0;
int bottom = 0;
top = 2 * Utils.convertDpToPixelInt(mContext, 8)
+ Utils.convertDpToPixelInt(mContext, 1);
bottom = Utils.convertDpToPixelInt(mContext, 12);
lp.setMargins(0, top, 0, bottom);
newFirstCard.setLayoutParams(lp);
newFirstCard.setPadding(0, Utils.convertDpToPixelInt(mContext, 8), 0, 0);
}
else {
clickedCard
.setBackgroundResource(com.fima.cardsui.R.drawable.card_background);
}
//這裡是最最關鍵得操作,大家知道為什麼要先刪除這個card檢視再加回來嗎?
//目的就是為了實現點選card後先使覆蓋在裡面的card先“彈”出來,之後在執行動畫滑下去。
//所以先把檢視刪除,再新增這樣這個card的檢視就在最外面了,多麼巧妙的設計哈
frameLayout.removeView(clickedCard);
frameLayout.addView(clickedCard);
}
//動畫結束時觸發的監聽回撥方法
@Override
public void onAnimationEnd(Animator animation) {
//之前只是把檢視的位置移動了,但是真正card在集合裡的物理位置並沒有改變
//所以和檢視同樣的操作,先刪除再新增,這時card位置就到最後面了
Card card = cardStack.remove(index);
cardStack.add(card);
//重設 重新整理
mAdapter.setItems(cardStack, cardStack.getPosition());
mAdapter.notifyDataSetChanged();
}
};
}
onClickOtherCard方法和onClickFirstCard方法差不多,就不帶著看了。
這個開源元件的幾個特點我都介紹完畢了。其實發現了不少這個元件的缺點,網上有個更好的card型別的開源元件,叫做cardslib。但是這個工程太大了,不太適合研究學習。但是他的架構和效果確實比這個好多了。
所以我最近正在看這個開源專案,它裡面有很多card的一些其他功能比如刪除後恢復,點選擴充套件card檢視等等。
我準備把這些功能都加入到現在這個開源元件中,並且儘量優化一下,因為這個元件一共才10多個類,完全可以把幾個關鍵的類拷貝到專案中從而省去匯入lib的麻煩。如果這篇文章受歡迎的話,我會盡早做出來並分享出來。
第一次寫技術blog,寫了好幾天,寫到後來發現前面寫的過於墨跡,註解太多了,所以後面我就儘量只標註關鍵的地方,如果大家有看不懂的可以留言,有錯誤也希望大家指出。最後希望大家多多支援哈