Merge "[DO NOT MERGE] Fix context menu position for RTL"

This commit is contained in:
Treehugger Robot
2018-02-16 19:54:26 +00:00
committed by Gerrit Code Review
8 changed files with 407 additions and 9 deletions

View File

@@ -256,7 +256,7 @@ public class MenuPopupHelper implements MenuHelper {
final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity, final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
if (hgrav == Gravity.RIGHT) { if (hgrav == Gravity.RIGHT) {
xOffset += mAnchorView.getWidth(); xOffset -= mAnchorView.getWidth();
} }
popup.setHorizontalOffset(xOffset); popup.setHorizontalOffset(xOffset);

View File

@@ -263,7 +263,6 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On
mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes); mShownAnchorView, mOverflowOnly, mPopupStyleAttr, mPopupStyleRes);
subPopup.setPresenterCallback(mPresenterCallback); subPopup.setPresenterCallback(mPresenterCallback);
subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu)); subPopup.setForceShowIcon(MenuPopup.shouldPreserveIconSpacing(subMenu));
subPopup.setGravity(mDropDownGravity);
// Pass responsibility for handling onDismiss to the submenu. // Pass responsibility for handling onDismiss to the submenu.
subPopup.setOnDismissListener(mOnDismissListener); subPopup.setOnDismissListener(mOnDismissListener);
@@ -273,8 +272,17 @@ final class StandardMenuPopup extends MenuPopup implements OnDismissListener, On
mMenu.close(false /* closeAllMenus */); mMenu.close(false /* closeAllMenus */);
// Show the new sub-menu popup at the same location as this popup. // Show the new sub-menu popup at the same location as this popup.
final int horizontalOffset = mPopup.getHorizontalOffset(); int horizontalOffset = mPopup.getHorizontalOffset();
final int verticalOffset = mPopup.getVerticalOffset(); final int verticalOffset = mPopup.getVerticalOffset();
// As xOffset of parent menu popup is subtracted with Anchor width for Gravity.RIGHT,
// So, again to display sub-menu popup in same xOffset, add the Anchor width.
final int hgrav = Gravity.getAbsoluteGravity(mDropDownGravity,
mAnchorView.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
if (hgrav == Gravity.RIGHT) {
horizontalOffset += mAnchorView.getWidth();
}
if (subPopup.tryShow(horizontalOffset, verticalOffset)) { if (subPopup.tryShow(horizontalOffset, verticalOffset)) {
if (mPresenterCallback != null) { if (mPresenterCallback != null) {
mPresenterCallback.onOpenSubMenu(subMenu); mPresenterCallback.onOpenSubMenu(subMenu);

View File

@@ -1041,6 +1041,13 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="android.view.menu.ContextMenuActivity" android:label="ContextMenu" android:theme="@android:style/Theme.Material">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
</intent-filter>
</activity>
<activity android:name="android.view.menu.MenuWith1Item" android:label="MenuWith1Item"> <activity android:name="android.view.menu.MenuWith1Item" android:label="MenuWith1Item">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
**
** Copyright 2018, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/context_menu_target_ltr"
android:orientation="horizontal"
android:layoutDirection="ltr"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="50px"
android:layout_marginEnd="50px">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="LTR"/>
</LinearLayout>
<LinearLayout
android:id="@+id/context_menu_target_rtl"
android:orientation="horizontal"
android:layoutDirection="rtl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="50px"
android:layout_marginEnd="50px">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="RTL"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.util;
import org.junit.Assert;
/**
* Utility used for testing that allows to poll for a certain condition to happen within a timeout.
*
* Code copied from com.android.compatibility.common.util.PollingCheck
*/
public abstract class PollingCheck {
private static final long DEFAULT_TIMEOUT = 3000;
private static final long TIME_SLICE = 50;
private final long mTimeout;
/**
* The condition that the PollingCheck should use to proceed successfully.
*/
public interface PollingCheckCondition {
/**
* @return Whether the polling condition has been met.
*/
boolean canProceed();
}
public PollingCheck(long timeout) {
mTimeout = timeout;
}
protected abstract boolean check();
/**
* Start running the polling check.
*/
public void run() {
if (check()) {
return;
}
long timeout = mTimeout;
while (timeout > 0) {
try {
Thread.sleep(TIME_SLICE);
} catch (InterruptedException e) {
Assert.fail("unexpected InterruptedException");
}
if (check()) {
return;
}
timeout -= TIME_SLICE;
}
Assert.fail("unexpected timeout");
}
/**
* Instantiate and start polling for a given condition with a default 3000ms timeout.
*
* @param condition The condition to check for success.
*/
public static void waitFor(final PollingCheckCondition condition) {
new PollingCheck(DEFAULT_TIMEOUT) {
@Override
protected boolean check() {
return condition.canProceed();
}
}.run();
}
/**
* Instantiate and start polling for a given condition.
*
* @param timeout Time out in ms
* @param condition The condition to check for success.
*/
public static void waitFor(long timeout, final PollingCheckCondition condition) {
new PollingCheck(timeout) {
@Override
protected boolean check() {
return condition.canProceed();
}
}.run();
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.menu;
import android.app.Activity;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
import com.android.frameworks.coretests.R;
public class ContextMenuActivity extends Activity {
static final String LABEL_ITEM = "Item";
static final String LABEL_SUBMENU = "Submenu";
static final String LABEL_SUBITEM = "Subitem";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.context_menu);
registerForContextMenu(getTargetLtr());
registerForContextMenu(getTargetRtl());
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
menu.add(LABEL_ITEM);
menu.addSubMenu(LABEL_SUBMENU).add(LABEL_SUBITEM);
}
View getTargetLtr() {
return findViewById(R.id.context_menu_target_ltr);
}
View getTargetRtl() {
return findViewById(R.id.context_menu_target_rtl);
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.menu;
import android.content.Context;
import android.graphics.Point;
import android.support.test.filters.MediumTest;
import android.test.ActivityInstrumentationTestCase;
import android.util.PollingCheck;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;
import android.widget.espresso.ContextMenuUtils;
@MediumTest
public class ContextMenuTest extends ActivityInstrumentationTestCase<ContextMenuActivity> {
public ContextMenuTest() {
super("com.android.frameworks.coretests", ContextMenuActivity.class);
}
public void testContextMenuPositionLtr() throws InterruptedException {
testMenuPosition(getActivity().getTargetLtr());
}
public void testContextMenuPositionRtl() throws InterruptedException {
testMenuPosition(getActivity().getTargetRtl());
}
private void testMenuPosition(View target) throws InterruptedException {
final int minScreenDimension = getMinScreenDimension();
if (minScreenDimension < 320) {
// Assume there is insufficient room for the context menu to be aligned properly.
return;
}
int offsetX = target.getWidth() / 2;
int offsetY = target.getHeight() / 2;
getInstrumentation().runOnMainSync(() -> target.performLongClick(offsetX, offsetY));
PollingCheck.waitFor(
() -> ContextMenuUtils.isMenuItemClickable(ContextMenuActivity.LABEL_SUBMENU));
ContextMenuUtils.assertContextMenuAlignment(target, offsetX, offsetY);
ContextMenuUtils.clickMenuItem(ContextMenuActivity.LABEL_SUBMENU);
PollingCheck.waitFor(
() -> ContextMenuUtils.isMenuItemClickable(ContextMenuActivity.LABEL_SUBITEM));
if (minScreenDimension < getCascadingMenuTreshold()) {
// A non-cascading submenu should be displayed at the same location as its parent.
// Not testing cascading submenu position, as it is positioned differently.
ContextMenuUtils.assertContextMenuAlignment(target, offsetX, offsetY);
}
}
/**
* Returns the minimum of the default display's width and height.
*/
private int getMinScreenDimension() {
final WindowManager windowManager = (WindowManager) getActivity().getSystemService(
Context.WINDOW_SERVICE);
final Display display = windowManager.getDefaultDisplay();
final Point displaySize = new Point();
display.getRealSize(displaySize);
return Math.min(displaySize.x, displaySize.y);
}
/**
* Returns the minimum display size where cascading submenus are supported.
*/
private int getCascadingMenuTreshold() {
// Use the same dimension resource as in MenuPopupHelper.createPopup().
return getActivity().getResources().getDimensionPixelSize(
com.android.internal.R.dimen.cascading_menus_min_smallest_width);
}
}

View File

@@ -17,25 +17,32 @@
package android.widget.espresso; package android.widget.espresso;
import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.matches; import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView; import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant; import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.hasFocus; import static android.support.test.espresso.matcher.ViewMatchers.hasFocus;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
import static android.support.test.espresso.matcher.ViewMatchers.isEnabled; import static android.support.test.espresso.matcher.ViewMatchers.isEnabled;
import static android.support.test.espresso.matcher.ViewMatchers.withText; import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import com.android.internal.view.menu.ListMenuItemView;
import android.support.test.espresso.NoMatchingRootException; import android.support.test.espresso.NoMatchingRootException;
import android.support.test.espresso.NoMatchingViewException; import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.matcher.ViewMatchers; import android.support.test.espresso.matcher.ViewMatchers;
import android.view.View;
import android.widget.MenuPopupWindow.MenuDropDownListView; import android.widget.MenuPopupWindow.MenuDropDownListView;
import com.android.internal.view.menu.ListMenuItemView;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
/** /**
* Espresso utility methods for the context menu. * Espresso utility methods for the context menu.
*/ */
@@ -82,10 +89,15 @@ public final class ContextMenuUtils {
private static void asssertContextMenuContainsItemWithEnabledState(String itemLabel, private static void asssertContextMenuContainsItemWithEnabledState(String itemLabel,
boolean enabled) { boolean enabled) {
onContextMenu().check(matches( onContextMenu().check(matches(
hasDescendant(allOf( hasDescendant(getVisibleMenuItemMatcher(itemLabel, enabled))));
isAssignableFrom(ListMenuItemView.class), }
enabled ? isEnabled() : not(isEnabled()),
hasDescendant(withText(itemLabel)))))); private static Matcher<View> getVisibleMenuItemMatcher(String itemLabel, boolean enabled) {
return allOf(
isAssignableFrom(ListMenuItemView.class),
hasDescendant(withText(itemLabel)),
enabled ? isEnabled() : not(isEnabled()),
isDisplayingAtLeast(90));
} }
/** /**
@@ -107,4 +119,70 @@ public final class ContextMenuUtils {
public static void assertContextMenuContainsItemDisabled(String itemLabel) { public static void assertContextMenuContainsItemDisabled(String itemLabel) {
asssertContextMenuContainsItemWithEnabledState(itemLabel, false); asssertContextMenuContainsItemWithEnabledState(itemLabel, false);
} }
/**
* Asserts that the context menu window is aligned to a given view with a given offset.
*
* @param anchor Anchor view.
* @param offsetX x offset
* @param offsetY y offset.
* @throws AssertionError if the assertion fails
*/
public static void assertContextMenuAlignment(View anchor, int offsetX, int offsetY) {
int [] expectedLocation = new int[2];
anchor.getLocationOnScreen(expectedLocation);
expectedLocation[0] += offsetX;
expectedLocation[1] += offsetY;
final boolean rtl = anchor.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
onContextMenu().check(matches(new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("root view ");
description.appendText(rtl ? "right" : "left");
description.appendText("=");
description.appendText(Integer.toString(offsetX));
description.appendText(", top=");
description.appendText(Integer.toString(offsetY));
}
@Override
public boolean matchesSafely(View view) {
View rootView = view.getRootView();
int [] actualLocation = new int[2];
rootView.getLocationOnScreen(actualLocation);
if (rtl) {
actualLocation[0] += rootView.getWidth();
}
return expectedLocation[0] == actualLocation[0]
&& expectedLocation[1] == actualLocation[1];
}
}));
}
/**
* Check is the menu item is clickable (i.e. visible and enabled).
*
* @param itemLabel Label of the item.
* @return True if the menu item is clickable.
*/
public static boolean isMenuItemClickable(String itemLabel) {
try {
onContextMenu().check(matches(
hasDescendant(getVisibleMenuItemMatcher(itemLabel, true))));
return true;
} catch (NoMatchingRootException | NoMatchingViewException | AssertionError e) {
return false;
}
}
/**
* Click on a menu item with the specified label
* @param itemLabel Label of the item.
*/
public static void clickMenuItem(String itemLabel) {
onView(getVisibleMenuItemMatcher(itemLabel, true))
.inRoot(withDecorView(hasFocus())).perform(click());
}
} }