Skip to content

Commit c6fd88e

Browse files
committed
Incorrect behavior of View clear focus.
The framework tries to have a focused view all the time. For that purpose when a view's focus is cleared the focus is given to the first focusable found from the top. The implementation of this behavior was causing the following issues: 1. If the fist focusable View tries to clear its focus it was getting focus but the onFocusChange callbacks were not properly invoked. Specifically, the onFocusChange for gaining focus was called first and then the same callback for clearing focus. Note that the callback for clearing focus is called when the View is already focused. Also note that at the end the View did not clear its focus, hence no focus change callbacks should be invoked. 2. If not the first focusable View tries to clear focus, the focus is given to another one but the callback for getting focus was called before the one for clearing, so client code may be mislead that there is more than one focused view at a time. 3. (Nit) The implementaion of clearFocus and unFocus in ViewGroup was calling the super implementaion when there is a focused child. Since there could be only one focused View, having a focused child means that the group is not focused and the call to the super implementation is not needed. 4. Added unit tests that verify the correct behavior, i.e. the focus of the first focused view cannot be cleared which means that no focus change callbacks are invoked. The callbacks should be called in expected order. Now the view focus clear precedes the view focus gain callback. However, in between is invoked the global focus change callback with the correct values. We may want to call that one after the View callbacks. If needed we can revisit this. Change-Id: Iee80baf5c75c82d3cda09679e4949483cad475f1
1 parent 36a561b commit c6fd88e

File tree

5 files changed

+207
-33
lines changed

5 files changed

+207
-33
lines changed

core/java/android/view/View.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3770,6 +3770,14 @@ public void clearFocus() {
37703770
}
37713771

37723772
if ((mPrivateFlags & FOCUSED) != 0) {
3773+
// If this is the first focusable do not clear focus since the we
3774+
// try to give it focus every time a view clears its focus. Hence,
3775+
// the view that would gain focus already has it.
3776+
View firstFocusable = getFirstFocusable();
3777+
if (firstFocusable == this) {
3778+
return;
3779+
}
3780+
37733781
mPrivateFlags &= ~FOCUSED;
37743782

37753783
if (mParent != null) {
@@ -3778,9 +3786,24 @@ public void clearFocus() {
37783786

37793787
onFocusChanged(false, 0, null);
37803788
refreshDrawableState();
3789+
3790+
// The view cleared focus and invoked the callbacks, so now is the
3791+
// time to give focus to the the first focusable to ensure that the
3792+
// gain focus is announced after clear focus.
3793+
if (firstFocusable != null) {
3794+
firstFocusable.requestFocus(FOCUS_FORWARD);
3795+
}
37813796
}
37823797
}
37833798

3799+
private View getFirstFocusable() {
3800+
ViewRootImpl viewRoot = getViewRootImpl();
3801+
if (viewRoot != null) {
3802+
return viewRoot.focusSearch(null, FOCUS_FORWARD);
3803+
}
3804+
return null;
3805+
}
3806+
37843807
/**
37853808
* Called to clear the focus of a view that is about to be removed.
37863809
* Doesn't call clearChildFocus, which prevents this view from taking

core/java/android/view/ViewGroup.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -675,11 +675,14 @@ public void clearChildFocus(View child) {
675675
*/
676676
@Override
677677
public void clearFocus() {
678-
super.clearFocus();
679-
680-
// clear any child focus if it exists
681-
if (mFocused != null) {
678+
if (DBG) {
679+
System.out.println(this + " clearFocus()");
680+
}
681+
if (mFocused == null) {
682+
super.clearFocus();
683+
} else {
682684
mFocused.clearFocus();
685+
mFocused = null;
683686
}
684687
}
685688

@@ -691,12 +694,12 @@ void unFocus() {
691694
if (DBG) {
692695
System.out.println(this + " unFocus()");
693696
}
694-
695-
super.unFocus();
696-
if (mFocused != null) {
697+
if (mFocused == null) {
698+
super.unFocus();
699+
} else {
697700
mFocused.unFocus();
701+
mFocused = null;
698702
}
699-
mFocused = null;
700703
}
701704

702705
/**

core/java/android/view/ViewRootImpl.java

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ public final class ViewRootImpl extends Handler implements ViewParent,
168168
View mView;
169169
View mFocusedView;
170170
View mRealFocusedView; // this is not set to null in touch mode
171+
View mOldFocusedView;
171172
int mViewVisibility;
172173
boolean mAppVisible = true;
173174
int mOrigWindowType = -1;
@@ -2226,32 +2227,33 @@ boolean scrollToRectOrFocus(Rect rectangle, boolean immediate) {
22262227

22272228
public void requestChildFocus(View child, View focused) {
22282229
checkThread();
2229-
if (mFocusedView != focused) {
2230-
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mFocusedView, focused);
2231-
scheduleTraversals();
2230+
2231+
if (DEBUG_INPUT_RESIZE) {
2232+
Log.v(TAG, "Request child focus: focus now " + focused);
22322233
}
2234+
2235+
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, focused);
2236+
scheduleTraversals();
2237+
22332238
mFocusedView = mRealFocusedView = focused;
2234-
if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Request child focus: focus now "
2235-
+ mFocusedView);
22362239
}
22372240

22382241
public void clearChildFocus(View child) {
22392242
checkThread();
22402243

2241-
View oldFocus = mFocusedView;
2244+
if (DEBUG_INPUT_RESIZE) {
2245+
Log.v(TAG, "Clearing child focus");
2246+
}
2247+
2248+
mOldFocusedView = mFocusedView;
22422249

2243-
if (DEBUG_INPUT_RESIZE) Log.v(TAG, "Clearing child focus");
2244-
mFocusedView = mRealFocusedView = null;
2245-
if (mView != null && !mView.hasFocus()) {
2246-
// If a view gets the focus, the listener will be invoked from requestChildFocus()
2247-
if (!mView.requestFocus(View.FOCUS_FORWARD)) {
2248-
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null);
2249-
}
2250-
} else if (oldFocus != null) {
2251-
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, null);
2250+
// Invoke the listener only if there is no view to take focus
2251+
if (focusSearch(null, View.FOCUS_FORWARD) == null) {
2252+
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(mOldFocusedView, null);
22522253
}
2253-
}
22542254

2255+
mFocusedView = mRealFocusedView = null;
2256+
}
22552257

22562258
public void focusableViewAvailable(View v) {
22572259
checkThread();
@@ -2724,6 +2726,7 @@ private boolean enterTouchMode() {
27242726
mView.unFocus();
27252727
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused, null);
27262728
mFocusedView = null;
2729+
mOldFocusedView = null;
27272730
return true;
27282731
}
27292732
}

core/tests/coretests/src/android/widget/focus/RequestFocus.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
import android.app.Activity;
2222
import android.os.Bundle;
2323
import android.os.Handler;
24-
import android.widget.LinearLayout;
2524
import android.widget.Button;
26-
import android.view.View;
2725

2826
/**
2927
* Exercises cases where elements of the UI are requestFocus()ed.

core/tests/coretests/src/android/widget/focus/RequestFocusTest.java

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,27 @@
1616

1717
package android.widget.focus;
1818

19-
import android.widget.focus.RequestFocus;
20-
import com.android.frameworks.coretests.R;
21-
2219
import android.os.Handler;
23-
import android.test.ActivityInstrumentationTestCase;
20+
import android.test.ActivityInstrumentationTestCase2;
21+
import android.test.UiThreadTest;
2422
import android.test.suitebuilder.annotation.LargeTest;
2523
import android.test.suitebuilder.annotation.MediumTest;
26-
import android.widget.Button;
2724
import android.util.AndroidRuntimeException;
25+
import android.view.View;
26+
import android.view.View.OnFocusChangeListener;
27+
import android.view.ViewTreeObserver.OnGlobalFocusChangeListener;
28+
import android.widget.Button;
29+
30+
import com.android.frameworks.coretests.R;
31+
32+
import java.util.ArrayList;
33+
import java.util.List;
2834

2935
/**
3036
* {@link RequestFocusTest} is set up to exercise cases where the views that
3137
* have focus become invisible or GONE.
3238
*/
33-
public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFocus> {
39+
public class RequestFocusTest extends ActivityInstrumentationTestCase2<RequestFocus> {
3440

3541
private Button mTopLeftButton;
3642
private Button mBottomLeftButton;
@@ -39,7 +45,7 @@ public class RequestFocusTest extends ActivityInstrumentationTestCase<RequestFoc
3945
private Handler mHandler;
4046

4147
public RequestFocusTest() {
42-
super("com.android.frameworks.coretests", RequestFocus.class);
48+
super(RequestFocus.class);
4349
}
4450

4551
@Override
@@ -94,4 +100,145 @@ public void testWrongThreadRequestFocusFails() throws Exception {
94100
e.getClass().getName());
95101
}
96102
}
103+
104+
/**
105+
* This tests checks the case in which the first focusable View clears focus.
106+
* In such a case the framework tries to give the focus to another View starting
107+
* from the top. Hence, the framework will try to give focus to the view that
108+
* wants to clear its focus. From a client perspective, the view does not loose
109+
* focus after the call, therefore no callback for focus change should be invoked.
110+
*
111+
* @throws Exception If an error occurs.
112+
*/
113+
@UiThreadTest
114+
public void testOnFocusChangeNotCalledIfFocusDoesNotMove() throws Exception {
115+
// Get the first focusable.
116+
Button button = mTopLeftButton;
117+
118+
// Make sure that the button is the first focusable and focus it.
119+
button.getRootView().requestFocus(View.FOCUS_DOWN);
120+
assertTrue(button.hasFocus());
121+
122+
// Attach on focus change listener that should not be called.
123+
button.setOnFocusChangeListener(new OnFocusChangeListener() {
124+
@Override
125+
public void onFocusChange(View v, boolean hasFocus) {
126+
throw new IllegalStateException("Unexpeced call to"
127+
+ "OnFocusChangeListener#onFocusChange");
128+
}
129+
});
130+
131+
// Attach on global focus change listener that should not be called.
132+
button.getViewTreeObserver().addOnGlobalFocusChangeListener(
133+
new OnGlobalFocusChangeListener() {
134+
@Override
135+
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
136+
throw new IllegalStateException("Unexpeced call to"
137+
+ "OnFocusChangeListener#onFocusChange");
138+
}
139+
});
140+
141+
// Try to clear focus.
142+
button.clearFocus();
143+
}
144+
145+
/**
146+
* This tests check whether the on focus change callbacks are invoked in
147+
* the proper order when a View loses focus and the framework gives it to
148+
* the fist focusable one.
149+
*
150+
* @throws Exception
151+
*/
152+
@UiThreadTest
153+
public void testOnFocusChangeCallbackOrder() throws Exception {
154+
// Get the first focusable.
155+
Button clearingFocusButton = mTopRightButton;
156+
Button gainingFocusButton = mTopLeftButton;
157+
158+
// Make sure that the clearing focus is not the first focusable.
159+
View focusCandidate = clearingFocusButton.getRootView().getParent().focusSearch(null,
160+
View.FOCUS_FORWARD);
161+
assertNotSame("The clearing focus button is not the first focusable.",
162+
clearingFocusButton, focusCandidate);
163+
assertSame("The gaining focus button is the first focusable.",
164+
gainingFocusButton, focusCandidate);
165+
166+
// Focus the clearing focus button.
167+
clearingFocusButton.requestFocus();
168+
assertTrue(clearingFocusButton.hasFocus());
169+
170+
// Register the invocation order checker.
171+
CallbackOrderChecker checker = new CallbackOrderChecker(clearingFocusButton,
172+
gainingFocusButton);
173+
clearingFocusButton.setOnFocusChangeListener(checker);
174+
gainingFocusButton.setOnFocusChangeListener(checker);
175+
clearingFocusButton.getViewTreeObserver().addOnGlobalFocusChangeListener(checker);
176+
177+
// Try to clear focus.
178+
clearingFocusButton.clearFocus();
179+
180+
// Check that no callback was invoked since focus did not move.
181+
checker.verify();
182+
}
183+
184+
/**
185+
* This class check whether the focus change callback are invoked in order.
186+
*/
187+
private class CallbackOrderChecker implements OnFocusChangeListener,
188+
OnGlobalFocusChangeListener {
189+
190+
private class CallbackInvocation {
191+
final String mMethodName;
192+
final Object[] mArguments;
193+
194+
CallbackInvocation(String methodName, Object[] arguments) {
195+
mMethodName = methodName;
196+
mArguments = arguments;
197+
}
198+
}
199+
200+
private final View mClearingFocusView;
201+
private final View mGainingFocusView;
202+
203+
private final List<CallbackInvocation> mInvocations = new ArrayList<CallbackInvocation>();
204+
205+
public CallbackOrderChecker(View clearingFocusView, View gainingFocusView) {
206+
mClearingFocusView = clearingFocusView;
207+
mGainingFocusView = gainingFocusView;
208+
}
209+
210+
@Override
211+
public void onFocusChange(View view, boolean hasFocus) {
212+
CallbackInvocation invocation = new CallbackInvocation(
213+
"OnFocusChangeListener#onFocusChange", new Object[] {view, hasFocus});
214+
mInvocations.add(invocation);
215+
}
216+
217+
@Override
218+
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
219+
CallbackInvocation invocation = new CallbackInvocation(
220+
"OnFocusChangeListener#onFocusChange", new Object[] {oldFocus, newFocus});
221+
mInvocations.add(invocation);
222+
}
223+
224+
public void verify() {
225+
assertSame("All focus change callback should be invoked.", 3, mInvocations.size());
226+
assertInvioked("Callback for View clearing focus explected.", 0,
227+
"OnFocusChangeListener#onFocusChange",
228+
new Object[] {mClearingFocusView, false});
229+
assertInvioked("Callback for View global focus change explected.", 1,
230+
"OnFocusChangeListener#onFocusChange", new Object[] {mClearingFocusView,
231+
mGainingFocusView});
232+
assertInvioked("Callback for View gaining focus explected.", 2,
233+
"OnFocusChangeListener#onFocusChange", new Object[] {mGainingFocusView, true});
234+
}
235+
236+
private void assertInvioked(String message, int order, String methodName,
237+
Object[] arguments) {
238+
CallbackInvocation invocation = mInvocations.get(order);
239+
assertEquals(message, methodName, invocation.mMethodName);
240+
assertEquals(message, arguments[0], invocation.mArguments[0]);
241+
assertEquals(message, arguments[1], invocation.mArguments[1]);
242+
}
243+
}
97244
}

0 commit comments

Comments
 (0)