随便扯扯
在现阶段的手机几乎都是触摸屏,按钮只有返回、菜单和主页键了(ios只有一个主页键了)。所以对控件的焦点的控制和分发几乎不用做,除了对EditText做一些特殊操作才会考虑到焦点问题。但是我们还是必须了解下,不然遇到焦点问题就二丈和尚摸不着头脑了。那么接下来和我一起学习一下吧。
如何给控件设置可获取焦点
Android中一般的控件中默认都是不可获取焦点的 但是一些特殊的控件Google爸爸已经给他们设置好了焦点控制(Button
, EditText
..)。一般的控件想要获取焦点就要android:focusable="true"
orview.setFocusable(true)
。当你希望触摸时也获取焦点android:focusableInTouchMode="true"
orview.setFocusableInTouchMode(true)
。请注意 当这个属性同时设置时 你第一下点击时不会触发点击事件,要第二下点击才会触发点击事件 原因:
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
可以看到当这个控件 第一次点击时isFocusable() && isFocusableInTouchMode() && !isFocused()
是满足条件的focusTaken
会置为true
所以不会进入
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
解决办法就扯远了可自行Google或者看Button
的源码来看他们是怎么解决的。
焦点分发
当我们按下遥控器的按键后我们首先进入该方法:
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
int groupNavigationDirection = 0;
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
groupNavigationDirection = View.FOCUS_FORWARD;
} else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
groupNavigationDirection = View.FOCUS_BACKWARD;
}
}
// If a modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())
&& groupNavigationDirection == 0) {
if (mView.dispatchKeyShortcutEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
源代码很长 但是我们只要注意两个点就行
first
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
就是当我们的view的dispatchKeyEvent(event)
方法返回true时就会消费这次按键事件。</p>
那我们先来看看View的dispatchKeyEvent(event)
方法
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
可见当View的OnKeyListener.onKey(this, event.getKeyCode(), event)
返回true时就会使dispatchKeyEvent(event)
返回true从而消费掉这次按键事件</p>
接下来我们来看看ViewGroup的dispatchKeyEvent(event)
方法
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
- 焦点是在他本身的,他就会调用父类的
dispatchKeyEvent(event)
当父类的已经拦截了消费了当前按键事件就马上消费掉不往下传递 - 当mFocused != Null并且就会调用mFocused的
dispatchKeyEvent(event)
。那么大家都会问了mFocused是何方圣神,mFocused就是当前ViewGroup中获取着的焦点的子View
second
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
这个方法就是处理接下来的焦点分发。
private boolean performFocusNavigation(KeyEvent event) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
识别按键
上面的一半代码主要就是处理了按键的方向。
获取已经获取焦点的View
当按下的是方向键时首先我们就会获取已经获取焦点的View View focused = mView.findFocus()
</p><span style="color:red">mView在这里就是最顶层的DecorView</span>。</p>
View的findFocus()
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}
- 当自己获取焦点着就返回自己否则返回null。</p>
ViewGroup的findFocus()
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
- 首先当自己时获取焦点状态就返回自己。
- 当mFocused != Null就调用获取焦点的子View的
findFocus()
方法来获取已经获取焦点的View。</p>
findFocus() == Null
当我们通过findFocus()
方法没有获取到View(就是页面没有View获取着焦点)。我们就是获取一个默认的焦点mView.restoreDefaultFocus()
而这里的mView就是DecorView所以会调用ViewGroup的restoreDefaultFocus()
</p>
ViewGroup的restoreDefaultFocus()
public boolean restoreDefaultFocus() {
if (mDefaultFocus != null
&& getDescendantFocusability() != FOCUS_BLOCK_DESCENDANTS
&& (mDefaultFocus.mViewFlags & VISIBILITY_MASK) == VISIBLE
&& mDefaultFocus.restoreDefaultFocus()) {
return true;
}
return super.restoreDefaultFocus();
}
mDefaultFocus就是默认的获取焦点的View。当他不为空而且可见就会调用他的restoreDefaultFocus()
。如果消费掉了就不往下传递,否则就调用父类(View)的restoreDefaultFocus()
</p>
View的restoreDefaultFocus()
public boolean restoreDefaultFocus() {
return requestFocus(View.FOCUS_DOWN);
}
默认就是请求下面的一个焦点因为是DecorView。所以基本上是页面最上面的一个View。
根据方向查找下一个获取的焦点的View
当当前获取焦点的View不为空时,我们就好按照方向来查找下一个应该获取焦点的View是哪个View v = focused.focusSearch(direction)
。
View的focusSearch(direction)
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
当父控件不为空时就调用父布局的focusSearch(view, direction)
来获取下一个获取焦点的View,为空时返回空。</p>
ViewGroup的focusSearch(view, direction)
public View focusSearch(View focused, int direction) {
if (isRootNamespace()) {
// root namespace means we should consider ourselves the top of the
// tree for focus searching; otherwise we could be focus searching
// into other tabs. see LocalActivityManager and TabHost for more info.
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
就是不停的调用父布局的focusSearch(view, direction)
直到根布局然后FocusFinder.getInstance().findNextFocus(this, focused, direction)
获取下一个获取焦点的View。</p>
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
首先获取有效的根布局的ViewGroup。focused肯定是不为空的,所以进入
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
// check for user specified next focus
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
while (userSetNextFocus != null) {
if (userSetNextFocus.isFocusable()
&& userSetNextFocus.getVisibility() == View.VISIBLE
&& (!userSetNextFocus.isInTouchMode()
|| userSetNextFocus.isFocusableInTouchMode())) {
return userSetNextFocus;
}
userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
}
return null;
}
这里先调用已获取焦点View的findUserSetNextFocus(root, direction)
来获取用户设置的下一个获取焦点的View。当这个View是可获取焦点的,并且可见的而且不再触摸模式或者在触摸模式并且可在触摸时获取焦点就返回这个View作为下一个可获取焦点的View。否则继续调用刚刚得到的View的findUserSetNextFocus(root, direction)
去查找下一个View
View findUserSetNextFocus(View root, @FocusDirection int direction) {
switch (direction) {
case FOCUS_LEFT:
if (mNextFocusLeftId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusLeftId);
case FOCUS_RIGHT:
if (mNextFocusRightId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusRightId);
case FOCUS_UP:
if (mNextFocusUpId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusUpId);
case FOCUS_DOWN:
if (mNextFocusDownId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusDownId);
case FOCUS_FORWARD:
if (mNextFocusForwardId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusForwardId);
case FOCUS_BACKWARD: {
if (mID == View.NO_ID) return null;
final int id = mID;
return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
@Override
public boolean test(View t) {
return t.mNextFocusForwardId == id;
}
});
}
}
return null;
}
这里的mNextFocusLeftId、mNextFocusRightId、mNextFocusUpId、mNextFocusDownId、mNextFocusForwardId,就是用户在xml中设置的android:nextFocusLeft="" android:nextFocusRight="" android:nextFocusUp="" android:nextFocusDown="" android:nextFocusForward=""
的id。
private View findViewInsideOutShouldExist(View root, int id) {
if (mMatchIdPredicate == null) {
mMatchIdPredicate = new MatchIdPredicate();
}
mMatchIdPredicate.mId = id;
View result = root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
if (result == null) {
Log.w(VIEW_LOG_TAG, "couldn't find view with id " + id);
}
return result;
}
这里就是调用根View的findViewByPredicateInsideOut(this, mMatchIdPredicate)
来获取下一个获取焦点的View
public final <T extends View> T findViewByPredicateInsideOut(
View start, Predicate<View> predicate) {
View childToSkip = null;
for (;;) {
T view = start.findViewByPredicateTraversal(predicate, childToSkip);
if (view != null || start == this) {
return view;
}
ViewParent parent = start.getParent();
if (parent == null || !(parent instanceof View)) {
return null;
}
childToSkip = start;
start = (View) parent;
}
}
这里就是一层一层往上的查找下一个应该获取焦点的View直到根View。</p>
View的findViewByPredicateTraversal(predicate, childToSkip)
protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
View childToSkip) {
if (predicate.test(this)) {
return (T) this;
}
return null;
}
ViewGroup的findViewByPredicateTraversal(predicate, childToSkip)
protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
View childToSkip) {
if (predicate.test(this)) {
return (T) this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if (v != childToSkip && (v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewByPredicate(predicate);
if (v != null) {
return (T) v;
}
}
}
return null;
}
MatchIdPredicate
private static class MatchIdPredicate implements Predicate<View> {
public int mId;
@Override
public boolean test(View view) {
return (view.mID == mId);
}
}
最后一步
查找到下一个获取焦点的View不为空而且不是一件获取焦点的View就requestFocus(direction, mTempRect)
并播放声音。
得出的结论
- 当我们需要做一个按键的拦截操作时,我们可以重写
dispatchKeyEvent(event)
。也可是直接设置setOnKeyLinstener
。 - 当我们有时会按键会看不到焦点到底去哪了或者感觉焦点“丢失”了,很有可能是被一些已经被覆盖住的View获取了焦点。所以我们要竟可能的把已经被覆盖住的View设置为GONE,防止焦点乱跑。
小窍门
我们可以在ViewRootImpl
的4643-4645行打断点看当前获取焦点的View和下一个应该获取焦点的View
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
<span style="color:black;font-weight:bold;font-size:36">如果你喜欢这篇文章请记得<a href="https://github.com/kingwang666/books">点赞</a>哦~</span>
网友评论