1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.wear.widget; 18 19 import static android.support.test.espresso.Espresso.onView; 20 import static android.support.test.espresso.action.ViewActions.swipeRight; 21 import static android.support.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; 22 import static android.support.test.espresso.matcher.ViewMatchers.withId; 23 24 import static androidx.wear.widget.util.AsyncViewActions.waitForMatchingView; 25 import static androidx.wear.widget.util.MoreViewAssertions.withPositiveVerticalScrollOffset; 26 27 import static org.hamcrest.Matchers.allOf; 28 import static org.junit.Assert.assertFalse; 29 import static org.junit.Assert.assertTrue; 30 31 import android.app.Activity; 32 import android.content.Intent; 33 import android.graphics.RectF; 34 import android.support.test.InstrumentationRegistry; 35 import android.support.test.espresso.ViewAction; 36 import android.support.test.espresso.action.GeneralLocation; 37 import android.support.test.espresso.action.GeneralSwipeAction; 38 import android.support.test.espresso.action.Press; 39 import android.support.test.espresso.action.Swipe; 40 import android.support.test.espresso.matcher.ViewMatchers; 41 import android.support.test.filters.SmallTest; 42 import android.support.test.rule.ActivityTestRule; 43 import android.support.test.runner.AndroidJUnit4; 44 import android.view.View; 45 46 import androidx.annotation.IdRes; 47 import androidx.recyclerview.widget.RecyclerView; 48 import androidx.wear.test.R; 49 import androidx.wear.widget.util.ArcSwipe; 50 import androidx.wear.widget.util.WakeLockRule; 51 52 import org.junit.Rule; 53 import org.junit.Test; 54 import org.junit.runner.RunWith; 55 56 @RunWith(AndroidJUnit4.class) 57 public class SwipeDismissFrameLayoutTest { 58 59 private static final long MAX_WAIT_TIME = 4000; //ms 60 private final SwipeDismissFrameLayout.Callback mDismissCallback = new DismissCallback(); 61 62 @Rule 63 public final WakeLockRule wakeLock = new WakeLockRule(); 64 65 @Rule 66 public final ActivityTestRule<SwipeDismissFrameLayoutTestActivity> activityRule = 67 new ActivityTestRule<>( 68 SwipeDismissFrameLayoutTestActivity.class, 69 true, /** initial touch mode */ 70 false /** launchActivity */ 71 ); 72 73 private int mLayoutWidth; 74 private int mLayoutHeight; 75 76 @Test 77 @SmallTest 78 public void testCanScrollHorizontally() { 79 // GIVEN a freshly setup SwipeDismissFrameLayout 80 setUpSimpleLayout(); 81 Activity activity = activityRule.getActivity(); 82 SwipeDismissFrameLayout testLayout = 83 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 84 // WHEN we check that the layout is horizontally scrollable from left to right. 85 // THEN the layout is found to be horizontally swipeable from left to right. 86 assertTrue(testLayout.canScrollHorizontally(-20)); 87 // AND the layout is found to NOT be horizontally swipeable from right to left. 88 assertFalse(testLayout.canScrollHorizontally(20)); 89 90 // WHEN we switch off the swipe-to-dismiss functionality for the layout 91 testLayout.setSwipeable(false); 92 // THEN the layout is found NOT to be horizontally swipeable from left to right. 93 assertFalse(testLayout.canScrollHorizontally(-20)); 94 // AND the layout is found to NOT be horizontally swipeable from right to left. 95 assertFalse(testLayout.canScrollHorizontally(20)); 96 } 97 98 @Test 99 @SmallTest 100 public void canScrollHorizontallyShouldBeFalseWhenInvisible() { 101 // GIVEN a freshly setup SwipeDismissFrameLayout 102 setUpSimpleLayout(); 103 Activity activity = activityRule.getActivity(); 104 final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 105 // GIVEN the layout is invisible 106 // Note: We have to run this on the main thread, because of thread checks in View.java. 107 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 108 @Override 109 public void run() { 110 testLayout.setVisibility(View.INVISIBLE); 111 } 112 }); 113 // WHEN we check that the layout is horizontally scrollable 114 // THEN the layout is found to be NOT horizontally swipeable from left to right. 115 assertFalse(testLayout.canScrollHorizontally(-20)); 116 // AND the layout is found to NOT be horizontally swipeable from right to left. 117 assertFalse(testLayout.canScrollHorizontally(20)); 118 } 119 120 @Test 121 @SmallTest 122 public void canScrollHorizontallyShouldBeFalseWhenGone() { 123 // GIVEN a freshly setup SwipeDismissFrameLayout 124 setUpSimpleLayout(); 125 Activity activity = activityRule.getActivity(); 126 final SwipeDismissFrameLayout testLayout = 127 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 128 // GIVEN the layout is gone 129 // Note: We have to run this on the main thread, because of thread checks in View.java. 130 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 131 @Override 132 public void run() { 133 testLayout.setVisibility(View.GONE); 134 } 135 }); 136 // WHEN we check that the layout is horizontally scrollable 137 // THEN the layout is found to be NOT horizontally swipeable from left to right. 138 assertFalse(testLayout.canScrollHorizontally(-20)); 139 // AND the layout is found to NOT be horizontally swipeable from right to left. 140 assertFalse(testLayout.canScrollHorizontally(20)); 141 } 142 143 @Test 144 @SmallTest 145 public void testSwipeDismissEnabledByDefault() { 146 // GIVEN a freshly setup SwipeDismissFrameLayout 147 setUpSimpleLayout(); 148 Activity activity = activityRule.getActivity(); 149 SwipeDismissFrameLayout testLayout = 150 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 151 // WHEN we check that the layout is dismissible 152 // THEN the layout is find to be dismissible 153 assertTrue(testLayout.isSwipeable()); 154 } 155 156 @Test 157 @SmallTest 158 public void testSwipeDismissesViewIfEnabled() { 159 // GIVEN a freshly setup SwipeDismissFrameLayout 160 setUpSimpleLayout(); 161 // WHEN we perform a swipe to dismiss 162 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 163 // AND hidden 164 assertHidden(R.id.swipe_dismiss_root); 165 } 166 167 @Test 168 @SmallTest 169 public void testSwipeDoesNotDismissViewIfDisabled() { 170 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 171 setUpSimpleLayout(); 172 Activity activity = activityRule.getActivity(); 173 SwipeDismissFrameLayout testLayout = 174 (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root); 175 testLayout.setSwipeable(false); 176 // WHEN we perform a swipe to dismiss 177 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 178 // THEN the layout is not hidden 179 assertNotHidden(R.id.swipe_dismiss_root); 180 } 181 182 @Test 183 @SmallTest 184 public void testAddRemoveCallback() { 185 // GIVEN a freshly setup SwipeDismissFrameLayout 186 setUpSimpleLayout(); 187 Activity activity = activityRule.getActivity(); 188 SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 189 // WHEN we remove the swipe callback 190 testLayout.removeCallback(mDismissCallback); 191 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight()); 192 // THEN the layout is not hidden 193 assertNotHidden(R.id.swipe_dismiss_root); 194 } 195 196 @Test 197 @SmallTest 198 public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable { 199 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 200 setUpSwipeDismissWithHorizontalRecyclerView(); 201 activityRule.runOnUiThread(new Runnable() { 202 @Override 203 public void run() { 204 Activity activity = activityRule.getActivity(); 205 RecyclerView testLayout = activity.findViewById(R.id.recycler_container); 206 // Scroll to a position from which the child is scrollable. 207 testLayout.scrollToPosition(50); 208 } 209 }); 210 211 InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 212 // WHEN we perform a swipe to dismiss from the center of the screen. 213 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); 214 // THEN the layout is not hidden 215 assertNotHidden(R.id.swipe_dismiss_root); 216 } 217 218 219 @Test 220 @SmallTest 221 public void testEdgeSwipeDoesDismissViewIfScrollable() { 222 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off. 223 setUpSwipeDismissWithHorizontalRecyclerView(); 224 // WHEN we perform a swipe to dismiss from the left edge of the screen. 225 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); 226 // THEN the layout is hidden 227 assertHidden(R.id.swipe_dismiss_root); 228 } 229 230 @Test 231 @SmallTest 232 public void testSwipeDoesNotDismissViewIfStartsInWrongPosition() { 233 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an 234 // inner circle. 235 setUpSwipeableRegion(); 236 // WHEN we perform a swipe to dismiss from the left edge of the screen. 237 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftEdge()); 238 // THEN the layout is not not hidden 239 assertNotHidden(R.id.swipe_dismiss_root); 240 } 241 242 @Test 243 @SmallTest 244 public void testSwipeDoesDismissViewIfStartsInRightPosition() { 245 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an 246 // inner circle. 247 setUpSwipeableRegion(); 248 // WHEN we perform a swipe to dismiss from the center of the screen. 249 onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter()); 250 // THEN the layout is hidden 251 assertHidden(R.id.swipe_dismiss_root); 252 } 253 254 /** 255 @Test public void testSwipeInPreferenceFragmentAndNavDrawer() { 256 // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned on, but only for an inner 257 // circle. 258 setUpPreferenceFragmentAndNavDrawer(); 259 // WHEN we perform a swipe to dismiss from the center of the screen to the bottom. 260 onView(withId(R.id.drawer_layout)).perform(swipeBottomFromCenter()); 261 // THEN the navigation drawer is shown. 262 assertPeeking(R.id.top_drawer); 263 }*/ 264 265 @Test 266 @SmallTest 267 public void testArcSwipeDoesNotTriggerDismiss() throws Throwable { 268 // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content 269 setUpSwipeDismissWithVerticalRecyclerView(); 270 int center = mLayoutHeight / 2; 271 int halfBound = mLayoutWidth / 2; 272 RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound); 273 // WHEN the view is scrolled on an arc from top to bottom. 274 onView(withId(R.id.swipe_dismiss_root)).perform(swipeTopFromBottomOnArc(bounds)); 275 // THEN the layout is not dismissed and not hidden. 276 assertNotHidden(R.id.swipe_dismiss_root); 277 // AND the content view is scrolled. 278 assertScrolledY(R.id.recycler_container); 279 } 280 281 /** 282 * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with 283 * a single static child. 284 */ 285 private void setUpSimpleLayout() { 286 activityRule.launchActivity( 287 new Intent() 288 .putExtra( 289 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 290 R.layout.swipe_dismiss_layout_testcase_1)); 291 setDismissCallback(); 292 } 293 294 295 /** 296 * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable 297 * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link 298 * RecyclerView} as a child, ready to accept an adapter. 299 */ 300 private void setUpSwipeDismissWithHorizontalRecyclerView() { 301 Intent launchIntent = new Intent(); 302 launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 303 R.layout.swipe_dismiss_layout_testcase_2); 304 launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true); 305 activityRule.launchActivity(launchIntent); 306 setDismissCallback(); 307 } 308 309 /** 310 * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable 311 * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link 312 * WearableRecyclerView} as a child, ready to accept an adapter. 313 */ 314 private void setUpSwipeDismissWithVerticalRecyclerView() { 315 Intent launchIntent = new Intent(); 316 launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 317 R.layout.swipe_dismiss_layout_testcase_2); 318 launchIntent.putExtra(SwipeDismissFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false); 319 activityRule.launchActivity(launchIntent); 320 setDismissCallback(); 321 } 322 323 /** 324 * Sets up a {@link SwipeDismissFrameLayout} in which only a certain region is allowed to react 325 * to swipe-dismiss gestures. 326 */ 327 private void setUpSwipeableRegion() { 328 activityRule.launchActivity( 329 new Intent() 330 .putExtra( 331 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 332 R.layout.swipe_dismiss_layout_testcase_1)); 333 setCallback( 334 new DismissCallback() { 335 @Override 336 public boolean onPreSwipeStart(SwipeDismissFrameLayout layout, float x, 337 float y) { 338 float normalizedX = x - mLayoutWidth / 2; 339 float normalizedY = y - mLayoutWidth / 2; 340 float squareX = normalizedX * normalizedX; 341 float squareY = normalizedY * normalizedY; 342 // 30 is an arbitrary number limiting the circle. 343 return Math.sqrt(squareX + squareY) < (mLayoutWidth / 2 - 30); 344 } 345 }); 346 } 347 348 /** 349 * Sets up a more involved test case where the layout consists of a 350 * {@code WearableNavigationDrawer} and a 351 * {@code androidx.wear.internal.view.SwipeDismissPreferenceFragment} 352 */ 353 /* 354 private void setUpPreferenceFragmentAndNavDrawer() { 355 activityRule.launchActivity( 356 new Intent() 357 .putExtra( 358 LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID, 359 R.layout.swipe_dismiss_layout_testcase_3)); 360 Activity activity = activityRule.getActivity(); 361 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 362 WearableNavigationDrawer wearableNavigationDrawer = 363 (WearableNavigationDrawer) activity.findViewById(R.id.top_drawer); 364 wearableNavigationDrawer.setAdapter( 365 new WearableNavigationDrawer.WearableNavigationDrawerAdapter() { 366 @Override 367 public String getItemText(int pos) { 368 return "test"; 369 } 370 371 @Override 372 public Drawable getItemDrawable(int pos) { 373 return null; 374 } 375 376 @Override 377 public void onItemSelected(int pos) { 378 return; 379 } 380 381 @Override 382 public int getCount() { 383 return 3; 384 } 385 }); 386 }); 387 }*/ 388 private void setDismissCallback() { 389 setCallback(mDismissCallback); 390 } 391 392 private void setCallback(SwipeDismissFrameLayout.Callback callback) { 393 Activity activity = activityRule.getActivity(); 394 SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root); 395 mLayoutWidth = testLayout.getWidth(); 396 mLayoutHeight = testLayout.getHeight(); 397 testLayout.addCallback(callback); 398 } 399 400 /** 401 * private static void assertPeeking(@IdRes int layoutId) { 402 * onView(withId(layoutId)) 403 * .perform( 404 * waitForMatchingView( 405 * allOf(withId(layoutId), isOpened(true)), MAX_WAIT_TIME)); 406 * } 407 */ 408 409 private static void assertHidden(@IdRes int layoutId) { 410 onView(withId(layoutId)) 411 .perform( 412 waitForMatchingView( 413 allOf(withId(layoutId), 414 withEffectiveVisibility(ViewMatchers.Visibility.GONE)), 415 MAX_WAIT_TIME)); 416 } 417 418 private static void assertNotHidden(@IdRes int layoutId) { 419 onView(withId(layoutId)) 420 .perform( 421 waitForMatchingView( 422 allOf(withId(layoutId), 423 withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)), 424 MAX_WAIT_TIME)); 425 } 426 427 private static void assertScrolledY(@IdRes int layoutId) { 428 onView(withId(layoutId)) 429 .perform( 430 waitForMatchingView( 431 allOf(withId(layoutId), withPositiveVerticalScrollOffset()), 432 MAX_WAIT_TIME)); 433 } 434 435 private static ViewAction swipeRightFromCenter() { 436 return new GeneralSwipeAction( 437 Swipe.SLOW, GeneralLocation.CENTER, GeneralLocation.CENTER_RIGHT, Press.FINGER); 438 } 439 440 private static ViewAction swipeRightFromLeftEdge() { 441 return new GeneralSwipeAction( 442 Swipe.SLOW, GeneralLocation.CENTER_LEFT, GeneralLocation.CENTER_RIGHT, 443 Press.FINGER); 444 } 445 446 private static ViewAction swipeTopFromBottomOnArc(RectF bounds) { 447 return new GeneralSwipeAction( 448 new ArcSwipe(ArcSwipe.Gesture.SLOW_ANTICLOCKWISE, bounds), 449 GeneralLocation.BOTTOM_CENTER, 450 GeneralLocation.TOP_CENTER, 451 Press.FINGER); 452 } 453 454 /** Helper class hiding the view after a successful swipe-to-dismiss. */ 455 private static class DismissCallback extends SwipeDismissFrameLayout.Callback { 456 457 @Override 458 public void onDismissed(SwipeDismissFrameLayout layout) { 459 layout.setVisibility(View.GONE); 460 } 461 } 462 } 463