jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2019 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.fragment.app |
| 18 | |
| 19 | import android.animation.LayoutTransition |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 20 | import android.content.Context |
| 21 | import android.graphics.Canvas |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 22 | import android.graphics.Insets |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 23 | import android.os.Bundle |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 24 | import android.view.LayoutInflater |
| 25 | import android.view.View |
| 26 | import android.view.ViewGroup |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 27 | import android.view.WindowInsets |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 28 | import android.view.animation.Animation |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 29 | import androidx.fragment.app.test.FragmentTestActivity |
| 30 | import androidx.fragment.test.R |
| 31 | import androidx.test.ext.junit.runners.AndroidJUnit4 |
Jeremy Woods | f6472c5 | 2019-09-20 12:30:06 -0700 | [diff] [blame] | 32 | import androidx.test.filters.MediumTest |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 33 | import androidx.test.filters.SdkSuppress |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 34 | import androidx.test.rule.ActivityTestRule |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 35 | import androidx.testutils.waitForExecution |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 36 | import com.google.common.truth.Truth.assertThat |
| 37 | import com.google.common.truth.Truth.assertWithMessage |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 38 | import org.junit.Assert.fail |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 39 | import org.junit.Before |
| 40 | import org.junit.Rule |
| 41 | import org.junit.Test |
| 42 | import org.junit.runner.RunWith |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 43 | import java.util.concurrent.CountDownLatch |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 44 | import java.util.concurrent.TimeUnit |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 45 | |
Jeremy Woods | f6472c5 | 2019-09-20 12:30:06 -0700 | [diff] [blame] | 46 | @MediumTest |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 47 | @RunWith(AndroidJUnit4::class) |
| 48 | class FragmentContainerViewTest { |
| 49 | @get:Rule |
| 50 | var activityRule = ActivityTestRule(FragmentTestActivity::class.java) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 51 | lateinit var context: Context |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 52 | |
| 53 | @Before |
| 54 | fun setupContainer() { |
| 55 | activityRule.setContentView(R.layout.fragment_container_view) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 56 | context = activityRule.activity.applicationContext |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 57 | } |
| 58 | |
Jeremy Woods | 2882e14 | 2019-09-17 13:56:12 -0700 | [diff] [blame] | 59 | @SdkSuppress(minSdkVersion = 18) // androidx.transition needs setLayoutTransition for API < 18 |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 60 | @Test |
| 61 | fun setLayoutTransitionUnsupported() { |
| 62 | val activity = activityRule.activity |
| 63 | val layout = FragmentContainerView(activity.applicationContext) |
| 64 | |
| 65 | try { |
| 66 | layout.layoutTransition = LayoutTransition() |
SatoShun | fb9531d | 2019-09-23 02:40:31 +0000 | [diff] [blame] | 67 | fail("setLayoutTransition should throw UnsupportedOperationException") |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 68 | } catch (e: UnsupportedOperationException) { |
| 69 | assertThat(e) |
| 70 | .hasMessageThat() |
| 71 | .contains("FragmentContainerView does not support Layout Transitions or " + |
| 72 | "animateLayoutChanges=\"true\".") |
| 73 | } |
| 74 | } |
| 75 | |
Jeremy Woods | 2882e14 | 2019-09-17 13:56:12 -0700 | [diff] [blame] | 76 | @SdkSuppress(maxSdkVersion = 17) // androidx.transition needs setLayoutTransition for API < 18 |
| 77 | @Test |
| 78 | fun setLayoutTransitionAllowed() { |
| 79 | val emptyLayoutTransition = LayoutTransition() |
| 80 | emptyLayoutTransition.setAnimator(LayoutTransition.APPEARING, null) |
| 81 | emptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, null) |
| 82 | emptyLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, null) |
| 83 | emptyLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, null) |
| 84 | emptyLayoutTransition.setAnimator(4 /*LayoutTransition.Changing*/, null) |
| 85 | |
| 86 | val containerView = FragmentContainerView(context) |
| 87 | containerView.layoutTransition = emptyLayoutTransition |
| 88 | } |
| 89 | |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 90 | // If view sets animateLayoutChanges to true, throw UnsupportedOperationException |
| 91 | @Test |
| 92 | fun animateLayoutChangesTrueUnsupported() { |
| 93 | try { |
| 94 | StrictViewFragment(R.layout.fragment_container_view_unsupported_operation) |
| 95 | } catch (e: UnsupportedOperationException) { |
| 96 | assertThat(e) |
| 97 | .hasMessageThat() |
| 98 | .contains("FragmentContainerView does not support Layout Transitions or " + |
| 99 | "animateLayoutChanges=\"true\".") |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | @Test |
| 104 | fun createFragmentWithFragmentContainerView() { |
| 105 | val activity = activityRule.activity |
| 106 | val fm = activity.supportFragmentManager |
| 107 | |
| 108 | val fragment = StrictViewFragment(R.layout.fragment_container_view) |
| 109 | fm.beginTransaction() |
| 110 | .add(R.id.fragment_container_view, fragment) |
| 111 | .commit() |
| 112 | activityRule.runOnUiThread { fm.executePendingTransactions() } |
| 113 | |
| 114 | assertWithMessage("Fragment View should be a FragmentContainerView") |
| 115 | .that(fragment.view) |
| 116 | .isInstanceOf(FragmentContainerView::class.java) |
| 117 | } |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 118 | |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 119 | @SdkSuppress(minSdkVersion = 29) // WindowInsets.Builder requires API 29 |
| 120 | @Test |
| 121 | fun windowInsetsDispatchToChildren() { |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 122 | val parentView = FragmentContainerView(context) |
| 123 | val childView = FragmentContainerView(context) |
| 124 | |
| 125 | parentView.fitsSystemWindows = true |
| 126 | |
| 127 | val sentInsets = WindowInsets.Builder() |
| 128 | .setSystemWindowInsets(Insets.of(4, 3, 2, 1)) |
| 129 | .build() |
| 130 | |
| 131 | var dispatchedToChild = false |
| 132 | childView.setOnApplyWindowInsetsListener { _, insets -> |
| 133 | // Ensure insets received by child are not consumed at all by the parent |
| 134 | assertThat(insets.systemWindowInsets).isEqualTo(sentInsets.systemWindowInsets) |
| 135 | dispatchedToChild = true |
| 136 | insets |
| 137 | } |
| 138 | |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 139 | childView.setTag(R.id.fragment_container_view_tag, Fragment()) |
| 140 | |
jbwoods | d46ea74 | 2019-06-14 16:18:00 -0700 | [diff] [blame] | 141 | parentView.addView(childView) |
| 142 | parentView.dispatchApplyWindowInsets(sentInsets) |
| 143 | |
| 144 | assertThat(dispatchedToChild).isTrue() |
| 145 | } |
| 146 | |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 147 | @Test |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 148 | fun addView() { |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 149 | val view = View(context) |
| 150 | val fragment = Fragment() |
| 151 | fragment.mView = view |
| 152 | |
Ian Lake | 20f7c62 | 2019-10-04 10:42:27 -0700 | [diff] [blame] | 153 | // Mimic what FragmentStateManager.createView() does |
| 154 | fragment.mView.setTag(androidx.fragment.R.id.fragment_container_view_tag, fragment) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 155 | |
| 156 | val fragmentContainerView = FragmentContainerView(context) |
| 157 | |
| 158 | assertWithMessage("FragmentContainerView should have no child views") |
| 159 | .that(fragmentContainerView.childCount).isEqualTo(0) |
| 160 | |
| 161 | fragmentContainerView.addView(view) |
| 162 | |
| 163 | assertWithMessage("FragmentContainerView should have one child view") |
| 164 | .that(fragmentContainerView.childCount).isEqualTo(1) |
| 165 | } |
| 166 | |
| 167 | @Test |
| 168 | fun addViewNotAssociatedWithFragment() { |
| 169 | val view = View(context) |
| 170 | |
| 171 | try { |
| 172 | FragmentContainerView(context).addView(view, 0, null) |
| 173 | fail("View without a Fragment added to FragmentContainerView should throw an exception") |
| 174 | } catch (e: IllegalStateException) { |
| 175 | assertThat(e) |
| 176 | .hasMessageThat().contains( |
| 177 | "Views added to a FragmentContainerView must be associated with a Fragment. " + |
| 178 | "View " + view + " is not associated with a Fragment." |
| 179 | ) |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | @Test |
| 184 | fun addViewInLayoutNotAssociatedWithFragment() { |
| 185 | val view = View(context) |
| 186 | |
| 187 | try { |
| 188 | FragmentContainerView(context).addViewInLayout(view, 0, null, false) |
| 189 | fail("View without a Fragment added to FragmentContainerView should throw an exception") |
| 190 | } catch (e: IllegalStateException) { |
| 191 | assertThat(e) |
| 192 | .hasMessageThat().contains( |
| 193 | "Views added to a FragmentContainerView must be associated with a Fragment. " + |
| 194 | "View " + view + " is not associated with a Fragment." |
| 195 | ) |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | @Test |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 200 | fun removeViewAt() { |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 201 | val childView2 = FragmentContainerView(context) |
| 202 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 203 | val view = setupRemoveTestsView(FragmentContainerView(context), childView2) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 204 | |
| 205 | view.removeViewAt(0) |
| 206 | |
| 207 | assertThat(view.childCount).isEqualTo(1) |
| 208 | assertThat(view.getChildAt(0)).isEqualTo(childView2) |
| 209 | } |
| 210 | |
| 211 | @Test |
| 212 | fun removeViewInLayout() { |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 213 | val childView1 = FragmentContainerView(context) |
| 214 | val childView2 = FragmentContainerView(context) |
| 215 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 216 | val view = setupRemoveTestsView(childView1, childView2) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 217 | |
| 218 | view.removeViewInLayout(childView1) |
| 219 | |
| 220 | assertThat(view.childCount).isEqualTo(1) |
| 221 | assertThat(view.getChildAt(0)).isEqualTo(childView2) |
| 222 | } |
| 223 | |
| 224 | @Test |
| 225 | fun removeView() { |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 226 | val childView1 = FragmentContainerView(context) |
| 227 | val childView2 = FragmentContainerView(context) |
| 228 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 229 | val view = setupRemoveTestsView(childView1, childView2) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 230 | |
| 231 | view.removeView(childView1) |
| 232 | |
| 233 | assertThat(view.getChildAt(0)).isEqualTo(childView2) |
| 234 | } |
| 235 | |
| 236 | @Test |
| 237 | fun removeViews() { |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 238 | val view = setupRemoveTestsView( |
| 239 | FragmentContainerView(context), |
| 240 | FragmentContainerView(context) |
| 241 | ) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 242 | |
| 243 | view.removeViews(1, 1) |
| 244 | |
| 245 | assertThat(view.childCount).isEqualTo(1) |
| 246 | } |
| 247 | |
| 248 | @Test |
| 249 | fun removeViewsInLayout() { |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 250 | val view = setupRemoveTestsView( |
| 251 | FragmentContainerView(context), |
| 252 | FragmentContainerView(context) |
| 253 | ) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 254 | |
| 255 | view.removeViewsInLayout(1, 1) |
| 256 | |
| 257 | assertThat(view.childCount).isEqualTo(1) |
| 258 | } |
| 259 | |
| 260 | @Test |
| 261 | fun removeAllViewsInLayout() { |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 262 | val removingView1 = ChildView(context) |
| 263 | val removingView2 = ChildView(context) |
| 264 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 265 | val view = setupRemoveTestsView( |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 266 | removingView1, |
| 267 | removingView2 |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 268 | ) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 269 | |
| 270 | view.removeAllViewsInLayout() |
| 271 | |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 272 | assertThat(removingView1.getAnimationCount).isEqualTo(2) |
| 273 | assertThat(removingView2.getAnimationCount).isEqualTo(2) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 274 | assertThat(view.childCount).isEqualTo(0) |
| 275 | } |
| 276 | |
| 277 | // removeDetachedView should not actually remove the view |
| 278 | @Test |
| 279 | fun removeDetachedView() { |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 280 | val childView1 = FragmentContainerView(context) |
| 281 | val childView2 = FragmentContainerView(context) |
| 282 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 283 | val view = setupRemoveTestsView(childView1, childView2) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 284 | |
| 285 | view.removeDetachedView(childView1, false) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 286 | |
| 287 | assertThat(view.childCount).isEqualTo(2) |
| 288 | assertThat(view.getChildAt(1)).isEqualTo(childView2) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 289 | } |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 290 | |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 291 | private fun setupRemoveTestsView( |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 292 | childView1: View, |
| 293 | childView2: View |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 294 | ): FragmentContainerView { |
| 295 | val view = FragmentContainerView(context) |
Jeremy Woods | 6c5151d | 2019-07-26 16:58:58 -0700 | [diff] [blame] | 296 | val fragment1 = Fragment() |
| 297 | val fragment2 = Fragment() |
| 298 | |
| 299 | fragment1.mView = childView1 |
| 300 | fragment2.mView = childView2 |
| 301 | |
| 302 | childView1.setTag(R.id.fragment_container_view_tag, fragment1) |
| 303 | childView2.setTag(R.id.fragment_container_view_tag, fragment2) |
| 304 | |
| 305 | view.addView(childView1) |
| 306 | view.addView(childView2) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 307 | |
| 308 | assertThat(view.childCount).isEqualTo(2) |
| 309 | assertThat(view.getChildAt(1)).isEqualTo(childView2) |
Jeremy Woods | 640fd63 | 2019-07-27 21:24:42 -0700 | [diff] [blame] | 310 | return view |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 311 | } |
| 312 | |
| 313 | // Disappearing child views should be drawn first before other child views. |
| 314 | @Test |
| 315 | fun drawDisappearingChildViewsFirst() { |
| 316 | val fm = activityRule.activity.supportFragmentManager |
| 317 | |
| 318 | val fragment1 = ChildViewFragment() |
| 319 | val fragment2 = ChildViewFragment() |
| 320 | |
| 321 | fm.beginTransaction() |
| 322 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 323 | .replace(R.id.fragment_container_view, fragment1) |
| 324 | .commit() |
| 325 | activityRule.waitForExecution() |
| 326 | |
| 327 | val frag1View = fragment1.mView as ChildView |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 328 | // wait for the first draw to finish |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 329 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 330 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 331 | .isTrue() |
| 332 | assertWithMessage("Timed out waiting for onAnimationEnd on Fragment 1") |
| 333 | .that(frag1View.onAnimationEndLatch.await(1, TimeUnit.SECONDS)) |
| 334 | .isTrue() |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 335 | |
| 336 | // reset the first drawn view for the transaction we care about. |
| 337 | drawnFirst = null |
| 338 | drawnFirstCountDownLatch = CountDownLatch(1) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 339 | |
| 340 | fm.beginTransaction() |
| 341 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 342 | .replace(R.id.fragment_container_view, fragment2) |
| 343 | .commit() |
| 344 | activityRule.waitForExecution() |
| 345 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 346 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 347 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 348 | .isTrue() |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 349 | assertThat(drawnFirst!!).isEqualTo(frag1View) |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 350 | } |
| 351 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 352 | // Disappearing child views should be drawn last if transaction is a pop. |
| 353 | @Test |
| 354 | fun drawDisappearingChildViewsLast() { |
| 355 | val fm = activityRule.activity.supportFragmentManager |
| 356 | |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 357 | val fragment1 = ChildViewFragment() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 358 | val fragment2 = ChildViewFragment() |
| 359 | |
| 360 | fm.beginTransaction() |
| 361 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 362 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 363 | .replace(R.id.fragment_container_view, fragment1, "1") |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 364 | .commit() |
| 365 | activityRule.waitForExecution() |
| 366 | |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 367 | val frag1View = fragment1.mView as ChildView |
| 368 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 369 | fm.beginTransaction() |
| 370 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 371 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 372 | .replace(R.id.fragment_container_view, fragment2, "2") |
| 373 | .addToBackStack(null) |
| 374 | .commit() |
| 375 | activityRule.waitForExecution() |
| 376 | |
| 377 | val frag2View = fragment2.mView as ChildView |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 378 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 379 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 380 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 381 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 382 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 1") |
| 383 | .that(frag1View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 384 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 385 | frag1View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 386 | |
| 387 | // reset the first drawn view for the transaction we care about. |
| 388 | drawnFirst = null |
| 389 | drawnFirstCountDownLatch = CountDownLatch(1) |
| 390 | |
| 391 | fm.popBackStack() |
| 392 | activityRule.waitForExecution() |
| 393 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 394 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 2") |
| 395 | .that(frag2View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 396 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 397 | frag2View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 398 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 399 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 400 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 401 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 402 | // The popped Fragment will be drawn last and therefore will be on top |
| 403 | assertThat(drawnFirst!!).isNotEqualTo(frag2View) |
| 404 | } |
| 405 | |
| 406 | @Test |
| 407 | fun drawDisappearingChildViewsLastAfterPopNoReordering() { |
| 408 | val fm = activityRule.activity.supportFragmentManager |
| 409 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 410 | val fragment1 = ChildViewFragment() |
| 411 | val fragment2 = ChildViewFragment() |
| 412 | |
| 413 | fm.beginTransaction() |
| 414 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 415 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 416 | .replace(R.id.fragment_container_view, fragment1, "1") |
| 417 | .commit() |
| 418 | activityRule.waitForExecution() |
| 419 | |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 420 | val frag1View = fragment1.mView as ChildView |
| 421 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 422 | fm.beginTransaction() |
| 423 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 424 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 425 | .replace(R.id.fragment_container_view, fragment2, "2") |
| 426 | .setPrimaryNavigationFragment(fragment2) |
| 427 | .addToBackStack(null) |
| 428 | .commit() |
| 429 | activityRule.waitForExecution() |
| 430 | |
| 431 | val frag2View = fragment2.mView as ChildView |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 432 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 433 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 434 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 435 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 436 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 1") |
| 437 | .that(frag1View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 438 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 439 | frag1View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 440 | |
| 441 | // reset the first drawn view for the transaction we care about. |
| 442 | drawnFirst = null |
| 443 | drawnFirstCountDownLatch = CountDownLatch(1) |
| 444 | |
| 445 | fm.popBackStack() |
| 446 | fm.beginTransaction() |
| 447 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 448 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 449 | .replace(R.id.fragment_container_view, fragment1, "1") |
| 450 | .commit() |
| 451 | activityRule.waitForExecution() |
| 452 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 453 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 2") |
| 454 | .that(frag2View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 455 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 456 | frag2View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 457 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 458 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 459 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 460 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 461 | assertThat(drawnFirst!!).isNotEqualTo(frag2View) |
| 462 | } |
| 463 | |
| 464 | @Test |
| 465 | fun drawDisappearingChildViewsLastAfterPopReorderingAllowed() { |
| 466 | val fm = activityRule.activity.supportFragmentManager |
| 467 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 468 | val fragment1 = ChildViewFragment() |
| 469 | val fragment2 = ChildViewFragment() |
| 470 | |
| 471 | fm.beginTransaction() |
| 472 | .setReorderingAllowed(true) |
| 473 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 474 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 475 | .replace(R.id.fragment_container_view, fragment1, "1") |
| 476 | .commit() |
| 477 | activityRule.waitForExecution() |
| 478 | |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 479 | val frag1View = fragment1.mView as ChildView |
| 480 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 481 | fm.beginTransaction() |
| 482 | .setReorderingAllowed(true) |
| 483 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 484 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 485 | .replace(R.id.fragment_container_view, fragment2, "2") |
| 486 | .setPrimaryNavigationFragment(fragment2) |
| 487 | .addToBackStack(null) |
| 488 | .commit() |
| 489 | activityRule.waitForExecution() |
| 490 | |
| 491 | val frag2View = fragment2.mView as ChildView |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 492 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 493 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 494 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 495 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 496 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 1") |
| 497 | .that(frag1View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 498 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 499 | frag1View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 500 | |
| 501 | // reset the first drawn view for the transaction we care about. |
| 502 | drawnFirst = null |
| 503 | drawnFirstCountDownLatch = CountDownLatch(1) |
| 504 | |
| 505 | fm.popBackStack() |
| 506 | fm.beginTransaction() |
| 507 | .setReorderingAllowed(true) |
| 508 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 509 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 510 | .replace(R.id.fragment_container_view, fragment1, "1") |
| 511 | .commit() |
| 512 | activityRule.waitForExecution() |
| 513 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 514 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 2") |
| 515 | .that(frag2View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 516 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 517 | frag2View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 518 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 519 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 520 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 521 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 522 | assertThat(drawnFirst!!).isNotEqualTo(frag2View) |
| 523 | } |
| 524 | |
| 525 | @Test |
| 526 | fun drawDisappearingChildViewsLastAfterPopReorderingAllowedAddNewFragment() { |
| 527 | val fm = activityRule.activity.supportFragmentManager |
| 528 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 529 | val fragment1 = ChildViewFragment() |
| 530 | val fragment2 = ChildViewFragment() |
| 531 | |
| 532 | fm.beginTransaction() |
| 533 | .setReorderingAllowed(true) |
| 534 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 535 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 536 | .replace(R.id.fragment_container_view, fragment1, "1") |
| 537 | .commit() |
| 538 | activityRule.waitForExecution() |
| 539 | |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 540 | val frag1View = fragment1.mView as ChildView |
| 541 | |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 542 | fm.beginTransaction() |
| 543 | .setReorderingAllowed(true) |
| 544 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 545 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 546 | .replace(R.id.fragment_container_view, fragment2, "2") |
| 547 | .setPrimaryNavigationFragment(fragment2) |
| 548 | .addToBackStack(null) |
| 549 | .commit() |
| 550 | activityRule.waitForExecution() |
| 551 | |
| 552 | val frag2View = fragment2.mView as ChildView |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 553 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 554 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 555 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 556 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 557 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 1") |
| 558 | .that(frag1View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 559 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 560 | frag1View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 561 | |
| 562 | // reset the first drawn view for the transaction we care about. |
| 563 | drawnFirst = null |
| 564 | drawnFirstCountDownLatch = CountDownLatch(1) |
| 565 | |
| 566 | fm.popBackStack() |
| 567 | fm.beginTransaction() |
| 568 | .setReorderingAllowed(true) |
| 569 | .setCustomAnimations(android.R.anim.slide_in_left, android.R.anim.slide_out_right, |
| 570 | android.R.anim.slide_in_left, android.R.anim.slide_out_right) |
| 571 | .add(R.id.fragment_container_view, ChildViewFragment(), "1") |
| 572 | .commit() |
| 573 | activityRule.waitForExecution() |
| 574 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 575 | assertWithMessage("Timed out waiting for onDetachFromWindow on Fragment 2") |
| 576 | .that(frag2View.onDetachFromWindowLatch.await(1, TimeUnit.SECONDS)) |
| 577 | .isTrue() |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 578 | frag2View.onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 579 | |
Ian Lake | 325987e | 2020-02-20 09:42:08 -0800 | [diff] [blame] | 580 | assertWithMessage("Timed out waiting for setDrawnFirstView") |
| 581 | .that(drawnFirstCountDownLatch.await(1, TimeUnit.SECONDS)) |
| 582 | .isTrue() |
Jeremy Woods | 9e4b8ce | 2019-08-21 00:07:10 -0700 | [diff] [blame] | 583 | // The view that was popped is drawn first which means it is on the bottom. |
| 584 | assertThat(drawnFirst!!).isEqualTo(frag2View) |
| 585 | } |
| 586 | |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 587 | class ChildViewFragment : StrictViewFragment() { |
| 588 | override fun onCreateView( |
| 589 | inflater: LayoutInflater, |
| 590 | container: ViewGroup?, |
| 591 | savedInstanceState: Bundle? |
| 592 | ) = ChildView(context) |
| 593 | } |
| 594 | |
| 595 | class ChildView(context: Context?) : View(context) { |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 596 | var getAnimationCount = 0 |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 597 | var onDetachFromWindowLatch = CountDownLatch(1) |
Jeremy Woods | f6472c5 | 2019-09-20 12:30:06 -0700 | [diff] [blame] | 598 | var onAnimationEndLatch = CountDownLatch(1) |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 599 | |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 600 | override fun onDraw(canvas: Canvas?) { |
| 601 | super.onDraw(canvas) |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 602 | setDrawnFirstView(this) |
| 603 | } |
Jeremy Woods | 991f268 | 2019-08-08 10:33:56 -0700 | [diff] [blame] | 604 | |
| 605 | override fun getAnimation(): Animation? { |
| 606 | getAnimationCount++ |
| 607 | return super.getAnimation() |
| 608 | } |
Jeremy Woods | 8b13acf | 2019-08-27 15:49:43 -0700 | [diff] [blame] | 609 | |
| 610 | override fun onDetachedFromWindow() { |
| 611 | onDetachFromWindowLatch.countDown() |
| 612 | super.onDetachedFromWindow() |
| 613 | } |
Jeremy Woods | f6472c5 | 2019-09-20 12:30:06 -0700 | [diff] [blame] | 614 | |
| 615 | override fun onAnimationEnd() { |
| 616 | onAnimationEndLatch.countDown() |
| 617 | super.onAnimationEnd() |
| 618 | } |
Jeremy Woods | f441f19 | 2019-07-22 10:52:10 -0700 | [diff] [blame] | 619 | } |
| 620 | |
| 621 | companion object { |
| 622 | var drawnFirst: View? = null |
| 623 | var drawnFirstCountDownLatch = CountDownLatch(1) |
| 624 | fun setDrawnFirstView(v: View) { |
| 625 | if (drawnFirst == null) { |
| 626 | drawnFirst = v |
| 627 | } |
| 628 | drawnFirstCountDownLatch.countDown() |
jbwoods | 2534c73 | 2019-06-20 12:42:37 -0700 | [diff] [blame] | 629 | } |
| 630 | } |
jbwoods | b1757aa | 2019-06-12 11:17:06 -0700 | [diff] [blame] | 631 | } |