安卓机顶盒开发中的焦点

一,焦点相关api说明

  • 安卓中的焦点其本质是一个被标记的View,具有唯一性,记录焦点的ViewViewGroup中的定义如下:
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
  • View中定义了一个名为setFocusable的方法,该方法用于设置该View是否可以接收到焦点标记,并非主动标记为焦点View,如下:
/**
 * Set whether this view can receive the focus.
 */
 public void setFocusable(boolean focusable) {
    ...
 }
  • View中的requestFocus方法定义如下:
/**
 * Call this to try to give focus to a specific view or to one of its
 * descendants.
 * @return Whether this view or one of its descendants actually took focus.
 */
public final boolean requestFocus() {
    return requestFocus(View.FOCUS_DOWN);
}

二,分析两个过程

这里我们分析较为常见的两种情况,第一种就是我们主动调用requestFocus去尝试标记焦点View的过程。第二种就是我们通过键盘或者遥控等输入设备触发系统处理焦点变化的过程。

1,requestFocus调用触发焦点变化
  • 先来看下ViewrequestFocus的调用,其依次调用如下方法:
requestFocus()  ->  requestFocus(int direction)   ->    
requestFocus(int direction, Rect previouslyFocusedRect)  ->  
requestFocusNoSearch(int direction, Rect previouslyFocusedRect) ->  
handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect)
  • 在来看下handleFocusGainInternal的定义:
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
    ...

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;

        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

        // 调用parent的requestChildFocus
        if (mParent != null) {
            mParent.requestChildFocus(this, this);
        }

        // 更新dispatchOnGlobalFocusChange
        if (mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }

        // 更新onFocusChanged
        onFocusChanged(true, direction, previouslyFocusedRect);
        refreshDrawableState();
    }
}

至此我们了解到handleFocusGainInternal有这样的三个处理:
- 1,调用parentrequestChildFocus
- 2,更新dispatchOnGlobalFocusChange
- 3,更新onFocusChanged
这里我们先说2、3两点。View中定义了一个名为getViewTreeObserver方法可以拿到ViewTreeObserver,该接口用于获取一些view tree中的全局事件,和焦点相关的有一个名为addOnGlobalFocusChangeListener的方法,通过该方法我们可以指定一个全局的焦点变化的监听器,当全局的焦点发生变化时,监听器接口的onGlobalFocusChanged会被更新。而此接口的更新正是在上述handleFocusGainInternal中完成的。当一个View自身可以接收焦点标记时,我们通常可以通过重写其内部的onFocusChanged方法监听到其自身焦点变化,这里的onFocusChanged方法的更新也位于handleFocusGainInternal中。至此,我们再来深入了解下handleFocusGainInternal中的第一个处理意义何在,首先我们来看,ViewGroup中的requestChildFocus定义如下:

@Override
public void requestChildFocus(View child, View focused) {
    ...

    // Unfocus us, if necessary
    super.unFocus(focused);

    // We had a previous notion of who had focus. Clear it.
    if (mFocused != child) {
        if (mFocused != null) {
            mFocused.unFocus(focused);
        }

        mFocused = child;
    }

    // 调用parent的requestChildFocus
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

我们注意到,ViewGroup中的requestChildFocus方法内部又调用了parent的requestChildFocus,这样就意味着当view tree中的一个View被标记为焦点时,则该view tree中的从焦点View到根View的所有ViewrequestChildFocus都会被调用。这意味着view tree中所有的View都会接收到焦点变化的消息。

2,输入事件触发焦点变化
  • 首先我们先来看下ViewRootImpl中的一段代码:
/**
 * Delivers post-ime input events to the view hierarchy.
 */
 final class ViewPostImeInputStage extends InputStage {
    ...
 }
  • 再继续往下看:
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
    return FINISH_HANDLED;
}
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
    int direction = 0;
    ...
}
  • 再往下看这里是重点:
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 FINISH_HANDLED;
            }
        }
        // Give the focused view a last chance to handle the dpad key.
        if (mView.dispatchUnhandledMove(focused, direction)) {
            return FINISH_HANDLED;
        }
    } else {
        // find the best view to give focus to in this non-touch-mode with no-focus
        View v = focusSearch(null, direction);
        if (v != null && v.requestFocus(direction)) {
            return FINISH_HANDLED;
        }
    }
}

我们注意到当输入事件发生时,会先寻找当前视图中被标记为焦点的View,如果不为空,则会调用焦点ViewfocusSearch方法寻找新的焦点。我们来分析一下这个方法,该方法在View中的定义如下:

/**
 * Find the nearest view in the specified direction that can take focus.
 * This does not actually give focus to that view.
 */
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.
 */
@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;
}

parent不为空的时候,就继续调用parentfocusSearch方法,这就意味着从当前焦点开始到根View的所有节点都会接收到寻找焦点的消息,要做处理,只需要重写特定节点的focusSearch方法即可。当寻找至根View时,则新的焦点会借助一个辅助类FocusFinder去寻找。FocusFinder中的相关逻辑我们稍后再说。

  • 我们再回到输入事件这里,这里View v = focused.focusSearch(direction);执行完之后,最终还是会调用requestFocus来尝试标记焦点,至于requestFocus方法,我们前面已经分析过了。其实View中提供了一个dispatchUnhandledMove方法,允许我们在输入事件触发之后焦点分发之前加入自己的焦点处理逻辑。这一点可以由下面代码看出:
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
    return FINISH_HANDLED;
}

而当findFocus方法返回空时,则会通过ViewRootImpl内部的focusSearch方法寻找新焦点。该方法内部则还是借助辅助类FocusFinder去寻找新焦点的。

待更新…


你可能感兴趣的:(AndroidTv)