Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4e6319b7 authored by Jeff Brown's avatar Jeff Brown
Browse files

Add initial support for TAB navigation.

Bug: 3286652
Change-Id: I813a0318b3b8d9c9bc791ea6a2427be11c08de00
parent 12d81c49
Loading
Loading
Loading
Loading
+35 −0
Original line number Diff line number Diff line
@@ -6730,6 +6730,17 @@
 visibility="public"
>
</field>
<field name="nextFocusForward"
 type="int"
 transient="false"
 volatile="false"
 value="16843594"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="nextFocusLeft"
 type="int"
 transient="false"
@@ -206743,6 +206754,17 @@
 visibility="public"
>
</method>
<method name="getNextFocusForwardId"
 return="int"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="getNextFocusLeftId"
 return="int"
 abstract="false"
@@ -209156,6 +209178,19 @@
<parameter name="nextFocusDownId" type="int">
</parameter>
</method>
<method name="setNextFocusForwardId"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="nextFocusForwardId" type="int">
</parameter>
</method>
<method name="setNextFocusLeftId"
 return="void"
 abstract="false"
+102 −0
Original line number Diff line number Diff line
@@ -19,6 +19,8 @@ package android.view;
import android.graphics.Rect;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

/**
 * The algorithm used for finding the next focusable view in a given direction
@@ -44,6 +46,7 @@ public class FocusFinder {
    Rect mFocusedRect = new Rect();
    Rect mOtherRect = new Rect();
    Rect mBestCandidateRect = new Rect();
    SequentialFocusComparator mSequentialFocusComparator = new SequentialFocusComparator();

    // enforce thread local access
    private FocusFinder() {}
@@ -76,6 +79,7 @@ public class FocusFinder {
            switch (direction) {
                case View.FOCUS_RIGHT:
                case View.FOCUS_DOWN:
                case View.FOCUS_FORWARD:
                    final int rootTop = root.getScrollY();
                    final int rootLeft = root.getScrollX();
                    mFocusedRect.set(rootLeft, rootTop, rootLeft, rootTop);
@@ -83,6 +87,7 @@ public class FocusFinder {

                case View.FOCUS_LEFT:
                case View.FOCUS_UP:
                case View.FOCUS_BACKWARD:
                    final int rootBottom = root.getScrollY() + root.getHeight();
                    final int rootRight = root.getScrollX() + root.getWidth();
                    mFocusedRect.set(rootRight, rootBottom,
@@ -107,6 +112,48 @@ public class FocusFinder {

    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        ArrayList<View> focusables = root.getFocusables(direction);
        if (focusables.isEmpty()) {
            // The focus cannot change.
            return null;
        }

        if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
            if (focused != null && !focusables.contains(focused)) {
                // Add the currently focused view to the list to have it sorted
                // along with the other views.
                focusables.add(focused);
            }

            try {
                // Note: This sort is stable.
                mSequentialFocusComparator.setRoot(root);
                Collections.sort(focusables, mSequentialFocusComparator);
            } finally {
                mSequentialFocusComparator.recycle();
            }

            final int count = focusables.size();
            switch (direction) {
                case View.FOCUS_FORWARD:
                    if (focused != null) {
                        int position = focusables.lastIndexOf(focused);
                        if (position >= 0 && position + 1 < count) {
                            return focusables.get(position + 1);
                        }
                    }
                    return focusables.get(0);

                case View.FOCUS_BACKWARD:
                    if (focused != null) {
                        int position = focusables.indexOf(focused);
                        if (position > 0) {
                            return focusables.get(position - 1);
                        }
                    }
                    return focusables.get(count - 1);
            }
            return null;
        }

        // initialize the best candidate to something impossible
        // (so the first plausible view will become the best choice)
@@ -477,4 +524,59 @@ public class FocusFinder {
        throw new IllegalArgumentException("direction must be one of "
                + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
    }

    /**
     * Sorts views according to their visual layout and geometry for default tab order.
     * This is used for sequential focus traversal.
     */
    private static final class SequentialFocusComparator implements Comparator<View> {
        private final Rect mFirstRect = new Rect();
        private final Rect mSecondRect = new Rect();
        private ViewGroup mRoot;

        public void recycle() {
            mRoot = null;
        }

        public void setRoot(ViewGroup root) {
            mRoot = root;
        }

        public int compare(View first, View second) {
            if (first == second) {
                return 0;
            }

            getRect(first, mFirstRect);
            getRect(second, mSecondRect);

            if (mFirstRect.top < mSecondRect.top) {
                return -1;
            } else if (mFirstRect.top > mSecondRect.top) {
                return 1;
            } else if (mFirstRect.left < mSecondRect.left) {
                return -1;
            } else if (mFirstRect.left > mSecondRect.left) {
                return 1;
            } else if (mFirstRect.bottom < mSecondRect.bottom) {
                return -1;
            } else if (mFirstRect.bottom > mSecondRect.bottom) {
                return 1;
            } else if (mFirstRect.right < mSecondRect.right) {
                return -1;
            } else if (mFirstRect.right > mSecondRect.right) {
                return 1;
            } else {
                // The view are distinct but completely coincident so we consider
                // them equal for our purposes.  Since the sort is stable, this
                // means that the views will retain their layout order relative to one another.
                return 0;
            }
        }

        private void getRect(View view, Rect rect) {
            view.getDrawingRect(rect);
            mRoot.offsetDescendantRectToMyCoords(view, rect);
        }
    }
}
+93 −22
Original line number Diff line number Diff line
@@ -64,6 +64,7 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.ScrollBarDrawable;
import com.android.internal.R;
import com.android.internal.util.Predicate;
import com.android.internal.view.menu.MenuBuilder;

import java.lang.ref.WeakReference;
@@ -2066,6 +2067,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
     */
    private int mNextFocusDownId = View.NO_ID;

    /**
     * When this view has focus and the next focus is {@link #FOCUS_FORWARD},
     * the user may specify which view to go to next.
     */
    int mNextFocusForwardId = View.NO_ID;

    private CheckForLongPress mPendingCheckForLongPress;
    private CheckForTap mPendingCheckForTap = null;
    private PerformClick mPerformClick;
@@ -2424,6 +2431,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
                case R.styleable.View_nextFocusDown:
                    mNextFocusDownId = a.getResourceId(attr, View.NO_ID);
                    break;
                case R.styleable.View_nextFocusForward:
                    mNextFocusForwardId = a.getResourceId(attr, View.NO_ID);
                    break;
                case R.styleable.View_minWidth:
                    mMinWidth = a.getDimensionPixelSize(attr, 0);
                    break;
@@ -3112,9 +3122,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
     * @param gainFocus True if the View has focus; false otherwise.
     * @param direction The direction focus has moved when requestFocus()
     *                  is called to give this view focus. Values are
     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT} or
     *                  {@link #FOCUS_RIGHT}. It may not always apply, in which
     *                  case use the default.
     *                  {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT},
     *                  {@link #FOCUS_RIGHT}, {@link #FOCUS_FORWARD}, or {@link #FOCUS_BACKWARD}.
     *                  It may not always apply, in which case use the default.
     * @param previouslyFocusedRect The rectangle, in this view's coordinate
     *        system, of the previously focused view.  If applicable, this will be
     *        passed in as finer grained information about where the focus is coming
@@ -3359,7 +3369,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * @return The user specified next focus ID.
     * Gets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusLeft
     */
@@ -3368,9 +3379,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * Set the id of the view to use for the next focus
     *
     * @param nextFocusLeftId
     * Sets the id of the view to use when the next focus is {@link #FOCUS_LEFT}.
     * @param nextFocusLeftId The next focus ID, or {@link #NO_ID} if the framework should
     * decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusLeft
     */
@@ -3379,7 +3390,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * @return The user specified next focus ID.
     * Gets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusRight
     */
@@ -3388,9 +3400,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * Set the id of the view to use for the next focus
     *
     * @param nextFocusRightId
     * Sets the id of the view to use when the next focus is {@link #FOCUS_RIGHT}.
     * @param nextFocusRightId The next focus ID, or {@link #NO_ID} if the framework should
     * decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusRight
     */
@@ -3399,7 +3411,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * @return The user specified next focus ID.
     * Gets the id of the view to use when the next focus is {@link #FOCUS_UP}.
     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusUp
     */
@@ -3408,9 +3421,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * Set the id of the view to use for the next focus
     *
     * @param nextFocusUpId
     * Sets the id of the view to use when the next focus is {@link #FOCUS_UP}.
     * @param nextFocusUpId The next focus ID, or {@link #NO_ID} if the framework should
     * decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusUp
     */
@@ -3419,7 +3432,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * @return The user specified next focus ID.
     * Gets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusDown
     */
@@ -3428,9 +3442,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
    }

    /**
     * Set the id of the view to use for the next focus
     *
     * @param nextFocusDownId
     * Sets the id of the view to use when the next focus is {@link #FOCUS_DOWN}.
     * @param nextFocusDownId The next focus ID, or {@link #NO_ID} if the framework should
     * decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusDown
     */
@@ -3438,6 +3452,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
        mNextFocusDownId = nextFocusDownId;
    }

    /**
     * Gets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
     * @return The next focus ID, or {@link #NO_ID} if the framework should decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusForward
     */
    public int getNextFocusForwardId() {
        return mNextFocusForwardId;
    }

    /**
     * Sets the id of the view to use when the next focus is {@link #FOCUS_FORWARD}.
     * @param nextFocusForwardId The next focus ID, or {@link #NO_ID} if the framework should
     * decide automatically.
     *
     * @attr ref android.R.styleable#View_nextFocusForward
     */
    public void setNextFocusForwardId(int nextFocusForwardId) {
        mNextFocusForwardId = nextFocusForwardId;
    }

    /**
     * Returns the visibility of this view and all of its ancestors
     *
@@ -3949,10 +3984,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility

    /**
     * If a user manually specified the next view id for a particular direction,
     * use the root to look up the view.  Once a view is found, it is cached
     * for future lookups.
     * use the root to look up the view.
     * @param root The root view of the hierarchy containing this view.
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
     * or FOCUS_BACKWARD.
     * @return The user specified next view, or null if there is none.
     */
    View findUserSetNextFocus(View root, int direction) {
@@ -3969,6 +4004,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
            case FOCUS_DOWN:
                if (mNextFocusDownId == View.NO_ID) return null;
                return findViewShouldExist(root, mNextFocusDownId);
            case FOCUS_FORWARD:
                if (mNextFocusForwardId == View.NO_ID) return null;
                return findViewShouldExist(root, mNextFocusForwardId);
            case FOCUS_BACKWARD: {
                final int id = mID;
                return root.findViewByPredicate(new Predicate<View>() {
                    @Override
                    public boolean apply(View t) {
                        return t.mNextFocusForwardId == id;
                    }
                });
            }
        }
        return null;
    }
@@ -9358,6 +9405,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
        return null;
    }

    /**
     * {@hide}
     * @param predicate The predicate to evaluate.
     * @return The first view that matches the predicate or null.
     */
    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
        if (predicate.apply(this)) {
            return this;
        }
        return null;
    }

    /**
     * Look for a child view with the given id.  If this view has the given
     * id, return this view.
@@ -9386,6 +9445,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility
        return findViewWithTagTraversal(tag);
    }

    /**
     * {@hide}
     * Look for a child view that matches the specified predicate.
     * If this view matches the predicate, return this view.
     *
     * @param predicate The predicate to evaluate.
     * @return The first view that matches the predicate or null.
     */
    public final View findViewByPredicate(Predicate<View> predicate) {
        return findViewByPredicateTraversal(predicate);
    }

    /**
     * Sets the identifier for this view. The identifier does not have to be
     * unique in this view's hierarchy. The identifier should be a positive
+28 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.view;

import android.animation.LayoutTransition;
import com.android.internal.R;
import com.android.internal.util.Predicate;

import android.content.Context;
import android.content.res.Configuration;
@@ -2515,6 +2516,33 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
        return null;
    }

    /**
     * {@hide}
     */
    @Override
    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
        if (predicate.apply(this)) {
            return this;
        }

        final View[] where = mChildren;
        final int len = mChildrenCount;

        for (int i = 0; i < len; i++) {
            View v = where[i];

            if ((v.mPrivateFlags & IS_ROOT_NAMESPACE) == 0) {
                v = v.findViewByPredicate(predicate);

                if (v != null) {
                    return v;
                }
            }
        }

        return null;
    }

    /**
     * Adds a child view. If no layout parameters are already set on the child, the
     * default parameters for this ViewGroup are set on the child.
+58 −46
Original line number Diff line number Diff line
@@ -2415,81 +2415,78 @@ public final class ViewRoot extends Handler implements ViewParent,
    }

    /**
     * @param keyCode The key code
     * @return True if the key is directional.
     * Returns true if the key is used for keyboard navigation.
     * @param keyEvent The key event.
     * @return True if the key is used for keyboard navigation.
     */
    static boolean isDirectional(int keyCode) {
        switch (keyCode) {
    private static boolean isNavigationKey(KeyEvent keyEvent) {
        switch (keyEvent.getKeyCode()) {
        case KeyEvent.KEYCODE_DPAD_LEFT:
        case KeyEvent.KEYCODE_DPAD_RIGHT:
        case KeyEvent.KEYCODE_DPAD_UP:
        case KeyEvent.KEYCODE_DPAD_DOWN:
        case KeyEvent.KEYCODE_DPAD_CENTER:
        case KeyEvent.KEYCODE_PAGE_UP:
        case KeyEvent.KEYCODE_PAGE_DOWN:
        case KeyEvent.KEYCODE_MOVE_HOME:
        case KeyEvent.KEYCODE_MOVE_END:
        case KeyEvent.KEYCODE_TAB:
        case KeyEvent.KEYCODE_SPACE:
        case KeyEvent.KEYCODE_ENTER:
            return true;
        }
        return false;
    }

    /**
     * Returns true if this key is a keyboard key.
     * Returns true if the key is used for typing.
     * @param keyEvent The key event.
     * @return whether this key is a keyboard key.
     * @return True if the key is used for typing.
     */
    private static boolean isKeyboardKey(KeyEvent keyEvent) {
      final int convertedKey = keyEvent.getUnicodeChar();
        return convertedKey > 0;
    private static boolean isTypingKey(KeyEvent keyEvent) {
        return keyEvent.getUnicodeChar() > 0;
    }



    /**
     * See if the key event means we should leave touch mode (and leave touch
     * mode if so).
     * See if the key event means we should leave touch mode (and leave touch mode if so).
     * @param event The key event.
     * @return Whether this key event should be consumed (meaning the act of
     *   leaving touch mode alone is considered the event).
     */
    private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) {
        // Only relevant in touch mode.
        if (!mAttachInfo.mInTouchMode) {
            return false;
        }

        // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP.
        final int action = event.getAction();
        if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) {
            return false;
        }

        // Don't leave touch mode if the IME told us not to.
        if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) {
            return false;
        }

        // only relevant if we are in touch mode
        if (!mAttachInfo.mInTouchMode) {
            return false;
        // If the key can be used for keyboard navigation then leave touch mode
        // and select a focused view if needed (in ensureTouchMode).
        // When a new focused view is selected, we consume the navigation key because
        // navigation doesn't make much sense unless a view already has focus so
        // the key's purpose is to set focus.
        if (isNavigationKey(event)) {
            return ensureTouchMode(false);
        }

        // if something like an edit text has focus and the user is typing,
        // leave touch mode
        //
        // note: the condition of not being a keyboard key is kind of a hacky
        // approximation of whether we think the focused view will want the
        // key; if we knew for sure whether the focused view would consume
        // the event, that would be better.
        if (isKeyboardKey(event) && mView != null && mView.hasFocus()) {
            mFocusedView = mView.findFocus();
            if ((mFocusedView instanceof ViewGroup)
                    && ((ViewGroup) mFocusedView).getDescendantFocusability() ==
                    ViewGroup.FOCUS_AFTER_DESCENDANTS) {
                // something has focus, but is holding it weakly as a container
                return false;
            }
            if (ensureTouchMode(false)) {
                throw new IllegalStateException("should not have changed focus "
                        + "when leaving touch mode while a view has focus.");
            }
        // If the key can be used for typing then leave touch mode
        // and select a focused view if needed (in ensureTouchMode).
        // Always allow the view to process the typing key.
        if (isTypingKey(event)) {
            ensureTouchMode(false);
            return false;
        }

        if (isDirectional(event.getKeyCode())) {
            // no view has focus, so we leave touch mode (and find something
            // to give focus to).  the event is consumed if we were able to
            // find something to give focus to.
            return ensureTouchMode(false);
        }
        return false;
    }

@@ -2640,16 +2637,31 @@ public final class ViewRoot extends Handler implements ViewParent,
            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;
            }

Loading