一、TouchMode
Android支持多種交互方式:D-pad、滾動球、觸摸屏等等。這些交互方式可以分為兩類:TouchMode和非TouchMode。TouchMode對應(yīng)觸摸屏;非TouchMode對應(yīng)實(shí)體鍵盤。兩種模式中對Focus的需求也不同,例如點(diǎn)擊button,在非TouchMode中需要先focus到button,再確認(rèn)觸發(fā)button的點(diǎn)擊事件;在TouchMode中不需要先focus這個button,觸摸按鍵直接觸發(fā)button的點(diǎn)擊事件。兩種模式的對應(yīng)的Event和Event分發(fā)模式也不同,TouchMode產(chǎn)生TouchEvent,對于[TouchEvent分發(fā)機(jī)制],非TouchMode產(chǎn)生KeyEvent,對應(yīng)分發(fā)機(jī)制較為簡單,直接分發(fā)給當(dāng)前獲取了焦點(diǎn)的View。
判斷是否是TouchMode
boolean isInTouchMode=mView.isInTouchMode();
設(shè)置TouchMode
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null) {
viewRoot.ensureTouchMode(false); //設(shè)置為非TouchMode
viewRoot.ensureTouchMode(true); //設(shè)置為TouchMode
}
二、focus相關(guān)屬性和方法
1. focusable/focusableInTouchModel
含義
focusable:view屬性,表示view是否可以獲取到焦點(diǎn)
focusableInTouchModel: view屬性,表示在TouchMode時view是否可以獲取到焦點(diǎn)。
使用
focusableInTouchModel和focusable屬性并不是單獨(dú)設(shè)置的,當(dāng)調(diào)用setFocusable()和setFocusableInTouchMode()設(shè)置時,會根據(jù)屬性值同時修改focusableInTouchModel和focusable。
| 方法 | focusable | focusableInTouchMode |
|---|---|---|
| setFocusable(true) | 設(shè)置為true | 不影響 |
| setFocusable(false) | 設(shè)置為false | 設(shè)置為false |
| setFocusableInTouchMode(true) | 設(shè)置為true | 設(shè)置為true |
| setFocusableInTouchMode(false) | 不影響 | 設(shè)置為false |
影響
對于非TouchMode,只有focusable等于true的view可以獲取到焦點(diǎn),獲取到焦點(diǎn)才可以在之后獲取到KeyEvent事件。
對于TouchMode,focusableInTouchModel等于true的view接受到TouchEvent后,會先嘗試獲取焦點(diǎn)。即點(diǎn)擊控件時,不會立即執(zhí)行相應(yīng)的點(diǎn)擊邏輯,而是先顯示焦點(diǎn)(即控件被選中),再點(diǎn)擊才執(zhí)行邏輯。

原理
public boolean onTouchEvent(MotionEvent event) {
...
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//檢查是否需要獲取焦點(diǎn)并嘗試獲取焦點(diǎn),獲取焦點(diǎn)成功后設(shè)置focusTaken=true
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
//當(dāng)focusTaken=true時不觸發(fā)performClick,即不觸發(fā)點(diǎn)擊事件
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
2. descendantFocusability
含義
descendantFocusability:ViewGroup屬性,定義了當(dāng)ViewGroup需要獲取焦點(diǎn)時執(zhí)行的策略,默認(rèn)值為afterDescendants。
- beforeDescendants:viewgroup會優(yōu)先其子類控件而獲取到焦點(diǎn)
- afterDescendants:viewgroup只有當(dāng)其子類控件不需要獲取焦點(diǎn)時才獲取焦點(diǎn)
- blocksDescendants:viewgroup子類控件不獲得焦點(diǎn)。
實(shí)現(xiàn)原理
ViewGroup中重寫了requestFocus()方法,根據(jù)descendantFocusability執(zhí)行不同的策略
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
//當(dāng)FOCUS_BLOCK_DESCENDANTS時,僅ViewGroup自身嘗試獲取焦點(diǎn)
return super.requestFocus(direction, previouslyFocusedRect);
case FOCUS_BEFORE_DESCENDANTS: {
//當(dāng)FOCUS_BEFORE_DESCENDANTS時,先ViewGroup自身嘗試獲取焦點(diǎn),獲取焦點(diǎn)失敗后遍歷子view嘗試獲取焦點(diǎn)
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
case FOCUS_AFTER_DESCENDANTS: {
//當(dāng)FOCUS_AFTER_DESCENDANTS時,先遍歷子view嘗試獲取焦點(diǎn),當(dāng)沒有子view獲取焦點(diǎn)時ViewGroup自身嘗試獲取焦點(diǎn)
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
}
3. 獲取焦點(diǎn)
嘗試獲取焦點(diǎn),獲取條件:
- TouchMode:focusable=true, focusableInTouchMode=true,
- 非TouchMode:focusable=true
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
//檢查focusable屬性
if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
(mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
//檢查TouchMode和focuableInTouchMode屬性
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
//檢查是否有ViewParent設(shè)置了blocksDescendants
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
//獲取焦點(diǎn)
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
4. isFocused/hasFocus
- hasFocus:Returns true if this view has focus itself, or is the ancestor of the view that has focus.
- isFocused:Returns true if this view has focus
三、focus機(jī)制
1. View的focus相關(guān)屬性
focusable=false, focusableInTouchMode=false
說明:TouchMode和非TouchMode都不獲取焦點(diǎn)
舉例:TextView、LinearLayoutView、RelativeLayoutView等,在TouchMode和非TouchMode中都不需要獲取焦點(diǎn)
focusable=true, focusableInTouchMode=false
說明:非TouchMode需要獲取焦點(diǎn),TouchMode不需要獲取焦點(diǎn)
舉例:Button,在非TouchMode時,需要獲取到焦點(diǎn)才能觸發(fā)對應(yīng)事件,在TouchMode時,不需要獲取焦點(diǎn)就能觸發(fā)對應(yīng)事件
focusable=true, focusableInTouchMode=true
說明:TouchMode和非TouchMode都需要獲取焦點(diǎn)
舉例:EditText,不管在哪種模式下,都需要先獲取到焦點(diǎn)才能進(jìn)行輸入
2. focus初始化焦點(diǎn)
當(dāng)Activity創(chuàng)建成功后,初始化focus,遍歷view并嘗試設(shè)置焦點(diǎn),根據(jù)descendantFocusability默認(rèn)為afterDescendants,會優(yōu)先遍歷子view,當(dāng)子view滿足focus設(shè)置條件時設(shè)置為焦點(diǎn)view。(此時為TouchMode,focus設(shè)置條件:focusable=true, focusableInTouchMode=true)
ViewRootImpl
if (mFirst) {
// handle first focus request
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()="
+ mView.hasFocus());
if (mView != null) {
if (!mView.hasFocus()) {
mView.requestFocus(View.FOCUS_FORWARD);
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view="
+ mView.findFocus());
} else {
if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view="
+ mView.findFocus());
}
}
}
3. focus自定義初始化焦點(diǎn)
<RequestFocus/>
適用于:xml布局文件,容器控件和非容器控件都可以使用,需要focusable等于true并且focusableInTouchMode等于true
<EditText
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<requestFocus />
</EditText>
實(shí)現(xiàn)原理:
LayoutInflater中處理,rInflate方法中處理<RequestFocus/>標(biāo)簽
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
//requestFocus標(biāo)簽
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
if (finishInflate) {
parent.onFinishInflate();
}
}
private void parseRequestFocus(XmlPullParser parser, View view)
throws XmlPullParserException, IOException {
//view獲取標(biāo)簽
view.requestFocus();
consumeChildElements(parser);
}
調(diào)用時機(jī):
setContentView()
requestFocus()
適用于:java代碼
四、focus常見問題分析
事件1: ListView中自定義包含ImageButton,Button,CheckBox等控件的子Item,點(diǎn)擊時這些子控件會將焦點(diǎn)獲取到,所以常常當(dāng)點(diǎn)擊item時變化的是子控件,item本身的點(diǎn)擊沒有響應(yīng)。
OnItemClick事件只有在ListView子view都是不可聚焦的情況下觸發(fā)。如果view可以設(shè)置focusable為false,則可以直接設(shè)置為false解決;如果view要求focusable必須為true,可以為每個子控件添加onClickListener,放棄使用OnItemClickListener。
事件2: 有一個ViewGroup A, 包含一個View B, 然后 A, B的focusableInTouchMode都是true, 第一次點(diǎn)擊B, onClickListener不會觸發(fā), 而后續(xù)點(diǎn)擊都可以。
focusable和focusableInTouchMode為true的view,在處理Touch事件時,會優(yōu)先獲取焦點(diǎn),只有在已獲取到焦點(diǎn)時才去觸發(fā)onClick事件。
事件3: EditText自動獲取焦點(diǎn)。
在焦點(diǎn)初始化中,focusable和focusableInTouchMode為true的view會獲取焦點(diǎn)??梢酝ㄟ^指定初始化焦點(diǎn)、設(shè)置優(yōu)先級更高的的可聚焦view解決。