??? 做Android開發的人都用過Selector,可以方便的實現View在不同狀態下的背景。不過,相信大部分開發者遇到過和我一樣的問題,本文會從源碼角度,解釋這些問題。
??????? 首先,這里簡單描述一下,我遇到的問題:
界面上有個全屏的LinearLayout A,A中有一個TextView B和Button C,其中,A的clickable=true,并設置了pressed時,背景色為灰色,B設置了pressed時,背景色為藍色
當手指觸摸C下方的空白區域時,看到了這樣的效果:
??????? 在這里看到,在沒有觸摸B的情況下,B的pressed = true,而C的pressed = false。 C的狀態暫且不討論,按照Android消息傳遞的原則,因為touch的point不在B內部,所以,touch消息應該不會交給B處理,那為什么B的pressed = true?
???????? 下面開始一步一步分析(本文分析的Android源碼為4.2.2)。
Pressed狀態的設定
??????? 從View.onTouchEvent函數看起(frameworks/base/core/java/android/view/View.java):
- /**?
- ?*?Implement?this?method?to?handle?touch?screen?motion?events.?
- ?*?
- ?*?@param?event?The?motion?event.?
- ?*?@return?True?if?the?event?was?handled,?false?otherwise.?
- ?*/??
- public?boolean?onTouchEvent(MotionEvent?event)?{??
- ????......??
- ????if?(((viewFlags?&?CLICKABLE)?==?CLICKABLE?||?//這里是為什么設置A的clickable為true的原因,否則,press?A的時候,沒有界面元素處理touch?event,最終會由Activity的onTouchEvent函數處理??
- ????????????(viewFlags?&?LONG_CLICKABLE)?==?LONG_CLICKABLE))?{??
- ????????switch?(event.getAction())?{??
- ????????????case?MotionEvent.ACTION_UP:??
- ????????????????......??
- ????????????????break;??
- ??
- ????????????case?MotionEvent.ACTION_DOWN:??
- ????????????????mHasPerformedLongPress?=?false;??
- ????????????????......??
- ????????????????//?Walk?up?the?hierarchy?to?determine?if?we're?inside?a?scrolling?container.??
- ????????????????boolean?isInScrollingContainer?=?isInScrollingContainer();//A已經是頂層元素了,沒有ScrollView之類的控件存在,所以,isInScrollingContainer?=?false??
- ??
- ????????????????//?For?views?inside?a?scrolling?container,?delay?the?pressed?feedback?for??
- ????????????????//?a?short?period?in?case?this?is?a?scroll.??
- ????????????????if?(isInScrollingContainer)?{??
- ????????????????????mPrivateFlags?|=?PFLAG_PREPRESSED;??
- ????????????????????if?(mPendingCheckForTap?==?null)?{??
- ????????????????????????mPendingCheckForTap?=?new?CheckForTap();??
- ????????????????????}??
- ????????????????????postDelayed(mPendingCheckForTap,?ViewConfiguration.getTapTimeout());??
- ????????????????}?else?{??
- ????????????????????//?Not?inside?a?scrolling?container,?so?show?the?feedback?right?away??
- ????????????????????setPressed(true);//A設置pressed?=?true??
- ????????????????????checkForLongClick(0);??
- ????????????????}??
- ????????????????break;??
- ??
- ????????????case?MotionEvent.ACTION_CANCEL:??
- ????????????????......??
- ????????????????break;??
- ??
- ????????????case?MotionEvent.ACTION_MOVE:??
- ????????????????......??
- ????????????????break;??
- ????????}??
- ????????return?true;??
- ????}??
- ??
- ????return?false;??
- }??
???????
??????? 從上面的代碼我們知道,當手指觸摸A的時候,A的pressed被設置為true。
Pressed狀態的傳遞
??????? 接著,我們看看setPressed函數的實現:
- /**?
- ?*?Sets?the?pressed?state?for?this?view.?
- ?*?
- ?*?@param?pressed?Pass?true?to?set?the?View's?internal?state?to?"pressed",?or?false?to?reverts?
- ?*????????????????the?View's?internal?state?from?a?previously?set?"pressed"?state.?
- ?*?@see?#isClickable()?
- ?*?@see?#setClickable(boolean)?
- ?*/??
- public?void?setPressed(boolean?pressed)?{??
- ????final?boolean?needsRefresh?=?pressed?!=?((mPrivateFlags?&?PFLAG_PRESSED)?==?PFLAG_PRESSED);??
- ??
- ????if?(pressed)?{??
- ????????mPrivateFlags?|=?PFLAG_PRESSED;??
- ????}?else?{??
- ????????mPrivateFlags?&=?~PFLAG_PRESSED;??
- ????}??
- ??
- ????if?(needsRefresh)?{??
- ????????refreshDrawableState();//切換背景圖片??
- ????}??
- ????dispatchSetPressed(pressed);??
- }??
??????? setPressed函數內部調用了dispatchSetPressed函數,這個讓人很在意(frameworks/base/core/java/android/view/ViewGroup.java):
- @Override??
- protected?void?dispatchSetPressed(boolean?pressed)?{??
- ????final?View[]?children?=?mChildren;??
- ????final?int?count?=?mChildrenCount;??
- ????for?(int?i?=?0;?i?<?count;?i++)?{??
- ????????final?View?child?=?children[i];??
- ????????//?Children?that?are?clickable?on?their?own?should?not??
- ????????//?show?a?pressed?state?when?their?parent?view?does.??
- ????????//?Clearing?a?pressed?state?always?propagates.??
- ????????if?(!pressed?||?(!child.isClickable()?&&?!child.isLongClickable()))?{??
- ????????????child.setPressed(pressed);??
- ????????}??
- ????}??
- }??
??????? 原來,dispatchSetPressed函數會把pressed狀態傳遞給所有clickable=false并且longclickable=false的子元素。
??????? 到這里,前面的現象就可以解釋了,因為C是button類,clickable=true,而B的clickable=false。所以,當A被觸摸時,B的pressed=true,而C的pressed=false。那么,可以回答下面幾個小問題了:
- 如果不希望A的pressed=true時,B的pressed = true,該如何修改?
- 設置B的clickable=true
- 如果希望A的pressed = true時,C的pressed = true,那又該如何修改?
- 設置C的clickable = false. 但是,這里可能又存在問題了,設置C的clickable=false,會導致button按鈕的onclicklistener無法工作,這是個嚴重的副作用。那么可以不修改clickable,而設置android:duplicateParentState為true。
- 設置C的clickable = false. 但是,這里可能又存在問題了,設置C的clickable=false,會導致button按鈕的onclicklistener無法工作,這是個嚴重的副作用。那么可以不修改clickable,而設置android:duplicateParentState為true。
?????? 那么duplicateParentState做了些什么呢?View.setDuplicateParentStateEnabled:
- public?void?setDuplicateParentStateEnabled(boolean?enabled)?{??
- ????setFlags(enabled???DUPLICATE_PARENT_STATE?:?0,?DUPLICATE_PARENT_STATE);??
- }??
????? 再看看View.onCreateDrawableState()
- /**?
- ????*?Generate?the?new?{@link?android.graphics.drawable.Drawable}?state?for?
- ????*?this?view.?This?is?called?by?the?view?
- ????*?system?when?the?cached?Drawable?state?is?determined?to?be?invalid.??To?
- ????*?retrieve?the?current?state,?you?should?use?{@link?#getDrawableState}.?
- ????*?
- ????*?@param?extraSpace?if?non-zero,?this?is?the?number?of?extra?entries?you?
- ????*???????????????????would?like?in?the?returned?array?in?which?you?can?place?your?own?
- ????*???????????????????states.?
- ????*?@return?Returns?an?array?holding?the?current?{@link?Drawable}?state?of?
- ????*?the?view.?
- ????*?@see?#mergeDrawableStates(int[],?int[])?
- ????*/??
- ???protected?int[]?onCreateDrawableState(int?extraSpace)?{??
- ???????if?((mViewFlags?&?DUPLICATE_PARENT_STATE)?==?DUPLICATE_PARENT_STATE?&&??
- ???????????????mParent?instanceof?View)?{??
- ???????????return?((View)?mParent).onCreateDrawableState(extraSpace);??
- ???????}??
- ??
- ???????......??
- ???}??
??????? 從上面的代碼,可以看到,當設置duplicateParentState為true時,View的DrawableState直接使用了其parent的。所以,他的drawable狀態會一直保持與其parent同步。
題外,為什么當ListView中包含focusable為true的控件時,OnItemClickListener不會觸發
??????? 因為ListView未重載onTouchEvent事件,所以,需要看其父類的AbsListView.onTouchEvent(frameworks/base/core/java/android/widget/AbsListView):
- @Override??
- public?boolean?onTouchEvent(MotionEvent?ev)?{??
- ????......??
- ????switch?(action?&?MotionEvent.ACTION_MASK)?{??
- ????case?MotionEvent.ACTION_DOWN:?{??
- ????????......??
- ????????break;??
- ????}??
- ??
- ????case?MotionEvent.ACTION_MOVE:?{??
- ????????......??
- ????????break;??
- ????}??
- ??
- ????case?MotionEvent.ACTION_UP:?{??
- ????????switch?(mTouchMode)?{??
- ????????case?TOUCH_MODE_DOWN:??
- ????????case?TOUCH_MODE_TAP:??
- ????????case?TOUCH_MODE_DONE_WAITING:??
- ????????????final?int?motionPosition?=?mMotionPosition;??
- ????????????final?View?child?=?getChildAt(motionPosition?-?mFirstPosition);??
- ??
- ????????????final?float?x?=?ev.getX();??
- ????????????final?boolean?inList?=?x?>?mListPadding.left?&&?x?<?getWidth()?-?mListPadding.right;??
- ??
- ????????????if?(child?!=?null?&&?!child.hasFocusable()?&&?inList)?{??
- ????????????????if?(mTouchMode?!=?TOUCH_MODE_DOWN)?{??
- ????????????????????child.setPressed(false);??
- ????????????????}??
- ??
- ????????????????if?(mPerformClick?==?null)?{??
- ????????????????????mPerformClick?=?new?PerformClick();??
- ????????????????}??
- ??
- ????????????????......??
- ????????????????performClick.run();??
- ????????????????......??
- ????????????}??
- ????????????......??
- ????????case?TOUCH_MODE_SCROLL:??
- ????????????......??
- ????????????break;??
- ??
- ????????case?TOUCH_MODE_OVERSCROLL:??
- ????????......??
- ????????break;??
- ????}??
- ????......??
- ????case?MotionEvent.ACTION_CANCEL:?{??
- ????????......??
- ????????break;??
- ????}??
- ??
- ????case?MotionEvent.ACTION_POINTER_UP:?{??
- ????????......??
- ????????break;??
- ????}??
- ??
- ????case?MotionEvent.ACTION_POINTER_DOWN:?{??
- ????????......??
- ????????break;??
- ????}??
- ????}??
- ??
- ????return?true;??
- }??
???????? 僅在child.hasFocusable()=false時, PerformClick對象才會執行ViewGroup.hasFocusable:
- /**?
- ?*?{@inheritDoc}?
- ?*/??
- @Override??
- public?boolean?hasFocusable()?{??
- ????if?((mViewFlags?&?VISIBILITY_MASK)?!=?VISIBLE)?{??
- ????????return?false;??
- ????}??
- ??
- ????if?(isFocusable())?{??
- ????????return?true;??
- ????}??
- ??
- ????final?int?descendantFocusability?=?getDescendantFocusability();??
- ????if?(descendantFocusability?!=?FOCUS_BLOCK_DESCENDANTS)?{??
- ????????final?int?count?=?mChildrenCount;??
- ????????final?View[]?children?=?mChildren;??
- ??
- ????????for?(int?i?=?0;?i?<?count;?i++)?{??
- ????????????final?View?child?=?children[i];??
- ????????????if?(child.hasFocusable())?{??
- ????????????????return?true;??
- ????????????}??
- ????????}??
- ????}??
- ??
- ????return?false;??
- }??
??????? 僅在所有的clild的hasFocusable為false時,ListView才會執行performClick(AbsListView.PerformClick):
- private?class?PerformClick?extends?WindowRunnnable?implements?Runnable?{??
- ????int?mClickMotionPosition;??
- ??
- ????public?void?run()?{??
- ????????//?The?data?has?changed?since?we?posted?this?action?in?the?event?queue,??
- ????????//?bail?out?before?bad?things?happen??
- ????????if?(mDataChanged)?return;??
- ??
- ????????final?ListAdapter?adapter?=?mAdapter;??
- ????????final?int?motionPosition?=?mClickMotionPosition;??
- ????????if?(adapter?!=?null?&&?mItemCount?>?0?&&??
- ????????????????motionPosition?!=?INVALID_POSITION?&&??
- ????????????????motionPosition?<?adapter.getCount()?&&?sameWindow())?{??
- ????????????final?View?view?=?getChildAt(motionPosition?-?mFirstPosition);??
- ????????????//?If?there?is?no?view,?something?bad?happened?(the?view?scrolled?off?the??
- ????????????//?screen,?etc.)?and?we?should?cancel?the?click??
- ????????????if?(view?!=?null)?{??
- ????????????????performItemClick(view,?motionPosition,?adapter.getItemId(motionPosition));??
- ????????????}??
- ????????}??
- ????}??
- }??
??????? 而PerformClick會調用performItemClick(AdsListView.performItemClick):
- @Override??
- ???public?boolean?performItemClick(View?view,?int?position,?long?id)?{??
- ???????boolean?handled?=?false;??
- ???????boolean?dispatchItemClick?=?true;??
- ??
- ???????......??
- ??
- ???????if?(dispatchItemClick)?{??
- ???????????handled?|=?super.performItemClick(view,?position,?id);??
- ???????}??
- ??
- ???????return?handled;??
- ???}??
?????? AdapterView.preformItemClick:
- /**?
- ?*?Call?the?OnItemClickListener,?if?it?is?defined.?
- ?*?
- ?*?@param?view?The?view?within?the?AdapterView?that?was?clicked.?
- ?*?@param?position?The?position?of?the?view?in?the?adapter.?
- ?*?@param?id?The?row?id?of?the?item?that?was?clicked.?
- ?*?@return?True?if?there?was?an?assigned?OnItemClickListener?that?was?
- ?*?????????called,?false?otherwise?is?returned.?
- ?*/??
- public?boolean?performItemClick(View?view,?int?position,?long?id)?{??
- ????if?(mOnItemClickListener?!=?null)?{??
- ????????playSoundEffect(SoundEffectConstants.CLICK);??
- ????????if?(view?!=?null)?{??
- ????????????view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);??
- ????????}??
- ????????mOnItemClickListener.onItemClick(this,?view,?position,?id);??
- ????????return?true;??
- ????}??
- ??
- ????return?false;??
- }??
??????? 所以,如果ListView item中包含focusable為true的控件(例如:button, radiobutton),會導致ItemClickListener失效。解決方案兩個:
設置特定的控件focusable = false
不使用onItemClickListener,而直接在Item上設置onClickListener監聽點擊事件。