前世嗶嗶
對(duì)于ViewPager,相信大家都不陌生,但是對(duì)于ViewFlipper大家確聽(tīng)的少,甚至都沒(méi)聽(tīng)過(guò),首先來(lái)了解下這貨是干嘛的,能實(shí)現(xiàn)什么效果:
* Simple {@link ViewAnimator} that will animate between two or more views
* that have been added to it. Only one child is shown at a time. If
* requested, can automatically flip between each child at a regular interval.
源碼中翻譯過(guò)來(lái)就是:
可以在兩個(gè)或多個(gè)視圖之間添加動(dòng)畫(huà),一次只能展示1個(gè)視圖,并可以自動(dòng)使用動(dòng)畫(huà)輪回播放翻轉(zhuǎn)。
所以,我們能用它實(shí)現(xiàn)的效果有:
- 跑馬燈
- 水平,垂直翻頁(yè)公告
- 視圖動(dòng)畫(huà)切換(圖片切換,場(chǎng)景切換等),不過(guò)用ViewPager實(shí)現(xiàn)更好
正確的打開(kāi)方式
第一步,聲明控件,并往里面添加需要切換的視圖,就隨便往里面扔兩只TextView好了
<ViewFlipper
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoStart="true"
android:flipInterval="500"
android:inAnimation="@anim/right_in"
android:outAnimation="@anim/left_out"
android:id="@+id/mViewFlipper"
>
<TextView
android:layout_width="match_parent"
android:layout_height="100dp"
android:gravity="center"
android:text="第一個(gè)視圖" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="第二個(gè)視圖" />
</ViewFlipper>
其中聲明的xml屬性如下:
android:autoStart: 為true則會(huì)自動(dòng)加載下一個(gè)View,默認(rèn)為false
android:flipInterval:設(shè)置View之間切換的動(dòng)畫(huà)時(shí)間間隔,默認(rèn)為3000
后面的屬性則是來(lái)自它的父類ViewAnimator的xml屬性(這個(gè)后面源碼會(huì)分析)
outAnimation,inAnimation:設(shè)置進(jìn)入和退出的動(dòng)畫(huà)
animateFirstView:加載第一個(gè)視圖是否使用動(dòng)畫(huà)加載,默認(rèn)為true
第二步,調(diào)用啟動(dòng)的代碼,當(dāng)然xml中的屬性使用java代碼也是可以設(shè)置的
調(diào)用startFlipping啟動(dòng),調(diào)用stopFlipping停止,調(diào)用showNext顯示下一個(gè)視圖,調(diào)用showPrevious顯示上一個(gè)視圖
用法其實(shí)很簡(jiǎn)單,實(shí)現(xiàn)的原理其實(shí)也非常的簡(jiǎn)單
妖魔鬼怪 一探究竟
一 繼承關(guān)系
此物繼承自ViewAnimator,而ViewAnimator又繼承自FrameLayout,所以,它內(nèi)部的View自然都有重疊屬性和視圖動(dòng)畫(huà)屬性
二 關(guān)鍵方法
關(guān)鍵的方法當(dāng)然是這兩個(gè)貨啦,這是源頭:
public void startFlipping() {
mStarted = true;
updateRunning();
}
/**
* No more flips
*/
public void stopFlipping() {
mStarted = false;
updateRunning();
}
check updateRunning=>
private void updateRunning(boolean flipNow) {
boolean running = mVisible && mStarted && mUserPresent;
if (running != mRunning) {
if (running) {
//展示當(dāng)前的視圖
showOnly(mWhichChild, flipNow);
//延遲執(zhí)行一個(gè)任務(wù)
postDelayed(mFlipRunnable, mFlipInterval);
} else {
removeCallbacks(mFlipRunnable);
}
mRunning = running;
}
if (LOGD) {
Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+ ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
}
}
這里就做了兩件事,調(diào)用showOnly展示當(dāng)前的視圖,延遲執(zhí)行mFlipRunnable任務(wù),如果不是在運(yùn)行中,取消掉該任務(wù)
check mFlipRunnable =>
private final Runnable mFlipRunnable = new Runnable() {
@Override
public void run() {
if (mRunning) {
showNext();
postDelayed(mFlipRunnable, mFlipInterval);
}
}
};
showNext就是切換到下一個(gè)視圖,所以這個(gè)任務(wù)的目的就是不斷延遲mFlipInterval毫秒切換到下一個(gè)視圖
check showNext=>
//和showPrevious一樣,最終都會(huì)調(diào)用這個(gè)方法,參數(shù)不同而已
@android.view.RemotableViewMethod
public void setDisplayedChild(int whichChild) {
mWhichChild = whichChild;
if (whichChild >= getChildCount()) {
mWhichChild = 0;
} else if (whichChild < 0) {
mWhichChild = getChildCount() - 1;
}
boolean hasFocus = getFocusedChild() != null;
// This will clear old focus if we had it
showOnly(mWhichChild);
if (hasFocus) {
// Try to retake focus if we had it
requestFocus(FOCUS_FORWARD);
}
}
這個(gè)方法的作用是顯示指定的視圖
所以,把這些串起來(lái)就是:調(diào)用startFlipping,就會(huì)啟動(dòng)mFlipRunnable任務(wù)并以mFlipInterval毫秒的時(shí)間在輪詢調(diào)用showOnly方法來(lái)顯示視圖,這個(gè)方法需要傳入一個(gè)當(dāng)前View的下標(biāo),所以通過(guò)mWhichChild來(lái)保存當(dāng)前View的下標(biāo),而mWhichChild的維護(hù)是通過(guò)setDisplayedChild方法來(lái)實(shí)現(xiàn)的,通過(guò)對(duì)當(dāng)前View的個(gè)數(shù)做加減操作并保證其不大于當(dāng)前子視圖的個(gè)數(shù),如果大于,則重置為0,所以保證了視圖的循環(huán)顯示
但是,動(dòng)畫(huà)是在哪里執(zhí)行的呢,當(dāng)然是在我們還沒(méi)看的showOnly方法里拉:
void showOnly(int childIndex, boolean animate) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (i == childIndex) {
if (animate && mInAnimation != null) {
child.startAnimation(mInAnimation);
}
child.setVisibility(View.VISIBLE);
mFirstTime = false;
} else {
if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
child.startAnimation(mOutAnimation);
} else if (child.getAnimation() == mInAnimation)
child.clearAnimation();
child.setVisibility(View.GONE);
}
}
}
這個(gè)方法需要傳入當(dāng)前需要執(zhí)行的動(dòng)畫(huà)的View下標(biāo),和一個(gè)是否需要執(zhí)行動(dòng)畫(huà)的標(biāo)志,當(dāng)然這個(gè)在ViewFlipper中是寫(xiě)死的為true,不然哪來(lái)的效果呢。
這個(gè)方法就做了兩件事,為當(dāng)前下標(biāo)匹配的View啟動(dòng)動(dòng)畫(huà)并將其顯示出來(lái),將其他的View隱藏掉并調(diào)用移出動(dòng)畫(huà)。
所以,ViewFlipper的原理其實(shí)就是顯示當(dāng)前的View,隱藏其他的View,并用動(dòng)畫(huà)來(lái)達(dá)到需要的效果,使用postDelayed循環(huán)調(diào)用來(lái)達(dá)到輪詢的目的,并沒(méi)有想象中的神秘,不是嗎?
今世嗶嗶
當(dāng)然在項(xiàng)目中使用還需要我們自己封裝一下,像那種標(biāo)題所說(shuō)的拼多多的那種布局,使用這個(gè)很容易實(shí)現(xiàn),但是需要注意數(shù)據(jù)源的賦值問(wèn)題,后端肯定是傳入一個(gè)集合過(guò)來(lái),那么如何計(jì)算下標(biāo)呢,其實(shí)我們從源碼中也能學(xué)習(xí)到,在輪詢到視圖的最后一個(gè)的時(shí)候,將下標(biāo)置為0就行了,然后執(zhí)行一次,下標(biāo)加1,這樣也就達(dá)到了數(shù)據(jù)源輪詢的目的
項(xiàng)目也有一個(gè)與拼多多類似的需求,一開(kāi)始是用的RecyclerView來(lái)做的,將adapter的itemCount置為無(wú)限大來(lái)達(dá)到輪詢的目的,雖然效果也能達(dá)到,但是每一個(gè)item里面還有個(gè)倒計(jì)時(shí),而每一次滾動(dòng),都會(huì)重新創(chuàng)建View,所以倒計(jì)時(shí)的狀態(tài)很難保存下來(lái),全部都亂了套,后來(lái)打算寫(xiě)個(gè)自定義,無(wú)意發(fā)現(xiàn)了這個(gè)控件,后來(lái)在gayHub上看到了基于此封裝的庫(kù),看了看源碼,寫(xiě)的挺好的,就非(bu)常(yao)愉(bi)快(lian)的拿過(guò)來(lái)用了,基本滿足我的需求,在此推薦給大家:傳送門(mén)