Android TV按键焦点原理浅谈

您所在的位置:网站首页 android事件分发图解 Android TV按键焦点原理浅谈

Android TV按键焦点原理浅谈

2022-12-29 04:15| 来源: 网络整理| 查看: 265

Android TV按键焦点原理浅谈

本篇主要阅读Android源码讲解TV的按键事件分发原理和焦点查找原理,源码基于Android9.0,首先思考几个问题:

当遥控器按下一个按键时按键事件是如何一步一步分发处理的 为什么有的设备长按遥控器第一次会先onKeyDown、onKeyUp,之后才是正常的一直onKeyDown直到松手才onKeyUp 当给View设置setOnKeyListener时,会先走View的onKeyDown回调还是OnKeyListener回调 Activity的onBackPressed方法什么情况下会调用 当按键按下方向键时焦点时如果未控制下一个获取焦点的时候,系统是如何知道该让哪一个控件获取焦点的

带着这些问题,我们一起来撸Android源码吧!了解了系统是如何处理的有便于我们解决TV上一些按键和焦点的问题。

一、按键事件入口

首先我们看下按键事件的入口ViewRootImpl类中的ViewPostImeInputStage内部类:

/** * Delivers post-ime input events to the view hierarchy. */ final class ViewPostImeInputStage extends InputStage { public ViewPostImeInputStage(InputStage next) { super(next); } @Override protected int onProcess(QueuedInputEvent q) { // 1.判断为按键事件则执行processKeyEvent方法 if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { final int source = q.mEvent.getSource(); // 2.判断为触摸事件则执行processPointerEvent方法 if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); // 3.判断为轨迹球事件则执行processTrackballEvent方法 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { return processTrackballEvent(q); // 4.判断为运动事件则执行processGenericMotionEvent方法 } else { return processGenericMotionEvent(q); } } }

可以看到注释1,2,3,4分别判断不同事件执行不同方法,本篇主要讨论的TV焦点事件,主要看下processKeyEvent方法:

private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; if (mUnhandledKeyManager.preViewDispatch(event)) { return FINISH_HANDLED; } // 1.分发按键,如果有消费返回true不继续往下执行 // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } // This dispatch is for windows that don't have a Window.Callback. Otherwise, // the Window.Callback usually will have already called this (see // DecorView.superDispatchKeyEvent) leaving this call a no-op. if (mUnhandledKeyManager.dispatch(mView, event)) { return FINISH_HANDLED; } int groupNavigationDirection = 0; if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_TAB) { if (KeyEventaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) { groupNavigationDirection = View.FOCUS_FORWARD; } else if (KeyEventaStateHasModifiers(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 && !KeyEventaStateHasNoModifiers(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 { // 2.如果按下按键则执行焦点导航逻辑 if (performFocusNavigation(event)) { return FINISH_HANDLED; } } } return FORWARD; } 二、按键事件分发流程

可以看到在该方法中执行了mView.dispatchKeyEvent方法,这里的View其实是DecorView,接着看下该方法:

@Override public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); final int action = event.getAction(); final boolean isDown = action == KeyEvent.ACTION_DOWN; // 1.如果是第一次按下则处理panel的快捷键 if (isDown && (event.getRepeatCount() == 0)) { // First handle chording of panel key: if a panel key is held // but not released, try to execute a shortcut in it. if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) { boolean handled = dispatchKeyShortcutEvent(event); if (handled) { return true; } } // If a panel is open, perform a shortcut on it without the // chorded panel key if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) { if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) { return true; } } } // 2.当Window没destroy且其Callback非空的话,交给其Callback处理 if (!mWindow.isDestroyed()) { final Window.Callback cb = mWindow.getCallback(); final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); if (handled) { return true; } } // 3.如果上面还没处理,则分发到PhoneWindow到onKeyDown、onKeyUp事件处理 return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event) : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event); }

上面首先判断了如果是第一次按下则处理panel的快捷键,如果处理了则不往下走,否则继续判断当窗口未销毁且回调非空则回调处理,如果处理了则不往下走,否则让PhoneWindow对应的onKeyDown,onKeyUp方法来处理。

接下来我们按照这个派发顺序依次来看看相关方法的实现,这里先看看Activity的dispatchKeyEvent实现:

/** * Called to process key events. You can override this to intercept all * key events before they are dispatched to the window. Be sure to call * this implementation for key events that should be handled normally. * * @param event The key event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchKeyEvent(KeyEvent event) { onUserInteraction(); // Let action bars open menus in response to the menu key prioritized over // the window handling it final int keyCode = event.getKeyCode(); if (keyCode == KeyEvent.KEYCODE_MENU && mActionBar != null && mActionBar.onMenuKeyEvent(event)) { return true; } Window win = getWindow(); // 1.从这里事件的处理交给了与之相关的window对象,实质是派发到了view层次结构 if (win.superDispatchKeyEvent(event)) { return true; } View decor = mDecor; if (decor == null) decor = win.getDecorView(); // 2.如果view层次结构没处理则交给KeyEvent本身的dispatch方法,Activity的各种回调方***被触发 return event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this); }

我们看第1点superDispatchKeyEvent方法,可以看到该方法为一个抽象方法,而它的实现是实现它的子类PhoneWindow:

@Override public boolean superDispatchKeyEvent(KeyEvent event) { return mDecor.superDispatchKeyEvent(event); }

该方法又回调用DecorView中的superDispatchKeyEvent方法:

public boolean superDispatchKeyEvent(KeyEvent event) { // Give priority to closing action modes if applicable. if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { final int action = event.getAction(); // Back cancels action modes first. if (mPrimaryActionMode != null) { if (action == KeyEvent.ACTION_UP) { mPrimaryActionMode.finish(); } return true; } } // 1.如果ViewGroup的dispatchKeyEvent方法消费掉了,返回true不走下面 if (super.dispatchKeyEvent(event)) { return true; } // 2.如果ViewRootImpl不为空且被ViewRootImpl的dispatchUnhandledKeyEvent方法消费了,则返回true return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event); }

此时,再来看下ViewGroup的dispatchKeyEvent方法:

@Override public boolean dispatchKeyEvent(KeyEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 1); } if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) { // 1.如果ViewGroup当前是获焦状态或者有边界,分发给View处理 if (super.dispatchKeyEvent(event)) { return true; } } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) { // 2.如果ViewGroup中有获取焦点的View并且ViewGroup有边界,则交给mFocused处理 if (mFocused.dispatchKeyEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 1); } return false; }

接着看下View的dispatchKeyEvent方法:

/** * Dispatch a key event to the next view on the focus path. This path runs * from the top of the view tree down to the currently focused view. If this * view has focus, it will dispatch to itself. Otherwise it will dispatch * the next node down the focus path. This method also fires any key * listeners. * * @param event The key event to be dispatched. * @return True if the event was handled, false otherwise. */ 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; // 1.如果OnKeyListener非空且view是ENABLED状态,则监听器优先触发 if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } // 2.调用KeyEvent的dispatch方法,并将view对象本身作为参数传递进去 if (event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)) { return true; } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }

该方法主要是判断如果有给View设置OnKeyListener事件且View为可用状态,则优先处理监听事件,其次调用KeyEvent的dispatch方法,接下来我们看下该方法:

/** * Deliver this key event to a {@link Callback} interface. If this is * an ACTION_MULTIPLE event and it is not handled, then an attempt will * be made to deliver a single normal event. * * @param receiver The Callback that will be given the event. * @param state State information retained across events. * @param target The target of the dispatch, for use in tracking. * * @return The return value from the Callback method that was called. */ public final boolean dispatch(Callback receiver, DispatcherState state, Object target) { switch (mAction) { case ACTION_DOWN: { mFlags &= ~FLAG_START_TRACKING; if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state + ": " + this); boolean res = receiver.onKeyDown(mKeyCode, this); if (state != null) { if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) { if (DEBUG) Log.v(TAG, " Start tracking!"); state.startTracking(this, target); } else if (isLongPress() && state.isTracking(this)) { try { if (receiver.onKeyLongPress(mKeyCode, this)) { if (DEBUG) Log.v(TAG, " Clear from long press!"); state.performedLongPress(this); res = true; } } catch (AbstractMethodError e) { } } } return res; } case ACTION_UP: if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state + ": " + this); if (state != null) { state.handleUpEvent(this); } return receiver.onKeyUp(mKeyCode, this); case ACTION_MULTIPLE: final int count = mRepeatCount; final int code = mKeyCode; if (receiver.onKeyMultiple(code, count, this)) { return true; } if (code != KeyEvent.KEYCODE_UNKNOWN) { mAction = ACTION_DOWN; mRepeatCount = 0; boolean handled = receiver.onKeyDown(code, this); if (handled) { mAction = ACTION_UP; receiver.onKeyUp(code, this); } mAction = ACTION_MULTIPLE; mRepeatCount = count; return handled; } return false; } return false; }

该方法主要处理了按下、弹起事件,其中按下如果mRepeatCount重复次数大于0判断为长按,则执行长按事件。

我们继续看下View的onKeyDown方法:

/** * Default implementation of {@link KeyEvent.Callback#onKeyDown(int, KeyEvent) * KeyEvent.Callback.onKeyDown()}: perform press of the view * when {@link KeyEvent#KEYCODE_DPAD_CENTER} or {@link KeyEvent#KEYCODE_ENTER} * is released, if the view is enabled and clickable. *

* Key presses in software keyboards will generally NOT trigger this * listener, although some may elect to do so in some situations. Do not * rely on this to catch software key presses. * * @param keyCode a key code that represents the button pressed, from * {@link android.view.KeyEvent} * @param event the KeyEvent object that defines the button action */ public boolean onKeyDown(int keyCode, KeyEvent event) { if (KeyEvent.isConfirmKey(keyCode)) { // 1.如果View为不可用状态,则返回true if ((mViewFlags & ENABLED_MASK) == DISABLED) { return true; } // 2.如果事件重复为0次并且View是可点击的或者可长按的,则设置按下View正中间坐标,检查长按 if (event.getRepeatCount() == 0) { // Long clickable items don't necessarily have to be clickable. final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE; if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) { // For the purposes of menu anchoring and drawable hotspots, // key events are considered to be at the center of the view. final float x = getWidth() / 2f; final float y = getHeight() / 2f; if (clickable) { setPressed(true, x, y); } checkForLongClick(0, x, y); return true; } } } return false; }

按下事件判断了如果为确认相关的按键才到下一步处理,判断点击或长按条件满足,执行按下View正中心坐标,然后执行checkForLongClick检查长按方法,看下该方法如下:

private void checkForLongClick(int delayOffset, float x, float y) { if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); // 1.延迟执行mPendingCheckForLongPress长按Runnable, postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }

我们经常会遇到电视按遥控器时长按会执行一次onKeyDown、onKeyUp,之后才是一直onKeyDown,松开后才执行onKeyUp,原因就在于这个检查长按方法是延迟的。delayOffset传进来的是0,所以延迟时间为ViewConfiguration.getLongPressTimeout(),即该类中定义的DEFAULT_LONG_PRESS_TIMEOUT常量。

同样的如果是触摸屏,可以看下View类中的onTouchEvent方法在按下操作的时候会开启CheckForTap线程检查是否是长按,该线程同样是延迟的,时间为ViewConfiguration.getTapTimeout(),即该类中的TAP_TIMEOUT常量,知道了这个你就知道如果写脚本或插件模拟长按应该间隔多长时间了,是不是一下你的模拟长按插件速度又可以更加准确快速的实现了。

不同版本系统定义的延迟时间有可能不一样,比如GoogleAPI 28的DEFAULT_LONG_PRESS_TIMEOUT是500,TAP_TIMEOUT是100,而API 30的DEFAULT_LONG_PRESS_TIMEOUT是400,TAP_TIMEOUT也是100。

接下来再看下Activity的onKeyDown:

/** * Called when a key was pressed down and not handled by any of the views * inside of the activity. So, for example, key presses while the cursor * is inside a TextView will not trigger the event (unless it is a navigation * to another object) because TextView handles its own key presses. * *

If the focused view didn't want this event, this method is called. * *

The default implementation takes care of {@link KeyEvent#KEYCODE_BACK} * by calling {@link #onBackPressed()}, though the behavior varies based * on the application compatibility mode: for * {@link android.os.Build.VERSION_CODES#ECLAIR} or later applications, * it will set up the dispatch to call {@link #onKeyUp} where the action * will be performed; for earlier applications, it will perform the * action immediately in on-down, as those versions of the platform * behaved. * *

Other additional default key handling may be performed * if configured with {@link #setDefaultKeyMode}. * * @return Return true to prevent this event from being propagated * further, or false to indicate that you have not handled * this event and it should continue to be propagated. * @see #onKeyUp * @see android.view.KeyEvent */ public boolean onKeyDown(int keyCode, KeyEvent event) { // 1.当按下返回键时调用onBackPressed,如果没走检查下是不是重写了onKeyDown方法retrun了true导致 if (keyCode == KeyEvent.KEYCODE_BACK) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.ECLAIR) { event.startTracking(); } else { onBackPressed(); } return true; } if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) { return false; } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) { Window w = getWindow(); if (w.hasFeature(Window.FEATURE_OPTIONS_PANEL) && w.performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE)) { return true; } return false; } else if (keyCode == KeyEvent.KEYCODE_TAB) { // Don't consume TAB here since it's used for navigation. Arrow keys // aren't considered "typing keys" so they already won't get consumed. return false; } else { // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_* boolean clearSpannable = false; boolean handled; if ((event.getRepeatCount() != 0) || event.isSystem()) { clearSpannable = true; handled = false; } else { handled = TextKeyListener.getInstance().onKeyDown( null, mDefaultKeySsb, keyCode, event); if (handled && mDefaultKeySsb.length() > 0) { // something useable has been typed - dispatch it now. final String str = mDefaultKeySsb.toString(); clearSpannable = true; switch (mDefaultKeyMode) { case DEFAULT_KEYS_DIALER: Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + str)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); break; case DEFAULT_KEYS_SEARCH_LOCAL: startSearch(str, false, null, false); break; case DEFAULT_KEYS_SEARCH_GLOBAL: startSearch(str, false, null, true); break; } } } if (clearSpannable) { mDefaultKeySsb.clear(); mDefaultKeySsb.clearSpans(); Selection.setSelection(mDefaultKeySsb,0); } return handled; } }

回到Decorview中的dispatchKeyEvent方法看看PhoneWindow的onKeyDown方法:

/** * A key was pressed down and not handled by anything else in the window. * * @see #onKeyUp * @see android.view.KeyEvent */ protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) { /* **************************************************************************** * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES. * * If your key handling must happen before the app gets a crack at the event, * it goes in PhoneWindowManager. * * If your key handling should happen in all windows, and does not depend on * the state of the current application, other than that the current * application can override the behavior by handling the event itself, it * should go in PhoneFallbackEventHandler. * * Only if your handling depends on the window, and the fact that it has * a DecorView, should it go here. * ****************************************************************************/ final KeyEvent.DispatcherState dispatcher = mDecor != null ? mDecor.getKeyDispatcherState() : null; //Log.i(TAG, "Key down: repeat=" + event.getRepeatCount() // + " flags=0x" + Integer.toHexString(event.getFlags())); switch (keyCode) { case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_MUTE: { // If we have a session send it the volume command, otherwise // use the suggested stream. if (mMediaController != null) { mMediaController.dispatchVolumeButtonEventAsSystemService(event); } else { getMediaSessionManager().dispatchVolumeKeyEventAsSystemService(event, mVolumeControlStreamType); } return true; } // These are all the recognized media key codes in // KeyEvent.isMediaKey() case KeyEvent.KEYCODE_MEDIA_PLAY: case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_MUTE: case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_STOP: case KeyEvent.KEYCODE_MEDIA_NEXT: case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: case KeyEvent.KEYCODE_MEDIA_RECORD: case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { if (mMediaController != null) { if (mMediaController.dispatchMediaButtonEventAsSystemService(event)) { return true; } } return false; } case KeyEvent.KEYCODE_MENU: { onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event); return true; } case KeyEvent.KEYCODE_BACK: { if (event.getRepeatCount() > 0) break; if (featureId < 0) break; // Currently don't do anything with long press. if (dispatcher != null) { dispatcher.startTracking(event, this); } return true; } } return false; }

onKeyUp方法也可以自己再看下,以上就是浅谈按键事件的分发流程了。

总结:

按键的分发方向为DecorView——Activity——ViewGroup——View 按键的消费方向View——Activity——PhoneWindow 各种Callback接口的处理优先级低于监听器,也就是说各种onXXXListener的方法优先被调用 三、焦点导航流程

上面讲解了按键事件分发流程,当上面分发完所有都没消费的时候,就会继续走ViewRootImpl的焦点导航流程,接下来看下performFocusNavigation方法:

private boolean performFocusNavigation(KeyEvent event) { int direction = 0; // 1.判断方向键上下左右和Tab键 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) { // 2.mView:即DecorView,DecorView是整个ViewTree的最顶层View,代表了整个应用的界面. View focused = mView.findFocus(); if (focused != null) { // 3.找到了当前获得焦点的focused,调用该焦点view的focusSearch方法 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); } // 4.找到的下一个可获取焦点的view不是当前已经获得焦点的view,则调用requestFocus方法 if (v.requestFocus(direction, mTempRect)) { playSoundEffect(SoundEffectConstants .getContantForFocusDirection(direction)); return true; } } // 5.给当前获取焦点的focused view 最后一次处理事件的机会 // Give the focused view a last chance to handle the dpad key. if (mView.dispatchUnhandledMove(focused, direction)) { return true; } } else { // 6.递归调用,重置默认焦点(整个视图树上只能有唯一一个默认焦点view) if (mView.restoreDefaultFocus()) { return true; } } } return false; } findFocus

首先我们看mView.findFocus(),该方法实际是调用了ViewGroup的findFocus方法:

/* * (non-Javadoc) * * @see android.view.View#findFocus() */ @Override 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; } focusSearch

该方法很简单,就是向下递归查找在当前页面已经获取焦点的View,继续看focused.focusSearch(direction)调用了View的focusSearch方法:

/** * Find the nearest view in the specified direction that can take focus. * This does not actually give focus to that view. * * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT * * @return The nearest focusable in the specified direction, or null if none * can be found. */ public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { return mParent.focusSearch(this, direction); } else { return null; } }

该方法向上递归查找,调用ViewGroup的focusSearch方法:

/** * Find the nearest view in the specified direction that wants to take * focus. * * @param focused The view that currently has focus * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and * FOCUS_RIGHT, or 0 for not applicable. */ @Override 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; }

如果是根命名空间,则调用FocusFinder的findNextFocus方法查找焦点,否则继续往上查找。继续看FocusFinder的findNextFocus方法:

/** * Find the next view to take focus in root's descendants, starting from the view * that currently is focused. * @param root Contains focused. Cannot be null. * @param focused Has focus now. * @param direction Direction to look. * @return The next focusable view, or null if none exists. */ public final View findNextFocus(ViewGroup root, View focused, int direction) { return findNextFocus(root, focused, null, direction); } private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; ViewGroup effectiveRoot = getEffectiveRoot(root, focused); if (focused != null) { // 1.查找用户指定的获取下一个焦点的view next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction); } if (next != null) { // 2.如果找到用户指定的焦点,则直接返回该焦点 return next; } ArrayList focusables = mTempList; try { focusables.clear(); // 3.添加effectiveRoot下的所有view到focusables集合中去,重写ViewGroup的该方法可以实现焦点记忆功能 effectiveRoot.addFocusables(focusables, direction); if (!focusables.isEmpty()) { // 4.根据系统默认的就近原则算法,查找下一个可获取焦点的最近的view next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }

可以看到该方法首先查找用户指定的下一个获取焦点的view,如果找到了直接返回该view,如果没找到继续下面先添加effectiveRoot下的所有view到focusables集合中去,然后调用findNextFocus方法查找系统可获取下一个焦点的最近view。

findNextUserSpecifiedFocus

我们先看下findNextUserSpecifiedFocus方法的实现:

private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { // check for user specified next focus View userSetNextFocus = focused.findUserSetNextFocus(root, direction); View cycleCheck = userSetNextFocus; boolean cycleStep = true; // we want the first toggle to yield false while (userSetNextFocus != null) { if (userSetNextFocus.isFocusable() && userSetNextFocus.getVisibility() == View.VISIBLE && (!userSetNextFocus.isInTouchMode() || userSetNextFocus.isFocusableInTouchMode())) { return userSetNextFocus; } userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction); if (cycleStep = !cycleStep) { cycleCheck = cycleCheck.findUserSetNextFocus(root, direction); if (cycleCheck == userSetNextFocus) { // found a cycle, user-specified focus forms a loop and none of the views // are currently focusable. break; } } } return null; }

通过用户指定焦点方式不是本篇的重点,这里就不贴出内部细节源码了。该方法实际就是调用View的findUserSetNextFocus方法来查找用户设置的下一个可获取焦点的view,然后在while循环中判断如果找到的是可以获取焦点并且可见的并且不是InTouchNode模式,则返回该焦点,否则继续循环查找直到找了一个循环没有找到可以获取焦点的或者userSetNextFocus为null跳出循环返回null。

findNextFocus

再来看下系统就近原则查找的findNextFocus方法:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList focusables) { if (focused != null) { if (focusedRect == null) { focusedRect = mFocusedRect; } // 1.给focusedRect赋值为已获取焦点的view视图的可见绘图边界 // fill in interesting rect from focused focused.getFocusedRect(focusedRect); root.offsetDescendantRectToMyCoords(focused, focusedRect); } else { if (focusedRect == null) { focusedRect = mFocusedRect; // make up a rect at top left or bottom right of root switch (direction) { case View.FOCUS_RIGHT: case View.FOCUS_DOWN: // 2.修改focusedRect左上角边界为root的左上角边缘 setFocusTopLeft(root, focusedRect); break; case View.FOCUS_FORWARD: if (root.isLayoutRtl()) { setFocusBottomRight(root, focusedRect); } else { setFocusTopLeft(root, focusedRect); } break; case View.FOCUS_LEFT: case View.FOCUS_UP: // 3.修改focusedRect右下角边界为root的右下角边缘 setFocusBottomRight(root, focusedRect); break; case View.FOCUS_BACKWARD: if (root.isLayoutRtl()) { setFocusTopLeft(root, focusedRect); } else { setFocusBottomRight(root, focusedRect); break; } } } } switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: // 4.在相对方向上找到下一个焦点 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, direction); case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: // 5.在绝对相对方向上找到下一个焦点 return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); } }

该方法主要通过findNextFocusInRelativeDirection在相对方向上找下一个焦点,该方法内部逻辑比较简单,这里就不贴出来了,进去看下就知道其实就是先给focusables排序,然后从中找到focused在其中的后一个或前一个view,如果没找到并且focusables不为空则返回focusables的第一个。

接下来我们重点看下findNextFocusInAbsoluteDirection方法:

View findNextFocusInAbsoluteDirection(ArrayList focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { // initialize the best candidate to something impossible // (so the first plausible view will become the best choice) mBestCandidateRect.set(focusedRect); switch(direction) { case View.FOCUS_LEFT: mBestCandidateRect.offset(focusedRect.width() + 1, 0); break; case View.FOCUS_RIGHT: mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); break; case View.FOCUS_UP: mBestCandidateRect.offset(0, focusedRect.height() + 1); break; case View.FOCUS_DOWN: mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); } View closest = null; int numFocusables = focusables.size(); for (int i = 0; i < numFocusables; i++) { View focusable = focusables.get(i); // only interested in other non-root views if (focusable == focused || focusable == root) continue; // get focus bounds of other view in same coordinate system focusable.getFocusedRect(mOtherRect); root.offsetDescendantRectToMyCoords(focusable, mOtherRect); if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } return closest; } isBetterCandidate

再看下isBetterCandidate方法,该方法很关键,内部包含一系列逻辑如何成为最佳候选者:

/** * Is rect1 a better candidate than rect2 for a focus search in a particular * direction from a source rect? This is the core routine that determines * the order of focus searching. * @param direction the direction (up, down, left, right) * @param source The source we are searching from * @param rect1 The candidate rectangle * @param rect2 The current best candidate. * @return Whether the candidate is the new best. */ boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { // 1.source = focusedRect;rect1 = mOtherRect;rect2 = mBestCandidateRect // 以向左寻焦为例,判断rect1是否右边在source右边的左侧并且rect1的左边在source左边的左侧 // to be a better candidate, need to at least be a candidate in the first // place :) if (!isCandidate(source, rect1, direction)) { return false; } // 2.判断react2是否在source左边,如果不是则选择react1,如果是继续下面判断 // we know that rect1 is a candidate.. if rect2 is not a candidate, // rect1 is better if (!isCandidate(source, rect2, direction)) { return true; } // 3.根据方向上是否重叠和距离判断谁更合适 // if rect1 is better by beam, it wins if (beamBeats(direction, source, rect1, rect2)) { return true; } // 4.交换react1和react1继续比较 // if rect2 is better, then rect1 cant' be :) if (beamBeats(direction, source, rect2, rect1)) { return false; } // 5.否则,继续比较距离 // otherwise, do fudge-tastic comparison of the major and minor axis return (getWeightedDistanceFor( majorAxisDistance(direction, source, rect1), minorAxisDistance(direction, source, rect1)) < getWeightedDistanceFor( majorAxisDistance(direction, source, rect2), minorAxisDistance(direction, source, rect2))); }

该方法英文注释很直观,就不中文翻译了,首先看下成为候选人的isCandidate方法:

/** * Is destRect a candidate for the next focus given the direction? This * checks whether the dest is at least partially to the direction of (e.g left of) * from source. * * Includes an edge case for an empty rect (which is used in some cases when * searching from a point on the screen). */ boolean isCandidate(Rect srcRect, Rect destRect, int direction) { switch (direction) { case View.FOCUS_LEFT: return (srcRect.right > destRect.right || srcRect.left >= destRect.right) && srcRect.left > destRect.left; case View.FOCUS_RIGHT: return (srcRect.left < destRect.left || srcRect.right destRect.bottom || srcRect.top >= destRect.bottom) && srcRect.top > destRect.top; case View.FOCUS_DOWN: return (srcRect.top < destRect.top || srcRect.bottom


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3