Loading core/java/android/view/View.java +23 −0 Original line number Diff line number Diff line Loading @@ -3770,6 +3770,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } if ((mPrivateFlags & FOCUSED) != 0) { // If this is the first focusable do not clear focus since the we // try to give it focus every time a view clears its focus. Hence, // the view that would gain focus already has it. View firstFocusable = getFirstFocusable(); if (firstFocusable == this) { return; } mPrivateFlags &= ~FOCUSED; if (mParent != null) { Loading @@ -3778,7 +3786,22 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(false, 0, null); refreshDrawableState(); // The view cleared focus and invoked the callbacks, so now is the // time to give focus to the the first focusable to ensure that the // gain focus is announced after clear focus. if (firstFocusable != null) { firstFocusable.requestFocus(FOCUS_FORWARD); } } } private View getFirstFocusable() { ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null) { return viewRoot.focusSearch(null, FOCUS_FORWARD); } return null; } /** Loading core/java/android/view/ViewGroup.java +11 −8 Original line number Diff line number Diff line Loading @@ -675,11 +675,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } if (mFocused == null) { super.clearFocus(); // clear any child focus if it exists if (mFocused != null) { } else { mFocused.clearFocus(); mFocused = null; } } Loading @@ -691,13 +694,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (DBG) { System.out.println(this + " unFocus()"); } if (mFocused == null) { super.unFocus(); if (mFocused != null) { } else { mFocused.unFocus(); } mFocused = null; } } /** * Returns the focused child of this view, if any. The child may have focus Loading core/java/android/view/ViewRootImpl.java +19 −16 Original line number Diff line number Diff line Loading @@ -168,6 +168,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, View mView; View mFocusedView; View mRealFocusedView; // this is not set to null in touch mode View mOldFocusedView; int mViewVisibility; boolean mAppVisible = true; int mOrigWindowType = -1; Loading Loading @@ -2226,32 +2227,33 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void requestChildFocus(View child, View focused) { checkThread(); if (mFocusedView != focused) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, focused); scheduleTraversals(); if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Request child focus: focus now " + focused); } mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, focused); scheduleTraversals(); mFocusedView = mRealFocusedView = focused; if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Request child focus: focus now " + mFocusedView); } public void clearChildFocus(View child) { checkThread(); View oldFocus = mFocusedView; if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Clearing child focus"); mFocusedView = mRealFocusedView = null; if (mView != null && !mView.hasFocus()) { // If a view gets the focus, the listener will be invoked from requestChildFocus() if (!mView.requestFocus(View.FOCUS_FORWARD)) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null); } } else if (oldFocus != null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null); if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Clearing child focus"); } mOldFocusedView = mFocusedView; // Invoke the listener only if there is no view to take focus if (focusSearch(null, View.FOCUS_FORWARD) == null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, null); } mFocusedView = mRealFocusedView = null; } public void focusableViewAvailable(View v) { checkThread(); Loading Loading @@ -2724,6 +2726,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, mView.unFocus(); mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); mFocusedView = null; mOldFocusedView = null; return true; } } Loading core/tests/coretests/src/android/widget/focus/RequestFocus.java +0 −2 Original line number Diff line number Diff line Loading @@ -21,9 +21,7 @@ import com.android.frameworks.coretests.R; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.widget.LinearLayout; import android.widget.Button; import android.view.View; /** * Exercises cases where elements of the UI are requestFocus()ed. Loading core/tests/coretests/src/android/widget/focus/RequestFocusTest.java +154 −7 Original line number Diff line number Diff line Loading @@ -16,21 +16,27 @@ package android.widget.focus; import android.widget.focus.RequestFocus; import com.android.frameworks.coretests.R; import android.os.Handler; import android.test.ActivityInstrumentationTestCase; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; import android.widget.Button; import android.util.AndroidRuntimeException; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import android.widget.Button; import com.android.frameworks.coretests.R; import java.util.ArrayList; import java.util.List; /** * {@link RequestFocusTest} is set up to exercise cases where the views that * have focus become invisible or GONE. */ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFocus> { public class RequestFocusTest extends ActivityInstrumentationTestCase2<RequestFocus> { private Button mTopLeftButton; private Button mBottomLeftButton; Loading @@ -39,7 +45,7 @@ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFoc private Handler mHandler; public RequestFocusTest() { super("com.android.frameworks.coretests", RequestFocus.class); super(RequestFocus.class); } @Override Loading Loading @@ -94,4 +100,145 @@ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFoc e.getClass().getName()); } } /** * This tests checks the case in which the first focusable View clears focus. * In such a case the framework tries to give the focus to another View starting * from the top. Hence, the framework will try to give focus to the view that * wants to clear its focus. From a client perspective, the view does not loose * focus after the call, therefore no callback for focus change should be invoked. * * @throws Exception If an error occurs. */ @UiThreadTest public void testOnFocusChangeNotCalledIfFocusDoesNotMove() throws Exception { // Get the first focusable. Button button = mTopLeftButton; // Make sure that the button is the first focusable and focus it. button.getRootView().requestFocus(View.FOCUS_DOWN); assertTrue(button.hasFocus()); // Attach on focus change listener that should not be called. button.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { throw new IllegalStateException("Unexpeced call to" + "OnFocusChangeListener#onFocusChange"); } }); // Attach on global focus change listener that should not be called. button.getViewTreeObserver().addOnGlobalFocusChangeListener( new OnGlobalFocusChangeListener() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { throw new IllegalStateException("Unexpeced call to" + "OnFocusChangeListener#onFocusChange"); } }); // Try to clear focus. button.clearFocus(); } /** * This tests check whether the on focus change callbacks are invoked in * the proper order when a View loses focus and the framework gives it to * the fist focusable one. * * @throws Exception */ @UiThreadTest public void testOnFocusChangeCallbackOrder() throws Exception { // Get the first focusable. Button clearingFocusButton = mTopRightButton; Button gainingFocusButton = mTopLeftButton; // Make sure that the clearing focus is not the first focusable. View focusCandidate = clearingFocusButton.getRootView().getParent().focusSearch(null, View.FOCUS_FORWARD); assertNotSame("The clearing focus button is not the first focusable.", clearingFocusButton, focusCandidate); assertSame("The gaining focus button is the first focusable.", gainingFocusButton, focusCandidate); // Focus the clearing focus button. clearingFocusButton.requestFocus(); assertTrue(clearingFocusButton.hasFocus()); // Register the invocation order checker. CallbackOrderChecker checker = new CallbackOrderChecker(clearingFocusButton, gainingFocusButton); clearingFocusButton.setOnFocusChangeListener(checker); gainingFocusButton.setOnFocusChangeListener(checker); clearingFocusButton.getViewTreeObserver().addOnGlobalFocusChangeListener(checker); // Try to clear focus. clearingFocusButton.clearFocus(); // Check that no callback was invoked since focus did not move. checker.verify(); } /** * This class check whether the focus change callback are invoked in order. */ private class CallbackOrderChecker implements OnFocusChangeListener, OnGlobalFocusChangeListener { private class CallbackInvocation { final String mMethodName; final Object[] mArguments; CallbackInvocation(String methodName, Object[] arguments) { mMethodName = methodName; mArguments = arguments; } } private final View mClearingFocusView; private final View mGainingFocusView; private final List<CallbackInvocation> mInvocations = new ArrayList<CallbackInvocation>(); public CallbackOrderChecker(View clearingFocusView, View gainingFocusView) { mClearingFocusView = clearingFocusView; mGainingFocusView = gainingFocusView; } @Override public void onFocusChange(View view, boolean hasFocus) { CallbackInvocation invocation = new CallbackInvocation( "OnFocusChangeListener#onFocusChange", new Object[] {view, hasFocus}); mInvocations.add(invocation); } @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { CallbackInvocation invocation = new CallbackInvocation( "OnFocusChangeListener#onFocusChange", new Object[] {oldFocus, newFocus}); mInvocations.add(invocation); } public void verify() { assertSame("All focus change callback should be invoked.", 3, mInvocations.size()); assertInvioked("Callback for View clearing focus explected.", 0, "OnFocusChangeListener#onFocusChange", new Object[] {mClearingFocusView, false}); assertInvioked("Callback for View global focus change explected.", 1, "OnFocusChangeListener#onFocusChange", new Object[] {mClearingFocusView, mGainingFocusView}); assertInvioked("Callback for View gaining focus explected.", 2, "OnFocusChangeListener#onFocusChange", new Object[] {mGainingFocusView, true}); } private void assertInvioked(String message, int order, String methodName, Object[] arguments) { CallbackInvocation invocation = mInvocations.get(order); assertEquals(message, methodName, invocation.mMethodName); assertEquals(message, arguments[0], invocation.mArguments[0]); assertEquals(message, arguments[1], invocation.mArguments[1]); } } } Loading
core/java/android/view/View.java +23 −0 Original line number Diff line number Diff line Loading @@ -3770,6 +3770,14 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal } if ((mPrivateFlags & FOCUSED) != 0) { // If this is the first focusable do not clear focus since the we // try to give it focus every time a view clears its focus. Hence, // the view that would gain focus already has it. View firstFocusable = getFirstFocusable(); if (firstFocusable == this) { return; } mPrivateFlags &= ~FOCUSED; if (mParent != null) { Loading @@ -3778,7 +3786,22 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal onFocusChanged(false, 0, null); refreshDrawableState(); // The view cleared focus and invoked the callbacks, so now is the // time to give focus to the the first focusable to ensure that the // gain focus is announced after clear focus. if (firstFocusable != null) { firstFocusable.requestFocus(FOCUS_FORWARD); } } } private View getFirstFocusable() { ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null) { return viewRoot.focusSearch(null, FOCUS_FORWARD); } return null; } /** Loading
core/java/android/view/ViewGroup.java +11 −8 Original line number Diff line number Diff line Loading @@ -675,11 +675,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } if (mFocused == null) { super.clearFocus(); // clear any child focus if it exists if (mFocused != null) { } else { mFocused.clearFocus(); mFocused = null; } } Loading @@ -691,13 +694,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (DBG) { System.out.println(this + " unFocus()"); } if (mFocused == null) { super.unFocus(); if (mFocused != null) { } else { mFocused.unFocus(); } mFocused = null; } } /** * Returns the focused child of this view, if any. The child may have focus Loading
core/java/android/view/ViewRootImpl.java +19 −16 Original line number Diff line number Diff line Loading @@ -168,6 +168,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, View mView; View mFocusedView; View mRealFocusedView; // this is not set to null in touch mode View mOldFocusedView; int mViewVisibility; boolean mAppVisible = true; int mOrigWindowType = -1; Loading Loading @@ -2226,32 +2227,33 @@ public final class ViewRootImpl extends Handler implements ViewParent, public void requestChildFocus(View child, View focused) { checkThread(); if (mFocusedView != focused) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, focused); scheduleTraversals(); if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Request child focus: focus now " + focused); } mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, focused); scheduleTraversals(); mFocusedView = mRealFocusedView = focused; if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Request child focus: focus now " + mFocusedView); } public void clearChildFocus(View child) { checkThread(); View oldFocus = mFocusedView; if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Clearing child focus"); mFocusedView = mRealFocusedView = null; if (mView != null && !mView.hasFocus()) { // If a view gets the focus, the listener will be invoked from requestChildFocus() if (!mView.requestFocus(View.FOCUS_FORWARD)) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null); } } else if (oldFocus != null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null); if (DEBUG_INPUT_RESIZE) { Log.v(TAG, "Clearing child focus"); } mOldFocusedView = mFocusedView; // Invoke the listener only if there is no view to take focus if (focusSearch(null, View.FOCUS_FORWARD) == null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, null); } mFocusedView = mRealFocusedView = null; } public void focusableViewAvailable(View v) { checkThread(); Loading Loading @@ -2724,6 +2726,7 @@ public final class ViewRootImpl extends Handler implements ViewParent, mView.unFocus(); mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null); mFocusedView = null; mOldFocusedView = null; return true; } } Loading
core/tests/coretests/src/android/widget/focus/RequestFocus.java +0 −2 Original line number Diff line number Diff line Loading @@ -21,9 +21,7 @@ import com.android.frameworks.coretests.R; import android.app.Activity; import android.os.Bundle; import android.os.Handler; import android.widget.LinearLayout; import android.widget.Button; import android.view.View; /** * Exercises cases where elements of the UI are requestFocus()ed. Loading
core/tests/coretests/src/android/widget/focus/RequestFocusTest.java +154 −7 Original line number Diff line number Diff line Loading @@ -16,21 +16,27 @@ package android.widget.focus; import android.widget.focus.RequestFocus; import com.android.frameworks.coretests.R; import android.os.Handler; import android.test.ActivityInstrumentationTestCase; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.MediumTest; import android.widget.Button; import android.util.AndroidRuntimeException; import android.view.View; import android.view.View.OnFocusChangeListener; import android.view.ViewTreeObserver.OnGlobalFocusChangeListener; import android.widget.Button; import com.android.frameworks.coretests.R; import java.util.ArrayList; import java.util.List; /** * {@link RequestFocusTest} is set up to exercise cases where the views that * have focus become invisible or GONE. */ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFocus> { public class RequestFocusTest extends ActivityInstrumentationTestCase2<RequestFocus> { private Button mTopLeftButton; private Button mBottomLeftButton; Loading @@ -39,7 +45,7 @@ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFoc private Handler mHandler; public RequestFocusTest() { super("com.android.frameworks.coretests", RequestFocus.class); super(RequestFocus.class); } @Override Loading Loading @@ -94,4 +100,145 @@ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFoc e.getClass().getName()); } } /** * This tests checks the case in which the first focusable View clears focus. * In such a case the framework tries to give the focus to another View starting * from the top. Hence, the framework will try to give focus to the view that * wants to clear its focus. From a client perspective, the view does not loose * focus after the call, therefore no callback for focus change should be invoked. * * @throws Exception If an error occurs. */ @UiThreadTest public void testOnFocusChangeNotCalledIfFocusDoesNotMove() throws Exception { // Get the first focusable. Button button = mTopLeftButton; // Make sure that the button is the first focusable and focus it. button.getRootView().requestFocus(View.FOCUS_DOWN); assertTrue(button.hasFocus()); // Attach on focus change listener that should not be called. button.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { throw new IllegalStateException("Unexpeced call to" + "OnFocusChangeListener#onFocusChange"); } }); // Attach on global focus change listener that should not be called. button.getViewTreeObserver().addOnGlobalFocusChangeListener( new OnGlobalFocusChangeListener() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { throw new IllegalStateException("Unexpeced call to" + "OnFocusChangeListener#onFocusChange"); } }); // Try to clear focus. button.clearFocus(); } /** * This tests check whether the on focus change callbacks are invoked in * the proper order when a View loses focus and the framework gives it to * the fist focusable one. * * @throws Exception */ @UiThreadTest public void testOnFocusChangeCallbackOrder() throws Exception { // Get the first focusable. Button clearingFocusButton = mTopRightButton; Button gainingFocusButton = mTopLeftButton; // Make sure that the clearing focus is not the first focusable. View focusCandidate = clearingFocusButton.getRootView().getParent().focusSearch(null, View.FOCUS_FORWARD); assertNotSame("The clearing focus button is not the first focusable.", clearingFocusButton, focusCandidate); assertSame("The gaining focus button is the first focusable.", gainingFocusButton, focusCandidate); // Focus the clearing focus button. clearingFocusButton.requestFocus(); assertTrue(clearingFocusButton.hasFocus()); // Register the invocation order checker. CallbackOrderChecker checker = new CallbackOrderChecker(clearingFocusButton, gainingFocusButton); clearingFocusButton.setOnFocusChangeListener(checker); gainingFocusButton.setOnFocusChangeListener(checker); clearingFocusButton.getViewTreeObserver().addOnGlobalFocusChangeListener(checker); // Try to clear focus. clearingFocusButton.clearFocus(); // Check that no callback was invoked since focus did not move. checker.verify(); } /** * This class check whether the focus change callback are invoked in order. */ private class CallbackOrderChecker implements OnFocusChangeListener, OnGlobalFocusChangeListener { private class CallbackInvocation { final String mMethodName; final Object[] mArguments; CallbackInvocation(String methodName, Object[] arguments) { mMethodName = methodName; mArguments = arguments; } } private final View mClearingFocusView; private final View mGainingFocusView; private final List<CallbackInvocation> mInvocations = new ArrayList<CallbackInvocation>(); public CallbackOrderChecker(View clearingFocusView, View gainingFocusView) { mClearingFocusView = clearingFocusView; mGainingFocusView = gainingFocusView; } @Override public void onFocusChange(View view, boolean hasFocus) { CallbackInvocation invocation = new CallbackInvocation( "OnFocusChangeListener#onFocusChange", new Object[] {view, hasFocus}); mInvocations.add(invocation); } @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { CallbackInvocation invocation = new CallbackInvocation( "OnFocusChangeListener#onFocusChange", new Object[] {oldFocus, newFocus}); mInvocations.add(invocation); } public void verify() { assertSame("All focus change callback should be invoked.", 3, mInvocations.size()); assertInvioked("Callback for View clearing focus explected.", 0, "OnFocusChangeListener#onFocusChange", new Object[] {mClearingFocusView, false}); assertInvioked("Callback for View global focus change explected.", 1, "OnFocusChangeListener#onFocusChange", new Object[] {mClearingFocusView, mGainingFocusView}); assertInvioked("Callback for View gaining focus explected.", 2, "OnFocusChangeListener#onFocusChange", new Object[] {mGainingFocusView, true}); } private void assertInvioked(String message, int order, String methodName, Object[] arguments) { CallbackInvocation invocation = mInvocations.get(order); assertEquals(message, methodName, invocation.mMethodName); assertEquals(message, arguments[0], invocation.mArguments[0]); assertEquals(message, arguments[1], invocation.mArguments[1]); } } }