最近有朋友展示了一種效果,就是ListView在滑動的過程中新加入的item會有一個從底部滑入的效果,我感覺這種效果還算不錯,就去想了想拿到我身上應該怎麼去實現這種效果,在試過幾種方案後,最後選擇了一種使用起來還算比較簡單的方式拿出來分享一下。
在開始分享之前,先來看看我們需要做成什麼效果吧,
恩,看到什麼效果了嗎?仔細看滑動過程中的底部,新加入的item會以一種動畫的形式加入,馬上,我們就來實現這種效果!
實現方式的選擇
當我第一次看到這種效果的時候,我首先想到的是LayoutAnimation
,不過很快就讓我否決了,爲什麼呢?我們能的動畫只是在新加入的item的起到效果,而其他的item是沒有這個效果的,這種方式用LayoutAnimation
是做不到的, 那我們應該怎麼實現呢?關鍵點就在動畫在什麼時候有效!前面我們說過很多次了,
新加入的item會以一種動畫的形式加入
我們只要抓住這一點往下想,很快就能想到Adapter
的getView
上,在新item加入的時候,Adapter的getView必定是去執行的,我們沿着這個思路往下走,很快我們又遇到問題了,
我們怎麼判斷已經滑動到底部了?
很多人會不假思索的回答,這個問題容易! 這不和ListView的分頁一樣嗎!當然不是了, ListView分頁的底部是判斷的數據集的最後一項,不太適用我們這裏,而且縱觀ListView的幾個方法,我們並沒有找到合適的方法去使用,所以,我們只能自己去判斷了,怎麼判斷?還是在監聽的OnScrollListener
裏,在這裏面判斷是不是往下滑動的,至於是不是到達底部了,交給getView去做!所以我們還需要在Adapter中知道ListView的存在,並給他設置滑動監聽。
假設,現在我們已經可以可以判斷滑動到底部了,我們用一個狀態變量isScrollDown
來表示,那是不是現在我們就可以在 getView
裏通過isScrollDown
來判斷是否給convertView
一個動畫呢?
基本的實現思路已經闡述完了,下面我們就來着手用代碼來實現我們的思路,我們需要解決以下問題,
- 儘量將這些代碼封裝好,以避免每次使用的時候都copy一遍代碼
- 具體用代碼怎麼判斷ListView往下滑動
對於第一個問題,我們採用大多數情況下使用的方式,就是:
自定義一個抽象的Adapter,實現BaseAdapter中的getView方法,並定義一個buildView
來代替getView的功能。
這樣做的好處就是,我們可以在getView中做我們想做的事,而不必在意convertView怎麼形成。這也非常符合我們的需求,所以代碼可以這麼寫,
public abstract class BaseFlyAdapter extends BaseAdapter {
public View getView(int position, View convertView, ViewGroup parent) {
View view = buildView(position, convertView, parent);
return view;
}
public abstract View buildView(int position, View convertView, ViewGroup parent);
}
上面說了,我們需要監聽ListView的滑動,這裏我們在BaseFlyAdapter中也一塊搞定!那我們的BaseFlyAdapter還需要一個ListView,並給他設置OnScrollListener
,
public abstract class BaseFlyAdapter extends BaseAdapter {
private ListView mListView;
public void bindView(ListView listView) {
mListView = listView;
mListView.setOnScrollListener(mScrollListener);
}
private OnScrollListener mScrollListener = new OnScrollListener() {
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
}
};
...
}
準備好了這些,我們就來實現如果判斷往下滑動
這個需求,先上代碼,
public abstract class BaseFlyAdapter extends BaseAdapter {
private ListView mListView;
private boolean isScrollDown; // 是否往下滑動
private int mFirstPosition; // 第一個可見item的位置
private int mFirstTop; // 第一個可以item的top值
public void bindView(ListView listView) {
mListView = listView;
mListView.setOnScrollListener(mScrollListener);
}
private OnScrollListener mScrollListener = new OnScrollListener() {
public void onScrollStateChanged(AbsListView view, int scrollState) {
if(scrollState == OnScrollListener.SCROLL_STATE_IDLE) isScrollDown = false;
}
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
View firstChild = view.getChildAt(0);
if(firstChild == null) return;
int top = firstChild.getTop();
isScrollDown = firstVisibleItem > mFirstPosition || mFirstTop > top;
mFirstTop = top;
mFirstPosition = firstVisibleItem;
}
};
}
具體的代碼都是mScrollListener
中,首先來看看onScrollStateChanged
,這裏面的代碼很簡單,就是在滑動停止的時候去恢復isScrollDown
變量。最重要的代碼還是在onScroll
中。下面我們來具體看看這裏面的實現。
首先我們獲取ListView的第一個view——firstChild
,如果這裏你不明白怎麼回事的話,可以去看看ListView的複用機制,接着我們來獲取到firstChild
的top值,這些都是爲下面去判斷是不是往下滑動做準備的,那怎麼判斷呢?
isScrollDown = firstVisibleItem > mFirstPosition || mFirstTop > top;
一個或的操作,這裏要考慮兩種情況,
- 新的item加入的時候,第一個item已經滑動出屏幕外。
- 當新的item加入,但第一個item還沒滑動出屏幕外。
對於第一種情況,是常規的一種情況,我們直接通過判斷第一個可見項,注意這裏的第一個可見項是指在我們數據集中的,並且判斷是不是大於我們之前保存的firstVisibleItem就ok, 可以看一下最後面的代碼,我們去保存了firstVisibleItem和第一個View的top值。對於第二種情況,我們只需要判斷View的top值是不是大於我們最後一次保存的值就ok。
ok,到現在爲止,我們可以知道ListView是不是往下滑動了,下面就開始給新加入的item添加動畫吧。爲了靈活,我們將動畫的定義放到外部,所以我們還需要給BaseFlyAdapter
一個方法去設置動畫,
public abstract class BaseFlyAdapter extends BaseAdapter {
private AnimationSet mAnimationSet;
...
public void setAnimation(AnimationSet set) {
mAnimationSet = set;
}
...
}
動畫設置好了,我們最後就來看看在getView中怎麼做吧。上面說了,動畫的執行是在getView中,
public abstract class BaseFlyAdapter extends BaseAdapter {
...
public View getView(int position, View convertView, ViewGroup parent) {
View view = buildView(position, convertView, parent);
if(isScrollDown && mAnimationSet != null) {
cancelAnimation();
view.startAnimation(mAnimationSet);
}
return view;
}
private void cancelAnimation() {
int count = mListView.getChildCount();
for(int i=0;i<count;i++) {
mListView.getChildAt(i).clearAnimation();
}
}
}
首先調用buildView,以前我們在getView中寫的代碼,現在需要放到buildView中了,然後我們去判斷現在是不是往下滑動,如果是往下滑動,首先cancel掉所有item的動畫,這樣做的目的是防止在某個瞬間多個item執行動畫,然後直接調用convertView.startAnimation來開始動畫。不過,還記得我們在上面的一句話嗎?
至於是不是到達底部了,交給getView去做!
我們看getView的代碼裏也沒有關於解決這個問題的代碼啊!對,這沒有錯,getView執行了,那肯定是新的item加入了,而且我們在getView中做了是不是往下滑動的判斷了,所以這個問題自然就解決了!
ok,ok, 萬事俱備了,下面就讓我們開始使用一下吧,
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ListView listView = (ListView) findViewById(R.id.list);
MyAdapter adapter = new MyAdapter();
adapter.bindView(listView);
adapter.setAnimation((AnimationSet) AnimationUtils.loadAnimation(this, R.anim.anim));
listView.setAdapter(adapter);
}
}
先去無視MyAdapter的代碼, 它肯定是繼承了我們定義的BaseFlyAdapter, 我們通過setAnimation來設置了一個動畫,我們先來看看這個動畫怎麼寫的吧,
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<translate
android:duration="500"
android:fromYDelta="50%"
android:toYDelta="0" />
</set>
不多說,一個簡單的translate動畫。
最後,我們再來看看MyAdapter吧,其實和我們繼承BaseAdapter一樣只不過我們不需要書寫getView的代碼,而是放到buildView中,
class MyAdapter extends BaseFlyAdapter {
public int getCount() {
return 100;
}
public Object getItem(int position) {
return "hello";
}
public long getItemId(int position) {
return position;
}
@Override
public View buildView(int position, View convertView, ViewGroup parent) {
if(convertView == null) {
convertView = View.inflate(parent.getContext(), R.layout.item, null);
}
return convertView;
}
}
好了,到現在你可以看一下效果啦,當然這裏我們把動畫的定義抽出來了,這樣做的好處就是可以隨意切換動畫,現在我們修改動畫爲alpha動畫,
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<alpha
android:duration="500"
android:fillAfter="true"
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
</set>
而我們的代碼不需要改變,再來看看效果,
很輕鬆,一個alpha的效果就完成了。就到這裏吧。