Androidベース-フォーカス配信ソース解析

20501 ワード

勝手なことを言う
現段階の携帯電話はほとんどタッチパネルで、ボタンは戻り、メニュー、ホームページキーしかありません(iosにはホームページキーが1つしかありません).したがって、コントロールのフォーカスの制御と配布はほとんど行われず、EditTextに対して特別な操作をする以外はフォーカスの問題を考慮しません.しかし、私たちはやはり理解しなければなりません.そうしないと、焦点の問題に遭遇すると、二丈和尚は頭がつかめません.では、これから私と一緒に勉強しましょう.
コントロールにフォーカスを設定する方法
Androidの一般的なコントロールではデフォルトではフォーカスを取得できませんが、いくつかの特殊なコントロールGoogleのお父さんはフォーカス制御を設定しています(ButtonEditText...一般的なコントロールでフォーカスを取得するにはandroid:focusable="true" or view.setFocusable(true)が必要です.タッチしたい場合は、フォーカスandroid:focusableInTouchMode="true" or view.setFocusableInTouchMode(true)も取得します.このプロパティが同時に設定されている場合、最初のクリックでクリックイベントがトリガーされず、2番目のクリックでクリックイベントがトリガーされる理由に注意してください.
                    // 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()が条件を満たすfocusTakentrueに設定されているので入らないことがわかります
                        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;
    }

ソースコードは長いですが、2つの点に注意すればいいです.
first
        if (mView.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }

私たちのviewのdispatchKeyEvent(event)メソッドがtrueに戻ると、今回のボタンイベントが消費されます.では、まず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;
}

ビューのOnKeyListener.onKey(this, event.getKeyCode(), event)がtrueを返すとdispatchKeyEvent(event)がtrueを返し、今回のキーイベントを消費することがわかります.次に、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は現在のView Groupで得られている焦点の子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 focused = mView.findFocus() mViewを取得します.ここでは最上位のDecorViewです.
    ViewのfindFocus()
    public View findFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
    }
    
  • 自分がフォーカスを取得したら自分に戻ります.そうしないとnullに戻ります.

  • 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は、フォーカスを取得したビューを取得するために、フォーカスを取得するサブビューのfindFocus()メソッドを呼び出す.

  • findFocus() == Null findFocus()メソッドでViewを取得しなかった場合(ページにViewがフォーカスを取得していない場合).デフォルトのフォーカスmView.restoreDefaultFocus()を取得します.ここでmViewはDecorViewなので、ViewGroupのrestoreDefaultFocus()を呼び出します.
    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()を呼び出す
    ViewのrestoreDefaultFocus()
    public boolean restoreDefaultFocus() {
        return requestFocus(View.FOCUS_DOWN);
    }
    

    デフォルトでは、DecorViewであるため、次のフォーカスを要求します.だから基本的にページの一番上の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が取得され、空の場合は空に戻ります.
    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を取得するまで呼び出されます.
    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 focusables = mTempList;
        try {
            focusables.clear();
            effectiveRoot.addFocusables(focusables, direction);
            if (!focusables.isEmpty()) {
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }
    

    まず、有効なルートレイアウトのView Groupを取得します.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の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() {
                    @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 findViewByPredicateInsideOut(
            View start, Predicate 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まで検索します.
    ViewのfindViewByPredicateTraversal(predicate, childToSkip)
    protected  T findViewByPredicateTraversal(Predicate predicate,
            View childToSkip) {
        if (predicate.test(this)) {
            return (T) this;
        }
        return null;
    }
    

    ViewGroupのfindViewByPredicateTraversal(predicate, childToSkip)
    protected  T findViewByPredicateTraversal(Predicate 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 {
        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);
    

    この文章が好きならいいねを押すを覚えておいてくださいね~