Android基于ViewFlipper實現(xiàn)圖片切換
在很多App上我們經(jīng)??吹竭@些效果:
淘寶首頁自動滾動的圖片展示效果支付寶應(yīng)用第一次啟動啟動的用戶引導(dǎo)畫面
要實現(xiàn)這些效果,有些控件可以幫助我們:
1. ViewPager
2. ViewFlipper
3. Fragment
好吧,我最終選擇了ViewFlipper,因為它的使用最干勁清爽(往里面放幾個View就行了),而且還支持自動播放。
不過,為了實現(xiàn)上面兩個效果,還得解決來年兩個問題:
1. 如何在ViewFlipper滾動的時候,獲得當前展示的child id?
2. 和ViewPager原生支持觸摸滑動翻頁不同,ViewFlipper需要自己監(jiān)聽手勢來解決。
問題1:其實,我挺奇怪的,Google既然為ViewAnimation做了個ViewFlipper的子類來支持自動翻滾,怎么不給它添加一個listener以監(jiān)聽翻滾事件呢?
好吧,既然google不給,那就自己加。首先看ViewFlipper的源碼(
/* ?*?Copyright?(C)?2006?The?Android?Open?Source?Project ?* ?*?Licensed?under?the?Apache?License,?Version?2.0?(the?"License"); ?*?you?may?not?use?this?file?except?in?compliance?with?the?License. ?*?You?may?obtain?a?copy?of?the?License?at ?* ?*??????http://www.apache.org/licenses/LICENSE-2.0 ?* ?*?Unless?required?by?applicable?law?or?agreed?to?in?writing,?software ?*?distributed?under?the?License?is?distributed?on?an?"AS?IS"?BASIS, ?*?WITHOUT?WARRANTIES?OR?CONDITIONS?OF?ANY?KIND,?either?express?or?implied. ?*?See?the?License?for?the?specific?language?governing?permissions?and ?*?limitations?under?the?License. ?*/ package?android.widget; import?android.content.BroadcastReceiver; import?android.content.Context; import?android.content.Intent; import?android.content.IntentFilter; import?android.content.res.TypedArray; import?android.os.Handler; import?android.os.Message; import?android.util.AttributeSet; import?android.util.Log; import?android.view.accessibility.AccessibilityEvent; import?android.view.accessibility.AccessibilityNodeInfo; import?android.widget.RemoteViews.RemoteView; /** ?*?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. ?* ?*?@attr?ref?android.R.styleable#ViewFlipper_flipInterval ?*?@attr?ref?android.R.styleable#ViewFlipper_autoStart ?*/ @RemoteView public?class?ViewFlipper?extends?ViewAnimator?{ ????private?static?final?String?TAG?=?"ViewFlipper"; ????private?static?final?boolean?LOGD?=?false; ????private?static?final?int?DEFAULT_INTERVAL?=?3000; ????private?int?mFlipInterval?=?DEFAULT_INTERVAL; ????private?boolean?mAutoStart?=?false; ????private?boolean?mRunning?=?false; ????private?boolean?mStarted?=?false; ????private?boolean?mVisible?=?false; ????private?boolean?mUserPresent?=?true; ????public?ViewFlipper(Context?context)?{ ????????super(context); ????} ????public?ViewFlipper(Context?context,?AttributeSet?attrs)?{ ????????super(context,?attrs); ????????TypedArray?a?=?context.obtainStyledAttributes(attrs, ????????????????com.android.internal.R.styleable.ViewFlipper); ????????mFlipInterval?=?a.getInt( ????????????????com.android.internal.R.styleable.ViewFlipper_flipInterval,?DEFAULT_INTERVAL); ????????mAutoStart?=?a.getBoolean( ????????????????com.android.internal.R.styleable.ViewFlipper_autoStart,?false); ????????a.recycle(); ????} ????private?final?BroadcastReceiver?mReceiver?=?new?BroadcastReceiver()?{ ????????@Override ????????public?void?onReceive(Context?context,?Intent?intent)?{ ????????????final?String?action?=?intent.getAction(); ????????????if?(Intent.ACTION_SCREEN_OFF.equals(action))?{ ????????????????mUserPresent?=?false; ????????????????updateRunning(); ????????????}?else?if?(Intent.ACTION_USER_PRESENT.equals(action))?{ ????????????????mUserPresent?=?true; ????????????????updateRunning(false); ????????????} ????????} ????}; ????@Override ????protected?void?onAttachedToWindow()?{ ????????super.onAttachedToWindow(); ????????//?Listen?for?broadcasts?related?to?user-presence ????????final?IntentFilter?filter?=?new?IntentFilter(); ????????filter.addAction(Intent.ACTION_SCREEN_OFF); ????????filter.addAction(Intent.ACTION_USER_PRESENT); ????????getContext().registerReceiver(mReceiver,?filter); ????????if?(mAutoStart)?{ ????????????//?Automatically?start?when?requested ????????????startFlipping(); ????????} ????} ????@Override ????protected?void?onDetachedFromWindow()?{ ????????super.onDetachedFromWindow(); ????????mVisible?=?false; ????????getContext().unregisterReceiver(mReceiver); ????????updateRunning(); ????} ????@Override ????protected?void?onWindowVisibilityChanged(int?visibility)?{ ????????super.onWindowVisibilityChanged(visibility); ????????mVisible?=?visibility?==?VISIBLE; ????????updateRunning(false); ????} ????/** ?????*?How?long?to?wait?before?flipping?to?the?next?view ?????* ?????*?@param?milliseconds ?????*????????????time?in?milliseconds ?????*/ ????@android.view.RemotableViewMethod ????public?void?setFlipInterval(int?milliseconds)?{ ????????mFlipInterval?=?milliseconds; ????} ????/** ?????*?Start?a?timer?to?cycle?through?child?views ?????*/ ????public?void?startFlipping()?{ ????????mStarted?=?true; ????????updateRunning(); ????} ????/** ?????*?No?more?flips ?????*/ ????public?void?stopFlipping()?{ ????????mStarted?=?false; ????????updateRunning(); ????} ????@Override ????public?void?onInitializeAccessibilityEvent(AccessibilityEvent?event)?{ ????????super.onInitializeAccessibilityEvent(event); ????????event.setClassName(ViewFlipper.class.getName()); ????} ????@Override ????public?void?onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo?info)?{ ????????super.onInitializeAccessibilityNodeInfo(info); ????????info.setClassName(ViewFlipper.class.getName()); ????} ????/** ?????*?Internal?method?to?start?or?stop?dispatching?flip?{@link?Message}?based ?????*?on?{@link?#mRunning}?and?{@link?#mVisible}?state. ?????*/ ????private?void?updateRunning()?{ ????????updateRunning(true); ????} ????/** ?????*?Internal?method?to?start?or?stop?dispatching?flip?{@link?Message}?based ?????*?on?{@link?#mRunning}?and?{@link?#mVisible}?state. ?????* ?????*?@param?flipNow?Determines?whether?or?not?to?execute?the?animation?now,?in ?????*????????????addition?to?queuing?future?flips.?If?omitted,?defaults?to ?????*????????????true. ?????*/ ????private?void?updateRunning(boolean?flipNow)?{ ????????boolean?running?=?mVisible?&&?mStarted?&&?mUserPresent; ????????if?(running?!=?mRunning)?{ ????????????if?(running)?{ ????????????????showOnly(mWhichChild,?flipNow); ????????????????Message?msg?=?mHandler.obtainMessage(FLIP_MSG); ????????????????mHandler.sendMessageDelayed(msg,?mFlipInterval); ????????????}?else?{ ????????????????mHandler.removeMessages(FLIP_MSG); ????????????} ????????????mRunning?=?running; ????????} ????????if?(LOGD)?{ ????????????Log.d(TAG,?"updateRunning()?mVisible="?+?mVisible?+?",?mStarted="?+?mStarted ????????????????????+?",?mUserPresent="?+?mUserPresent?+?",?mRunning="?+?mRunning); ????????} ????} ????/** ?????*?Returns?true?if?the?child?views?are?flipping. ?????*/ ????public?boolean?isFlipping()?{ ????????return?mStarted; ????} ????/** ?????*?Set?if?this?view?automatically?calls?{@link?#startFlipping()}?when?it ?????*?becomes?attached?to?a?window. ?????*/ ????public?void?setAutoStart(boolean?autoStart)?{ ????????mAutoStart?=?autoStart; ????} ????/** ?????*?Returns?true?if?this?view?automatically?calls?{@link?#startFlipping()} ?????*?when?it?becomes?attached?to?a?window. ?????*/ ????public?boolean?isAutoStart()?{ ????????return?mAutoStart; ????} ????private?final?int?FLIP_MSG?=?1; ????private?final?Handler?mHandler?=?new?Handler()?{ ????????@Override ????????public?void?handleMessage(Message?msg)?{ ????????????if?(msg.what?==?FLIP_MSG)?{ ????????????????if?(mRunning)?{ ????????????????????showNext(); ????????????????????msg?=?obtainMessage(FLIP_MSG); ????????????????????sendMessageDelayed(msg,?mFlipInterval); ????????????????} ????????????} ????????} ????}; }
代碼還是挺簡單易懂的,就是用了一個Handler來定時調(diào)用showNext()。
可惜,mHandler成員是private+final的,我們無法改動它了。
不過,既然ViewFlipper不是一個final類,那就好辦,自定義類繼承ViewFlipper,重載showNext()函數(shù):
package?com.example.viewflipper; import?android.widget.ViewFlipper; /** ?*?Created?by?ray?on?7/4/13. ?*/ public?class?MyViewFilpper?extends?ViewFlipper?{ ????private?OnDisplayChagnedListener?mListener; ????public?MyViewFilpper(android.content.Context?context)?{ ????????super(context); ????} ????public?MyViewFilpper(android.content.Context?context,?android.util.AttributeSet?attrs)?{ ????????super(context,?attrs); ????} ????public?void?setOnDisplayChagnedListener(OnDisplayChagnedListener?listener)?{ ????????if?(mListener?!=?listener)?{ ????????????this.mListener?=?listener; ????????} ????} ????@Override ????public?void?showNext()?{ ????????super.showNext(); ????????if(mListener?!=?null){ ????????????mListener.OnDisplayChildChanging(this,?super.getDisplayedChild()); ????????} ????} ????@Override ????public?void?showPrevious()?{ ????????super.showPrevious(); ????????if(mListener?!=?null){ ????????????mListener.OnDisplayChildChanging(this,?super.getDisplayedChild()); ????????} ????} ????public?interface?OnDisplayChagnedListener?{ ????????void?OnDisplayChildChanging(ViewFlipper?view,?int?index); ????} }
問題2,就比較好解決了,OnTouchListener + GestureDetector 監(jiān)聽flipper事件,這里就不貼代碼了。
好吧,這樣基本問題都被解決了,為了讓效果好一點,又給ViewFlipper加上了進入動畫和離開動畫。
好吧,下方的1,2,3有點難看,大家湊合下。
當目前為止,本人對效果還算滿意,但是,還存在兩個問題:
1. 沒有處理GestureDetectorListener的onScroll函數(shù),也就是說,在用戶的手指離開屏幕前,不會有滾動效果。
2. 因為是通GestureDetector是通過TouchListener工作的,作為一個容器,如果內(nèi)部元素消耗掉了touch事件,則touchlistener無法工作,手勢的監(jiān)聽自然也無從談起。不過,僅僅是應(yīng)付圖片瀏覽的話,還是有辦法解決的:
a. 對于用戶引導(dǎo)這樣的場景,圖片展示是不會消耗touch消息的。
b. 而淘寶首頁的圖片動態(tài)則不同,在用戶點擊圖片的時候,把頁面轉(zhuǎn)到商品的展示頁面,這種情況下,一般會考慮使用ImageButton控件來處理Click事件,這樣就導(dǎo)致手勢無法監(jiān)聽。解決方案是,仍舊使用ImageVIew控件,但是在GestureListener的OnSingleTapUp事件上,獲取當前展示的頁面,然后根據(jù)頁面信息轉(zhuǎn)到商品展示頁面。
但是,如果是一些比較復(fù)雜的情況,就比較難處理,雖然通過重載onInterceptTouchEvent函數(shù),父容器也能監(jiān)聽所有由子控件消耗的消息,但是也可能存在一些邏輯問題:例如在ViewFlipper容器內(nèi)包含了一個Gallary控件,那scroll消息應(yīng)該由哪個控件來處理,亦或都處理?這就是個比較費腦筋的問題了,設(shè)計UI的時候,可以考慮設(shè)計的簡單些。