Merge "Adding support to render GuidedStepFragment in full/half screen mode." into mnc-ub-dev
diff --git a/annotations/src/android/support/annotation/VisibleForTesting.java b/annotations/src/android/support/annotation/VisibleForTesting.java
index bb02ab4..0c893ff 100644
--- a/annotations/src/android/support/annotation/VisibleForTesting.java
+++ b/annotations/src/android/support/annotation/VisibleForTesting.java
@@ -17,12 +17,12 @@
import java.lang.annotation.Retention;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
/**
* Denotes that the class, method or field has its visibility relaxed, so that it is more widely
* visible than otherwise necessary to make code testable.
*/
-@Retention(SOURCE)
+@Retention(CLASS)
public @interface VisibleForTesting {
}
diff --git a/build.gradle b/build.gradle
index ae0817b..174738c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -153,6 +153,19 @@
// Disable unique names for SNAPSHOTS so they can be updated in place.
setUniqueVersion(false)
+ doLast {
+ // Remove any invalid maven-metadata.xml files that may have been created
+ // for SNAPSHOT versions that are *not* uniquely versioned.
+ pom*.each { pom ->
+ if (pom.version.endsWith('-SNAPSHOT')) {
+ final File artifactDir = new File(rootProject.ext.supportRepoOut,
+ pom.groupId.replace('.', '/')
+ + '/' + pom.artifactId
+ + '/' + pom.version)
+ delete fileTree(dir: artifactDir, include: 'maven-metadata.xml*')
+ }
+ }
+ }
}
}
}
diff --git a/design/src/android/support/design/widget/BottomSheetBehavior.java b/design/src/android/support/design/widget/BottomSheetBehavior.java
index aa39ddd..1be223d 100644
--- a/design/src/android/support/design/widget/BottomSheetBehavior.java
+++ b/design/src/android/support/design/widget/BottomSheetBehavior.java
@@ -198,6 +198,9 @@
@Override
public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+ if (!child.isShown()) {
+ return false;
+ }
int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_UP:
@@ -227,6 +230,9 @@
@Override
public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+ if (!child.isShown()) {
+ return false;
+ }
mViewDragHelper.processTouchEvent(event);
return true;
}
diff --git a/design/src/android/support/design/widget/CollapsingTextHelper.java b/design/src/android/support/design/widget/CollapsingTextHelper.java
index 9a38c7d..8768826 100644
--- a/design/src/android/support/design/widget/CollapsingTextHelper.java
+++ b/design/src/android/support/design/widget/CollapsingTextHelper.java
@@ -105,8 +105,7 @@
public CollapsingTextHelper(View view) {
mView = view;
- mTextPaint = new TextPaint();
- mTextPaint.setAntiAlias(true);
+ mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
mCollapsedBounds = new Rect();
mExpandedBounds = new Rect();
@@ -436,10 +435,6 @@
final float ascent;
final float descent;
-
- // Update the TextPaint to the current text size
- mTextPaint.setTextSize(mCurrentTextSize);
-
if (drawTexture) {
ascent = mTextureAscent * mScale;
descent = mTextureDescent * mScale;
@@ -536,6 +531,8 @@
if (mTextToDraw == null || updateDrawText) {
mTextPaint.setTextSize(mCurrentTextSize);
mTextPaint.setTypeface(mCurrentTypeface);
+ // Use linear text scaling if we're scaling the canvas
+ mTextPaint.setLinearText(mScale != 1f);
// If we don't currently have text to draw, or the text size has changed, ellipsize...
final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
diff --git a/design/tests/AndroidManifest.xml b/design/tests/AndroidManifest.xml
index af26af3..4f65f92 100755
--- a/design/tests/AndroidManifest.xml
+++ b/design/tests/AndroidManifest.xml
@@ -39,6 +39,10 @@
android:name="android.support.design.widget.BottomSheetBehaviorActivity"/>
<activity
+ android:theme="@style/Theme.AppCompat.NoActionBar"
+ android:name="android.support.design.widget.NavigationViewActivity"/>
+
+ <activity
android:name="android.support.v7.app.AppCompatActivity"/>
</application>
diff --git a/design/tests/res/layout/design_navigation_view.xml b/design/tests/res/layout/design_navigation_view.xml
new file mode 100644
index 0000000..3f4e03b
--- /dev/null
+++ b/design/tests/res/layout/design_navigation_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<android.support.v4.widget.DrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+ <!-- As the main content view, the view below consumes the entire
+ space available using match_parent in both dimensions. Note that
+ this child does not specify android:layout_gravity attribute. -->
+ <FrameLayout
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <!-- android:layout_gravity="start" tells DrawerLayout to treat
+ this as a sliding drawer on the starting side, which is
+ left for left-to-right locales. The navigation view extends
+ the full height of the container. A
+ solid background is used for contrast with the content view.
+ android:fitsSystemWindows="true" tells the system to have
+ DrawerLayout span the full height of the screen, including the
+ system status bar on Lollipop+ versions of the plaform. -->
+ <android.support.design.widget.NavigationView
+ android:id="@+id/start_drawer"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:background="#333"
+ android:fitsSystemWindows="true"
+ app:menu="@menu/navigation_view_content"
+ app:itemIconTint="@color/emerald_translucent"
+ app:itemTextColor="@color/emerald_text"
+ app:itemBackground="@color/sand_fill"
+ app:itemTextAppearance="@style/TextMediumStyle" />
+
+</android.support.v4.widget.DrawerLayout>
+
diff --git a/design/tests/res/menu/navigation_view_content.xml b/design/tests/res/menu/navigation_view_content.xml
new file mode 100644
index 0000000..24fd830
--- /dev/null
+++ b/design/tests/res/menu/navigation_view_content.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 Google Inc.
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/destination_home"
+ android:title="@string/navigate_home" />
+ <item android:id="@+id/destination_profile"
+ android:title="@string/navigate_profile" />
+ <item android:id="@+id/destination_people"
+ android:title="@string/navigate_people" />
+ <item android:id="@+id/destination_settings"
+ android:title="@string/navigate_settings" />
+</menu>
diff --git a/design/tests/res/values/colors.xml b/design/tests/res/values/colors.xml
new file mode 100644
index 0000000..d1a6a1c
--- /dev/null
+++ b/design/tests/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<resources>
+ <color name="emerald_translucent">#8020A060</color>
+ <color name="emerald_text">#30B050</color>
+ <color name="sand_fill">#F0E020</color>
+</resources>
diff --git a/design/tests/res/values/dimens.xml b/design/tests/res/values/dimens.xml
index c20e4f8..c402941 100644
--- a/design/tests/res/values/dimens.xml
+++ b/design/tests/res/values/dimens.xml
@@ -18,4 +18,6 @@
<dimen name="tab_width_limit_medium">100dip</dimen>
<dimen name="tab_width_limit_large">120dip</dimen>
<dimen name="bottom_sheet_peek_height">128dp</dimen>
+
+ <dimen name="text_medium_size">20sp</dimen>
</resources>
\ No newline at end of file
diff --git a/design/tests/res/values/strings.xml b/design/tests/res/values/strings.xml
index 854e711..05dd55e 100644
--- a/design/tests/res/values/strings.xml
+++ b/design/tests/res/values/strings.xml
@@ -15,4 +15,9 @@
-->
<resources>
<string name="tab_layout_text">Tab text!</string>
+
+ <string name="navigate_home">Home</string>
+ <string name="navigate_profile">Profile</string>
+ <string name="navigate_people">People</string>
+ <string name="navigate_settings">Settings</string>
</resources>
\ No newline at end of file
diff --git a/design/tests/res/values/styles.xml b/design/tests/res/values/styles.xml
new file mode 100644
index 0000000..1fda2e6
--- /dev/null
+++ b/design/tests/res/values/styles.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+<resources>
+ <style name="TextMediumStyle" parent="@android:style/TextAppearance.Medium">
+ <item name="android:textSize">@dimen/text_medium_size</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/testutils/DrawerLayoutActions.java b/design/tests/src/android/support/design/testutils/DrawerLayoutActions.java
new file mode 100755
index 0000000..e4ea867
--- /dev/null
+++ b/design/tests/src/android/support/design/testutils/DrawerLayoutActions.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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.support.design.testutils;
+
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v4.widget.DrawerLayout;
+import android.view.View;
+import org.hamcrest.Matcher;
+
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+
+public class DrawerLayoutActions {
+ /**
+ * Opens the drawer at the specified edge gravity.
+ */
+ public static ViewAction openDrawer(final int drawerEdgeGravity) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Opens the drawer";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.openDrawer(drawerEdgeGravity);
+
+ // Wait for a full second to let the inner ViewDragHelper complete the operation
+ uiController.loopMainThreadForAtLeast(1000);
+ }
+ };
+ }
+
+ /**
+ * Closes the drawer at the specified edge gravity.
+ */
+ public static ViewAction closeDrawer(final int drawerEdgeGravity) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Closes the drawer";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.closeDrawer(drawerEdgeGravity);
+
+ // Wait for a full second to let the inner ViewDragHelper complete the operation
+ uiController.loopMainThreadForAtLeast(1000);
+ }
+ };
+ }
+}
diff --git a/design/tests/src/android/support/design/widget/BaseInstrumentationTestCase.java b/design/tests/src/android/support/design/widget/BaseInstrumentationTestCase.java
index bda213c..0c09e7b 100644
--- a/design/tests/src/android/support/design/widget/BaseInstrumentationTestCase.java
+++ b/design/tests/src/android/support/design/widget/BaseInstrumentationTestCase.java
@@ -17,26 +17,17 @@
package android.support.design.widget;
import android.app.Activity;
-import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
-import android.test.ActivityInstrumentationTestCase2;
-import org.junit.Before;
+import org.junit.Rule;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
-public abstract class BaseInstrumentationTestCase<A extends Activity>
- extends ActivityInstrumentationTestCase2<A> {
+public abstract class BaseInstrumentationTestCase<A extends Activity> {
+ @Rule
+ public final ActivityTestRule<A> mActivityTestRule;
protected BaseInstrumentationTestCase(Class<A> activityClass) {
- super(activityClass);
+ mActivityTestRule = new ActivityTestRule<A>(activityClass);
}
-
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- injectInstrumentation(InstrumentationRegistry.getInstrumentation());
- getActivity();
- }
-
}
diff --git a/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java b/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
index 65da332..3d21033 100644
--- a/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
+++ b/design/tests/src/android/support/design/widget/BottomSheetBehaviorTest.java
@@ -16,13 +16,12 @@
package android.support.design.widget;
-import org.hamcrest.Matcher;
-import org.junit.Test;
-
import android.support.annotation.NonNull;
import android.support.design.test.R;
+import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso;
import android.support.test.espresso.IdlingResource;
+import android.support.test.espresso.action.CoordinatesProvider;
import android.support.test.espresso.action.GeneralLocation;
import android.support.test.espresso.action.GeneralSwipeAction;
import android.support.test.espresso.action.Press;
@@ -34,6 +33,8 @@
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.ViewGroup;
+import org.hamcrest.Matcher;
+import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
@@ -127,8 +128,21 @@
public void testSwipeDownToCollapse() {
checkSetState(BottomSheetBehavior.STATE_EXPANDED, ViewMatchers.isDisplayed());
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
- .perform(DesignViewActions.withCustomConstraints(ViewActions.swipeDown(),
- ViewMatchers.isDisplayingAtLeast(5)));
+ .perform(DesignViewActions.withCustomConstraints(new GeneralSwipeAction(
+ Swipe.FAST, GeneralLocation.TOP_CENTER,
+ // Manually calculate the ending coordinates to make sure that the bottom
+ // sheet is collapsed, not hidden
+ new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ BottomSheetBehavior behavior = getBehavior();
+ return new float[]{
+ // x: center of the bottom sheet
+ view.getWidth() / 2,
+ // y: just above the peek height
+ view.getHeight() - behavior.getPeekHeight()};
+ }
+ }, Press.FINGER), ViewMatchers.isDisplayingAtLeast(5)));
// Avoid a deadlock (b/26160710)
registerIdlingResourceCallback();
try {
@@ -162,8 +176,12 @@
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
.perform(DesignViewActions.withCustomConstraints(
new GeneralSwipeAction(Swipe.FAST,
- GeneralLocation.VISIBLE_CENTER, GeneralLocation.TOP_CENTER,
- Press.FINGER),
+ GeneralLocation.VISIBLE_CENTER, new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ return new float[]{view.getWidth() / 2, 0};
+ }
+ }, Press.FINGER),
ViewMatchers.isDisplayingAtLeast(5)));
// Avoid a deadlock (b/26160710)
registerIdlingResourceCallback();
@@ -176,10 +194,46 @@
}
}
- private void checkSetState(int state, Matcher<View> matcher) {
+ @Test
+ @MediumTest
+ public void testInvisible() {
+ // Make the bottomsheet invisible
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ getBottomSheet().setVisibility(View.INVISIBLE);
+ assertThat(getBehavior().getState(), is(BottomSheetBehavior.STATE_COLLAPSED));
+ }
+ });
+ // Swipe up as if to expand it
+ Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
+ .perform(DesignViewActions.withCustomConstraints(
+ new GeneralSwipeAction(Swipe.FAST,
+ GeneralLocation.VISIBLE_CENTER, new CoordinatesProvider() {
+ @Override
+ public float[] calculateCoordinates(View view) {
+ return new float[]{view.getWidth() / 2, 0};
+ }
+ }, Press.FINGER),
+ not(ViewMatchers.isDisplayed())));
+ // Check that the bottom sheet stays the same collapsed state
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ assertThat(getBehavior().getState(), is(BottomSheetBehavior.STATE_COLLAPSED));
+ }
+ });
+ }
+
+ private void checkSetState(final int state, Matcher<View> matcher) {
registerIdlingResourceCallback();
try {
- getBehavior().setState(state);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ getBehavior().setState(state);
+ }
+ });
Espresso.onView(ViewMatchers.withId(R.id.bottom_sheet))
.check(ViewAssertions.matches(matcher));
assertThat(getBehavior().getState(), is(state));
@@ -202,15 +256,15 @@
}
private ViewGroup getBottomSheet() {
- return getActivity().mBottomSheet;
+ return mActivityTestRule.getActivity().mBottomSheet;
}
private BottomSheetBehavior getBehavior() {
- return getActivity().mBehavior;
+ return mActivityTestRule.getActivity().mBehavior;
}
private CoordinatorLayout getCoordinatorLayout() {
- return getActivity().mCoordinatorLayout;
+ return mActivityTestRule.getActivity().mCoordinatorLayout;
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java b/design/tests/src/android/support/design/widget/NavigationViewActivity.java
similarity index 64%
copy from v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
copy to design/tests/src/android/support/design/widget/NavigationViewActivity.java
index 4c78e3d..c3e810c 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
+++ b/design/tests/src/android/support/design/widget/NavigationViewActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -13,8 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.support.v7.widget;
+package android.support.design.widget;
-public class ViewInfoStoreTest {
+import android.support.design.test.R;
+public class NavigationViewActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.design_navigation_view;
+ }
}
diff --git a/design/tests/src/android/support/design/widget/NavigationViewTest.java b/design/tests/src/android/support/design/widget/NavigationViewTest.java
new file mode 100755
index 0000000..0f99cb5
--- /dev/null
+++ b/design/tests/src/android/support/design/widget/NavigationViewTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2016 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.support.design.widget;
+
+import android.content.res.Resources;
+import android.support.design.test.R;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.test.suitebuilder.annotation.SmallTest;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.support.design.testutils.DrawerLayoutActions.closeDrawer;
+import static android.support.design.testutils.DrawerLayoutActions.openDrawer;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.*;
+import static org.hamcrest.core.AllOf.allOf;
+
+public class NavigationViewTest
+ extends BaseInstrumentationTestCase<NavigationViewActivity> {
+ private DrawerLayout mDrawerLayout;
+
+ private NavigationView mNavigationView;
+
+ public NavigationViewTest() {
+ super(NavigationViewActivity.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final NavigationViewActivity activity = mActivityTestRule.getActivity();
+ mDrawerLayout = (DrawerLayout) activity.findViewById(R.id.drawer_layout);
+ mNavigationView = (NavigationView) mDrawerLayout.findViewById(R.id.start_drawer);
+
+ // Close the drawer to reset the state for the next test
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(GravityCompat.START));
+ }
+
+ @Test
+ @SmallTest
+ public void testBasics() {
+ // Open our drawer
+ onView(withId(R.id.drawer_layout)).perform(openDrawer(GravityCompat.START));
+
+ final Resources res = mActivityTestRule.getActivity().getResources();
+
+ // Check that we have the expected menu items in our NavigationView
+ onView(allOf(withText(res.getString(R.string.navigate_home)),
+ isDescendantOfA(withId(R.id.start_drawer)))).check(matches(isDisplayed()));
+ onView(allOf(withText(res.getString(R.string.navigate_profile)),
+ isDescendantOfA(withId(R.id.start_drawer)))).check(matches(isDisplayed()));
+ onView(allOf(withText(res.getString(R.string.navigate_people)),
+ isDescendantOfA(withId(R.id.start_drawer)))).check(matches(isDisplayed()));
+ onView(allOf(withText(res.getString(R.string.navigate_settings)),
+ isDescendantOfA(withId(R.id.start_drawer)))).check(matches(isDisplayed()));
+ }
+}
diff --git a/design/tests/src/android/support/design/widget/SnackbarBucketTests.java b/design/tests/src/android/support/design/widget/SnackbarBucketTests.java
index e4b1b68..db8d33a 100644
--- a/design/tests/src/android/support/design/widget/SnackbarBucketTests.java
+++ b/design/tests/src/android/support/design/widget/SnackbarBucketTests.java
@@ -16,9 +16,6 @@
package android.support.design.widget;
-import org.junit.Test;
-
-import android.content.Intent;
import android.os.SystemClock;
import android.support.design.test.R;
import android.support.test.espresso.ViewAction;
@@ -26,6 +23,7 @@
import android.support.test.espresso.action.ViewActions;
import android.test.suitebuilder.annotation.MediumTest;
import android.view.View;
+import org.junit.Test;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -33,6 +31,8 @@
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
public class SnackbarBucketTests extends BaseInstrumentationTestCase<SnackbarBucketTestsActivity> {
@@ -121,6 +121,6 @@
}
private CoordinatorLayout getCoordinatorLayout() {
- return getActivity().mCoordinatorLayout;
+ return mActivityTestRule.getActivity().mCoordinatorLayout;
}
}
diff --git a/design/tests/src/android/support/design/widget/TabLayoutWithLayoutItems.java b/design/tests/src/android/support/design/widget/TabLayoutWithLayoutItems.java
index 61e5768..603c2d4 100755
--- a/design/tests/src/android/support/design/widget/TabLayoutWithLayoutItems.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutWithLayoutItems.java
@@ -16,12 +16,14 @@
package android.support.design.widget;
-import org.junit.Test;
-
import android.support.design.test.R;
+import android.support.test.InstrumentationRegistry;
import android.support.v7.app.AppCompatActivity;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.LayoutInflater;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
public class TabLayoutWithLayoutItems extends BaseInstrumentationTestCase<AppCompatActivity> {
@@ -31,11 +33,12 @@
@Test
@SmallTest
- public void testInflateTabLayoutWithTabItems() throws Throwable {
- runTestOnUiThread(new Runnable() {
+ public void testInflateTabLayoutWithTabItems() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
- final LayoutInflater inflater = LayoutInflater.from(getActivity());
+ final LayoutInflater inflater =
+ LayoutInflater.from(mActivityTestRule.getActivity());
final TabLayout tabLayout = (TabLayout) inflater.inflate(
R.layout.design_tabs_items, null);
@@ -43,7 +46,8 @@
// Tab 0 has text, but no icon or custom view
TabLayout.Tab tab = tabLayout.getTabAt(0);
- assertEquals(getActivity().getString(R.string.tab_layout_text), tab.getText());
+ assertEquals(mActivityTestRule.getActivity().getString(R.string.tab_layout_text),
+ tab.getText());
assertNull(tab.getIcon());
assertNull(tab.getCustomView());
@@ -66,12 +70,20 @@
@Test(expected = IllegalArgumentException.class)
@SmallTest
public void testInflateTabLayoutWithNonTabItem() throws Throwable {
- runTestOnUiThread(new Runnable() {
- @Override
+ final Throwable[] exceptions = new Throwable[1];
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
public void run() {
- final LayoutInflater inflater = LayoutInflater.from(getActivity());
- inflater.inflate(R.layout.design_tabs_with_non_tabitems, null);
+ try {
+ final LayoutInflater inflater =
+ LayoutInflater.from(mActivityTestRule.getActivity());
+ inflater.inflate(R.layout.design_tabs_with_non_tabitems, null);
+ } catch (Throwable throwable) {
+ exceptions[0] = throwable;
+ }
}
});
+ if (exceptions[0] != null) {
+ throw exceptions[0];
+ }
}
}
diff --git a/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerActivity.java b/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerActivity.java
index 6fe99a5..ff92b36 100644
--- a/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerActivity.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerActivity.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2016 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.support.design.widget;
import android.support.design.test.R;
diff --git a/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerTest.java b/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerTest.java
index 0a86bce..15d3780 100755
--- a/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerTest.java
+++ b/design/tests/src/android/support/design/widget/TabLayoutWithViewPagerTest.java
@@ -34,6 +34,7 @@
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import org.hamcrest.Matcher;
+import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
@@ -43,6 +44,7 @@
import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
public class TabLayoutWithViewPagerTest
extends BaseInstrumentationTestCase<TabLayoutWithViewPagerActivity> {
@@ -165,10 +167,9 @@
super(TabLayoutWithViewPagerActivity.class);
}
+ @Before
public void setUp() throws Exception {
- super.setUp();
-
- final TabLayoutWithViewPagerActivity activity = getActivity();
+ final TabLayoutWithViewPagerActivity activity = mActivityTestRule.getActivity();
mTabLayout = (TabLayout) activity.findViewById(R.id.tabs);
mViewPager = (ViewPager) activity.findViewById(R.id.tabs_viewpager);
@@ -395,7 +396,7 @@
@DimenRes int tabMaxWidthResId) {
assertEquals("Scrollable tab mode", TabLayout.MODE_SCROLLABLE, mTabLayout.getTabMode());
- final Resources res = getActivity().getResources();
+ final Resources res = mActivityTestRule.getActivity().getResources();
final int minTabWidth = (tabMinWidthResId == 0) ? -1 :
res.getDimensionPixelSize(tabMinWidthResId);
final int maxTabWidth = (tabMaxWidthResId == 0) ? -1 :
diff --git a/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_arcto_golden.png b/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_arcto_golden.png
deleted file mode 100644
index f16e603..0000000
--- a/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_arcto_golden.png
+++ /dev/null
Binary files differ
diff --git a/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png b/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png
deleted file mode 100644
index 94ac7f0..0000000
--- a/graphics/drawable/static/tests/res/drawable-nodpi/vector_icon_implicit_lineto_golden.png
+++ /dev/null
Binary files differ
diff --git a/graphics/drawable/static/tests/res/drawable/vector_icon_arcto.xml b/graphics/drawable/static/tests/res/drawable/vector_icon_arcto.xml
deleted file mode 100644
index a4904f2..0000000
--- a/graphics/drawable/static/tests/res/drawable/vector_icon_arcto.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
- Copyright (C) 2015 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.
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="20dp"
- android:height="50dp"
- android:viewportWidth="20.0"
- android:viewportHeight="50.0">
- <path
- android:pathData="M14.285706,47.362198A50.71429,62.14286 0,0 0,1.0630035 5.5146027"
- android:fillColor="#ff55ff"/>
-</vector>
\ No newline at end of file
diff --git a/graphics/drawable/static/tests/res/drawable/vector_icon_implicit_lineto.xml b/graphics/drawable/static/tests/res/drawable/vector_icon_implicit_lineto.xml
deleted file mode 100644
index 41b3e1c..0000000
--- a/graphics/drawable/static/tests/res/drawable/vector_icon_implicit_lineto.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (C) 2015 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.
- */
--->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:height="64dp"
- android:width="64dp"
- android:viewportHeight="64"
- android:viewportWidth="64">
-
- <path
- android:fillColor="#FF000000"
- android:pathData="m0,0 32,0 0,32 -32,0 0,-32z"/>
-
-</vector>
\ No newline at end of file
diff --git a/graphics/drawable/static/tests/src/android/support/graphics/drawable/tests/VectorDrawableTest.java b/graphics/drawable/static/tests/src/android/support/graphics/drawable/tests/VectorDrawableTest.java
index 4163eb2..85fd597 100644
--- a/graphics/drawable/static/tests/src/android/support/graphics/drawable/tests/VectorDrawableTest.java
+++ b/graphics/drawable/static/tests/src/android/support/graphics/drawable/tests/VectorDrawableTest.java
@@ -64,8 +64,6 @@
R.drawable.vector_icon_stroke_3,
R.drawable.vector_icon_scale_1,
R.drawable.vector_icon_scale_2,
- R.drawable.vector_icon_implicit_lineto,
- R.drawable.vector_icon_arcto,
};
private static final int[] GOLDEN_IMAGES = new int[]{
@@ -94,8 +92,6 @@
R.drawable.vector_icon_stroke_3_golden,
R.drawable.vector_icon_scale_1_golden,
R.drawable.vector_icon_scale_2_golden,
- R.drawable.vector_icon_implicit_lineto_golden,
- R.drawable.vector_icon_arcto_golden,
};
private static final int TEST_ICON = R.drawable.vector_icon_create;
@@ -288,7 +284,7 @@
public void testGetConstantState() {
VectorDrawableCompat vectorDrawable =
- VectorDrawableCompat.create(mResources, R.drawable.vector_icon_arcto, mTheme);
+ VectorDrawableCompat.create(mResources, R.drawable.vector_icon_delete, mTheme);
Drawable.ConstantState constantState = vectorDrawable.getConstantState();
assertNotNull(constantState);
assertEquals(0, constantState.getChangingConfigurations());
diff --git a/percent/src/android/support/percent/PercentLayoutHelper.java b/percent/src/android/support/percent/PercentLayoutHelper.java
index a76eed9..956b428 100644
--- a/percent/src/android/support/percent/PercentLayoutHelper.java
+++ b/percent/src/android/support/percent/PercentLayoutHelper.java
@@ -319,6 +319,20 @@
info.mPreservedParams.height == ViewGroup.LayoutParams.WRAP_CONTENT;
}
+ /* package */ static class PercentMarginLayoutParams extends ViewGroup.MarginLayoutParams {
+ // These two flags keep track of whether we're computing the LayoutParams width and height
+ // in the fill pass based on the aspect ratio. This allows the fill pass to be re-entrant
+ // as the framework code can call onMeasure() multiple times before the onLayout() is
+ // called. Those multiple invocations of onMeasure() are not guaranteed to be called with
+ // the same set of width / height.
+ private boolean mIsHeightComputedFromAspectRatio;
+ private boolean mIsWidthComputedFromAspectRatio;
+
+ public PercentMarginLayoutParams(int width, int height) {
+ super(width, height);
+ }
+ }
+
/**
* Container for information about percentage dimensions and margins. It acts as an extension
* for {@code LayoutParams}.
@@ -342,7 +356,7 @@
public float aspectRatio;
- /* package */ final ViewGroup.MarginLayoutParams mPreservedParams;
+ /* package */ final PercentMarginLayoutParams mPreservedParams;
public PercentLayoutInfo() {
widthPercent = -1f;
@@ -353,7 +367,7 @@
bottomMarginPercent = -1f;
startMarginPercent = -1f;
endMarginPercent = -1f;
- mPreservedParams = new ViewGroup.MarginLayoutParams(0, 0);
+ mPreservedParams = new PercentMarginLayoutParams(0, 0);
}
/**
@@ -369,8 +383,12 @@
// necessarily be true, as the user might explicitly set it to 0. However, we use this
// information only for the aspect ratio. If the user set the aspect ratio attribute,
// it means they accept or soon discover that it will be disregarded.
- final boolean widthNotSet = params.width == 0 && widthPercent < 0;
- final boolean heightNotSet = params.height == 0 && heightPercent < 0;
+ final boolean widthNotSet =
+ (mPreservedParams.mIsWidthComputedFromAspectRatio
+ || mPreservedParams.width == 0) && (widthPercent < 0);
+ final boolean heightNotSet =
+ (mPreservedParams.mIsHeightComputedFromAspectRatio
+ || mPreservedParams.height == 0) && (heightPercent < 0);
if (widthPercent >= 0) {
params.width = (int) (widthHint * widthPercent);
@@ -383,9 +401,13 @@
if (aspectRatio >= 0) {
if (widthNotSet) {
params.width = (int) (params.height * aspectRatio);
+ // Keep track that we've filled the width based on the height and aspect ratio.
+ mPreservedParams.mIsWidthComputedFromAspectRatio = true;
}
if (heightNotSet) {
params.height = (int) (params.width / aspectRatio);
+ // Keep track that we've filled the height based on the width and aspect ratio.
+ mPreservedParams.mIsHeightComputedFromAspectRatio = true;
}
}
@@ -490,8 +512,20 @@
* {@link PercentLayoutHelper.PercentLayoutInfo#fillLayoutParams}.
*/
public void restoreLayoutParams(ViewGroup.LayoutParams params) {
- params.width = mPreservedParams.width;
- params.height = mPreservedParams.height;
+ if (!mPreservedParams.mIsWidthComputedFromAspectRatio) {
+ // Only restore the width if we didn't compute it based on the height and
+ // aspect ratio in the fill pass.
+ params.width = mPreservedParams.width;
+ }
+ if (!mPreservedParams.mIsHeightComputedFromAspectRatio) {
+ // Only restore the height if we didn't compute it based on the width and
+ // aspect ratio in the fill pass.
+ params.height = mPreservedParams.height;
+ }
+
+ // Reset the tracking flags.
+ mPreservedParams.mIsWidthComputedFromAspectRatio = false;
+ mPreservedParams.mIsHeightComputedFromAspectRatio = false;
}
}
diff --git a/percent/tests/AndroidManifest.xml b/percent/tests/AndroidManifest.xml
index 224187a..c4cc597 100644
--- a/percent/tests/AndroidManifest.xml
+++ b/percent/tests/AndroidManifest.xml
@@ -27,6 +27,8 @@
<uses-library android:name="android.test.runner" />
<activity android:name="android.support.percent.TestFrameActivity"/>
+ <activity android:name="android.support.percent.TestRelativeActivity"/>
+ <activity android:name="android.support.percent.TestRelativeRtlActivity"/>
</application>
<instrumentation
diff --git a/percent/tests/java/android/support/percent/BaseInstrumentationTestCase.java b/percent/tests/java/android/support/percent/BaseInstrumentationTestCase.java
index 997820c..1fff432 100644
--- a/percent/tests/java/android/support/percent/BaseInstrumentationTestCase.java
+++ b/percent/tests/java/android/support/percent/BaseInstrumentationTestCase.java
@@ -17,26 +17,33 @@
package android.support.percent;
import android.app.Activity;
+import android.app.Instrumentation;
import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.test.ActivityInstrumentationTestCase2;
+import junit.framework.Assert;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.runner.RunWith;
+import static junit.framework.Assert.assertEquals;
+
@RunWith(AndroidJUnit4.class)
-public abstract class BaseInstrumentationTestCase<A extends Activity>
- extends ActivityInstrumentationTestCase2<A> {
+public abstract class BaseInstrumentationTestCase<A extends Activity> {
+ @Rule
+ public final ActivityTestRule<A> mActivityTestRule;
protected BaseInstrumentationTestCase(Class<A> activityClass) {
- super(activityClass);
+ mActivityTestRule = new ActivityTestRule<A>(activityClass);
}
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- injectInstrumentation(InstrumentationRegistry.getInstrumentation());
- getActivity();
+ protected static void assertFuzzyEquals(String description, float expected, float actual) {
+ // On devices with certain screen densities we may run into situations where multiplying
+ // container width / height by a certain fraction ends up in a number that is almost but
+ // not exactly a round float number. For example, we can do float math to compute 15%
+ // of 1440 pixels and get 216.00002 due to inexactness of float math. This is why our
+ // tolerance is slightly bigger than 1 pixel in the comparison below.
+ assertEquals(description, expected, actual, 1.1f);
}
-
}
diff --git a/percent/tests/java/android/support/percent/PercentFrameTest.java b/percent/tests/java/android/support/percent/PercentFrameTest.java
index ff7bd6a..67d2ae5 100644
--- a/percent/tests/java/android/support/percent/PercentFrameTest.java
+++ b/percent/tests/java/android/support/percent/PercentFrameTest.java
@@ -27,6 +27,7 @@
import org.junit.Before;
import org.junit.Test;
+import static android.support.percent.LayoutDirectionActions.setLayoutDirection;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
@@ -42,27 +43,12 @@
@Before
public void setUp() throws Exception {
- super.setUp();
-
- final TestFrameActivity activity = getActivity();
+ final TestFrameActivity activity = mActivityTestRule.getActivity();
mPercentFrameLayout = (PercentFrameLayout) activity.findViewById(R.id.container);
mContainerWidth = mPercentFrameLayout.getWidth();
mContainerHeight = mPercentFrameLayout.getHeight();
}
- private void assertFuzzyEquals(String description, float expected, float actual) {
- float difference = actual - expected;
- // On devices with certain screen densities we may run into situations where multiplying
- // container width / height by a certain fraction ends up in a number that is almost but
- // not exactly a round float number. For example, we can do float math to compute 15%
- // of 1440 pixels and get 216.00002 due to inexactness of float math. This is why our
- // tolerance is slightly bigger than 1 pixel in the comparison below.
- if (Math.abs(difference) > 1.1) {
- Assert.fail(description + ": the difference between expected [" + expected +
- "] and actual [" + actual + "] is not within the tolerance bound");
- }
- }
-
@Test
@UiThreadTest
public void testWidthHeight() {
@@ -175,7 +161,6 @@
int childRight = childToTest.getRight();
int childBottom = childToTest.getBottom();
- //Debug.waitForDebugger();
assertFuzzyEquals("Child width as 60% of the container",
0.6f * mContainerWidth, childWidth);
assertFuzzyEquals("Child height as 60% of the container",
@@ -205,10 +190,10 @@
if (Build.VERSION.SDK_INT >= 17) {
// Force our child to inherit parent's layout direction
onView(withId(R.id.child_margin_start)).perform(
- LayoutDirectionActions.setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_INHERIT));
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_INHERIT));
// And force the container to RTL mode
onView(withId(R.id.container)).perform(
- LayoutDirectionActions.setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
// Force a full measure + layout pass on the container
mPercentFrameLayout.measure(
@@ -249,10 +234,10 @@
if (Build.VERSION.SDK_INT >= 17) {
// Force our child to inherit parent's layout direction
onView(withId(R.id.child_margin_end)).perform(
- LayoutDirectionActions.setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_INHERIT));
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_INHERIT));
// And force the container to RTL mode
onView(withId(R.id.container)).perform(
- LayoutDirectionActions.setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
// Force a full measure + layout pass on the container
mPercentFrameLayout.measure(
diff --git a/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
new file mode 100644
index 0000000..634637c
--- /dev/null
+++ b/percent/tests/java/android/support/percent/PercentRelativeRtlTest.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2016 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.support.percent;
+
+import android.os.Build;
+import android.support.percent.test.R;
+import android.support.v4.view.ViewCompat;
+import android.test.UiThreadTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.support.percent.LayoutDirectionActions.setLayoutDirection;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+/**
+ * The arrangement of child views in the layout class in the default LTR (left-to-right) direction
+ * is as follows:
+ *
+ * +---------------------------------------------+
+ * | |
+ * | TTTTTTTTTTTTTTTTTTTTT |
+ * | |
+ * | S |
+ * | S CCCCCCCCCCCCCCCCCC |
+ * | S CCCCCCCCCCCCCCCCCC |
+ * | S CCCCCCCCCCCCCCCCCC E |
+ * | S CCCCCCCCCCCCCCCCCC E |
+ * | S CCCCCCCCCCCCCCCCCC E |
+ * | CCCCCCCCCCCCCCCCCC E |
+ * | CCCCCCCCCCCCCCCCCC E |
+ * | E |
+ * | |
+ * | BBBBBBBBBBBBBBBBBBBBB |
+ * | |
+ * +---------------------------------------------+
+ *
+ * The arrangement of child views in the layout class in the RTL (right-to-left) direction
+ * is as follows:
+ *
+ * +---------------------------------------------+
+ * | |
+ * | TTTTTTTTTTTTTTTTTTTTT |
+ * | |
+ * | S |
+ * | CCCCCCCCCCCCCCCCCC S |
+ * | CCCCCCCCCCCCCCCCCC S |
+ * | E CCCCCCCCCCCCCCCCCC S |
+ * | E CCCCCCCCCCCCCCCCCC S |
+ * | E CCCCCCCCCCCCCCCCCC S |
+ * | E CCCCCCCCCCCCCCCCCC |
+ * | E CCCCCCCCCCCCCCCCCC |
+ * | E |
+ * | |
+ * | BBBBBBBBBBBBBBBBBBBBB |
+ * | |
+ * +---------------------------------------------+
+ *
+ * Child views are exercising the following percent-based constraints supported by
+ * <code>PercentRelativeLayout</code>:
+ *
+ * <ul>
+ * <li>Top child (marked with T) - width, aspect ratio, top margin, start margin.</li>
+ * <li>Start child (marked with S) - height, aspect ratio, top margin, start margin.</li>
+ * <li>Bottom child (marked with B) - width, aspect ratio, bottom margin, end margin.</li>
+ * <li>Right child (marked with E) - height, aspect ratio, bottom margin, end margin.</li>
+ * <li>Center child (marked with C) - margin (all sides) from the other four children.</li>
+ * </ul>
+ *
+ * Under LTR direction (pre-v17 devices and v17+ with default direction of en-US locale) we are
+ * testing the same assertions as <code>PercentRelativeTest</code>. Under RTL direction (on v17+
+ * devices with Espresso-powered direction switch) we are testing the reverse assertions along the
+ * X axis for all child views.
+ */
+@SmallTest
+public class PercentRelativeRtlTest extends BaseInstrumentationTestCase<TestRelativeRtlActivity> {
+ private PercentRelativeLayout mPercentRelativeLayout;
+ private int mContainerWidth;
+ private int mContainerHeight;
+
+ public PercentRelativeRtlTest() {
+ super(TestRelativeRtlActivity.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final TestRelativeRtlActivity activity = mActivityTestRule.getActivity();
+ mPercentRelativeLayout = (PercentRelativeLayout) activity.findViewById(R.id.container);
+ mContainerWidth = mPercentRelativeLayout.getWidth();
+ mContainerHeight = mPercentRelativeLayout.getHeight();
+ }
+
+ private void switchToRtl() {
+ // Force the container to RTL mode
+ onView(withId(R.id.container)).perform(
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
+
+ // Force a full measure + layout pass on the container
+ mPercentRelativeLayout.measure(
+ View.MeasureSpec.makeMeasureSpec(mContainerWidth, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(mContainerHeight, View.MeasureSpec.EXACTLY));
+ mPercentRelativeLayout.layout(mPercentRelativeLayout.getLeft(),
+ mPercentRelativeLayout.getTop(), mPercentRelativeLayout.getRight(),
+ mPercentRelativeLayout.getBottom());
+ }
+
+ @Test
+ @UiThreadTest
+ public void testTopChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_top);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ switchToRtl();
+
+ final int childRight = childToTest.getRight();
+ assertFuzzyEquals("Child start margin as 20% of the container",
+ 0.2f * mContainerWidth, mContainerWidth - childRight);
+ } else {
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child start margin as 20% of the container",
+ 0.2f * mContainerWidth, childLeft);
+ }
+
+ final int childTop = childToTest.getTop();
+ assertFuzzyEquals("Child top margin as 5% of the container",
+ 0.05f * mContainerHeight, childTop);
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child width as 50% of the container",
+ 0.5f * mContainerWidth, childWidth);
+ assertFuzzyEquals("Child aspect ratio of 2000%",
+ 0.05f * childWidth, childHeight);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testStartChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_start);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ switchToRtl();
+
+ final int childRight = childToTest.getRight();
+ assertFuzzyEquals("Child start margin as 5% of the container",
+ 0.05f * mContainerWidth, mContainerWidth - childRight);
+ } else {
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child start margin as 5% of the container",
+ 0.05f * mContainerWidth, childLeft);
+ }
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child height as 50% of the container",
+ 0.5f * mContainerHeight, childHeight);
+ assertFuzzyEquals("Child aspect ratio of 5%",
+ 0.05f * childHeight, childWidth);
+
+ final int childTop = childToTest.getTop();
+
+ assertFuzzyEquals("Child top margin as 20% of the container",
+ 0.2f * mContainerHeight, childTop);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testBottomChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ switchToRtl();
+
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child end margin as 20% of the container",
+ 0.2f * mContainerWidth, childLeft);
+ } else {
+ final int childRight = childToTest.getRight();
+ assertFuzzyEquals("Child end margin as 20% of the container",
+ 0.2f * mContainerWidth, mContainerWidth - childRight);
+ }
+
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child width as 40% of the container",
+ 0.4f * mContainerWidth, childWidth);
+ assertFuzzyEquals("Child aspect ratio of 2000%",
+ 0.05f * childWidth, childHeight);
+
+ final int childBottom = childToTest.getBottom();
+
+ assertFuzzyEquals("Child bottom margin as 5% of the container",
+ 0.05f * mContainerHeight, mContainerHeight - childBottom);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testEndChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_end);
+
+ if (Build.VERSION.SDK_INT >= 17) {
+ switchToRtl();
+
+ final int childLeft = childToTest.getLeft();
+ assertFuzzyEquals("Child end margin as 5% of the container",
+ 0.05f * mContainerWidth, childLeft);
+ } else {
+ final int childRight = childToTest.getRight();
+ assertFuzzyEquals("Child end margin as 5% of the container",
+ 0.05f * mContainerWidth, mContainerWidth - childRight);
+ }
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child height as 50% of the container",
+ 0.4f * mContainerHeight, childHeight);
+ assertFuzzyEquals("Child aspect ratio of 5%",
+ 0.05f * childHeight, childWidth);
+
+ final int childBottom = childToTest.getBottom();
+
+ assertFuzzyEquals("Child bottom margin as 20% of the container",
+ 0.2f * mContainerHeight, mContainerHeight - childBottom);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCenterChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
+
+ boolean supportsRtl = Build.VERSION.SDK_INT >= 17;
+ if (supportsRtl) {
+ switchToRtl();
+ }
+
+ final int childLeft = childToTest.getLeft();
+ final int childTop = childToTest.getTop();
+ final int childRight = childToTest.getRight();
+ final int childBottom = childToTest.getBottom();
+
+ final View leftChild = supportsRtl
+ ? mPercentRelativeLayout.findViewById(R.id.child_end)
+ : mPercentRelativeLayout.findViewById(R.id.child_start);
+ assertFuzzyEquals("Child left margin as 10% of the container",
+ leftChild.getRight() + 0.1f * mContainerWidth, childLeft);
+
+ final View topChild = mPercentRelativeLayout.findViewById(R.id.child_top);
+ assertFuzzyEquals("Child top margin as 10% of the container",
+ topChild.getBottom() + 0.1f * mContainerHeight, childTop);
+
+ final View rightChild = supportsRtl
+ ? mPercentRelativeLayout.findViewById(R.id.child_start)
+ : mPercentRelativeLayout.findViewById(R.id.child_end);
+ assertFuzzyEquals("Child right margin as 10% of the container",
+ rightChild.getLeft() - 0.1f * mContainerWidth, childRight);
+
+ final View bottomChild = mPercentRelativeLayout.findViewById(R.id.child_bottom);
+ assertFuzzyEquals("Child bottom margin as 10% of the container",
+ bottomChild.getTop() - 0.1f * mContainerHeight, childBottom);
+ }
+}
diff --git a/percent/tests/java/android/support/percent/PercentRelativeTest.java b/percent/tests/java/android/support/percent/PercentRelativeTest.java
new file mode 100644
index 0000000..31d406e
--- /dev/null
+++ b/percent/tests/java/android/support/percent/PercentRelativeTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2016 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.support.percent;
+
+import android.support.percent.test.R;
+import android.test.UiThreadTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * The arrangement of child views in the layout class is as follows:
+ *
+ * +---------------------------------------------+
+ * | |
+ * | TTTTTTTTTTTTTTTTTTTTT |
+ * | |
+ * | L |
+ * | L CCCCCCCCCCCCCCCCCC |
+ * | L CCCCCCCCCCCCCCCCCC |
+ * | L CCCCCCCCCCCCCCCCCC R |
+ * | L CCCCCCCCCCCCCCCCCC R |
+ * | L CCCCCCCCCCCCCCCCCC R |
+ * | CCCCCCCCCCCCCCCCCC R |
+ * | CCCCCCCCCCCCCCCCCC R |
+ * | R |
+ * | |
+ * | BBBBBBBBBBBBBBBBBBBBB |
+ * | |
+ * +---------------------------------------------+
+ *
+ * Child views are exercising the following percent-based constraints supported by
+ * <code>PercentRelativeLayout</code>:
+ *
+ * <ul>
+ * <li>Top child (marked with T) - width, aspect ratio, top margin, left margin.</li>
+ * <li>Left child (marked with L) - height, aspect ratio, top margin, left margin.</li>
+ * <li>Bottom child (marked with B) - width, aspect ratio, bottom margin, right margin.</li>
+ * <li>Right child (marked with R) - height, aspect ratio, bottom margin, right margin.</li>
+ * <li>Center child (marked with C) - margin (all sides) from the other four children.</li>
+ * </ul>
+ */
+@SmallTest
+public class PercentRelativeTest extends BaseInstrumentationTestCase<TestRelativeActivity> {
+ private PercentRelativeLayout mPercentRelativeLayout;
+ private int mContainerWidth;
+ private int mContainerHeight;
+
+ public PercentRelativeTest() {
+ super(TestRelativeActivity.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final TestRelativeActivity activity = mActivityTestRule.getActivity();
+ mPercentRelativeLayout = (PercentRelativeLayout) activity.findViewById(R.id.container);
+ mContainerWidth = mPercentRelativeLayout.getWidth();
+ mContainerHeight = mPercentRelativeLayout.getHeight();
+ }
+
+ @Test
+ @UiThreadTest
+ public void testTopChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_top);
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child width as 50% of the container",
+ 0.5f * mContainerWidth, childWidth);
+ assertFuzzyEquals("Child aspect ratio of 2000%",
+ 0.05f * childWidth, childHeight);
+
+ final int childLeft = childToTest.getLeft();
+ final int childTop = childToTest.getTop();
+
+ assertFuzzyEquals("Child left margin as 20% of the container",
+ 0.2f * mContainerWidth, childLeft);
+ assertFuzzyEquals("Child top margin as 5% of the container",
+ 0.05f * mContainerHeight, childTop);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testLeftChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_left);
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child height as 50% of the container",
+ 0.5f * mContainerHeight, childHeight);
+ assertFuzzyEquals("Child aspect ratio of 5%",
+ 0.05f * childHeight, childWidth);
+
+ final int childLeft = childToTest.getLeft();
+ final int childTop = childToTest.getTop();
+
+ assertFuzzyEquals("Child left margin as 5% of the container",
+ 0.05f * mContainerWidth, childLeft);
+ assertFuzzyEquals("Child top margin as 20% of the container",
+ 0.2f * mContainerHeight, childTop);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testBottomChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_bottom);
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child width as 40% of the container",
+ 0.4f * mContainerWidth, childWidth);
+ assertFuzzyEquals("Child aspect ratio of 2000%",
+ 0.05f * childWidth, childHeight);
+
+ final int childRight = childToTest.getRight();
+ final int childBottom = childToTest.getBottom();
+
+ assertFuzzyEquals("Child right margin as 20% of the container",
+ 0.2f * mContainerWidth, mContainerWidth - childRight);
+ assertFuzzyEquals("Child bottom margin as 5% of the container",
+ 0.05f * mContainerHeight, mContainerHeight - childBottom);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testRightChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_right);
+
+ final int childWidth = childToTest.getWidth();
+ final int childHeight = childToTest.getHeight();
+
+ assertFuzzyEquals("Child height as 50% of the container",
+ 0.4f * mContainerHeight, childHeight);
+ assertFuzzyEquals("Child aspect ratio of 5%",
+ 0.05f * childHeight, childWidth);
+
+ final int childRight = childToTest.getRight();
+ final int childBottom = childToTest.getBottom();
+
+ assertFuzzyEquals("Child right margin as 5% of the container",
+ 0.05f * mContainerWidth, mContainerWidth - childRight);
+ assertFuzzyEquals("Child bottom margin as 20% of the container",
+ 0.2f * mContainerHeight, mContainerHeight - childBottom);
+ }
+
+ @Test
+ @UiThreadTest
+ public void testCenterChild() {
+ final View childToTest = mPercentRelativeLayout.findViewById(R.id.child_center);
+
+ final int childLeft = childToTest.getLeft();
+ final int childTop = childToTest.getTop();
+ final int childRight = childToTest.getRight();
+ final int childBottom = childToTest.getBottom();
+
+ final View leftChild = mPercentRelativeLayout.findViewById(R.id.child_left);
+ assertFuzzyEquals("Child left margin as 10% of the container",
+ leftChild.getRight() + 0.1f * mContainerWidth, childLeft);
+
+ final View topChild = mPercentRelativeLayout.findViewById(R.id.child_top);
+ assertFuzzyEquals("Child top margin as 10% of the container",
+ topChild.getBottom() + 0.1f * mContainerHeight, childTop);
+
+ final View rightChild = mPercentRelativeLayout.findViewById(R.id.child_right);
+ assertFuzzyEquals("Child right margin as 10% of the container",
+ rightChild.getLeft() - 0.1f * mContainerWidth, childRight);
+
+ final View bottomChild = mPercentRelativeLayout.findViewById(R.id.child_bottom);
+ assertFuzzyEquals("Child bottom margin as 10% of the container",
+ bottomChild.getTop() - 0.1f * mContainerHeight, childBottom);
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java b/percent/tests/java/android/support/percent/TestRelativeActivity.java
similarity index 70%
rename from v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
rename to percent/tests/java/android/support/percent/TestRelativeActivity.java
index 4c78e3d..e0d451f 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
+++ b/percent/tests/java/android/support/percent/TestRelativeActivity.java
@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.support.v7.widget;
-public class ViewInfoStoreTest {
+package android.support.percent;
+import android.support.percent.test.R;
+
+public class TestRelativeActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.percent_relative_layout;
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java b/percent/tests/java/android/support/percent/TestRelativeRtlActivity.java
similarity index 64%
copy from v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
copy to percent/tests/java/android/support/percent/TestRelativeRtlActivity.java
index 4c78e3d..02a2fff 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/ViewInfoStoreTest.java
+++ b/percent/tests/java/android/support/percent/TestRelativeRtlActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.support.v7.widget;
-public class ViewInfoStoreTest {
+package android.support.percent;
+import android.support.percent.test.R;
+
+public class TestRelativeRtlActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.percent_relative_layout_rtl;
+ }
}
diff --git a/percent/tests/res/layout/percent_relative_layout.xml b/percent/tests/res/layout/percent_relative_layout.xml
new file mode 100644
index 0000000..7bdfe9b
--- /dev/null
+++ b/percent/tests/res/layout/percent_relative_layout.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<android.support.percent.PercentRelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- This child is anchored along the top edge of the parent, testing percent-based
+ left margin, top margin and width, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_top"
+ android:layout_alignParentTop="true"
+ app:layout_widthPercent="50%"
+ app:layout_aspectRatio="2000%"
+ app:layout_marginLeftPercent="20%"
+ app:layout_marginTopPercent="5%"
+ android:background="#5690E0" />
+
+ <!-- This child is anchored along the left edge of the parent, testing percent-based
+ left margin, top margin and height, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_left"
+ android:layout_alignParentLeft="true"
+ app:layout_heightPercent="50%"
+ app:layout_aspectRatio="5%"
+ app:layout_marginLeftPercent="5%"
+ app:layout_marginTopPercent="20%"
+ android:background="#902030" />
+
+ <!-- This child is anchored along the bottom edge of the parent, testing percent-based
+ right margin, bottom margin and width, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_bottom"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ app:layout_widthPercent="40%"
+ app:layout_aspectRatio="2000%"
+ app:layout_marginRightPercent="20%"
+ app:layout_marginBottomPercent="5%"
+ android:background="#A0B040" />
+
+ <!-- This child is anchored along the right edge of the parent, testing percent-based
+ right margin, bottom margin and height, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_right"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ app:layout_heightPercent="40%"
+ app:layout_aspectRatio="5%"
+ app:layout_marginRightPercent="5%"
+ app:layout_marginBottomPercent="20%"
+ android:background="#20C0A0" />
+
+ <!-- This child is anchored to be in the "center" of the parent by anchoring it with
+ percent-based margins relatively to its edge-aligned siblings. -->
+ <View
+ android:id="@+id/child_center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/child_top"
+ android:layout_toRightOf="@+id/child_left"
+ android:layout_toLeftOf="@+id/child_right"
+ android:layout_above="@+id/child_bottom"
+ app:layout_marginPercent="10%"
+ android:background="#F07020" />
+
+</android.support.percent.PercentRelativeLayout>
+
diff --git a/percent/tests/res/layout/percent_relative_layout_rtl.xml b/percent/tests/res/layout/percent_relative_layout_rtl.xml
new file mode 100644
index 0000000..2d0e813
--- /dev/null
+++ b/percent/tests/res/layout/percent_relative_layout_rtl.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!-- Test layout for testing RTL-aware attributes on PercentRelativeLayout. Note that
+ we do not need to use layout_marginStartPercent *and* layout_marginLeftPercent
+ on the same child view to make it work correctly on different platform versions,
+ while we do have to use layout_alignParentEnd *and* layout_alignParentRight (as
+ the core RelativeLayout needs the old attribute on older pre-v17 platform versions).
+-->
+<android.support.percent.PercentRelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- This child is anchored along the top edge of the parent, testing percent-based
+ start margin, top margin and width, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_top"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ app:layout_widthPercent="50%"
+ app:layout_aspectRatio="2000%"
+ app:layout_marginStartPercent="20%"
+ app:layout_marginTopPercent="5%"
+ android:background="#5690E0" />
+
+ <!-- This child is anchored along the start edge of the parent, testing percent-based
+ start margin, top margin and height, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_start"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentLeft="true"
+ app:layout_heightPercent="50%"
+ app:layout_aspectRatio="5%"
+ app:layout_marginStartPercent="5%"
+ app:layout_marginTopPercent="20%"
+ android:background="#902030" />
+
+ <!-- This child is anchored along the bottom edge of the parent, testing percent-based
+ end margin, bottom margin and width, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_bottom"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ app:layout_widthPercent="40%"
+ app:layout_aspectRatio="2000%"
+ app:layout_marginEndPercent="20%"
+ app:layout_marginBottomPercent="5%"
+ android:background="#A0B040" />
+
+ <!-- This child is anchored along the end edge of the parent, testing percent-based
+ end margin, bottom margin and height, as well as the aspect ratio. -->
+ <View
+ android:id="@+id/child_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ app:layout_heightPercent="40%"
+ app:layout_aspectRatio="5%"
+ app:layout_marginEndPercent="5%"
+ app:layout_marginBottomPercent="20%"
+ android:background="#20C0A0" />
+
+ <!-- This child is anchored to be in the "center" of the parent by anchoring it with
+ percent-based margins relatively to its edge-aligned siblings. -->
+ <View
+ android:id="@+id/child_center"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_below="@+id/child_top"
+ android:layout_toEndOf="@+id/child_start"
+ android:layout_toRightOf="@+id/child_start"
+ android:layout_toStartOf="@+id/child_end"
+ android:layout_toLeftOf="@+id/child_end"
+ android:layout_above="@+id/child_bottom"
+ app:layout_marginPercent="10%"
+ android:background="#F07020" />
+
+</android.support.percent.PercentRelativeLayout>
+
diff --git a/v17/leanback/api/current.txt b/v17/leanback/api/current.txt
index 0df488f..d90f9f6 100644
--- a/v17/leanback/api/current.txt
+++ b/v17/leanback/api/current.txt
@@ -1030,9 +1030,12 @@
method public android.content.Intent getIntent();
method public java.util.List<android.support.v17.leanback.widget.GuidedAction> getSubActions();
method public java.lang.CharSequence getTitle();
+ method public boolean hasAnyEditable();
+ method public boolean hasEditableActivatorView();
method public boolean hasMultilineDescription();
method public boolean hasNext();
method public boolean hasSubActions();
+ method public boolean hasTextEditable();
method public boolean infoOnly();
method public boolean isChecked();
method public boolean isDescriptionEditable();
@@ -1071,36 +1074,36 @@
public static abstract class GuidedAction.BuilderBase {
ctor public GuidedAction.BuilderBase(android.content.Context);
method protected final void applyValues(android.support.v17.leanback.widget.GuidedAction);
- method public abstract T build();
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> checkSetId(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> checked(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> clickAction(long);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> description(java.lang.CharSequence);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> description(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> descriptionEditInputType(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> descriptionEditable(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> descriptionInputType(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editDescription(java.lang.CharSequence);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editDescription(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editInputType(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editTitle(java.lang.CharSequence);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editTitle(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> editable(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> enabled(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> focusable(boolean);
+ method public B checkSetId(int);
+ method public B checked(boolean);
+ method public B clickAction(long);
+ method public B description(java.lang.CharSequence);
+ method public B description(int);
+ method public B descriptionEditInputType(int);
+ method public B descriptionEditable(boolean);
+ method public B descriptionInputType(int);
+ method public B editDescription(java.lang.CharSequence);
+ method public B editDescription(int);
+ method public B editInputType(int);
+ method public B editTitle(java.lang.CharSequence);
+ method public B editTitle(int);
+ method public B editable(boolean);
+ method public B enabled(boolean);
+ method public B focusable(boolean);
method public android.content.Context getContext();
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> hasNext(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> icon(android.graphics.drawable.Drawable);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> icon(int);
- method public deprecated android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> iconResourceId(int, android.content.Context);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> id(long);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> infoOnly(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> inputType(int);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> intent(android.content.Intent);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> multilineDescription(boolean);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> subActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> title(java.lang.CharSequence);
- method public android.support.v17.leanback.widget.GuidedAction.BuilderBase<T> title(int);
+ method public B hasEditableActivatorView(boolean);
+ method public B hasNext(boolean);
+ method public B icon(android.graphics.drawable.Drawable);
+ method public B icon(int);
+ method public deprecated B iconResourceId(int, android.content.Context);
+ method public B id(long);
+ method public B infoOnly(boolean);
+ method public B inputType(int);
+ method public B intent(android.content.Intent);
+ method public B multilineDescription(boolean);
+ method public B subActions(java.util.List<android.support.v17.leanback.widget.GuidedAction>);
+ method public B title(java.lang.CharSequence);
+ method public B title(int);
}
public class GuidedActionEditText extends android.widget.EditText implements android.support.v17.leanback.widget.ImeKeyMonitor {
@@ -1124,6 +1127,7 @@
method public void onAnimateItemFocused(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
method public void onAnimateItemPressed(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, boolean);
method public void onAnimateItemPressedCancelled(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ method public void onBindActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
method public void onBindCheckMarkView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
method public void onBindChevronView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
method public void onBindViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
@@ -1137,12 +1141,14 @@
method public int onProvideItemLayoutId();
method public int onProvideItemLayoutId(int);
method public int onProvideLayoutId();
+ method public boolean onUpdateActivatorView(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
method public void onUpdateExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
method public void setAsButtonActions();
method public void setEditingMode(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction, boolean);
method public void setExpandedViewHolder(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
method protected void setupImeOptions(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder, android.support.v17.leanback.widget.GuidedAction);
method public void startExpandedTransition(android.support.v17.leanback.widget.GuidedActionsStylist.ViewHolder);
+ field public static final int VIEW_TYPE_DATE_PICKER = 1; // 0x1
field public static final int VIEW_TYPE_DEFAULT = 0; // 0x0
}
@@ -1160,10 +1166,32 @@
method public android.widget.ImageView getIconView();
method public android.widget.TextView getTitleView();
method public boolean isInEditing();
+ method public boolean isInEditingActivatorView();
method public boolean isInEditingDescription();
+ method public boolean isInEditingText();
+ method public boolean isInEditingTitle();
method public boolean isSubAction();
}
+ public class GuidedDatePickerAction extends android.support.v17.leanback.widget.GuidedAction {
+ ctor public GuidedDatePickerAction();
+ method public long getDate();
+ method public java.lang.String getDatePickerFormat();
+ method public void setDate(long);
+ }
+
+ public static final class GuidedDatePickerAction.Builder extends android.support.v17.leanback.widget.GuidedDatePickerAction.BuilderBase {
+ ctor public GuidedDatePickerAction.Builder(android.content.Context);
+ method public android.support.v17.leanback.widget.GuidedDatePickerAction build();
+ }
+
+ public static abstract class GuidedDatePickerAction.BuilderBase extends android.support.v17.leanback.widget.GuidedAction.BuilderBase {
+ ctor public GuidedDatePickerAction.BuilderBase(android.content.Context);
+ method protected final void applyDatePickerValues(android.support.v17.leanback.widget.GuidedDatePickerAction);
+ method public B date(long);
+ method public B datePickerFormat(java.lang.String);
+ }
+
public class HeaderItem {
ctor public HeaderItem(long, java.lang.String);
ctor public HeaderItem(java.lang.String);
@@ -1945,67 +1973,47 @@
package android.support.v17.leanback.widget.picker {
- public class DatePicker extends android.support.v17.leanback.widget.picker.Picker {
- ctor public DatePicker(android.content.Context, android.util.AttributeSet, int);
- method public java.lang.String getDatePickerFormat();
- method public java.util.Calendar getMaxDate();
- method public java.util.Calendar getMinDate();
- method public void setDatePickerFormat(java.lang.String);
- method public void setMaxDate(long);
- method public void setMinDate(long);
- method public void updateDate(int, int, int, boolean);
- }
-
public class Picker extends android.widget.FrameLayout {
ctor public Picker(android.content.Context, android.util.AttributeSet, int);
- method public void addPickerValueListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
- method public int getActiveColumn();
+ method public void addOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
+ method public float getActivatedVisibleItemCount();
method public android.support.v17.leanback.widget.picker.PickerColumn getColumnAt(int);
method public int getColumnsCount();
- method protected int getPickerId();
method protected int getPickerItemHeightPixels();
- method protected int getPickerItemLayoutId();
- method protected int getPickerItemTextViewId();
- method protected int getPickerSeparatorLayoutId();
- method protected int getRootLayoutId();
- method protected java.lang.String getSeparator();
- method public float getVisiblePickerItems();
- method public float getVisiblePickerItemsInExpand();
- method public boolean isExpanded();
- method public boolean isToggleExpandOnClick();
- method public void onColumnValueChange(int, int);
- method public void removePickerValueListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
- method public void setActiveColumn(int);
- method public void setColumns(java.util.ArrayList<android.support.v17.leanback.widget.picker.PickerColumn>);
- method public void setExpanded(boolean);
- method public void setToggleExpandOnClick(boolean);
- method public void setVisiblePickerItems(float);
- method public void setVisiblePickerItemsInExpand(float);
- method public void updateAdapter(int);
- method public void updateValue(int, int, boolean);
+ method public final int getPickerItemLayoutId();
+ method public final int getPickerItemTextViewId();
+ method public int getSelectedColumn();
+ method public final java.lang.CharSequence getSeparator();
+ method public float getVisibleItemCount();
+ method public void onColumnValueChanged(int, int);
+ method public void removeOnValueChangedListener(android.support.v17.leanback.widget.picker.Picker.PickerValueListener);
+ method public void setActivatedVisibleItemCount(float);
+ method public void setColumnAt(int, android.support.v17.leanback.widget.picker.PickerColumn);
+ method public void setColumnValue(int, int, boolean);
+ method public void setColumns(java.util.List<android.support.v17.leanback.widget.picker.PickerColumn>);
+ method public final void setPickerItemTextViewId(int);
+ method public void setSelectedColumn(int);
+ method public final void setSeparator(java.lang.CharSequence);
+ method public void setVisibleItemCount(float);
}
public static abstract interface Picker.PickerValueListener {
method public abstract void onValueChanged(android.support.v17.leanback.widget.picker.Picker, int);
}
- public class PickerColumn implements android.os.Parcelable {
+ public class PickerColumn {
ctor public PickerColumn();
- ctor public PickerColumn(android.os.Parcel);
- method public int describeContents();
method public int getCurrentValue();
- method public int getItemsCount();
+ method public java.lang.CharSequence getEntryAt(int);
+ method public java.lang.String getEntryFormat();
+ method public int getItemCount();
method public int getMaxValue();
method public int getMinValue();
- method public java.lang.String getValueLabelAt(int);
- method public java.lang.String getValueLabelFormat();
- method public boolean setCurrentValue(int);
- method public boolean setMaxValue(int);
- method public boolean setMinValue(int);
- method public void setValueLabelFormat(java.lang.String);
- method public void setValueStaticLabels(java.lang.String[]);
- method public void writeToParcel(android.os.Parcel, int);
- field public static android.os.Parcelable.Creator<android.support.v17.leanback.widget.picker.PickerColumn> CREATOR;
+ method public void setCurrentValue(int);
+ method public void setEntries(java.lang.CharSequence[]);
+ method public void setEntryFormat(java.lang.String);
+ method public void setMaxValue(int);
+ method public void setMinValue(int);
}
}
diff --git a/v17/leanback/res/layout/lb_guidance.xml b/v17/leanback/res/layout/lb_guidance.xml
index 28c0220..59f8ac5 100644
--- a/v17/leanback/res/layout/lb_guidance.xml
+++ b/v17/leanback/res/layout/lb_guidance.xml
@@ -20,6 +20,7 @@
android:layout_height="match_parent" >
<RelativeLayout
+ android:id="@+id/guidance_container"
style="?attr/guidanceContainerStyle" >
<ImageView
diff --git a/v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml b/v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml
new file mode 100644
index 0000000..98278d9
--- /dev/null
+++ b/v17/leanback/res/layout/lb_guidedactions_datepicker_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 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.
+-->
+<!-- Layout for an action item displayed in the 2 pane actions fragment. -->
+<android.support.v17.leanback.widget.NonOverlappingLinearLayoutWithForeground
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/guidedActionItemContainerStyle" >
+
+ <ImageView
+ android:id="@+id/guidedactions_item_icon"
+ style="?attr/guidedActionItemIconStyle"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/guidedactions_item_title"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical"
+ style="?attr/guidedActionItemTitleStyle" />
+
+ <android.support.v17.leanback.widget.picker.DatePicker
+ android:id="@+id/guidedactions_activator_item"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+</android.support.v17.leanback.widget.NonOverlappingLinearLayoutWithForeground>
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index 1f7f3dd..6d7c74b 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -384,6 +384,7 @@
<!-- Style for the title view in a GuidanceStylist's default layout. -->
<style name="Widget.Leanback.GuidanceTitleStyle">
+ <item name="android:importantForAccessibility">no</item>
<item name="android:layout_toStartOf">@id/guidance_icon</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
@@ -402,6 +403,7 @@
<!-- Style for the description view in a GuidanceStylist's default layout. -->
<style name="Widget.Leanback.GuidanceDescriptionStyle">
+ <item name="android:importantForAccessibility">no</item>
<item name="android:layout_below">@id/guidance_title</item>
<item name="android:layout_toStartOf">@id/guidance_icon</item>
<item name="android:layout_width">wrap_content</item>
@@ -419,6 +421,7 @@
<!-- Style for the breadcrumb view in a GuidanceStylist's default layout. -->
<style name="Widget.Leanback.GuidanceBreadcrumbStyle">
+ <item name="android:importantForAccessibility">no</item>
<item name="android:layout_above">@id/guidance_title</item>
<item name="android:layout_toStartOf">@id/guidance_icon</item>
<item name="android:layout_width">wrap_content</item>
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
index 28103dc..515946c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -1032,6 +1032,7 @@
mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
mAdapterGroup.addAdpter(mSubAdapter, null);
mAdapterGroup.setEditListener(editListener);
+ mActionsStylist.setEditListener(editListener);
mActionsStylist.getActionsGridView().setAdapter(mAdapter);
if (mActionsStylist.getSubActionsGridView() != null) {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
index 94c946f..bedd570 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepSupportFragment.java
@@ -1034,6 +1034,7 @@
mAdapterGroup.addAdpter(mAdapter, mButtonAdapter);
mAdapterGroup.addAdpter(mSubAdapter, null);
mAdapterGroup.setEditListener(editListener);
+ mActionsStylist.setEditListener(editListener);
mActionsStylist.getActionsGridView().setAdapter(mAdapter);
if (mActionsStylist.getSubActionsGridView() != null) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
index 3fcdbba..7284a53 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
@@ -20,6 +20,7 @@
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.v17.leanback.R;
+import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
@@ -154,6 +155,7 @@
mBreadcrumbView = (TextView) guidanceView.findViewById(R.id.guidance_breadcrumb);
mDescriptionView = (TextView) guidanceView.findViewById(R.id.guidance_description);
mIconView = (ImageView) guidanceView.findViewById(R.id.guidance_icon);
+ View guidanceContainer = guidanceView.findViewById(R.id.guidance_container);
// We allow any of the cached subviews to be null, so that subclasses can choose not to
// display a particular piece of information.
@@ -169,6 +171,16 @@
if (mIconView != null) {
mIconView.setImageDrawable(guidance.getIconDrawable());
}
+ if (guidanceContainer != null) {
+ CharSequence contentDescription = guidanceContainer.getContentDescription();
+ if (TextUtils.isEmpty(contentDescription)) {
+ guidanceContainer.setContentDescription(new StringBuilder()
+ .append(guidance.getBreadcrumb()).append('\n')
+ .append(guidance.getTitle()).append('\n')
+ .append(guidance.getDescription())
+ .toString());
+ }
+ }
return guidanceView;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
index c5bf7a5..977c201 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
@@ -38,7 +38,7 @@
* user input on selection, in which case they will be displayed with a chevron indicator.
* <p>
* GuidedAction recommends to use {@link Builder}. When application subclass GuidedAction, it
- * can subclass {@link BuilderBase}, implement {@link BuilderBase#build()} where it should
+ * can subclass {@link BuilderBase}, implement its own builder() method where it should
* call {@link BuilderBase#applyValues(GuidedAction)}.
*/
public class GuidedAction extends Action {
@@ -97,12 +97,17 @@
*/
public static final long ACTION_ID_NO = -9;
+ static final int EDITING_NONE = 0;
+ static final int EDITING_TITLE = 1;
+ static final int EDITING_DESCRIPTION = 2;
+ static final int EDITING_ACTIVATOR_VIEW = 3;
+
/**
* Base builder class to build a {@link GuidedAction} object. When subclass GuidedAction, you
- * can override this BuilderBase class, implements {@link #build()} and call
+ * can override this BuilderBase class, implements your build() method which should call
* {@link #applyValues(GuidedAction)}. When using GuidedAction directly, use {@link Builder}.
*/
- public abstract static class BuilderBase<T extends GuidedAction> {
+ public abstract static class BuilderBase<B extends BuilderBase> {
private Context mContext;
private long mId;
private CharSequence mTitle;
@@ -114,8 +119,7 @@
private boolean mMultilineDescription;
private boolean mHasNext;
private boolean mInfoOnly;
- private boolean mEditable = false;
- private boolean mDescriptionEditable = false;
+ private int mEditable = EDITING_NONE;
private int mInputType = InputType.TYPE_CLASS_TEXT;
private int mDescriptionInputType = InputType.TYPE_CLASS_TEXT;
private int mEditInputType = InputType.TYPE_CLASS_TEXT;
@@ -143,12 +147,6 @@
}
/**
- * Builds the GuidedAction corresponding to this Builder.
- * @return the GuidedAction as configured through this BuilderBase.
- */
- public abstract T build();
-
- /**
* Subclass of BuilderBase should call this function to apply values.
* @param action GuidedAction to apply BuilderBase values.
*/
@@ -164,7 +162,6 @@
// Subclass values
action.mIntent = mIntent;
action.mEditable = mEditable;
- action.mDescriptionEditable = mDescriptionEditable;
action.mInputType = mInputType;
action.mDescriptionInputType = mDescriptionInputType;
action.mEditInputType = mEditInputType;
@@ -187,7 +184,7 @@
* {@link GuidedAction#ACTION_ID_YES} {@link GuidedAction#ACTION_ID_NO}.
* @return The same BuilderBase object.
*/
- public BuilderBase<T> clickAction(long id) {
+ public B clickAction(long id) {
if (id == ACTION_ID_OK) {
mId = ACTION_ID_OK;
mTitle = mContext.getString(android.R.string.ok);
@@ -207,7 +204,7 @@
mId = ACTION_ID_NO;
mTitle = mContext.getString(android.R.string.no);
}
- return this;
+ return (B) this;
}
/**
@@ -215,9 +212,9 @@
* it is typically used to determine what to do when an action is clicked.
* @param id The ID to associate with this action.
*/
- public BuilderBase<T> id(long id) {
+ public B id(long id) {
mId = id;
- return this;
+ return (B) this;
}
/**
@@ -225,9 +222,9 @@
* action to be taken on click, e.g. "Continue" or "Cancel".
* @param title The title for this action.
*/
- public BuilderBase<T> title(CharSequence title) {
+ public B title(CharSequence title) {
mTitle = title;
- return this;
+ return (B) this;
}
/**
@@ -235,9 +232,9 @@
* action to be taken on click, e.g. "Continue" or "Cancel".
* @param titleResourceId The resource id of title for this action.
*/
- public BuilderBase<T> title(@StringRes int titleResourceId) {
+ public B title(@StringRes int titleResourceId) {
mTitle = getContext().getString(titleResourceId);
- return this;
+ return (B) this;
}
/**
@@ -245,9 +242,9 @@
* replaces the string of title.
* @param editTitle The optional title text to edit when TextView is activated.
*/
- public BuilderBase<T> editTitle(CharSequence editTitle) {
+ public B editTitle(CharSequence editTitle) {
mEditTitle = editTitle;
- return this;
+ return (B) this;
}
/**
@@ -256,9 +253,9 @@
* @param editTitleResourceId String resource id of the optional title text to edit when
* TextView is activated.
*/
- public BuilderBase<T> editTitle(@StringRes int editTitleResourceId) {
+ public B editTitle(@StringRes int editTitleResourceId) {
mEditTitle = getContext().getString(editTitleResourceId);
- return this;
+ return (B) this;
}
/**
@@ -266,9 +263,9 @@
* providing extra information on what the action will do.
* @param description The description for this action.
*/
- public BuilderBase<T> description(CharSequence description) {
+ public B description(CharSequence description) {
mDescription = description;
- return this;
+ return (B) this;
}
/**
@@ -276,9 +273,9 @@
* providing extra information on what the action will do.
* @param descriptionResourceId String resource id of the description for this action.
*/
- public BuilderBase<T> description(@StringRes int descriptionResourceId) {
+ public B description(@StringRes int descriptionResourceId) {
mDescription = getContext().getString(descriptionResourceId);
- return this;
+ return (B) this;
}
/**
@@ -286,9 +283,9 @@
* description replaces the string of description.
* @param description The description to edit for this action.
*/
- public BuilderBase<T> editDescription(CharSequence description) {
+ public B editDescription(CharSequence description) {
mEditDescription = description;
- return this;
+ return (B) this;
}
/**
@@ -297,9 +294,9 @@
* @param descriptionResourceId String resource id of the description to edit for this
* action.
*/
- public BuilderBase<T> editDescription(@StringRes int descriptionResourceId) {
+ public B editDescription(@StringRes int descriptionResourceId) {
mEditDescription = getContext().getString(descriptionResourceId);
- return this;
+ return (B) this;
}
/**
@@ -307,18 +304,18 @@
* directly when the action is clicked.
* @param intent The intent associated with this action.
*/
- public BuilderBase<T> intent(Intent intent) {
+ public B intent(Intent intent) {
mIntent = intent;
- return this;
+ return (B) this;
}
/**
* Sets the action's icon drawable.
* @param icon The drawable for the icon associated with this action.
*/
- public BuilderBase<T> icon(Drawable icon) {
+ public B icon(Drawable icon) {
mIcon = icon;
- return this;
+ return (B) this;
}
/**
@@ -330,7 +327,7 @@
* @deprecated Use {@link #icon(int)}.
*/
@Deprecated
- public BuilderBase<T> iconResourceId(@DrawableRes int iconResourceId, Context context) {
+ public B iconResourceId(@DrawableRes int iconResourceId, Context context) {
return icon(ContextCompat.getDrawable(context, iconResourceId));
}
@@ -340,7 +337,7 @@
* {@link #icon(Drawable)}.
* @param iconResourceId The resource ID for the icon associated with this action.
*/
- public BuilderBase<T> icon(@DrawableRes int iconResourceId) {
+ public B icon(@DrawableRes int iconResourceId) {
return icon(ContextCompat.getDrawable(getContext(), iconResourceId));
}
@@ -349,24 +346,48 @@
* checked, or belong to a check set.
* @param editable Whether this action is editable.
*/
- public BuilderBase<T> editable(boolean editable) {
- mEditable = editable;
+ public B editable(boolean editable) {
+ if (!editable && mEditable == EDITING_TITLE) {
+ mEditable = EDITING_NONE;
+ return (B) this;
+ }
+ mEditable = EDITING_TITLE;
if (mChecked || mCheckSetId != NO_CHECK_SET) {
throw new IllegalArgumentException("Editable actions cannot also be checked");
}
- return this;
+ return (B) this;
}
/**
* Indicates whether this action's description is editable
* @param editable Whether this action description is editable.
*/
- public BuilderBase<T> descriptionEditable(boolean editable) {
- mDescriptionEditable = editable;
+ public B descriptionEditable(boolean editable) {
+ if (!editable && mEditable == EDITING_DESCRIPTION) {
+ mEditable = EDITING_NONE;
+ return (B) this;
+ }
+ mEditable = EDITING_DESCRIPTION;
if (mChecked || mCheckSetId != NO_CHECK_SET) {
throw new IllegalArgumentException("Editable actions cannot also be checked");
}
- return this;
+ return (B) this;
+ }
+
+ /**
+ * Indicates whether this action has a view can be activated to edit, e.g. a DatePicker.
+ * @param editable Whether this action has view can be activated to edit.
+ */
+ public B hasEditableActivatorView(boolean editable) {
+ if (!editable && mEditable == EDITING_ACTIVATOR_VIEW) {
+ mEditable = EDITING_NONE;
+ return (B) this;
+ }
+ mEditable = EDITING_ACTIVATOR_VIEW;
+ if (mChecked || mCheckSetId != NO_CHECK_SET) {
+ throw new IllegalArgumentException("Editable actions cannot also be checked");
+ }
+ return (B) this;
}
/**
@@ -374,9 +395,9 @@
*
* @param inputType InputType for the action title not in editing.
*/
- public BuilderBase<T> inputType(int inputType) {
+ public B inputType(int inputType) {
mInputType = inputType;
- return this;
+ return (B) this;
}
/**
@@ -384,9 +405,9 @@
*
* @param inputType InputType for the action description not in editing.
*/
- public BuilderBase<T> descriptionInputType(int inputType) {
+ public B descriptionInputType(int inputType) {
mDescriptionInputType = inputType;
- return this;
+ return (B) this;
}
@@ -395,9 +416,9 @@
*
* @param inputType InputType for the action title in editing.
*/
- public BuilderBase<T> editInputType(int inputType) {
+ public B editInputType(int inputType) {
mEditInputType = inputType;
- return this;
+ return (B) this;
}
/**
@@ -405,9 +426,9 @@
*
* @param inputType InputType for the action description in editing.
*/
- public BuilderBase<T> descriptionEditInputType(int inputType) {
+ public B descriptionEditInputType(int inputType) {
mDescriptionEditInputType = inputType;
- return this;
+ return (B) this;
}
@@ -415,12 +436,12 @@
* Indicates whether this action is initially checked.
* @param checked Whether this action is checked.
*/
- public BuilderBase<T> checked(boolean checked) {
+ public B checked(boolean checked) {
mChecked = checked;
- if (mEditable || mDescriptionEditable) {
+ if (mEditable != EDITING_NONE) {
throw new IllegalArgumentException("Editable actions cannot also be checked");
}
- return this;
+ return (B) this;
}
/**
@@ -430,12 +451,12 @@
* @param checkSetId The check set ID, or {@link GuidedAction#NO_CHECK_SET} to indicate not
* radio or checkbox, or {@link GuidedAction#CHECKBOX_CHECK_SET_ID} to indicate a checkbox.
*/
- public BuilderBase<T> checkSetId(int checkSetId) {
+ public B checkSetId(int checkSetId) {
mCheckSetId = checkSetId;
- if (mEditable || mDescriptionEditable) {
+ if (mEditable != EDITING_NONE) {
throw new IllegalArgumentException("Editable actions cannot also be in check sets");
}
- return this;
+ return (B) this;
}
/**
@@ -443,36 +464,36 @@
* appropriately.
* @param multilineDescription Whether this action has a multiline description.
*/
- public BuilderBase<T> multilineDescription(boolean multilineDescription) {
+ public B multilineDescription(boolean multilineDescription) {
mMultilineDescription = multilineDescription;
- return this;
+ return (B) this;
}
/**
* Indicates whether this action has a next state and should display a chevron.
* @param hasNext Whether this action has a next state.
*/
- public BuilderBase<T> hasNext(boolean hasNext) {
+ public B hasNext(boolean hasNext) {
mHasNext = hasNext;
- return this;
+ return (B) this;
}
/**
* Indicates whether this action is for information purposes only and cannot be clicked.
* @param infoOnly Whether this action has a next state.
*/
- public BuilderBase<T> infoOnly(boolean infoOnly) {
+ public B infoOnly(boolean infoOnly) {
mInfoOnly = infoOnly;
- return this;
+ return (B) this;
}
/**
* Indicates whether this action is enabled. If not enabled, an action cannot be clicked.
* @param enabled Whether the action is enabled.
*/
- public BuilderBase<T> enabled(boolean enabled) {
+ public B enabled(boolean enabled) {
mEnabled = enabled;
- return this;
+ return (B) this;
}
/**
@@ -480,9 +501,9 @@
* @param focusable
* @return The same BuilderBase object.
*/
- public BuilderBase<T> focusable(boolean focusable) {
+ public B focusable(boolean focusable) {
mFocusable = focusable;
- return this;
+ return (B) this;
}
/**
@@ -490,16 +511,16 @@
* @param subActions
* @return The same BuilderBase object.
*/
- public BuilderBase<T> subActions(List<GuidedAction> subActions) {
+ public B subActions(List<GuidedAction> subActions) {
mSubActions = subActions;
- return this;
+ return (B) this;
}
}
/**
* Builds a {@link GuidedAction} object.
*/
- public static class Builder extends BuilderBase<GuidedAction> {
+ public static class Builder extends BuilderBase<Builder> {
/**
* @deprecated Use {@link GuidedAction.Builder#GuidedAction.Builder(Context)}.
@@ -517,7 +538,10 @@
super(context);
}
- @Override
+ /**
+ * Builds the GuidedAction corresponding to this Builder.
+ * @return The GuidedAction as configured through this Builder.
+ */
public GuidedAction build() {
GuidedAction action = new GuidedAction();
applyValues(action);
@@ -528,8 +552,7 @@
private CharSequence mEditTitle;
private CharSequence mEditDescription;
- private boolean mEditable;
- private boolean mDescriptionEditable;
+ private int mEditable;
private int mInputType;
private int mDescriptionInputType;
private int mEditInputType;
@@ -642,11 +665,20 @@
}
/**
+ * Returns whether this action has any editable part, e.g. editable title, editable description
+ * or editable activate view.
+ * @return true if this action has any editable part, false otherwise.
+ */
+ public boolean hasAnyEditable() {
+ return mEditable != EDITING_NONE;
+ }
+
+ /**
* Returns whether this action title is editable.
* @return true if the action title is editable, false otherwise.
*/
public boolean isEditable() {
- return mEditable;
+ return mEditable == EDITING_TITLE;
}
/**
@@ -654,7 +686,23 @@
* @return true if the action description is editable, false otherwise.
*/
public boolean isDescriptionEditable() {
- return mDescriptionEditable;
+ return mEditable == EDITING_DESCRIPTION;
+ }
+
+ /**
+ * Returns if this action has editable title or editable description.
+ * @return True if this action has editable title or editable description, false otherwise.
+ */
+ public boolean hasTextEditable() {
+ return mEditable == EDITING_TITLE || mEditable == EDITING_DESCRIPTION;
+ }
+
+ /**
+ * Returns whether this action can be activated to edit, e.g. a DatePicker.
+ * @return true if the action can be activated to edit.
+ */
+ public boolean hasEditableActivatorView() {
+ return mEditable == EDITING_ACTIVATOR_VIEW;
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
index e0fad0a..ee44656 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapter.java
@@ -108,9 +108,13 @@
GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
getRecyclerView().getChildViewHolder(v);
GuidedAction action = avh.getAction();
- if (action.isEditable() || action.isDescriptionEditable()) {
+ if (action.hasTextEditable()) {
if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
mGroup.openIme(GuidedActionAdapter.this, avh);
+ } else if (action.hasEditableActivatorView()) {
+ if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
+ getGuidedActionsStylist().setEditingMode(avh, avh.getAction(),
+ !avh.isInEditingActivatorView());
} else {
handleCheckedActions(avh);
if (action.isEnabled() && !action.infoOnly()) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
index 858a0ed..6fa3d89 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionAdapterGroup.java
@@ -95,7 +95,7 @@
(GuidedActionsStylist.ViewHolder) adapter.getGuidedActionsStylist()
.getActionsGridView().findViewHolderForPosition(index);
if (vh != null) {
- if (vh.getAction().isEditable() || vh.getAction().isDescriptionEditable()) {
+ if (vh.getAction().hasTextEditable()) {
if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme of next Action");
// open Ime on next action.
openIme(adapter, vh);
@@ -122,7 +122,7 @@
public void openIme(GuidedActionAdapter adapter, GuidedActionsStylist.ViewHolder avh) {
adapter.getGuidedActionsStylist().setEditingMode(avh, avh.getAction(), true);
View v = avh.getEditingView();
- if (v == null) {
+ if (v == null || !avh.isInEditingText()) {
return;
}
InputMethodManager mgr = (InputMethodManager)
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
index 2a95dfe..b39e2f5 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
@@ -30,7 +30,10 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.widget.GuidedActionAdapter.EditListener;
import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.picker.DatePicker;
+import android.support.v17.leanback.widget.picker.Picker;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
@@ -39,30 +42,40 @@
import android.util.Log;
import android.util.TypedValue;
import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewPropertyAnimator;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Checkable;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
+import java.util.Calendar;
import java.util.Collections;
import java.util.List;
+import static android.support.v17.leanback.widget.GuidedAction.EDITING_NONE;
+import static android.support.v17.leanback.widget.GuidedAction.EDITING_TITLE;
+import static android.support.v17.leanback.widget.GuidedAction.EDITING_DESCRIPTION;
+import static android.support.v17.leanback.widget.GuidedAction.EDITING_ACTIVATOR_VIEW;
+
/**
* GuidedActionsStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
* to supply the right-side panel where users can take actions. It consists of a container for the
* list of actions, and a stationary selector view that indicates visually the location of focus.
* GuidedActionsStylist has two different layouts: default is for normal actions including text,
- * radio, checkbox etc, the other when {@link #setAsButtonActions()} is called is recommended for
- * button actions such as "yes", "no".
+ * radio, checkbox, DatePicker, etc, the other when {@link #setAsButtonActions()} is called is
+ * recommended for button actions such as "yes", "no".
* <p>
* Many aspects of the base GuidedActionsStylist can be customized through theming; see the
* theme attributes below. Note that these attributes are not set on individual elements in layout
@@ -72,8 +85,16 @@
* <p>
* If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
* override the {@link #onProvideLayoutId} method to change the layout used to display the
- * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
- * used to display each action.
+ * list container and selector; override {@link #onProvideItemLayoutId(int)} and
+ * {@link #getItemViewType(GuidedAction)} method to change the layout used to display each action.
+ * <p>
+ * To support a "click to activate" view similar to DatePicker, app needs:
+ * <li> Override {@link #onProvideItemLayoutId(int)} and {@link #getItemViewType(GuidedAction)},
+ * provides a layout id for the action.
+ * <li> The layout must include a widget with id "guidedactions_activator_item", the widget is
+ * toggled edit mode by {@link View#setActivated(boolean)}.
+ * <li> Override {@link #onBindActivatorView(ViewHolder, GuidedAction)} to populate values into View.
+ * <li> Override {@link #onUpdateActivatorView(ViewHolder, GuidedAction)} to update action.
* <p>
* Note: If an alternate list layout is provided, the following view IDs must be supplied:
* <ul>
@@ -136,6 +157,11 @@
public static final int VIEW_TYPE_DEFAULT = 0;
/**
+ * ViewType for DatePicker.
+ */
+ public static final int VIEW_TYPE_DATE_PICKER = 1;
+
+ /**
* ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link
* GuidedActionsStylist} may also wish to subclass this in order to add fields.
* @see GuidedAction
@@ -146,13 +172,29 @@
private View mContentView;
private TextView mTitleView;
private TextView mDescriptionView;
+ private View mActivatorView;
private ImageView mIconView;
private ImageView mCheckmarkView;
private ImageView mChevronView;
- private boolean mInEditing;
- private boolean mInEditingDescription;
+ private int mEditingMode = EDITING_NONE;
private final boolean mIsSubAction;
+ final AccessibilityDelegate mDelegate = new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setChecked(mAction != null && mAction.isChecked());
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setCheckable(
+ mAction != null && mAction.getCheckSetId() != GuidedAction.NO_CHECK_SET);
+ info.setChecked(mAction != null && mAction.isChecked());
+ }
+ };
+
/**
* Constructs an ViewHolder and caches the relevant subviews.
*/
@@ -168,11 +210,14 @@
mContentView = v.findViewById(R.id.guidedactions_item_content);
mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title);
+ mActivatorView = v.findViewById(R.id.guidedactions_activator_item);
mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description);
mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon);
mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark);
mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron);
mIsSubAction = isSubAction;
+
+ v.setAccessibilityDelegate(mDelegate);
}
/**
@@ -235,26 +280,56 @@
}
/**
- * Returns true if the TextView is in editing title or description, false otherwise.
+ * Returns true if in editing title, description, or activator View, false otherwise.
*/
public boolean isInEditing() {
- return mInEditing;
+ return mEditingMode != EDITING_NONE;
+ }
+
+ /**
+ * Returns true if in editing title, description, so IME would be open.
+ * @return True if in editing title, description, so IME would be open, false otherwise.
+ */
+ public boolean isInEditingText() {
+ return mEditingMode == EDITING_TITLE || mEditingMode == EDITING_DESCRIPTION;
+ }
+
+ /**
+ * Returns true if the TextView is in editing title, false otherwise.
+ */
+ public boolean isInEditingTitle() {
+ return mEditingMode == EDITING_TITLE;
}
/**
* Returns true if the TextView is in editing description, false otherwise.
*/
public boolean isInEditingDescription() {
- return mInEditingDescription;
+ return mEditingMode == EDITING_DESCRIPTION;
}
/**
- * @return Current editing title view or description view or null if not in editing.
+ * Returns true if is in editing activator view with id guidedactions_activator_item, false
+ * otherwise.
+ */
+ public boolean isInEditingActivatorView() {
+ return mEditingMode == EDITING_ACTIVATOR_VIEW;
+ }
+
+ /**
+ * @return Current editing title view or description view or activator view or null if not
+ * in editing.
*/
public View getEditingView() {
- if (mInEditing) {
- return mInEditingDescription ? mDescriptionView : mTitleView;
- } else {
+ switch(mEditingMode) {
+ case EDITING_TITLE:
+ return mTitleView;
+ case EDITING_DESCRIPTION:
+ return mDescriptionView;
+ case EDITING_ACTIVATOR_VIEW:
+ return mActivatorView;
+ case EDITING_NONE:
+ default:
return null;
}
}
@@ -297,6 +372,8 @@
private int mVerticalPadding;
private int mDisplayHeight;
+ private EditListener mEditListener;
+
private GuidedAction mExpandedAction = null;
private Object mExpandTransition;
@@ -428,6 +505,9 @@
* @return View type that used in {@link #onProvideItemLayoutId(int)}.
*/
public int getItemViewType(GuidedAction action) {
+ if (action instanceof GuidedDatePickerAction) {
+ return VIEW_TYPE_DATE_PICKER;
+ }
return VIEW_TYPE_DEFAULT;
}
@@ -451,19 +531,24 @@
/**
* Provides the resource ID of the layout defining the view for an individual guided actions.
* Subclasses may override to provide their own customized layouts. The base implementation
- * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
- * the substituted layout should contain matching IDs for any views that should be managed by
- * the base class; this can be achieved by starting with a copy of the base layout file. Note
- * that in order for the item to support editing, the title view should both subclass {@link
- * android.widget.EditText} and implement {@link ImeKeyMonitor}; see {@link
- * GuidedActionEditText}.
+ * supports:
+ * <li>{@link android.support.v17.leanback.R.layout#lb_guidedactions_item}
+ * <li>{{@link android.support.v17.leanback.R.layout#lb_guidedactions_datepicker_item}. If
+ * overridden, the substituted layout should contain matching IDs for any views that should be
+ * managed by the base class; this can be achieved by starting with a copy of the base layout
+ * file. Note that in order for the item to support editing, the title view should both subclass
+ * {@link android.widget.EditText} and implement {@link ImeKeyMonitor}; see
+ * {@link GuidedActionEditText}.
+ *
* @param viewType View type returned by {@link #getItemViewType(GuidedAction)}
* @return The resource ID of the layout to be inflated to define the view to display an
- * individual GuidedAction.
+ * individual GuidedAction.
*/
public int onProvideItemLayoutId(int viewType) {
if (viewType == VIEW_TYPE_DEFAULT) {
return onProvideItemLayoutId();
+ } else if (viewType == VIEW_TYPE_DATE_PICKER) {
+ return R.layout.lb_guidedactions_datepicker_item;
} else {
throw new RuntimeException("ViewType " + viewType +
" not supported in GuidedActionsStylist");
@@ -549,6 +634,9 @@
vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
}
}
+ if (vh.mActivatorView != null) {
+ onBindActivatorView(vh, action);
+ }
setEditingMode(vh, action, false);
if (action.isFocusable()) {
vh.itemView.setFocusable(true);
@@ -580,8 +668,7 @@
}
public void setEditingMode(ViewHolder vh, GuidedAction action, boolean editing) {
- if (editing != vh.mInEditing) {
- vh.mInEditing = editing;
+ if (editing != vh.isInEditing() && !isInExpandTransition()) {
onEditingModeChange(vh, action, editing);
}
}
@@ -604,12 +691,15 @@
descriptionView.setVisibility(View.VISIBLE);
descriptionView.setInputType(action.getDescriptionEditInputType());
}
- vh.mInEditingDescription = true;
- } else {
- vh.mInEditingDescription = false;
+ vh.mEditingMode = EDITING_DESCRIPTION;
+ } else if (action.isEditable()){
if (titleView != null) {
titleView.setInputType(action.getEditInputType());
}
+ vh.mEditingMode = EDITING_TITLE;
+ } else if (vh.mActivatorView != null) {
+ onEditActivatorView(vh, action, editing);
+ vh.mEditingMode = EDITING_ACTIVATOR_VIEW;
}
} else {
if (titleView != null) {
@@ -618,18 +708,22 @@
if (descriptionView != null) {
descriptionView.setText(action.getDescription());
}
- if (vh.mInEditingDescription) {
+ if (vh.mEditingMode == EDITING_DESCRIPTION) {
if (descriptionView != null) {
descriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
View.GONE : View.VISIBLE);
descriptionView.setInputType(action.getDescriptionInputType());
}
- vh.mInEditingDescription = false;
- } else {
+ } else if (vh.mEditingMode == EDITING_TITLE) {
if (titleView != null) {
titleView.setInputType(action.getInputType());
}
+ } else if (vh.mEditingMode == EDITING_ACTIVATOR_VIEW) {
+ if (vh.mActivatorView != null) {
+ onEditActivatorView(vh, action, editing);
+ }
}
+ vh.mEditingMode = EDITING_NONE;
}
}
@@ -716,6 +810,76 @@
}
/**
+ * Performs binding activator view value to action. Default implementation supports
+ * GuidedDatePickerAction, subclass may override to add support of other views.
+ * @param vh ViewHolder of activator view.
+ * @param action GuidedAction to bind.
+ */
+ public void onBindActivatorView(ViewHolder vh, GuidedAction action) {
+ if (action instanceof GuidedDatePickerAction) {
+ GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
+ DatePicker dateView = (DatePicker) vh.mActivatorView;
+ dateView.setDatePickerFormat(dateAction.getDatePickerFormat());
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(((GuidedDatePickerAction) action).getDate());
+ dateView.updateDate(c.get(Calendar.YEAR), c.get(Calendar.MONTH),
+ c.get(Calendar.DAY_OF_MONTH), false);
+ }
+ }
+
+ /**
+ * Performs updating GuidedAction from activator view. Default implementation supports
+ * GuidedDatePickerAction, subclass may override to add support of other views.
+ * @param vh ViewHolder of activator view.
+ * @param action GuidedAction to update.
+ * @return True if value has been updated, false otherwise.
+ */
+ public boolean onUpdateActivatorView(ViewHolder vh, GuidedAction action) {
+ if (action instanceof GuidedDatePickerAction) {
+ GuidedDatePickerAction dateAction = (GuidedDatePickerAction) action;
+ DatePicker dateView = (DatePicker) vh.mActivatorView;
+ if (dateAction.getDate() != dateView.getDate()) {
+ dateAction.setDate(dateView.getDate());
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets listener for reporting view being edited.
+ * @hide
+ */
+ public void setEditListener(EditListener listener) {
+ mEditListener = listener;
+ }
+
+ void onEditActivatorView(final ViewHolder vh, final GuidedAction action,
+ boolean editing) {
+ if (editing) {
+ vh.mActivatorView.requestFocus();
+ setExpandedViewHolder(vh);
+ vh.mActivatorView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!isInExpandTransition()) {
+ setEditingMode(vh, action, false);
+ }
+ }
+ });
+ } else {
+ if (onUpdateActivatorView(vh, action)) {
+ if (mEditListener != null) {
+ mEditListener.onGuidedActionEdited(action);
+ }
+ }
+ vh.itemView.requestFocus();
+ setExpandedViewHolder(null);
+ vh.mActivatorView.setOnClickListener(null);
+ }
+ }
+
+ /**
* Sets states of chevron view, called by {@link #onBindViewHolder(ViewHolder, GuidedAction)}.
* Subclass may override.
*
@@ -750,7 +914,7 @@
* hide the other items in main list. When null, collapse the sub actions list.
*/
public void setExpandedViewHolder(ViewHolder avh) {
- if (mSubActionsGridView == null || isInExpandTransition()) {
+ if (isInExpandTransition()) {
return;
}
if (isExpandTransitionSupported()) {
@@ -804,12 +968,15 @@
onUpdateExpandedViewHolder(avh);
return;
}
+ boolean isSubActionTransition = focusAvh.getAction().hasSubActions();
Object set = TransitionHelper.createTransitionSet(false);
+ float slideDistance = isSubActionTransition ? focusAvh.itemView.getHeight() :
+ focusAvh.itemView.getHeight() * 0.5f;
Object slideAndFade = TransitionHelper.createFadeAndShortSlide(Gravity.TOP | Gravity.BOTTOM,
- (float) focusAvh.itemView.getHeight());
+ slideDistance);
Object changeFocusItemTransform = TransitionHelper.createChangeTransform();
Object changeFocusItemBounds = TransitionHelper.createChangeBounds(false);
- Object fadeGrid = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
+ Object fade = TransitionHelper.createFadeTransition(TransitionHelper.FADE_IN |
TransitionHelper.FADE_OUT);
Object changeGridBounds = TransitionHelper.createChangeBounds(false);
if (avh == null) {
@@ -817,7 +984,7 @@
TransitionHelper.setStartDelay(changeFocusItemTransform, 100);
TransitionHelper.setStartDelay(changeFocusItemBounds, 100);
} else {
- TransitionHelper.setStartDelay(fadeGrid, 100);
+ TransitionHelper.setStartDelay(fade, 100);
TransitionHelper.setStartDelay(changeGridBounds, 100);
TransitionHelper.setStartDelay(changeFocusItemTransform, 50);
TransitionHelper.setStartDelay(changeFocusItemBounds, 50);
@@ -827,19 +994,25 @@
.getChildViewHolder(mActionsGridView.getChildAt(i));
if (vh == focusAvh) {
// going to expand/collapse this one.
- TransitionHelper.include(changeFocusItemTransform, vh.itemView);
- TransitionHelper.include(changeFocusItemBounds, vh.itemView);
+ if (isSubActionTransition) {
+ TransitionHelper.include(changeFocusItemTransform, vh.itemView);
+ TransitionHelper.include(changeFocusItemBounds, vh.itemView);
+ }
} else {
// going to slide this item to top / bottom.
TransitionHelper.include(slideAndFade, vh.itemView);
+ TransitionHelper.exclude(fade, vh.itemView, true);
}
}
- TransitionHelper.include(fadeGrid, mSubActionsGridView);
TransitionHelper.include(changeGridBounds, mSubActionsGridView);
TransitionHelper.addTransition(set, slideAndFade);
- TransitionHelper.addTransition(set, changeFocusItemTransform);
- TransitionHelper.addTransition(set, changeFocusItemBounds);
- TransitionHelper.addTransition(set, fadeGrid);
+ // note that we don't run ChangeBounds for activating view due to the rounding problem
+ // of multiple level views ChangeBounds animation causing vertical jittering.
+ if (isSubActionTransition) {
+ TransitionHelper.addTransition(set, changeFocusItemTransform);
+ TransitionHelper.addTransition(set, changeFocusItemBounds);
+ }
+ TransitionHelper.addTransition(set, fade);
TransitionHelper.addTransition(set, changeGridBounds);
mExpandTransition = set;
TransitionHelper.addTransitionListener(mExpandTransition, new TransitionListener() {
@@ -917,7 +1090,7 @@
updateChevronAndVisibility(vh);
}
if (mSubActionsGridView != null) {
- if (avh != null) {
+ if (avh != null && avh.getAction().hasSubActions()) {
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
lp.topMargin = avh.itemView.getTop();
@@ -928,7 +1101,7 @@
mSubActionsGridView.setSelectedPosition(0);
((GuidedActionAdapter) mSubActionsGridView.getAdapter())
.setActions(avh.getAction().getSubActions());
- } else {
+ } else if (mSubActionsGridView.getVisibility() == View.VISIBLE) {
mSubActionsGridView.setVisibility(View.INVISIBLE);
ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) mSubActionsGridView.getLayoutParams();
@@ -946,9 +1119,17 @@
if (mExpandedAction == null) {
vh.itemView.setVisibility(View.VISIBLE);
vh.itemView.setTranslationY(0);
+ if (vh.mActivatorView != null) {
+ vh.mActivatorView.setActivated(false);
+ }
} else if (vh.getAction() == mExpandedAction) {
vh.itemView.setVisibility(View.VISIBLE);
- vh.itemView.setTranslationY(- vh.itemView.getHeight());
+ if (vh.getAction().hasSubActions()) {
+ vh.itemView.setTranslationY(- vh.itemView.getHeight());
+ } else if (vh.mActivatorView != null) {
+ vh.itemView.setTranslationY(0);
+ vh.mActivatorView.setActivated(true);
+ }
} else {
vh.itemView.setVisibility(View.INVISIBLE);
vh.itemView.setTranslationY(0);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
new file mode 100644
index 0000000..9daed2a
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedDatePickerAction.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2015 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.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.support.v17.leanback.widget.picker.DatePicker;
+
+import java.util.Calendar;
+
+/**
+ * Subclass of GuidedAction that can choose a date. The Action is editable by default; to make it
+ * read only, call hasEditableActivatorView(false) on the Builder.
+ */
+public class GuidedDatePickerAction extends GuidedAction {
+
+ /**
+ * Base Builder class to build GuidedDatePickerAction. Subclass this BuilderBase when app needs
+ * to subclass GuidedDatePickerAction, implement your build() which should call
+ * {@link #applyDatePickerValues(GuidedDatePickerAction)}. When using GuidedDatePickerAction
+ * directly, use {@link Builder}.
+ */
+ public abstract static class BuilderBase<B extends BuilderBase>
+ extends GuidedAction.BuilderBase<B> {
+
+ private String mDatePickerFormat;
+ private long mDate;
+
+ public BuilderBase(Context context) {
+ super(context);
+ Calendar c = Calendar.getInstance();
+ mDate = c.getTimeInMillis();
+ hasEditableActivatorView(true);
+ }
+
+ /**
+ * Sets format of date Picker. When the format is not specified,
+ * a default format of current locale will be used.
+ * @param format Format of showing Date, e.g. "YMD"
+ * @return This Builder object.
+ */
+ public B datePickerFormat(String format) {
+ mDatePickerFormat = format;
+ return (B) this;
+ }
+
+ /**
+ * Sets a Date for date picker, see {@link Calendar#getTimeInMillis()}.
+ * @param date See {@link Calendar#getTimeInMillis()}.
+ * @return This Builder Object.
+ */
+ public B date(long date) {
+ mDate = date;
+ return (B) this;
+ }
+
+ /**
+ * Apply values to GuidedDatePickerAction.
+ * @param action GuidedDatePickerAction to apply values.
+ */
+ protected final void applyDatePickerValues(GuidedDatePickerAction action) {
+ super.applyValues(action);
+ action.mDatePickerFormat = mDatePickerFormat;
+ action.mDate = mDate;
+ }
+
+ }
+
+ /**
+ * Builder class to build a GuidedDatePickerAction.
+ */
+ public final static class Builder extends BuilderBase<Builder> {
+ public Builder(Context context) {
+ super(context);
+ }
+
+ /**
+ * Builds the GuidedDatePickerAction corresponding to this Builder.
+ * @return The GuidedDatePickerAction as configured through this Builder.
+ */
+ public GuidedDatePickerAction build() {
+ GuidedDatePickerAction action = new GuidedDatePickerAction();
+ applyDatePickerValues(action);
+ return action;
+ }
+ }
+
+ private String mDatePickerFormat;
+ private long mDate;
+
+ /**
+ * Returns format of date Picker or null if not specified. When the
+ * format is not specified, a default format of current locale will be used.
+ * @return Format of showing Date, e.g. "YMD". Returns null if using current locale's default.
+ */
+ public String getDatePickerFormat() {
+ return mDatePickerFormat;
+ }
+
+ /**
+ * Get current value of DatePicker;
+ * @return Current value of DatePicker;
+ */
+ public long getDate() {
+ return mDate;
+ }
+
+ /**
+ * Sets current value of DatePicker;
+ * @param date New value to update current value of DatePicker;
+ */
+ public void setDate(long date) {
+ mDate = date;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
index 00c6f3c..209b7e6 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/DatePicker.java
@@ -39,8 +39,8 @@
* @attr ref R.styleable#lbDatePicker_android_maxDate
* @attr ref R.styleable#lbDatePicker_android_minDate
* @attr ref R.styleable#lbDatePicker_datePickerFormat
+ * @hide
*/
-
public class DatePicker extends Picker {
static final String LOG_TAG = "DatePicker";
@@ -62,23 +62,20 @@
Calendar mCurrentDate;
Calendar mTempDate;
+ public DatePicker(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
updateCurrentLocale();
+ setSeparator(mConstant.dateSeparator);
final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
R.styleable.lbDatePicker);
String minDate = attributesArray.getString(R.styleable.lbDatePicker_android_minDate);
String maxDate = attributesArray.getString(R.styleable.lbDatePicker_android_maxDate);
- String datePickerFormat = attributesArray
- .getString(R.styleable.lbDatePicker_datePickerFormat);
- if (TextUtils.isEmpty(datePickerFormat)) {
- datePickerFormat = new String(
- android.text.format.DateFormat.getDateFormatOrder(context));
- }
- setDatePickerFormat(datePickerFormat);
-
mTempDate.clear();
if (!TextUtils.isEmpty(minDate)) {
if (!parseDate(minDate, mTempDate)) {
@@ -87,7 +84,7 @@
} else {
mTempDate.set(1900, 0, 1);
}
- setMinDate(mTempDate.getTimeInMillis());
+ mMinDate.setTimeInMillis(mTempDate.getTimeInMillis());
mTempDate.clear();
if (!TextUtils.isEmpty(maxDate)) {
@@ -97,8 +94,15 @@
} else {
mTempDate.set(2100, 0, 1);
}
- setMaxDate(mTempDate.getTimeInMillis());
+ mMaxDate.setTimeInMillis(mTempDate.getTimeInMillis());
+ String datePickerFormat = attributesArray
+ .getString(R.styleable.lbDatePicker_datePickerFormat);
+ if (TextUtils.isEmpty(datePickerFormat)) {
+ datePickerFormat = new String(
+ android.text.format.DateFormat.getDateFormatOrder(context));
+ }
+ setDatePickerFormat(datePickerFormat);
}
private boolean parseDate(String date, Calendar outDate) {
@@ -112,10 +116,14 @@
}
/**
- * Changes format of showing dates, e.g. 'YMD'.
+ * Changes format of showing dates. For example "YMD".
* @param datePickerFormat Format of showing dates.
*/
public void setDatePickerFormat(String datePickerFormat) {
+ if (TextUtils.isEmpty(datePickerFormat)) {
+ datePickerFormat = new String(
+ android.text.format.DateFormat.getDateFormatOrder(getContext()));
+ }
datePickerFormat = datePickerFormat.toUpperCase();
if (TextUtils.equals(mDatePickerFormat, datePickerFormat)) {
return;
@@ -132,14 +140,14 @@
}
columns.add(mYearColumn = new PickerColumn());
mColYearIndex = i;
- mYearColumn.setValueLabelFormat("%d");
+ mYearColumn.setEntryFormat("%d");
break;
case 'M':
if (mMonthColumn != null) {
throw new IllegalArgumentException("datePicker format error");
}
columns.add(mMonthColumn = new PickerColumn());
- mMonthColumn.setValueStaticLabels(mConstant.months);
+ mMonthColumn.setEntries(mConstant.months);
mColMonthIndex = i;
break;
case 'D':
@@ -147,7 +155,7 @@
throw new IllegalArgumentException("datePicker format error");
}
columns.add(mDayColumn = new PickerColumn());
- mDayColumn.setValueLabelFormat("%02d");
+ mDayColumn.setEntryFormat("%02d");
mColDayIndex = i;
break;
default:
@@ -159,20 +167,14 @@
}
/**
- * Get format of showing dates, e.g. 'YMD'. Default value is from
- * {@link android.text.format.DateFormat#getDateFormatOrder}.
+ * Get format of showing dates. For example "YMD". Default value is from
+ * {@link android.text.format.DateFormat#getDateFormatOrder(Context)}.
* @return Format of showing dates.
*/
public String getDatePickerFormat() {
return mDatePickerFormat;
}
- @Override
- protected String getSeparator() {
- return mConstant.dateSeparator;
- }
-
-
private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
if (oldCalendar == null) {
return Calendar.getInstance(locale);
@@ -192,13 +194,13 @@
mCurrentDate = getCalendarForLocale(mCurrentDate, mConstant.locale);
if (mMonthColumn != null) {
- mMonthColumn.setValueStaticLabels(mConstant.months);
- updateAdapter(mColMonthIndex);
+ mMonthColumn.setEntries(mConstant.months);
+ setColumnAt(mColMonthIndex, mMonthColumn);
}
}
@Override
- public void onColumnValueChange(int column, int newVal) {
+ public final void onColumnValueChanged(int column, int newVal) {
mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
// take care of wrapping of days and months to update greater fields
int oldVal = getColumnAt(column).getCurrentValue();
@@ -248,10 +250,8 @@
*
* @return The minimal supported date.
*/
- public Calendar getMinDate() {
- final Calendar minDate = Calendar.getInstance();
- minDate.setTimeInMillis(mMinDate.getTimeInMillis());
- return minDate;
+ public long getMinDate() {
+ return mMinDate.getTimeInMillis();
}
/**
@@ -284,10 +284,18 @@
*
* @return The maximal supported date.
*/
- public Calendar getMaxDate() {
- final Calendar maxDate = Calendar.getInstance();
- maxDate.setTimeInMillis(mMaxDate.getTimeInMillis());
- return maxDate;
+ public long getMaxDate() {
+ return mMaxDate.getTimeInMillis();
+ }
+
+ /**
+ * Gets current date value in milliseconds since January 1, 1970 00:00:00 in
+ * {@link TimeZone#getDefault()} time zone.
+ *
+ * @return Current date values.
+ */
+ public long getDate() {
+ return mCurrentDate.getTimeInMillis();
}
private void setDate(int year, int month, int dayOfMonth) {
@@ -321,72 +329,88 @@
|| mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
}
+ private static boolean updateMin(PickerColumn column, int value) {
+ if (value != column.getMinValue()) {
+ column.setMinValue(value);
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean updateMax(PickerColumn column, int value) {
+ if (value != column.getMaxValue()) {
+ column.setMaxValue(value);
+ return true;
+ }
+ return false;
+ }
+
private void updateSpinners(boolean animation) {
// set the spinner ranges respecting the min and max dates
boolean dayRangeChanged = false;
boolean monthRangeChanged = false;
if (mCurrentDate.equals(mMinDate)) {
if (mDayColumn != null) {
- dayRangeChanged |= mDayColumn.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
- dayRangeChanged |= mDayColumn
- .setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |= updateMin(mDayColumn, mCurrentDate.get(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |=
+ updateMax(mDayColumn, mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
}
if (mMonthColumn != null) {
- monthRangeChanged |= mMonthColumn.setMinValue(mCurrentDate.get(Calendar.MONTH));
+ monthRangeChanged |= updateMin(mMonthColumn, mCurrentDate.get(Calendar.MONTH));
monthRangeChanged |=
- mMonthColumn.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
+ updateMax(mMonthColumn, mCurrentDate.getActualMaximum(Calendar.MONTH));
}
} else if (mCurrentDate.equals(mMaxDate)) {
if (mDayColumn != null) {
- dayRangeChanged |= mDayColumn
- .setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
- dayRangeChanged |= mDayColumn.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |=
+ updateMin(mDayColumn, mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |= updateMax(mDayColumn, mCurrentDate.get(Calendar.DAY_OF_MONTH));
}
if (mMonthColumn != null) {
monthRangeChanged |=
- mMonthColumn.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
- monthRangeChanged |= mMonthColumn.setMaxValue(mCurrentDate.get(Calendar.MONTH));
+ updateMin(mMonthColumn, mCurrentDate.getActualMinimum(Calendar.MONTH));
+ monthRangeChanged |= updateMax(mMonthColumn, mCurrentDate.get(Calendar.MONTH));
}
} else {
if (mDayColumn != null) {
- dayRangeChanged |= mDayColumn
- .setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
- dayRangeChanged |= mDayColumn
- .setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |=
+ updateMin(mDayColumn, mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
+ dayRangeChanged |=
+ updateMax(mDayColumn, mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
}
if (mMonthColumn != null) {
monthRangeChanged |=
- mMonthColumn.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
+ updateMin(mMonthColumn, mCurrentDate.getActualMinimum(Calendar.MONTH));
monthRangeChanged |=
- mMonthColumn.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
+ updateMax(mMonthColumn, mCurrentDate.getActualMaximum(Calendar.MONTH));
}
}
// year spinner range does not change based on the current date
boolean yearRangeChanged = false;
if (mYearColumn != null) {
- yearRangeChanged |= mYearColumn.setMinValue(mMinDate.get(Calendar.YEAR));
- yearRangeChanged |= mYearColumn.setMaxValue(mMaxDate.get(Calendar.YEAR));
+ yearRangeChanged |= updateMin(mYearColumn, mMinDate.get(Calendar.YEAR));
+ yearRangeChanged |= updateMax(mYearColumn, mMaxDate.get(Calendar.YEAR));
}
if (dayRangeChanged) {
- updateAdapter(mColDayIndex);
+ setColumnAt(mColDayIndex, mDayColumn);
}
if (monthRangeChanged) {
- updateAdapter(mColMonthIndex);
+ setColumnAt(mColMonthIndex, mMonthColumn);
}
if (yearRangeChanged) {
- updateAdapter(mColYearIndex);
+ setColumnAt(mColYearIndex, mYearColumn);
}
// set the spinner values
if (mYearColumn != null) {
- updateValue(mColYearIndex, mCurrentDate.get(Calendar.YEAR), animation);
+ setColumnValue(mColYearIndex, mCurrentDate.get(Calendar.YEAR), animation);
}
if (mMonthColumn != null) {
- updateValue(mColMonthIndex, mCurrentDate.get(Calendar.MONTH), animation);
+ setColumnValue(mColMonthIndex, mCurrentDate.get(Calendar.MONTH), animation);
}
if (mDayColumn != null) {
- updateValue(mColDayIndex, mCurrentDate.get(Calendar.DAY_OF_MONTH), animation);
+ setColumnValue(mColDayIndex, mCurrentDate.get(Calendar.DAY_OF_MONTH), animation);
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
index 1157a11..8e5bcdc 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/Picker.java
@@ -36,28 +36,19 @@
/**
* Picker is a widget showing multiple customized {@link PickerColumn}s. The PickerColumns are
- * initialized in {@link #setColumns(ArrayList)}. Call {@link #updateAdapter(int)} if the column
- * value range or labels change. Call {@link #updateValue(int, int, boolean)} to update the current
- * value of PickerColumn.
+ * initialized in {@link #setColumns(List)}. Call {@link #setColumnAt(int, PickerColumn)} if the
+ * column value range or labels change. Call {@link #setColumnValue(int, int, boolean)} to update
+ * the current value of PickerColumn.
* <p>
* Picker has two states and will change height:
- * <li>{@link #isExpanded()} is true: Picker shows typically three items vertically (see
- * {@link #getVisiblePickerItemsInExpand()}}. Columns other than {@link #getActiveColumn()} still
+ * <li>{@link #isActivated()} is true: Picker shows typically three items vertically (see
+ * {@link #getActivatedVisibleItemCount()}}. Columns other than {@link #getSelectedColumn()} still
* shows one item if the Picker is focused. On a touch screen device, the Picker will not get focus
* so it always show three items on all columns. On a non-touch device (a TV), the Picker will show
* three items only on currently activated column. If the Picker has focus, it will intercept DPAD
* directions and select activated column.
- * <li>{@link #isExpanded()} is false: Picker shows one item vertically (see
- * {@link #getVisiblePickerItems()}) on all columns. The size of Picker shrinks.
- * <li>The expand mode will be toggled if the Picker has focus and {@link #isToggleExpandOnClick()}
- * is true. Summarize Typically use cases:
- * <li>On a touch screen based device, the Picker focusableInTouchMode=false. It won't get focus, it
- * wont toggle expand mode on click or touch, should call {@link #setExpanded(boolean)} with true,
- * so that user always sees three items on all columns.
- * <li>On a TV: the Picker focusable=true. It will get focus and toggle into expand mode when user
- * clicks on it, toggle can be disabled by {@link #setToggleExpandOnClick(boolean)} with false. Only
- * the activated column shows multiple items and the activated column is selected by DPAD left or
- * right.
+ * <li>{@link #isActivated()} is false: Picker shows one item vertically (see
+ * {@link #getVisibleItemCount()}) on all columns. The size of Picker shrinks.
*/
public class Picker extends FrameLayout {
@@ -65,7 +56,6 @@
public void onValueChanged(Picker picker, int column);
}
- private String mSeparator;
private ViewGroup mRootView;
private ViewGroup mPickerView;
private List<VerticalGridView> mColumnViews = new ArrayList<VerticalGridView>();
@@ -79,62 +69,57 @@
private Interpolator mDecelerateInterpolator;
private Interpolator mAccelerateInterpolator;
private ArrayList<PickerValueListener> mListeners;
- private boolean mExpanded;
- private float mVisibleItemsInExpand = 3;
+ private float mVisibleItemsActivated = 3;
private float mVisibleItems = 1;
- private int mActivatedColumn = 0;
- private boolean mToggleExpandOnClick = true;
+ private int mSelectedColumn = 0;
+
+ private CharSequence mSeparator;
+ private int mPickerItemLayoutId = R.layout.lb_picker_item;
+ private int mPickerItemTextViewId = 0;
/**
- * Classes extending {@link Picker} can choose to override this method to
- * supply the separator string
+ * Gets separator string between columns.
*/
- protected String getSeparator() {
+ public final CharSequence getSeparator() {
return mSeparator;
}
/**
- * Classes extending {@link Picker} can choose to override this method to
- * supply the {@link Picker}'s root layout id
+ * Sets separator String between Picker columns.
+ * @param seperator Separator String between Picker columns.
*/
- protected int getRootLayoutId() {
- return R.layout.lb_picker;
- }
-
- /**
- * Classes extending {@link Picker} can choose to override this method to
- * supply the {@link Picker}'s id from within the layout provided by
- * {@link Picker#getRootLayoutId()}
- */
- protected int getPickerId() {
- return R.id.picker;
- }
-
- /**
- * Classes extending {@link Picker} can choose to override this method to
- * supply the {@link Picker}'s separator's layout id
- */
- protected int getPickerSeparatorLayoutId() {
- return R.layout.lb_picker_separator;
+ public final void setSeparator(CharSequence seperator) {
+ mSeparator = seperator;
}
/**
* Classes extending {@link Picker} can choose to override this method to
* supply the {@link Picker}'s item's layout id
*/
- protected int getPickerItemLayoutId() {
- return R.layout.lb_picker_item;
+ public final int getPickerItemLayoutId() {
+ return mPickerItemLayoutId;
}
/**
- * Classes extending {@link Picker} can choose to override this method to
- * supply the {@link Picker}'s item's {@link TextView}'s id from within the
+ * Returns the {@link Picker}'s item's {@link TextView}'s id from within the
* layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
* layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
* TextView}.
*/
- protected int getPickerItemTextViewId() {
- return 0;
+ public final int getPickerItemTextViewId() {
+ return mPickerItemTextViewId;
+ }
+
+ /**
+ * Sets the {@link Picker}'s item's {@link TextView}'s id from within the
+ * layout provided by {@link Picker#getPickerItemLayoutId()} or 0 if the
+ * layout provided by {@link Picker#getPickerItemLayoutId()} is a {link
+ * TextView}.
+ * @param textViewId View id of TextView inside a Picker item, or 0 if the Picker item is a
+ * TextView.
+ */
+ public final void setPickerItemTextViewId(int textViewId) {
+ mPickerItemTextViewId = textViewId;
}
/**
@@ -164,15 +149,15 @@
mAccelerateInterpolator = new AccelerateInterpolator(2.5F);
LayoutInflater inflater = LayoutInflater.from(getContext());
- mRootView = (ViewGroup) inflater.inflate(getRootLayoutId(), this, true);
- mPickerView = (ViewGroup) mRootView.findViewById(getPickerId());
+ mRootView = (ViewGroup) inflater.inflate(R.layout.lb_picker, this, true);
+ mPickerView = (ViewGroup) mRootView.findViewById(R.id.picker);
}
/**
* Get nth PickerColumn.
* @param colIndex Index of PickerColumn.
- * @return PickerColumn at colIndex or null if {@link #setColumns(ArrayList)} is not called yet.
+ * @return PickerColumn at colIndex or null if {@link #setColumns(List)} is not called yet.
*/
public PickerColumn getColumnAt(int colIndex) {
if (mColumns == null) {
@@ -183,7 +168,7 @@
/**
* Get number of PickerColumns.
- * @return Number of PickerColumns or 0 if {@link #setColumns(ArrayList)} is not called yet.
+ * @return Number of PickerColumns or 0 if {@link #setColumns(List)} is not called yet.
*/
public int getColumnsCount() {
if (mColumns == null) {
@@ -196,12 +181,12 @@
* Set columns and create Views.
* @param columns PickerColumns to be shown in the Picker.
*/
- public void setColumns(ArrayList<PickerColumn> columns) {
+ public void setColumns(List<PickerColumn> columns) {
mColumnViews.clear();
mPickerView.removeAllViews();
mColumns = new ArrayList<PickerColumn>(columns);
- if (mActivatedColumn > mColumns.size() - 1) {
- mActivatedColumn = mColumns.size() - 1;
+ if (mSelectedColumn > mColumns.size() - 1) {
+ mSelectedColumn = mColumns.size() - 1;
}
LayoutInflater inflater = LayoutInflater.from(getContext());
int totalCol = getColumnsCount();
@@ -225,7 +210,7 @@
// add a separator if not the last element
if (i != totalCol - 1 && getSeparator() != null) {
TextView separator = (TextView) inflater.inflate(
- getPickerSeparatorLayoutId(), mPickerView, false);
+ R.layout.lb_picker_separator, mPickerView, false);
separator.setText(getSeparator());
mPickerView.addView(separator);
}
@@ -240,8 +225,10 @@
* When column labels change or column range changes, call this function to re-populate the
* selection list.
* @param columnIndex Index of column to update.
+ * @param column New column to update.
*/
- public void updateAdapter(int columnIndex) {
+ public void setColumnAt(int columnIndex, PickerColumn column) {
+ mColumns.set(columnIndex, column);
VerticalGridView columnView = mColumnViews.get(columnIndex);
PickerScrollArrayAdapter adapter = (PickerScrollArrayAdapter) columnView.getAdapter();
if (adapter != null && !columnView.isComputingLayout()) {
@@ -255,8 +242,10 @@
* @param value New value of the column.
* @param runAnimation True to scroll to the value or false otherwise.
*/
- public void updateValue(int columnIndex, int value, boolean runAnimation) {
- if (mColumns.get(columnIndex).setCurrentValue(value)) {
+ public void setColumnValue(int columnIndex, int value, boolean runAnimation) {
+ PickerColumn column = mColumns.get(columnIndex);
+ if (column.getCurrentValue() != value) {
+ column.setCurrentValue(value);
notifyValueChanged(columnIndex);
VerticalGridView columnView = mColumnViews.get(columnIndex);
if (columnView != null) {
@@ -278,14 +267,22 @@
}
}
- public void addPickerValueListener(PickerValueListener listener) {
+ /**
+ * Register a callback to be invoked when the picker's value has changed.
+ * @param listener The callback to ad
+ */
+ public void addOnValueChangedListener(PickerValueListener listener) {
if (mListeners == null) {
mListeners = new ArrayList<Picker.PickerValueListener>();
}
mListeners.add(listener);
}
- public void removePickerValueListener(PickerValueListener listener) {
+ /**
+ * Remove a previously installed value changed callback
+ * @param listener The callback to remove.
+ */
+ public void removeOnValueChangedListener(PickerValueListener listener) {
if (mListeners != null) {
mListeners.remove(listener);
}
@@ -307,7 +304,7 @@
private void setOrAnimateAlpha(View view, boolean selected, int colIndex,
boolean animate) {
- boolean columnShownAsActivated = colIndex == mActivatedColumn || !isFocused();
+ boolean columnShownAsActivated = colIndex == mSelectedColumn || !isFocused();
if (selected) {
// set alpha for main item (selected) in the column
if (columnShownAsActivated) {
@@ -344,15 +341,17 @@
/**
* Classes extending {@link Picker} can override this function to supply the
- * behavior when a list has been scrolled. Subclass may call {@link #updateValue(int, int,
- * boolean)} and or {@link #updateAdapter(int)}. Subclass should not directly call
+ * behavior when a list has been scrolled. Subclass may call {@link #setColumnValue(int, int,
+ * boolean)} and or {@link #setColumnAt(int,PickerColumn)}. Subclass should not directly call
* {@link PickerColumn#setCurrentValue(int)} which does not update internal state or notify
* listeners.
* @param columnIndex index of which column was changed.
* @param newValue A new value desired to be set on the column.
*/
- public void onColumnValueChange(int columnIndex, int newValue) {
- if (mColumns.get(columnIndex).setCurrentValue(newValue)) {
+ public void onColumnValueChanged(int columnIndex, int newValue) {
+ PickerColumn column = mColumns.get(columnIndex);
+ if (column.getCurrentValue() != newValue) {
+ column.setCurrentValue(newValue);
notifyValueChanged(columnIndex);
}
}
@@ -402,7 +401,7 @@
public void onBindViewHolder(ViewHolder holder, int position) {
if (holder.textView != null && mData != null) {
- holder.textView.setText(mData.getValueLabelAt(mData.getMinValue() + position));
+ holder.textView.setText(mData.getEntryAt(mData.getMinValue() + position));
}
setOrAnimateAlpha(holder.itemView,
(mColumnViews.get(mColIndex).getSelectedPosition() == position),
@@ -415,7 +414,7 @@
}
public int getItemCount() {
- return mData == null ? 0 : mData.getItemsCount();
+ return mData == null ? 0 : mData.getItemCount();
}
}
@@ -432,7 +431,7 @@
updateColumnAlpha(colIndex, true);
if (child != null) {
int newValue = mColumns.get(colIndex).getMinValue() + position;
- onColumnValueChange(colIndex, newValue);
+ onColumnValueChanged(colIndex, newValue);
}
}
@@ -440,7 +439,7 @@
@Override
public boolean dispatchKeyEvent(android.view.KeyEvent event) {
- if (isExpanded()) {
+ if (isActivated()) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -449,20 +448,20 @@
if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL?
keyCode == KeyEvent.KEYCODE_DPAD_LEFT :
keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ) {
- if (mActivatedColumn < getColumnsCount() - 1) {
- setActiveColumn(mActivatedColumn + 1);
+ if (mSelectedColumn < getColumnsCount() - 1) {
+ setSelectedColumn(mSelectedColumn + 1);
}
} else {
- if (mActivatedColumn > 0) {
- setActiveColumn(mActivatedColumn - 1);
+ if (mSelectedColumn > 0) {
+ setSelectedColumn(mSelectedColumn - 1);
}
}
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
- if (event.getAction() == KeyEvent.ACTION_DOWN && mActivatedColumn >= 0) {
- VerticalGridView gridView = mColumnViews.get(mActivatedColumn);
+ if (event.getAction() == KeyEvent.ACTION_DOWN && mSelectedColumn >= 0) {
+ VerticalGridView gridView = mColumnViews.get(mSelectedColumn);
if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
int newPosition = gridView.getSelectedPosition() - 1;
if (newPosition >= 0) {
@@ -500,90 +499,82 @@
private void updateColumnSize(VerticalGridView columnView) {
ViewGroup.LayoutParams lp = columnView.getLayoutParams();
- lp.height = (int) (getPickerItemHeightPixels() * (isExpanded() ?
- getVisiblePickerItemsInExpand() : getVisiblePickerItems()));
+ lp.height = (int) (getPickerItemHeightPixels() * (isActivated() ?
+ getActivatedVisibleItemCount() : getVisibleItemCount()));
columnView.setLayoutParams(lp);
}
/**
- * Returns number of visible items showing in a column when it's expanded, it's 3 by default.
- * @return Number of visible items showing in a column when it's expanded.
+ * Returns number of visible items showing in a column when it's activated. The default value
+ * is 3.
+ * @return Number of visible items showing in a column when it's activated.
*/
- public float getVisiblePickerItemsInExpand() {
- return mVisibleItemsInExpand;
+ public float getActivatedVisibleItemCount() {
+ return mVisibleItemsActivated;
}
/**
- * Change number of visible items showing in a column when it's expanded.
- * @param visiblePickerItems Number of visible items showing in a column when it's expanded.
+ * Changes number of visible items showing in a column when it's activated. The default value
+ * is 3.
+ * @param visiblePickerItems Number of visible items showing in a column when it's activated.
*/
- public void setVisiblePickerItemsInExpand(float visiblePickerItems) {
+ public void setActivatedVisibleItemCount(float visiblePickerItems) {
if (visiblePickerItems <= 0) {
throw new IllegalArgumentException();
}
- if (mVisibleItemsInExpand != visiblePickerItems) {
- mVisibleItemsInExpand = visiblePickerItems;
- if (isExpanded()) {
+ if (mVisibleItemsActivated != visiblePickerItems) {
+ mVisibleItemsActivated = visiblePickerItems;
+ if (isActivated()) {
updateColumnSize();
}
}
}
/**
- * Returns number of visible items showing in a column when it's not expanded, it's 1 by
- * default.
- * @return Number of visible items showing in a column when it's not expanded.
+ * Returns number of visible items showing in a column when it's not activated. The default
+ * value is 1.
+ * @return Number of visible items showing in a column when it's not activated.
*/
- public float getVisiblePickerItems() {
+ public float getVisibleItemCount() {
return 1;
}
/**
- * Change number of visible items showing in a column when it's not expanded, it's 1 by default.
- * @param pickerItems Number of visible items showing in a column when it's not expanded.
+ * Changes number of visible items showing in a column when it's not activated. The default
+ * value is 1.
+ * @param pickerItems Number of visible items showing in a column when it's not activated.
*/
- public void setVisiblePickerItems(float pickerItems) {
+ public void setVisibleItemCount(float pickerItems) {
if (pickerItems <= 0) {
throw new IllegalArgumentException();
}
if (mVisibleItems != pickerItems) {
mVisibleItems = pickerItems;
- if (!isExpanded()) {
+ if (!isActivated()) {
updateColumnSize();
}
}
}
- /**
- * Change expanded state of Picker, the height LayoutParams will be changed.
- * @see #getVisiblePickerItemsInExpand()
- * @see #getVisiblePickerItems()
- * @param expanded New expanded state of Picker.
- */
- public void setExpanded(boolean expanded) {
- if (mExpanded != expanded) {
- mExpanded = expanded;
+ @Override
+ public void setActivated(boolean activated) {
+ if (activated != isActivated()) {
+ super.setActivated(activated);
updateColumnSize();
+ } else {
+ super.setActivated(activated);
}
}
/**
- * Returns true if the Picker is currently expanded, false otherwise.
- * @return True if the Picker is currently expanded, false otherwise.
- */
- public boolean isExpanded() {
- return mExpanded;
- }
-
- /**
- * Change current activated column. Shows multiple items on activate column if Picker has
- * focus. Show multiple items on all column if Picker has no focus (e.g. a Touchscreen
+ * Change current selected column. Picker shows multiple items on selected column if Picker has
+ * focus. Picker shows multiple items on all column if Picker has no focus (e.g. a Touchscreen
* screen).
* @param columnIndex Index of column to activate.
*/
- public void setActiveColumn(int columnIndex) {
- if (mActivatedColumn != columnIndex) {
- mActivatedColumn = columnIndex;
+ public void setSelectedColumn(int columnIndex) {
+ if (mSelectedColumn != columnIndex) {
+ mSelectedColumn = columnIndex;
for (int i = 0; i < mColumnViews.size(); i++) {
updateColumnAlpha(i, true);
}
@@ -594,35 +585,8 @@
* Get current activated column index.
* @return Current activated column index.
*/
- public int getActiveColumn() {
- return mActivatedColumn;
- }
-
- /**
- * Enable or disable toggle on click when Picker has focus.
- * @param toggleExpandOnClick True to enable toggle on click when Picker has focus, false
- * otherwise.
- */
- public void setToggleExpandOnClick(boolean toggleExpandOnClick) {
- mToggleExpandOnClick = toggleExpandOnClick;
- }
-
- /**
- * Returns true if toggle on click is enabled when Picker has focus, false otherwise.
- * @return True if toggle on click is enabled when Picker has focus, false otherwise.
- */
- public boolean isToggleExpandOnClick() {
- return mToggleExpandOnClick;
- }
-
- @Override
- public boolean performClick() {
- if (isFocused() && isToggleExpandOnClick()) {
- setExpanded(!isExpanded());
- super.performClick();
- return true;
- }
- return super.performClick();
+ public int getSelectedColumn() {
+ return mSelectedColumn;
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
index c4c0c57..f2a2c5a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/picker/PickerColumn.java
@@ -14,54 +14,40 @@
package android.support.v17.leanback.widget.picker;
-import android.os.Parcel;
-import android.os.Parcelable;
-
/**
* Picker column class used by {@link Picker}, defines a contiguous value ranges and associated
* labels. A PickerColumn has a minValue and maxValue to choose between. The Picker column has
* a current value.
- * The labels can be dynamically generated from value by {@link #setValueLabelFormat(String)} or
- * a list of static labels set by {@link #setValueStaticLabels(String[])}.
+ * The labels can be dynamically generated from value by {@link #setEntryFormat(String)} or
+ * a list of static labels set by {@link #setEntries(CharSequence[])}.
*/
-public class PickerColumn implements Parcelable {
+public class PickerColumn {
private int mCurrentValue;
private int mMinValue;
private int mMaxValue;
- private String[] mStaticLabels;
- private String mValueFormat;
+ private CharSequence[] mStaticEntrys;
+ private String mEntryFormat;
public PickerColumn() {
}
- public PickerColumn(Parcel source) {
- mValueFormat = source.readString();
- int count = source.readInt();
- if (count > 0) {
- mStaticLabels = new String[count];
- source.readStringArray(mStaticLabels);
- }
- mCurrentValue = source.readInt();
- mMinValue = source.readInt();
- mMaxValue = source.readInt();
- }
-
/**
- * Set string format to display label for value, e.g. "%02d". The string format is only
- * used when {@link #setValueStaticLabels(String[])} is not called.
- * @param valueFormat String format to display label for value.
+ * Set string format to display label for value. For example "%02d".
+ * {@link #setEntries(CharSequence[])} overrides the format.
+ *
+ * @param valueFormat String format to display label for value between minValue and maxValue.
*/
- public void setValueLabelFormat(String valueFormat) {
- mValueFormat = valueFormat;
+ public void setEntryFormat(String valueFormat) {
+ mEntryFormat = valueFormat;
}
/**
- * Return string format to display label for value, e.g. "%02d".
+ * Return string format to display label for value. For example "%02d".
* @return String format to display label for value.
*/
- public String getValueLabelFormat() {
- return mValueFormat;
+ public String getEntryFormat() {
+ return mEntryFormat;
}
/**
@@ -69,21 +55,22 @@
* labels[labels.length - 1].
* @param labels Static labels for each value between minValue and maxValue.
*/
- public void setValueStaticLabels(String[] labels) {
- mStaticLabels = labels;
+ public void setEntries(CharSequence[] labels) {
+ mStaticEntrys = labels;
}
/**
- * Get a label for value. The label can be static ({@link #setValueStaticLabels(String[])} or
- * dynamically generated (@link {@link #setValueLabelFormat(String)}.
+ * Get a label for value. The label can be static ({@link #setEntries(CharSequence[])}
+ * or dynamically generated (@link {@link #setEntryFormat(String)}.
+ *
* @param value Value between minValue and maxValue.
* @return Label for the value.
*/
- public String getValueLabelAt(int value) {
- if (mStaticLabels == null) {
- return String.format(mValueFormat, value);
+ public CharSequence getEntryAt(int value) {
+ if (mStaticEntrys == null) {
+ return String.format(mEntryFormat, value);
}
- return mStaticLabels[value];
+ return mStaticEntrys[value];
}
/**
@@ -96,97 +83,49 @@
/**
* Sets current value of the Column.
- * @return True if current value has changed.
*/
- public boolean setCurrentValue(int value) {
- if (mCurrentValue != value) {
- mCurrentValue = value;
- return true;
- }
- return false;
+ public void setCurrentValue(int value) {
+ mCurrentValue = value;
}
/**
- * Get total items count between minValue(inclusive) and maxValue (inclusive).
- * @return Total items count between minValue(inclusive) and maxValue (inclusive).
+ * Get total items count between minValue and maxValue.
+ * @return Total items count between minValue and maxValue.
*/
- public int getItemsCount() {
+ public int getItemCount() {
return mMaxValue - mMinValue + 1;
}
/**
- * Returns minimal value (inclusive) of the Column.
- * @return Minimal value (inclusive) of the Column.
+ * Returns minimal value of the Column.
+ * @return Minimal value of the Column.
*/
public int getMinValue() {
return mMinValue;
}
/**
- * Returns maximum value (inclusive) of the Column.
- * @return Maximum value (inclusive) of the Column.
+ * Returns maximum value of the Column.
+ * @return Maximum value of the Column.
*/
public int getMaxValue() {
return mMaxValue;
}
/**
- * Sets minimal value (inclusive) of the Column.
+ * Sets minimal value of the Column.
* @param minValue New minimal value to set.
- * @return True if minimal value changes.
*/
- public boolean setMinValue(int minValue) {
- if (minValue != mMinValue) {
- mMinValue = minValue;
- return true;
- }
- return false;
+ public void setMinValue(int minValue) {
+ mMinValue = minValue;
}
/**
- * Sets maximum value (inclusive) of the Column.
+ * Sets maximum value of the Column.
* @param maxValue New maximum value to set.
- * @return True if maximum value changes.
*/
- public boolean setMaxValue(int maxValue) {
- if (maxValue != mMaxValue) {
- mMaxValue = maxValue;
- return true;
- }
- return false;
- }
-
- public static Parcelable.Creator<PickerColumn>
- CREATOR = new Parcelable.Creator<PickerColumn>() {
-
- @Override
- public PickerColumn createFromParcel(Parcel source) {
- return new PickerColumn(source);
- }
-
- @Override
- public PickerColumn[] newArray(int size) {
- return new PickerColumn[size];
- }
- };
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mValueFormat);
- if (mStaticLabels != null) {
- dest.writeInt(mStaticLabels.length);
- dest.writeStringArray(mStaticLabels);
- } else {
- dest.writeInt(0);
- }
- dest.writeInt(mCurrentValue);
- dest.writeInt(mMinValue);
- dest.writeInt(mMaxValue);
+ public void setMaxValue(int maxValue) {
+ mMaxValue = maxValue;
}
}
diff --git a/v4/api21/android/support/v4/graphics/drawable/DrawableCompatLollipop.java b/v4/api21/android/support/v4/graphics/drawable/DrawableCompatLollipop.java
index 3d0d9e6..b8ab03a 100644
--- a/v4/api21/android/support/v4/graphics/drawable/DrawableCompatLollipop.java
+++ b/v4/api21/android/support/v4/graphics/drawable/DrawableCompatLollipop.java
@@ -19,9 +19,6 @@
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
/**
* Implementation of drawable compatibility that can call L APIs.
@@ -51,16 +48,8 @@
public static Drawable wrapForTinting(final Drawable drawable) {
if (!(drawable instanceof DrawableWrapperLollipop)) {
- return new DrawableWrapperLollipop(drawable, shouldForceCompatTinting(drawable));
+ return new DrawableWrapperLollipop(drawable);
}
return drawable;
}
-
- private static boolean shouldForceCompatTinting(Drawable drawable) {
- // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible
- // functionality instead. We also do the same for DrawableContainers and DrawableWrappers
- // since they may contain GradientDrawable instances.
- return drawable instanceof GradientDrawable || drawable instanceof DrawableContainer
- || drawable instanceof InsetDrawable;
- }
}
diff --git a/v4/api21/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java b/v4/api21/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java
index 9533afd..9f3a99e 100644
--- a/v4/api21/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java
+++ b/v4/api21/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java
@@ -22,18 +22,20 @@
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableContainer;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.os.Build;
+import android.support.annotation.Nullable;
class DrawableWrapperLollipop extends DrawableWrapperKitKat {
- private final boolean mUseCompatTinting;
-
DrawableWrapperLollipop(Drawable drawable) {
- this(drawable, false);
+ super(drawable);
}
- DrawableWrapperLollipop(Drawable drawable, boolean useCompatTinting) {
- super(drawable);
- mUseCompatTinting = useCompatTinting;
+ DrawableWrapperLollipop(DrawableWrapperState state, Resources resources) {
+ super(state, resources);
}
@Override
@@ -52,23 +54,13 @@
}
@Override
- public void applyTheme(Resources.Theme t) {
- mDrawable.applyTheme(t);
- }
-
- @Override
- public boolean canApplyTheme() {
- return mDrawable.canApplyTheme();
- }
-
- @Override
public Rect getDirtyBounds() {
return mDrawable.getDirtyBounds();
}
@Override
public void setTintList(ColorStateList tint) {
- if (mUseCompatTinting) {
+ if (isCompatTintEnabled()) {
setCompatTintList(tint);
} else {
mDrawable.setTintList(tint);
@@ -77,7 +69,7 @@
@Override
public void setTint(int tintColor) {
- if (mUseCompatTinting) {
+ if (isCompatTintEnabled()) {
setCompatTint(tintColor);
} else {
mDrawable.setTint(tintColor);
@@ -86,7 +78,7 @@
@Override
public void setTintMode(PorterDuff.Mode tintMode) {
- if (mUseCompatTinting) {
+ if (isCompatTintEnabled()) {
setCompatTintMode(tintMode);
} else {
mDrawable.setTintMode(tintMode);
@@ -106,6 +98,28 @@
@Override
protected boolean isCompatTintEnabled() {
- return mUseCompatTinting;
+ if (Build.VERSION.SDK_INT == 21) {
+ final Drawable drawable = mDrawable;
+ return drawable instanceof GradientDrawable || drawable instanceof DrawableContainer
+ || drawable instanceof InsetDrawable;
+ }
+ return false;
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ return new DrawableWrapperStateLollipop(mState, null);
+ }
+
+ private static class DrawableWrapperStateLollipop extends DrawableWrapperState {
+ DrawableWrapperStateLollipop(@Nullable DrawableWrapperState orig,
+ @Nullable Resources res) {
+ super(orig, res);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new DrawableWrapperLollipop(this, res);
+ }
}
}
diff --git a/v4/api23/android/support/v4/widget/TextViewCompatApi23.java b/v4/api23/android/support/v4/widget/TextViewCompatApi23.java
index 8370bfc..ad21409 100644
--- a/v4/api23/android/support/v4/widget/TextViewCompatApi23.java
+++ b/v4/api23/android/support/v4/widget/TextViewCompatApi23.java
@@ -18,10 +18,11 @@
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
+import android.support.annotation.StyleRes;
import android.widget.TextView;
class TextViewCompatApi23 {
- public static void setTextAppearance(@NonNull TextView textView, @IdRes int resId) {
+ public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
textView.setTextAppearance(resId);
}
}
diff --git a/v4/build.gradle b/v4/build.gradle
index 5abdbb9..8b26642 100644
--- a/v4/build.gradle
+++ b/v4/build.gradle
@@ -65,8 +65,12 @@
// when manipulating the libraryVariants.
compile files(internalJar.archivePath)
- androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
- androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
+ androidTestCompile ('com.android.support.test:runner:0.4.1') {
+ exclude module: 'support-annotations'
+ }
+ androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.1') {
+ exclude module: 'support-annotations'
+ }
testCompile 'junit:junit:4.12'
}
diff --git a/v4/donut/android/support/v4/graphics/drawable/DrawableWrapperDonut.java b/v4/donut/android/support/v4/graphics/drawable/DrawableWrapperDonut.java
index f742c15..d04a2ae 100644
--- a/v4/donut/android/support/v4/graphics/drawable/DrawableWrapperDonut.java
+++ b/v4/donut/android/support/v4/graphics/drawable/DrawableWrapperDonut.java
@@ -17,12 +17,15 @@
package android.support.v4.graphics.drawable;
import android.content.res.ColorStateList;
+import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
/**
* Drawable which delegates all calls to it's wrapped {@link android.graphics.drawable.Drawable}.
@@ -32,19 +35,49 @@
*/
class DrawableWrapperDonut extends Drawable implements Drawable.Callback, DrawableWrapper {
- static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN;
-
- private ColorStateList mTintList;
- private PorterDuff.Mode mTintMode = DEFAULT_MODE;
+ static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
private int mCurrentColor;
private PorterDuff.Mode mCurrentMode;
private boolean mColorFilterSet;
+ DrawableWrapperState mState;
+ private boolean mMutated;
+
Drawable mDrawable;
- DrawableWrapperDonut(Drawable drawable) {
- setWrappedDrawable(drawable);
+ DrawableWrapperDonut(@NonNull DrawableWrapperState state, @Nullable Resources res) {
+ mState = state;
+ updateLocalState(res);
+ }
+ /**
+ * Creates a new wrapper around the specified drawable.
+ *
+ * @param dr the drawable to wrap
+ */
+ DrawableWrapperDonut(@Nullable Drawable dr) {
+ mState = mutateConstantState();
+ mDrawable = dr;
+ }
+
+ /**
+ * Initializes local dynamic properties from state. This should be called
+ * after significant state changes, e.g. from the One True Constructor and
+ * after inflating or applying a theme.
+ */
+ private void updateLocalState(@Nullable Resources res) {
+ if (mState != null && mState.mDrawableState != null) {
+ final Drawable dr = newDrawableFromState(mState.mDrawableState, res);
+ setWrappedDrawable(dr);
+ }
+ }
+
+ /**
+ * Allows us to call ConstantState.newDrawable(*) is a API safe way
+ */
+ protected Drawable newDrawableFromState(@NonNull Drawable.ConstantState state,
+ @Nullable Resources res) {
+ return state.newDrawable();
}
@Override
@@ -66,7 +99,9 @@
@Override
public int getChangingConfigurations() {
- return mDrawable.getChangingConfigurations();
+ return super.getChangingConfigurations()
+ | (mState != null ? mState.getChangingConfigurations() : 0)
+ | mDrawable.getChangingConfigurations();
}
@Override
@@ -91,7 +126,7 @@
@Override
public boolean isStateful() {
- final ColorStateList tintList = isCompatTintEnabled() ? mTintList : null;
+ final ColorStateList tintList = isCompatTintEnabled() ? mState.mTint : null;
return (tintList != null && tintList.isStateful()) || mDrawable.isStateful();
}
@@ -153,18 +188,43 @@
}
@Override
- public Drawable mutate() {
- Drawable wrapped = mDrawable;
- Drawable mutated = wrapped.mutate();
- if (mutated != wrapped) {
- // If mutate() returned a new instance, update our reference
- setWrappedDrawable(mutated);
+ @Nullable
+ public ConstantState getConstantState() {
+ if (mState != null && mState.canConstantState()) {
+ mState.mChangingConfigurations = getChangingConfigurations();
+ return mState;
}
- // We return ourselves, since only the wrapped drawable needs to mutate
+ return null;
+ }
+
+ @Override
+ public Drawable mutate() {
+ if (!mMutated && super.mutate() == this) {
+ mState = mutateConstantState();
+ if (mDrawable != null) {
+ mDrawable.mutate();
+ }
+ if (mState != null) {
+ mState.mDrawableState = mDrawable != null ? mDrawable.getConstantState() : null;
+ }
+ mMutated = true;
+ }
return this;
}
/**
+ * Mutates the constant state and returns the new state.
+ * <p>
+ * This method should never call the super implementation; it should always
+ * mutate and return its own constant state.
+ *
+ * @return the new state
+ */
+ DrawableWrapperState mutateConstantState() {
+ return new DrawableWrapperStateDonut(mState, null);
+ }
+
+ /**
* {@inheritDoc}
*/
public void invalidateDrawable(Drawable who) {
@@ -197,18 +257,14 @@
@Override
public void setCompatTintList(ColorStateList tint) {
- if (mTintList != tint) {
- mTintList = tint;
- updateTint(getState());
- }
+ mState.mTint = tint;
+ updateTint(getState());
}
@Override
public void setCompatTintMode(PorterDuff.Mode tintMode) {
- if (mTintMode != tintMode) {
- mTintMode = tintMode;
- updateTint(getState());
- }
+ mState.mTintMode = tintMode;
+ updateTint(getState());
}
private boolean updateTint(int[] state) {
@@ -217,12 +273,15 @@
return false;
}
- if (mTintList != null && mTintMode != null) {
- final int color = mTintList.getColorForState(state, mTintList.getDefaultColor());
- if (!mColorFilterSet || color != mCurrentColor || mTintMode != mCurrentMode) {
- setColorFilter(color, mTintMode);
+ final ColorStateList tintList = mState.mTint;
+ final PorterDuff.Mode tintMode = mState.mTintMode;
+
+ if (tintList != null && tintMode != null) {
+ final int color = tintList.getColorForState(state, tintList.getDefaultColor());
+ if (!mColorFilterSet || color != mCurrentColor || tintMode != mCurrentMode) {
+ setColorFilter(color, tintMode);
mCurrentColor = color;
- mCurrentMode = mTintMode;
+ mCurrentMode = tintMode;
mColorFilterSet = true;
return true;
}
@@ -243,25 +302,25 @@
/**
* Sets the current wrapped {@link Drawable}
*/
- public void setWrappedDrawable(Drawable drawable) {
+ public void setWrappedDrawable(Drawable dr) {
if (mDrawable != null) {
mDrawable.setCallback(null);
}
- mDrawable = null;
- if (drawable != null) {
- // Copy over the bounds from the drawable
- setBounds(drawable.getBounds());
- // Set ourselves as the callback for invalidations
- drawable.setCallback(this);
- } else {
- // Clear our bounds
- setBounds(0, 0, 0, 0);
+ mDrawable = dr;
+
+ if (dr != null) {
+ dr.setCallback(this);
+ // Only call setters for data that's stored in the base Drawable.
+ dr.setVisible(isVisible(), true);
+ dr.setState(getState());
+ dr.setLevel(getLevel());
+ dr.setBounds(getBounds());
+ if (mState != null) {
+ mState.mDrawableState = dr.getConstantState();
+ }
}
- mDrawable = drawable;
-
- // Invalidate ourselves
invalidateSelf();
}
@@ -269,4 +328,50 @@
// It's enabled by default on Donut
return true;
}
+
+ protected static abstract class DrawableWrapperState extends Drawable.ConstantState {
+ int mChangingConfigurations;
+ Drawable.ConstantState mDrawableState;
+
+ ColorStateList mTint = null;
+ PorterDuff.Mode mTintMode = DEFAULT_TINT_MODE;
+
+ DrawableWrapperState(@Nullable DrawableWrapperState orig, @Nullable Resources res) {
+ if (orig != null) {
+ mChangingConfigurations = orig.mChangingConfigurations;
+ mDrawableState = orig.mDrawableState;
+ mTint = orig.mTint;
+ mTintMode = orig.mTintMode;
+ }
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return newDrawable(null);
+ }
+
+ public abstract Drawable newDrawable(@Nullable Resources res);
+
+ @Override
+ public int getChangingConfigurations() {
+ return mChangingConfigurations
+ | (mDrawableState != null ? mDrawableState.getChangingConfigurations() : 0);
+ }
+
+ boolean canConstantState() {
+ return mDrawableState != null;
+ }
+ }
+
+ private static class DrawableWrapperStateDonut extends DrawableWrapperState {
+ DrawableWrapperStateDonut(
+ @Nullable DrawableWrapperState orig, @Nullable Resources res) {
+ super(orig, res);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new DrawableWrapperDonut(this, res);
+ }
+ }
}
diff --git a/v4/api22/android/support/v4/graphics/drawable/DrawableCompatApi22.java b/v4/eclair/android/support/v4/graphics/drawable/DrawableCompatEclair.java
similarity index 68%
rename from v4/api22/android/support/v4/graphics/drawable/DrawableCompatApi22.java
rename to v4/eclair/android/support/v4/graphics/drawable/DrawableCompatEclair.java
index e0455bb..01316a8 100644
--- a/v4/api22/android/support/v4/graphics/drawable/DrawableCompatApi22.java
+++ b/v4/eclair/android/support/v4/graphics/drawable/DrawableCompatEclair.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 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.
@@ -19,16 +19,13 @@
import android.graphics.drawable.Drawable;
/**
- * Implementation of drawable compatibility that can call Lollipop-MR1 APIs.
+ * Implementation of drawable compatibility that can call Eclair APIs.
*/
-class DrawableCompatApi22 {
-
+class DrawableCompatEclair {
public static Drawable wrapForTinting(Drawable drawable) {
- // We need to wrap to force an invalidation on any state change
- if (!(drawable instanceof DrawableWrapperLollipop)) {
- return new DrawableWrapperLollipop(drawable);
+ if (!(drawable instanceof DrawableWrapperEclair)) {
+ return new DrawableWrapperEclair(drawable);
}
return drawable;
}
-
}
diff --git a/v4/eclair/android/support/v4/graphics/drawable/DrawableWrapperEclair.java b/v4/eclair/android/support/v4/graphics/drawable/DrawableWrapperEclair.java
new file mode 100644
index 0000000..11a65ee
--- /dev/null
+++ b/v4/eclair/android/support/v4/graphics/drawable/DrawableWrapperEclair.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2016 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.support.v4.graphics.drawable;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+
+class DrawableWrapperEclair extends DrawableWrapperDonut {
+
+ DrawableWrapperEclair(Drawable drawable) {
+ super(drawable);
+ }
+
+ DrawableWrapperEclair(DrawableWrapperState state, Resources resources) {
+ super(state, resources);
+ }
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ return new DrawableWrapperStateEclair(mState, null);
+ }
+
+ @Override
+ protected Drawable newDrawableFromState(Drawable.ConstantState state, Resources res) {
+ return state.newDrawable(res);
+ }
+
+ private static class DrawableWrapperStateEclair extends DrawableWrapperState {
+ DrawableWrapperStateEclair(@Nullable DrawableWrapperState orig,
+ @Nullable Resources res) {
+ super(orig, res);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new DrawableWrapperEclair(this, res);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v4/honeycomb/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java
index f9fd7d8..18970ed 100644
--- a/v4/honeycomb/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java
+++ b/v4/honeycomb/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java
@@ -16,7 +16,9 @@
package android.support.v4.graphics.drawable;
+import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
class DrawableWrapperHoneycomb extends DrawableWrapperDonut {
@@ -24,8 +26,29 @@
super(drawable);
}
+ DrawableWrapperHoneycomb(DrawableWrapperState state, Resources resources) {
+ super(state, resources);
+ }
+
@Override
public void jumpToCurrentState() {
mDrawable.jumpToCurrentState();
}
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ return new DrawableWrapperStateHoneycomb(mState, null);
+ }
+
+ private static class DrawableWrapperStateHoneycomb extends DrawableWrapperState {
+ DrawableWrapperStateHoneycomb(@Nullable DrawableWrapperState orig,
+ @Nullable Resources res) {
+ super(orig, res);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new DrawableWrapperHoneycomb(this, res);
+ }
+ }
}
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index 4d6585a..0fbe357 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -784,7 +784,9 @@
*/
@Override
public void startActivityForResult(Intent intent, int requestCode) {
- if (mStartedActivityFromFragment) {
+ // If this was started from a Fragment we've already checked the upper 16 bits were not in
+ // use, and then repurposed them for the Fragment's index.
+ if (!mStartedActivityFromFragment) {
if (requestCode != -1 && (requestCode&0xffff0000) != 0) {
throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
}
diff --git a/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
index 97545ef..a24a0a0 100644
--- a/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
+++ b/v4/java/android/support/v4/graphics/drawable/DrawableCompat.java
@@ -103,9 +103,19 @@
}
/**
+ * Interface implementation for devices with at least v5 APIs.
+ */
+ static class EclairDrawableImpl extends BaseDrawableImpl {
+ @Override
+ public Drawable wrap(Drawable drawable) {
+ return DrawableCompatEclair.wrapForTinting(drawable);
+ }
+ }
+
+ /**
* Interface implementation for devices with at least v11 APIs.
*/
- static class HoneycombDrawableImpl extends BaseDrawableImpl {
+ static class HoneycombDrawableImpl extends EclairDrawableImpl {
@Override
public void jumpToCurrentState(Drawable drawable) {
DrawableCompatHoneycomb.jumpToCurrentState(drawable);
@@ -186,19 +196,9 @@
}
/**
- * Interface implementation for devices with at least L APIs.
- */
- static class LollipopMr1DrawableImpl extends LollipopDrawableImpl {
- @Override
- public Drawable wrap(Drawable drawable) {
- return DrawableCompatApi22.wrapForTinting(drawable);
- }
- }
-
- /**
* Interface implementation for devices with at least M APIs.
*/
- static class MDrawableImpl extends LollipopMr1DrawableImpl {
+ static class MDrawableImpl extends LollipopDrawableImpl {
@Override
public void setLayoutDirection(Drawable drawable, int layoutDirection) {
DrawableCompatApi23.setLayoutDirection(drawable, layoutDirection);
@@ -208,6 +208,12 @@
public int getLayoutDirection(Drawable drawable) {
return DrawableCompatApi23.getLayoutDirection(drawable);
}
+
+ @Override
+ public Drawable wrap(Drawable drawable) {
+ // No need to wrap on M+
+ return drawable;
+ }
}
/**
@@ -218,8 +224,6 @@
final int version = android.os.Build.VERSION.SDK_INT;
if (version >= 23) {
IMPL = new MDrawableImpl();
- } else if (version >= 22) {
- IMPL = new LollipopMr1DrawableImpl();
} else if (version >= 21) {
IMPL = new LollipopDrawableImpl();
} else if (version >= 19) {
@@ -228,6 +232,8 @@
IMPL = new JellybeanMr1DrawableImpl();
} else if (version >= 11) {
IMPL = new HoneycombDrawableImpl();
+ } else if (version >= 5) {
+ IMPL = new EclairDrawableImpl();
} else {
IMPL = new BaseDrawableImpl();
}
diff --git a/v4/java/android/support/v4/widget/DrawerLayout.java b/v4/java/android/support/v4/widget/DrawerLayout.java
index 82eb16c..0e2e829 100644
--- a/v4/java/android/support/v4/widget/DrawerLayout.java
+++ b/v4/java/android/support/v4/widget/DrawerLayout.java
@@ -688,6 +688,9 @@
*/
@LockMode
public int getDrawerLockMode(View drawerView) {
+ if (!isDrawerView(drawerView)) {
+ throw new IllegalArgumentException("View " + drawerView + " is not a drawer");
+ }
final int drawerGravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
return getDrawerLockMode(drawerGravity);
}
diff --git a/v4/java/android/support/v4/widget/TextViewCompat.java b/v4/java/android/support/v4/widget/TextViewCompat.java
index 5df1ae6..7af3087 100644
--- a/v4/java/android/support/v4/widget/TextViewCompat.java
+++ b/v4/java/android/support/v4/widget/TextViewCompat.java
@@ -18,9 +18,11 @@
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.support.annotation.DrawableRes;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.StyleRes;
import android.widget.TextView;
/**
@@ -40,10 +42,11 @@
@Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end,
@Nullable Drawable bottom);
void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom);
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom);
int getMaxLines(TextView textView);
int getMinLines(TextView textView);
- void setTextAppearance(@NonNull TextView textView, @IdRes int resId);
+ void setTextAppearance(@NonNull TextView textView, @StyleRes int resId);
}
static class BaseTextViewCompatImpl implements TextViewCompatImpl {
@@ -63,7 +66,8 @@
@Override
public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom) {
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom) {
textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
}
@@ -78,7 +82,7 @@
}
@Override
- public void setTextAppearance(TextView textView, int resId) {
+ public void setTextAppearance(TextView textView, @StyleRes int resId) {
TextViewCompatDonut.setTextAppearance(textView, resId);
}
}
@@ -113,7 +117,8 @@
@Override
public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom) {
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom) {
TextViewCompatJbMr1.setCompoundDrawablesRelativeWithIntrinsicBounds(textView,
start, top, end, bottom);
}
@@ -138,7 +143,8 @@
@Override
public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom) {
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom) {
TextViewCompatJbMr2.setCompoundDrawablesRelativeWithIntrinsicBounds(textView,
start, top, end, bottom);
}
@@ -146,7 +152,7 @@
static class Api23TextViewCompatImpl extends JbMr2TextViewCompatImpl {
@Override
- public void setTextAppearance(@NonNull TextView textView, @IdRes int resId) {
+ public void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
TextViewCompatApi23.setTextAppearance(textView, resId);
}
}
@@ -228,7 +234,8 @@
* @attr ref android.R.styleable#TextView_drawableBottom
*/
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom) {
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom) {
IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom);
}
@@ -259,7 +266,7 @@
* @param textView The TextView against which to invoke the method.
* @param resId The resource identifier of the style to apply.
*/
- public static void setTextAppearance(@NonNull TextView textView, @IdRes int resId) {
+ public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
IMPL.setTextAppearance(textView, resId);
}
}
diff --git a/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java b/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java
deleted file mode 100644
index 6e5cfd5..0000000
--- a/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * Copyright (C) 2013 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.support.v4.media.routing;
-
-import android.content.Context;
-import android.hardware.display.DisplayManager;
-import android.os.Build;
-import android.os.Handler;
-import android.util.Log;
-import android.view.Display;
-
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-class MediaRouterJellybeanMr1 extends MediaRouterJellybean {
- private static final String TAG = "MediaRouterJellybeanMr1";
-
- public static Object createCallback(Callback callback) {
- return new CallbackProxy<Callback>(callback);
- }
-
- public static final class RouteInfo {
- public static boolean isEnabled(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled();
- }
-
- public static Display getPresentationDisplay(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay();
- }
- }
-
- public static interface Callback extends MediaRouterJellybean.Callback {
- public void onRoutePresentationDisplayChanged(Object routeObj);
- }
-
- /**
- * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
- * flag to perform an active scan does not exist in JB MR1 so we need to force
- * wifi display scans directly through the DisplayManager.
- * Do not use on JB MR2 and above.
- */
- public static final class ActiveScanWorkaround implements Runnable {
- // Time between wifi display scans when actively scanning in milliseconds.
- private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
-
- private final DisplayManager mDisplayManager;
- private final Handler mHandler;
- private Method mScanWifiDisplaysMethod;
-
- private boolean mActivelyScanningWifiDisplays;
-
- public ActiveScanWorkaround(Context context, Handler handler) {
- if (Build.VERSION.SDK_INT != 17) {
- throw new UnsupportedOperationException();
- }
-
- mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
- mHandler = handler;
- try {
- mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
- } catch (NoSuchMethodException ex) {
- }
- }
-
- public void setActiveScanRouteTypes(int routeTypes) {
- // On JB MR1, there is no API to scan wifi display routes.
- // Instead we must make a direct call into the DisplayManager to scan
- // wifi displays on this version but only when live video routes are requested.
- // See also the JellybeanMr2Impl implementation of this method.
- // This was fixed in JB MR2 by adding a new overload of addCallback() to
- // enable active scanning on request.
- if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
- if (!mActivelyScanningWifiDisplays) {
- if (mScanWifiDisplaysMethod != null) {
- mActivelyScanningWifiDisplays = true;
- mHandler.post(this);
- } else {
- Log.w(TAG, "Cannot scan for wifi displays because the "
- + "DisplayManager.scanWifiDisplays() method is "
- + "not available on this device.");
- }
- }
- } else {
- if (mActivelyScanningWifiDisplays) {
- mActivelyScanningWifiDisplays = false;
- mHandler.removeCallbacks(this);
- }
- }
- }
-
- @Override
- public void run() {
- if (mActivelyScanningWifiDisplays) {
- try {
- mScanWifiDisplaysMethod.invoke(mDisplayManager);
- } catch (IllegalAccessException ex) {
- Log.w(TAG, "Cannot scan for wifi displays.", ex);
- } catch (InvocationTargetException ex) {
- Log.w(TAG, "Cannot scan for wifi displays.", ex);
- }
- mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
- }
- }
- }
-
- /**
- * Workaround the fact that the isConnecting() method does not exist in JB MR1.
- * Do not use on JB MR2 and above.
- */
- public static final class IsConnectingWorkaround {
- private Method mGetStatusCodeMethod;
- private int mStatusConnecting;
-
- public IsConnectingWorkaround() {
- if (Build.VERSION.SDK_INT != 17) {
- throw new UnsupportedOperationException();
- }
-
- try {
- Field statusConnectingField =
- android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
- mStatusConnecting = statusConnectingField.getInt(null);
- mGetStatusCodeMethod =
- android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
- } catch (NoSuchFieldException ex) {
- } catch (NoSuchMethodException ex) {
- } catch (IllegalAccessException ex) {
- }
- }
-
- public boolean isConnecting(Object routeObj) {
- android.media.MediaRouter.RouteInfo route =
- (android.media.MediaRouter.RouteInfo)routeObj;
-
- if (mGetStatusCodeMethod != null) {
- try {
- int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
- return statusCode == mStatusConnecting;
- } catch (IllegalAccessException ex) {
- } catch (InvocationTargetException ex) {
- }
- }
-
- // Assume not connecting.
- return false;
- }
- }
-
- static class CallbackProxy<T extends Callback>
- extends MediaRouterJellybean.CallbackProxy<T> {
- public CallbackProxy(T callback) {
- super(callback);
- }
-
- @Override
- public void onRoutePresentationDisplayChanged(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route) {
- mCallback.onRoutePresentationDisplayChanged(route);
- }
- }
-}
diff --git a/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java b/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java
deleted file mode 100644
index 92a1607..0000000
--- a/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2013 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.support.v4.media.routing;
-
-class MediaRouterJellybeanMr2 extends MediaRouterJellybeanMr1 {
- public static Object getDefaultRoute(Object routerObj) {
- return ((android.media.MediaRouter)routerObj).getDefaultRoute();
- }
-
- public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
- ((android.media.MediaRouter)routerObj).addCallback(types,
- (android.media.MediaRouter.Callback)callbackObj, flags);
- }
-
- public static final class RouteInfo {
- public static CharSequence getDescription(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
- }
-
- public static boolean isConnecting(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
- }
- }
-
- public static final class UserRouteInfo {
- public static void setDescription(Object routeObj, CharSequence description) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
- }
- }
-}
diff --git a/v4/jellybean-mr2/android/support/v4/widget/TextViewCompatJbMr2.java b/v4/jellybean-mr2/android/support/v4/widget/TextViewCompatJbMr2.java
index ec9fe61..73f9666 100644
--- a/v4/jellybean-mr2/android/support/v4/widget/TextViewCompatJbMr2.java
+++ b/v4/jellybean-mr2/android/support/v4/widget/TextViewCompatJbMr2.java
@@ -17,6 +17,7 @@
package android.support.v4.widget;
import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.widget.TextView;
@@ -36,7 +37,8 @@
}
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
- int start, int top, int end, int bottom) {
+ @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
+ @DrawableRes int bottom) {
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom);
}
diff --git a/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java b/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java
deleted file mode 100644
index 3cf6727..0000000
--- a/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java
+++ /dev/null
@@ -1,442 +0,0 @@
-/*
- * Copyright (C) 2013 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.support.v4.media.routing;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.util.Log;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.List;
-
-class MediaRouterJellybean {
- private static final String TAG = "MediaRouterJellybean";
-
- public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
- public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
- public static final int ROUTE_TYPE_USER = 0x00800000;
-
- public static final int ALL_ROUTE_TYPES =
- MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
- | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
- | MediaRouterJellybean.ROUTE_TYPE_USER;
-
- public static Object getMediaRouter(Context context) {
- return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
- }
-
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public static List getRoutes(Object routerObj) {
- final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
- final int count = router.getRouteCount();
- List out = new ArrayList(count);
- for (int i = 0; i < count; i++) {
- out.add(router.getRouteAt(i));
- }
- return out;
- }
-
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public static List getCategories(Object routerObj) {
- final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
- final int count = router.getCategoryCount();
- List out = new ArrayList(count);
- for (int i = 0; i < count; i++) {
- out.add(router.getCategoryAt(i));
- }
- return out;
- }
-
- public static Object getSelectedRoute(Object routerObj, int type) {
- return ((android.media.MediaRouter)routerObj).getSelectedRoute(type);
- }
-
- public static void selectRoute(Object routerObj, int types, Object routeObj) {
- ((android.media.MediaRouter)routerObj).selectRoute(types,
- (android.media.MediaRouter.RouteInfo)routeObj);
- }
-
- public static void addCallback(Object routerObj, int types, Object callbackObj) {
- ((android.media.MediaRouter)routerObj).addCallback(types,
- (android.media.MediaRouter.Callback)callbackObj);
- }
-
- public static void removeCallback(Object routerObj, Object callbackObj) {
- ((android.media.MediaRouter)routerObj).removeCallback(
- (android.media.MediaRouter.Callback)callbackObj);
- }
-
- public static Object createRouteCategory(Object routerObj,
- String name, boolean isGroupable) {
- return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable);
- }
-
- public static Object createUserRoute(Object routerObj, Object categoryObj) {
- return ((android.media.MediaRouter)routerObj).createUserRoute(
- (android.media.MediaRouter.RouteCategory)categoryObj);
- }
-
- public static void addUserRoute(Object routerObj, Object routeObj) {
- ((android.media.MediaRouter)routerObj).addUserRoute(
- (android.media.MediaRouter.UserRouteInfo)routeObj);
- }
-
- public static void removeUserRoute(Object routerObj, Object routeObj) {
- ((android.media.MediaRouter)routerObj).removeUserRoute(
- (android.media.MediaRouter.UserRouteInfo)routeObj);
- }
-
- public static Object createCallback(Callback callback) {
- return new CallbackProxy<Callback>(callback);
- }
-
- public static Object createVolumeCallback(VolumeCallback callback) {
- return new VolumeCallbackProxy<VolumeCallback>(callback);
- }
-
- public static final class RouteInfo {
- public static CharSequence getName(Object routeObj, Context context) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
- }
-
- public static CharSequence getStatus(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus();
- }
-
- public static int getSupportedTypes(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes();
- }
-
- public static Object getCategory(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory();
- }
-
- public static Drawable getIconDrawable(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable();
- }
-
- public static int getPlaybackType(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType();
- }
-
- public static int getPlaybackStream(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream();
- }
-
- public static int getVolume(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume();
- }
-
- public static int getVolumeMax(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax();
- }
-
- public static int getVolumeHandling(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling();
- }
-
- public static Object getTag(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getTag();
- }
-
- public static void setTag(Object routeObj, Object tag) {
- ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag);
- }
-
- public static void requestSetVolume(Object routeObj, int volume) {
- ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume);
- }
-
- public static void requestUpdateVolume(Object routeObj, int direction) {
- ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction);
- }
-
- public static Object getGroup(Object routeObj) {
- return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup();
- }
-
- public static boolean isGroup(Object routeObj) {
- return routeObj instanceof android.media.MediaRouter.RouteGroup;
- }
- }
-
- public static final class RouteGroup {
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public static List getGroupedRoutes(Object groupObj) {
- final android.media.MediaRouter.RouteGroup group =
- (android.media.MediaRouter.RouteGroup)groupObj;
- final int count = group.getRouteCount();
- List out = new ArrayList(count);
- for (int i = 0; i < count; i++) {
- out.add(group.getRouteAt(i));
- }
- return out;
- }
- }
-
- public static final class UserRouteInfo {
- public static void setName(Object routeObj, CharSequence name) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name);
- }
-
- public static void setStatus(Object routeObj, CharSequence status) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status);
- }
-
- public static void setIconDrawable(Object routeObj, Drawable icon) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon);
- }
-
- public static void setPlaybackType(Object routeObj, int type) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type);
- }
-
- public static void setPlaybackStream(Object routeObj, int stream) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream);
- }
-
- public static void setVolume(Object routeObj, int volume) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume);
- }
-
- public static void setVolumeMax(Object routeObj, int volumeMax) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax);
- }
-
- public static void setVolumeHandling(Object routeObj, int volumeHandling) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling);
- }
-
- public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback(
- (android.media.MediaRouter.VolumeCallback)volumeCallbackObj);
- }
-
- public static void setRemoteControlClient(Object routeObj, Object rccObj) {
- ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient(
- (android.media.RemoteControlClient)rccObj);
- }
- }
-
- public static final class RouteCategory {
- public static CharSequence getName(Object categoryObj, Context context) {
- return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context);
- }
-
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public static List getRoutes(Object categoryObj) {
- ArrayList out = new ArrayList();
- ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out);
- return out;
- }
-
- public static int getSupportedTypes(Object categoryObj) {
- return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes();
- }
-
- public static boolean isGroupable(Object categoryObj) {
- return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable();
- }
- }
-
- public static interface Callback {
- public void onRouteSelected(int type, Object routeObj);
- public void onRouteUnselected(int type, Object routeObj);
- public void onRouteAdded(Object routeObj);
- public void onRouteRemoved(Object routeObj);
- public void onRouteChanged(Object routeObj);
- public void onRouteGrouped(Object routeObj, Object groupObj, int index);
- public void onRouteUngrouped(Object routeObj, Object groupObj);
- public void onRouteVolumeChanged(Object routeObj);
- }
-
- public static interface VolumeCallback {
- public void onVolumeSetRequest(Object routeObj, int volume);
- public void onVolumeUpdateRequest(Object routeObj, int direction);
- }
-
- /**
- * Workaround for limitations of selectRoute() on JB and JB MR1.
- * Do not use on JB MR2 and above.
- */
- public static final class SelectRouteWorkaround {
- private Method mSelectRouteIntMethod;
-
- public SelectRouteWorkaround() {
- if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
- throw new UnsupportedOperationException();
- }
- try {
- mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
- "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
- } catch (NoSuchMethodException ex) {
- }
- }
-
- public void selectRoute(Object routerObj, int types, Object routeObj) {
- android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
- android.media.MediaRouter.RouteInfo route =
- (android.media.MediaRouter.RouteInfo)routeObj;
-
- int routeTypes = route.getSupportedTypes();
- if ((routeTypes & ROUTE_TYPE_USER) == 0) {
- // Handle non-user routes.
- // On JB and JB MR1, the selectRoute() API only supports programmatically
- // selecting user routes. So instead we rely on the hidden selectRouteInt()
- // method on these versions of the platform.
- // This limitation was removed in JB MR2.
- if (mSelectRouteIntMethod != null) {
- try {
- mSelectRouteIntMethod.invoke(router, types, route);
- return; // success!
- } catch (IllegalAccessException ex) {
- Log.w(TAG, "Cannot programmatically select non-user route. "
- + "Media routing may not work.", ex);
- } catch (InvocationTargetException ex) {
- Log.w(TAG, "Cannot programmatically select non-user route. "
- + "Media routing may not work.", ex);
- }
- } else {
- Log.w(TAG, "Cannot programmatically select non-user route "
- + "because the platform is missing the selectRouteInt() "
- + "method. Media routing may not work.");
- }
- }
-
- // Default handling.
- router.selectRoute(types, route);
- }
- }
-
- /**
- * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
- * Do not use on JB MR2 and above.
- */
- public static final class GetDefaultRouteWorkaround {
- private Method mGetSystemAudioRouteMethod;
-
- public GetDefaultRouteWorkaround() {
- if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
- throw new UnsupportedOperationException();
- }
- try {
- mGetSystemAudioRouteMethod =
- android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
- } catch (NoSuchMethodException ex) {
- }
- }
-
- public Object getDefaultRoute(Object routerObj) {
- android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
-
- if (mGetSystemAudioRouteMethod != null) {
- try {
- return mGetSystemAudioRouteMethod.invoke(router);
- } catch (IllegalAccessException ex) {
- } catch (InvocationTargetException ex) {
- }
- }
-
- // Could not find the method or it does not work.
- // Return the first route and hope for the best.
- return router.getRouteAt(0);
- }
- }
-
- static class CallbackProxy<T extends Callback>
- extends android.media.MediaRouter.Callback {
- protected final T mCallback;
-
- public CallbackProxy(T callback) {
- mCallback = callback;
- }
-
- @Override
- public void onRouteSelected(android.media.MediaRouter router,
- int type, android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteSelected(type, route);
- }
-
- @Override
- public void onRouteUnselected(android.media.MediaRouter router,
- int type, android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteUnselected(type, route);
- }
-
- @Override
- public void onRouteAdded(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteAdded(route);
- }
-
- @Override
- public void onRouteRemoved(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteRemoved(route);
- }
-
- @Override
- public void onRouteChanged(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteChanged(route);
- }
-
- @Override
- public void onRouteGrouped(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route,
- android.media.MediaRouter.RouteGroup group, int index) {
- mCallback.onRouteGrouped(route, group, index);
- }
-
- @Override
- public void onRouteUngrouped(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route,
- android.media.MediaRouter.RouteGroup group) {
- mCallback.onRouteUngrouped(route, group);
- }
-
- @Override
- public void onRouteVolumeChanged(android.media.MediaRouter router,
- android.media.MediaRouter.RouteInfo route) {
- mCallback.onRouteVolumeChanged(route);
- }
- }
-
- static class VolumeCallbackProxy<T extends VolumeCallback>
- extends android.media.MediaRouter.VolumeCallback {
- protected final T mCallback;
-
- public VolumeCallbackProxy(T callback) {
- mCallback = callback;
- }
-
- @Override
- public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route,
- int volume) {
- mCallback.onVolumeSetRequest(route, volume);
- }
-
- @Override
- public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route,
- int direction) {
- mCallback.onVolumeUpdateRequest(route, direction);
- }
- }
-}
diff --git a/v4/kitkat/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java b/v4/kitkat/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java
index dc70c6e..67a5a8c 100644
--- a/v4/kitkat/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java
+++ b/v4/kitkat/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java
@@ -16,7 +16,9 @@
package android.support.v4.graphics.drawable;
+import android.content.res.Resources;
import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
class DrawableWrapperKitKat extends DrawableWrapperHoneycomb {
@@ -24,6 +26,10 @@
super(drawable);
}
+ DrawableWrapperKitKat(DrawableWrapperState state, Resources resources) {
+ super(state, resources);
+ }
+
@Override
public void setAutoMirrored(boolean mirrored) {
mDrawable.setAutoMirrored(mirrored);
@@ -33,4 +39,21 @@
public boolean isAutoMirrored() {
return mDrawable.isAutoMirrored();
}
+
+ @Override
+ DrawableWrapperState mutateConstantState() {
+ return new DrawableWrapperStateKitKat(mState, null);
+ }
+
+ private static class DrawableWrapperStateKitKat extends DrawableWrapperState {
+ DrawableWrapperStateKitKat(@Nullable DrawableWrapperState orig,
+ @Nullable Resources res) {
+ super(orig, res);
+ }
+
+ @Override
+ public Drawable newDrawable(@Nullable Resources res) {
+ return new DrawableWrapperKitKat(this, res);
+ }
+ }
}
diff --git a/v4/tests/AndroidManifest.xml b/v4/tests/AndroidManifest.xml
index f99c5d1..85ffd7d 100644
--- a/v4/tests/AndroidManifest.xml
+++ b/v4/tests/AndroidManifest.xml
@@ -20,7 +20,8 @@
<uses-sdk
android:minSdkVersion="4"
android:targetSdkVersion="23"
- tools:overrideLibrary="android.support.test,android.support.test.espresso, android.support.test.espresso.idling"/>
+ tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
+ android.support.test.espresso, android.support.test.espresso.idling"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -29,7 +30,7 @@
<application android:supportsRtl="true">
<uses-library android:name="android.test.runner" />
- <activity android:name="android.support.v4.widget.test.TextViewTestActivity"/>
+ <activity android:name="android.support.v4.widget.TextViewTestActivity"/>
<activity android:name="android.support.v4.view.ViewPagerWithTitleStripActivity"/>
diff --git a/v4/tests/java/android/support/v4/BaseInstrumentationTestCase.java b/v4/tests/java/android/support/v4/BaseInstrumentationTestCase.java
new file mode 100644
index 0000000..5f9ce85
--- /dev/null
+++ b/v4/tests/java/android/support/v4/BaseInstrumentationTestCase.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2016 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.support.v4;
+
+import android.app.Activity;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public abstract class BaseInstrumentationTestCase<A extends Activity> {
+ @Rule
+ public final ActivityTestRule<A> mActivityTestRule;
+
+ protected BaseInstrumentationTestCase(Class<A> activityClass) {
+ mActivityTestRule = new ActivityTestRule<A>(activityClass);
+ }
+}
diff --git a/v4/tests/java/android/support/v4/BaseTestActivity.java b/v4/tests/java/android/support/v4/BaseTestActivity.java
new file mode 100755
index 0000000..e0682ce
--- /dev/null
+++ b/v4/tests/java/android/support/v4/BaseTestActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 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.support.v4;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.WindowManager;
+
+public abstract class BaseTestActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ final int contentView = getContentViewLayoutResId();
+ if (contentView > 0) {
+ setContentView(contentView);
+ }
+ onContentViewSet();
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ protected abstract int getContentViewLayoutResId();
+
+ protected void onContentViewSet() {}
+}
diff --git a/v4/tests/java/android/support/v4/testutils/LayoutDirectionActions.java b/v4/tests/java/android/support/v4/testutils/LayoutDirectionActions.java
new file mode 100755
index 0000000..25ae971
--- /dev/null
+++ b/v4/tests/java/android/support/v4/testutils/LayoutDirectionActions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.support.v4.testutils;
+
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v4.view.ViewCompat;
+import android.view.View;
+import org.hamcrest.Matcher;
+
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+
+public class LayoutDirectionActions {
+ /**
+ * Sets layout direction on the view.
+ */
+ public static ViewAction setLayoutDirection(final int layoutDirection) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
+
+ @Override
+ public String getDescription() {
+ return "set layout direction";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ ViewCompat.setLayoutDirection(view, layoutDirection);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+}
diff --git a/v4/tests/java/android/support/v4/testutils/TextViewActions.java b/v4/tests/java/android/support/v4/testutils/TextViewActions.java
new file mode 100644
index 0000000..f67e8c0
--- /dev/null
+++ b/v4/tests/java/android/support/v4/testutils/TextViewActions.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2016 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.support.v4.testutils;
+
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.StyleRes;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v4.widget.TextViewCompat;
+import android.view.View;
+import android.widget.TextView;
+import org.hamcrest.Matcher;
+
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+
+public class TextViewActions {
+ /**
+ * Sets max lines count on <code>TextView</code>.
+ */
+ public static ViewAction setMaxLines(final int maxLines) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set max lines";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ textView.setMaxLines(maxLines);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets min lines count on <code>TextView</code>.
+ */
+ public static ViewAction setMinLines(final int minLines) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set min lines";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ textView.setMinLines(minLines);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets text content on <code>TextView</code>.
+ */
+ public static ViewAction setText(final @StringRes int stringResId) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set text";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ textView.setText(stringResId);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets text appearance on <code>TextView</code>.
+ */
+ public static ViewAction setTextAppearance(final @StyleRes int styleResId) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set text appearance";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ TextViewCompat.setTextAppearance(textView, styleResId);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets compound drawables on <code>TextView</code>.
+ */
+ public static ViewAction setCompoundDrawablesRelative(final @Nullable Drawable start,
+ final @Nullable Drawable top, final @Nullable Drawable end,
+ final @Nullable Drawable bottom) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set compound drawables";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ TextViewCompat.setCompoundDrawablesRelative(textView, start, top, end, bottom);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets compound drawables on <code>TextView</code>.
+ */
+ public static ViewAction setCompoundDrawablesRelativeWithIntrinsicBounds(
+ final @Nullable Drawable start, final @Nullable Drawable top,
+ final @Nullable Drawable end, final @Nullable Drawable bottom) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set compound drawables";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ textView, start, top, end, bottom);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets compound drawables on <code>TextView</code>.
+ */
+ public static ViewAction setCompoundDrawablesRelativeWithIntrinsicBounds(
+ final @DrawableRes int start, final @DrawableRes int top, final @DrawableRes int end,
+ final @DrawableRes int bottom) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(TextView.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "TextView set compound drawables";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ TextView textView = (TextView) view;
+ TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ textView, start, top, end, bottom);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+}
diff --git a/v4/tests/java/android/support/v4/view/BaseViewPagerTest.java b/v4/tests/java/android/support/v4/view/BaseViewPagerTest.java
index 166fb1e..13d8372 100644
--- a/v4/tests/java/android/support/v4/view/BaseViewPagerTest.java
+++ b/v4/tests/java/android/support/v4/view/BaseViewPagerTest.java
@@ -17,47 +17,32 @@
import android.app.Activity;
import android.graphics.Color;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.support.v4.test.R;
+import android.support.v4.testutils.TestUtilsAssertions;
+import android.support.v4.testutils.TestUtilsMatchers;
+import android.test.suitebuilder.annotation.SmallTest;
import android.text.TextUtils;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
-
-import android.support.test.espresso.Espresso;
-import android.support.test.espresso.UiController;
-import android.support.v4.test.R;
-import android.support.v4.testutils.TestUtilsAssertions;
-import android.support.v4.testutils.TestUtilsMatchers;
-import android.support.v4.view.ViewPager;
-import android.support.v4.view.PagerTitleStrip;
-import android.support.v4.widget.TestActivity;
-
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.suitebuilder.annotation.SmallTest;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
import java.util.ArrayList;
-import org.hamcrest.Matcher;
-
import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.swipeLeft;
import static android.support.test.espresso.action.ViewActions.swipeRight;
+import static android.support.test.espresso.assertion.PositionAssertions.*;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
-import static android.support.test.espresso.assertion.PositionAssertions.isBelow;
-import static android.support.test.espresso.assertion.PositionAssertions.isBottomAlignedWith;
-import static android.support.test.espresso.assertion.PositionAssertions.isLeftAlignedWith;
-import static android.support.test.espresso.assertion.PositionAssertions.isRightAlignedWith;
-import static android.support.test.espresso.assertion.PositionAssertions.isTopAlignedWith;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
-import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-
+import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
/**
* Base class for testing <code>ViewPager</code>. Most of the testing logic should be in this
@@ -67,8 +52,7 @@
* Testing logic that does depend on the specific pager title implementation is pushed into the
* extending classes in <code>assertStripInteraction()</code> method.
*/
-public abstract class BaseViewPagerTest<T extends Activity>
- extends ActivityInstrumentationTestCase2<T> {
+public abstract class BaseViewPagerTest<T extends Activity> extends BaseInstrumentationTestCase<T> {
protected ViewPager mViewPager;
protected static class BasePagerAdapter<Q> extends PagerAdapter {
@@ -181,14 +165,12 @@
}
public BaseViewPagerTest(Class<T> activityClass) {
- super("android.support.v4.view", activityClass);
+ super(activityClass);
}
- @Override
+ @Before
public void setUp() throws Exception {
- super.setUp();
-
- final T activity = getActivity();
+ final T activity = mActivityTestRule.getActivity();
mViewPager = (ViewPager) activity.findViewById(R.id.pager);
ColorPagerAdapter adapter = new ColorPagerAdapter();
@@ -199,13 +181,12 @@
ViewPagerActions.scrollToPage(0));
}
- @Override
+ @After
public void tearDown() throws Exception {
onView(withId(R.id.pager)).perform(ViewPagerActions.setAdapter(null));
-
- super.tearDown();
}
+ @Test
@SmallTest
public void testPageSelections() {
assertEquals("Initial state", 0, mViewPager.getCurrentItem());
@@ -232,6 +213,7 @@
}
+ @Test
@SmallTest
public void testPageSwipes() {
assertEquals("Initial state", 0, mViewPager.getCurrentItem());
@@ -257,6 +239,7 @@
assertEquals("Swipe right beyond first page", 0, mViewPager.getCurrentItem());
}
+ @Test
@SmallTest
public void testPageSwipesComposite() {
assertEquals("Initial state", 0, mViewPager.getCurrentItem());
@@ -275,6 +258,7 @@
assertEquals("Swipe right beyond first page and then left", 1, mViewPager.getCurrentItem());
}
+ @Test
@SmallTest
public void testPageContent() {
assertEquals("Initial state", 0, mViewPager.getCurrentItem());
@@ -315,6 +299,7 @@
TestUtilsMatchers.backgroundColor(Color.BLUE))));
}
+ @Test
@SmallTest
public void testAdapterChange() {
// Verify that we have the expected initial adapter
@@ -427,6 +412,7 @@
}
}
+ @Test
@SmallTest
public void testPagerStrip() {
// Set an adapter with 5 pages
diff --git a/v4/tests/java/android/support/v4/view/GravityCompatTest.java b/v4/tests/java/android/support/v4/view/GravityCompatTest.java
index db62b0c..2e2f180 100644
--- a/v4/tests/java/android/support/v4/view/GravityCompatTest.java
+++ b/v4/tests/java/android/support/v4/view/GravityCompatTest.java
@@ -17,15 +17,10 @@
import android.graphics.Rect;
import android.os.Build;
-import android.view.Gravity;
-import android.view.View;
-
import android.support.v4.testutils.TestUtils;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import android.view.Gravity;
public class GravityCompatTest extends AndroidTestCase {
@SmallTest
diff --git a/v4/tests/java/android/support/v4/view/MarginLayoutParamsCompatTest.java b/v4/tests/java/android/support/v4/view/MarginLayoutParamsCompatTest.java
index db08a1a..057db99 100644
--- a/v4/tests/java/android/support/v4/view/MarginLayoutParamsCompatTest.java
+++ b/v4/tests/java/android/support/v4/view/MarginLayoutParamsCompatTest.java
@@ -15,16 +15,10 @@
*/
package android.support.v4.view;
-import android.view.View;
-
-import android.support.v4.view.MarginLayoutParamsCompat;
-import android.support.v4.view.ViewCompat;
-
import android.os.Build;
-import android.view.ViewGroup;
-
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import android.view.ViewGroup;
public class MarginLayoutParamsCompatTest extends AndroidTestCase {
@SmallTest
diff --git a/v4/tests/java/android/support/v4/view/ViewCompatTest.java b/v4/tests/java/android/support/v4/view/ViewCompatTest.java
index 483d16b..29a0f5a 100644
--- a/v4/tests/java/android/support/v4/view/ViewCompatTest.java
+++ b/v4/tests/java/android/support/v4/view/ViewCompatTest.java
@@ -15,12 +15,9 @@
*/
package android.support.v4.view;
-import android.view.View;
-
-import android.support.v4.view.ViewCompat;
-
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
public class ViewCompatTest extends AndroidTestCase {
@SmallTest
diff --git a/v4/tests/java/android/support/v4/view/ViewPagerActions.java b/v4/tests/java/android/support/v4/view/ViewPagerActions.java
index b1e1a76..2b37e6f 100644
--- a/v4/tests/java/android/support/v4/view/ViewPagerActions.java
+++ b/v4/tests/java/android/support/v4/view/ViewPagerActions.java
@@ -22,20 +22,12 @@
import android.support.test.espresso.action.GeneralClickAction;
import android.support.test.espresso.action.Press;
import android.support.test.espresso.action.Tap;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.TextView;
-
import org.hamcrest.Matcher;
-import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
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.isDisplayingAtLeast;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
-
-import static org.hamcrest.Matchers.allOf;
public class ViewPagerActions {
/**
diff --git a/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripActivity.java b/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripActivity.java
index edc4da9..2cb3048 100644
--- a/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripActivity.java
+++ b/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripActivity.java
@@ -21,8 +21,6 @@
import android.support.v4.test.R;
import android.view.WindowManager;
-import java.util.ArrayList;
-
public class ViewPagerWithTabStripActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
diff --git a/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripTest.java b/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripTest.java
index 706d2e2..3e4a82f 100644
--- a/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripTest.java
+++ b/v4/tests/java/android/support/v4/view/ViewPagerWithTabStripTest.java
@@ -15,7 +15,6 @@
*/
package android.support.v4.view;
-import android.support.v4.view.PagerTitleStrip;
import android.support.v4.test.R;
import static android.support.test.espresso.Espresso.onView;
@@ -23,8 +22,8 @@
import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
-
import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertEquals;
/**
* Provides assertions that depend on the interactive nature of <code>PagerTabStrip</code>.
diff --git a/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripActivity.java b/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripActivity.java
index 28dcb07..49d6e76 100644
--- a/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripActivity.java
+++ b/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripActivity.java
@@ -21,8 +21,6 @@
import android.support.v4.test.R;
import android.view.WindowManager;
-import java.util.ArrayList;
-
public class ViewPagerWithTitleStripActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
diff --git a/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripTest.java b/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripTest.java
index 528db86..8d831dc 100644
--- a/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripTest.java
+++ b/v4/tests/java/android/support/v4/view/ViewPagerWithTitleStripTest.java
@@ -15,7 +15,6 @@
*/
package android.support.v4.view;
-import android.support.v4.view.PagerTitleStrip;
import android.support.v4.test.R;
import static android.support.test.espresso.Espresso.onView;
@@ -23,8 +22,8 @@
import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
-
import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertEquals;
/**
* Provides assertions that depend on the non-interactive nature of <code>PagerTabStrip</code>.
diff --git a/v4/tests/java/android/support/v4/widget/TextViewCompatTest.java b/v4/tests/java/android/support/v4/widget/TextViewCompatTest.java
index d0f5b00..3ef236f 100644
--- a/v4/tests/java/android/support/v4/widget/TextViewCompatTest.java
+++ b/v4/tests/java/android/support/v4/widget/TextViewCompatTest.java
@@ -17,35 +17,27 @@
package android.support.v4.widget;
-import org.junit.After;
+import android.content.res.Resources;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.support.v4.test.R;
+import android.support.v4.testutils.TestUtils;
+import android.support.v4.view.ViewCompat;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.widget.TextView;
import org.junit.Before;
import org.junit.Test;
-import org.junit.runner.RunWith;
-import android.app.Instrumentation;
-import android.content.res.Resources;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.ColorDrawable;
-import android.os.Looper;
-import android.test.ActivityInstrumentationTestCase2;
-import android.test.UiThreadTest;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.support.annotation.ColorInt;
-import android.support.annotation.LayoutRes;
-import android.support.test.InstrumentationRegistry;
-import android.support.v4.test.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v4.testutils.TestUtils;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.TextView;
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.v4.testutils.LayoutDirectionActions.setLayoutDirection;
+import static android.support.v4.testutils.TextViewActions.*;
+import static org.junit.Assert.*;
-public class TextViewCompatTest extends ActivityInstrumentationTestCase2<TestActivity> {
+public class TextViewCompatTest extends BaseInstrumentationTestCase<TextViewTestActivity> {
private static final String TAG = "TextViewCompatTest";
private TextView mTextView;
@@ -72,127 +64,75 @@
}
public TextViewCompatTest() {
- super("android.support.v4.widget", TestActivity.class);
+ super(TextViewTestActivity.class);
}
- @Override
- public void tearDown() throws Exception {
- if (mTextView != null) {
- removeTextView();
- }
-
- getInstrumentation().waitForIdleSync();
- super.tearDown();
+ @Before
+ public void setUp() {
+ mTextView = (TextView) mActivityTestRule.getActivity().findViewById(R.id.text_view);
}
- private boolean isMainThread() {
- return Looper.myLooper() == Looper.getMainLooper();
- }
-
- private void removeTextView() {
- if (mTextView == null) {
- return;
- }
- if (!isMainThread()) {
- getInstrumentation().waitForIdleSync();
- }
- try {
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- getActivity().mContainer.removeAllViews();
- }
- });
- } catch (Throwable throwable) {
- Log.e(TAG, "", throwable);
- }
- mTextView = null;
- }
-
- private void createAndAddTextView() {
- final TestActivity activity = getActivity();
- mTextView = new TextView(activity);
- activity.mContainer.addView(mTextView);
-
- // Explicitly measure and layout the text view. This way the core TextView updates its
- // internal tracking of various visual facets so those can be tested in the relevant
- // tests - such as, for example, where each drawable is positioned relative to the text.
- final DisplayMetrics metrics = getActivity().getResources().getDisplayMetrics();
- int textViewWidthPx = TestUtils.convertSizeDipsToPixels(metrics, 200);
- int textViewHeightPx = TestUtils.convertSizeDipsToPixels(metrics, 60);
- mTextView.measure(
- View.MeasureSpec.makeMeasureSpec(textViewWidthPx, View.MeasureSpec.EXACTLY),
- View.MeasureSpec.makeMeasureSpec(textViewHeightPx, View.MeasureSpec.EXACTLY));
- mTextView.layout(0, 0, textViewWidthPx, textViewHeightPx);
- }
-
- @UiThreadTest
+ @Test
@SmallTest
public void testMaxLines() throws Throwable {
- createAndAddTextView();
final int maxLinesCount = 4;
- mTextView.setMaxLines(maxLinesCount);
+ onView(withId(R.id.text_view)).perform(setMaxLines(maxLinesCount));
assertEquals("Empty view: Max lines must match", TextViewCompat.getMaxLines(mTextView),
maxLinesCount);
- mTextView.setText(R.string.test_text_short);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_short));
assertEquals("Short text: Max lines must match", TextViewCompat.getMaxLines(mTextView),
maxLinesCount);
- mTextView.setText(R.string.test_text_medium);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_medium));
assertEquals("Medium text: Max lines must match", TextViewCompat.getMaxLines(mTextView),
maxLinesCount);
- mTextView.setText(R.string.test_text_long);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
assertEquals("Long text: Max lines must match", TextViewCompat.getMaxLines(mTextView),
maxLinesCount);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testMinLines() throws Throwable {
- createAndAddTextView();
final int minLinesCount = 3;
- mTextView.setMinLines(minLinesCount);
+ onView(withId(R.id.text_view)).perform(setMinLines(minLinesCount));
assertEquals("Empty view: Min lines must match", TextViewCompat.getMinLines(mTextView),
minLinesCount);
- mTextView.setText(R.string.test_text_short);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_short));
assertEquals("Short text: Min lines must match", TextViewCompat.getMinLines(mTextView),
minLinesCount);
- mTextView.setText(R.string.test_text_medium);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_medium));
assertEquals("Medium text: Min lines must match", TextViewCompat.getMinLines(mTextView),
minLinesCount);
- mTextView.setText(R.string.test_text_long);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
assertEquals("Long text: Min lines must match", TextViewCompat.getMinLines(mTextView),
minLinesCount);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testStyle() throws Throwable {
- createAndAddTextView();
+ onView(withId(R.id.text_view)).perform(setTextAppearance(R.style.TextMediumStyle));
- TextViewCompat.setTextAppearance(mTextView, R.style.TextMediumStyle);
-
- final Resources res = getActivity().getResources();
+ final Resources res = mActivityTestRule.getActivity().getResources();
assertTrue("Styled text view: style",
mTextView.getTypeface().isItalic() || (mTextView.getPaint().getTextSkewX() < 0));
assertEquals("Styled text view: color", mTextView.getTextColors().getDefaultColor(),
res.getColor(R.color.text_color));
assertEquals("Styled text view: size", mTextView.getTextSize(),
- (float) res.getDimensionPixelSize(R.dimen.text_medium_size));
+ (float) res.getDimensionPixelSize(R.dimen.text_medium_size), 1.0f);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testCompoundDrawablesRelative() throws Throwable {
- createAndAddTextView();
-
final Drawable drawableStart = new ColorDrawable(0xFFFF0000);
drawableStart.setBounds(0, 0, 20, 20);
final Drawable drawableTop = new ColorDrawable(0xFF00FF00);
@@ -200,9 +140,9 @@
final Drawable drawableEnd = new ColorDrawable(0xFF0000FF);
drawableEnd.setBounds(0, 0, 25, 20);
- mTextView.setText(R.string.test_text_medium);
- TextViewCompat.setCompoundDrawablesRelative(mTextView, drawableStart, drawableTop,
- drawableEnd, null);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_medium));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelative(drawableStart,
+ drawableTop, drawableEnd, null));
final Drawable[] drawablesAbsolute = mTextView.getCompoundDrawables();
@@ -227,12 +167,10 @@
assertNull("Compound drawable: bottom", drawablesAbsolute[3]);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testCompoundDrawablesRelativeRtl() throws Throwable {
- createAndAddTextView();
-
- ViewCompat.setLayoutDirection(mTextView, ViewCompat.LAYOUT_DIRECTION_RTL);
+ onView(withId(R.id.text_view)).perform(setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
final Drawable drawableStart = new ColorDrawable(0xFFFF0000);
drawableStart.setBounds(0, 0, 20, 20);
@@ -241,9 +179,9 @@
final Drawable drawableEnd = new ColorDrawable(0xFF0000FF);
drawableEnd.setBounds(0, 0, 25, 20);
- mTextView.setText(R.string.test_text_medium);
- TextViewCompat.setCompoundDrawablesRelative(mTextView, drawableStart, drawableTop,
- drawableEnd, null);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_medium));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelative(drawableStart,
+ drawableTop, drawableEnd, null));
// Check to see whether our text view is under RTL mode
if (ViewCompat.getLayoutDirection(mTextView) != ViewCompat.LAYOUT_DIRECTION_RTL) {
@@ -276,18 +214,16 @@
assertNull("Compound drawable: bottom", drawablesAbsolute[3]);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testCompoundDrawablesRelativeWithIntrinsicBounds() throws Throwable {
- createAndAddTextView();
-
final Drawable drawableStart = new TestDrawable(0xFFFF0000, 30, 20);
final Drawable drawableEnd = new TestDrawable(0xFF0000FF, 25, 45);
final Drawable drawableBottom = new TestDrawable(0xFF00FF00, 15, 35);
- mTextView.setText(R.string.test_text_long);
- TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mTextView, drawableStart,
- null, drawableEnd, drawableBottom);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelativeWithIntrinsicBounds(
+ drawableStart, null, drawableEnd, drawableBottom));
final Drawable[] drawablesAbsolute = mTextView.getCompoundDrawables();
@@ -312,20 +248,18 @@
drawablesAbsolute[3].getBounds().height(), 35);
}
- @UiThreadTest
+ @Test
@SmallTest
public void testCompoundDrawablesRelativeWithIntrinsicBoundsRtl() throws Throwable {
- createAndAddTextView();
-
- ViewCompat.setLayoutDirection(mTextView, ViewCompat.LAYOUT_DIRECTION_RTL);
+ onView(withId(R.id.text_view)).perform(setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
final Drawable drawableStart = new TestDrawable(0xFFFF0000, 30, 20);
final Drawable drawableEnd = new TestDrawable(0xFF0000FF, 25, 45);
final Drawable drawableBottom = new TestDrawable(0xFF00FF00, 15, 35);
- mTextView.setText(R.string.test_text_long);
- TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mTextView, drawableStart,
- null, drawableEnd, drawableBottom);
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelativeWithIntrinsicBounds(
+ drawableStart, null, drawableEnd, drawableBottom));
// Check to see whether our text view is under RTL mode
if (ViewCompat.getLayoutDirection(mTextView) != ViewCompat.LAYOUT_DIRECTION_RTL) {
@@ -358,18 +292,16 @@
drawablesAbsolute[3].getBounds().height(), 35);
}
- @UiThreadTest
+ @Test
@MediumTest
public void testCompoundDrawablesRelativeWithIntrinsicBoundsById() throws Throwable {
- createAndAddTextView();
-
- mTextView.setText(R.string.test_text_long);
- TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mTextView,
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.test_drawable_red, 0,
- R.drawable.test_drawable_green, R.drawable.test_drawable_blue);
+ R.drawable.test_drawable_green, R.drawable.test_drawable_blue));
final Drawable[] drawablesAbsolute = mTextView.getCompoundDrawables();
- final Resources res = getActivity().getResources();
+ final Resources res = mActivityTestRule.getActivity().getResources();
// The entire left drawable should be the specific red color
TestUtils.assertAllPixelsOfColor("Compound drawable: left color",
@@ -404,17 +336,15 @@
res.getDimensionPixelSize(R.dimen.drawable_small_size));
}
- @UiThreadTest
+ @Test
@MediumTest
public void testCompoundDrawablesRelativeWithIntrinsicBoundsByIdRtl() throws Throwable {
- createAndAddTextView();
+ onView(withId(R.id.text_view)).perform(setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
- ViewCompat.setLayoutDirection(mTextView, ViewCompat.LAYOUT_DIRECTION_RTL);
-
- mTextView.setText(R.string.test_text_long);
- TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mTextView,
+ onView(withId(R.id.text_view)).perform(setText(R.string.test_text_long));
+ onView(withId(R.id.text_view)).perform(setCompoundDrawablesRelativeWithIntrinsicBounds(
R.drawable.test_drawable_red, 0,
- R.drawable.test_drawable_green, R.drawable.test_drawable_blue);
+ R.drawable.test_drawable_green, R.drawable.test_drawable_blue));
// Check to see whether our text view is under RTL mode
if (ViewCompat.getLayoutDirection(mTextView) != ViewCompat.LAYOUT_DIRECTION_RTL) {
@@ -423,7 +353,7 @@
}
final Drawable[] drawablesAbsolute = mTextView.getCompoundDrawables();
- final Resources res = getActivity().getResources();
+ final Resources res = mActivityTestRule.getActivity().getResources();
// The entire left / end drawable should be the specific green color
TestUtils.assertAllPixelsOfColor("Compound drawable: left color",
diff --git a/v4/tests/java/android/support/v4/widget/test/TextViewTestActivity.java b/v4/tests/java/android/support/v4/widget/TextViewTestActivity.java
similarity index 68%
rename from v4/tests/java/android/support/v4/widget/test/TextViewTestActivity.java
rename to v4/tests/java/android/support/v4/widget/TextViewTestActivity.java
index 7366127..dcaab70 100644
--- a/v4/tests/java/android/support/v4/widget/test/TextViewTestActivity.java
+++ b/v4/tests/java/android/support/v4/widget/TextViewTestActivity.java
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package android.support.v4.widget.test;
+package android.support.v4.widget;
+import android.support.v4.BaseTestActivity;
+import android.support.v4.test.R;
-import android.app.Activity;
-
-public class TextViewTestActivity extends Activity {
-
+public class TextViewTestActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.text_view_activity;
+ }
}
diff --git a/v4/tests/res/layout/text_view_activity.xml b/v4/tests/res/layout/text_view_activity.xml
new file mode 100644
index 0000000..ba5d688
--- /dev/null
+++ b/v4/tests/res/layout/text_view_activity.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/text_view"
+ android:layout_width="200dip"
+ android:layout_height="60dip" />
+</FrameLayout>
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index 6f6fefb..3116eaf 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -24,7 +24,10 @@
tools:overrideLibrary="android.support.test, android.app, android.support.test.rule,
android.support.test.espresso, android.support.test.espresso.idling"/>
- <application android:theme="@style/Theme.AppCompat">
+ <application
+ android:theme="@style/Theme.AppCompat"
+ android:supportsRtl="true">
+
<uses-library android:name="android.test.runner"/>
<activity
@@ -42,6 +45,11 @@
android:theme="@style/Theme.SampleDrawerLayout" />
<activity
+ android:name="android.support.v7.app.DrawerLayoutDoubleActivity"
+ android:label="@string/drawer_layout_activity"
+ android:theme="@style/Theme.SampleDrawerLayout" />
+
+ <activity
android:name="android.support.v7.app.DrawerDynamicLayoutActivity"
android:label="@string/drawer_layout_activity"
android:theme="@style/Theme.SampleDrawerLayout" />
@@ -53,7 +61,7 @@
<activity
android:name="android.support.v7.widget.PopupTestActivity"
- android:label="@string/list_popup_window_activity"
+ android:label="@string/popup_activity"
android:theme="@style/Theme.AppCompat.Light" />
<activity
diff --git a/v7/appcompat/tests/res/layout/drawer_double_layout.xml b/v7/appcompat/tests/res/layout/drawer_double_layout.xml
new file mode 100644
index 0000000..1af5be3
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/drawer_double_layout.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 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.
+-->
+
+<!--
+ A DrawerLayout is indended to be used as the top-level content view
+ using match_parent for both width and height to consume the full space available.
+-->
+<android.support.v7.custom.CustomDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drawer_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+ <!-- As the main content view, the view below consumes the entire
+ space available using match_parent in both dimensions. Note that
+ this child does not specify android:layout_gravity attribute. -->
+ <LinearLayout
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <!-- This will be set as the support action bar of the activity at runtime.
+ It needs to be a dynamic runtime call for correct vertical layering of
+ the drawer and the toolbar. -->
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize" />
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbarStyle="outsideOverlay">
+ <TextView
+ android:id="@+id/content_text"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:text="@string/drawer_layout_summary"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:padding="16dp"/>
+ </ScrollView>
+ </LinearLayout>
+
+ <!-- android:layout_gravity="start" tells DrawerLayout to treat
+ this as a sliding drawer on the starting side, which is
+ left for left-to-right locales. The drawer is given a fixed
+ width in dp and extends the full height of the container. A
+ solid background is used for contrast with the content view.
+ android:fitsSystemWindows="true" tells the system to have
+ DrawerLayout span the full height of the screen, including the
+ system status bar on Lollipop+ versions of the plaform. -->
+ <ListView
+ android:id="@+id/start_drawer"
+ android:layout_width="300dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start"
+ android:background="#ff333333"
+ android:fitsSystemWindows="true"/>
+
+ <!-- End drawer with fixed width. -->
+ <FrameLayout
+ android:id="@+id/end_drawer"
+ android:layout_width="250dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="end"
+ android:background="#444" />
+</android.support.v7.custom.CustomDrawerLayout>
+
diff --git a/v7/appcompat/tests/res/layout/drawer_layout.xml b/v7/appcompat/tests/res/layout/drawer_layout.xml
index 7bed9df..c65eb72 100644
--- a/v7/appcompat/tests/res/layout/drawer_layout.xml
+++ b/v7/appcompat/tests/res/layout/drawer_layout.xml
@@ -27,34 +27,32 @@
<!-- As the main content view, the view below consumes the entire
space available using match_parent in both dimensions. Note that
this child does not specify android:layout_gravity attribute. -->
- <FrameLayout
- android:id="@+id/content"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ <LinearLayout
+ android:id="@+id/content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
<!-- This will be set as the support action bar of the activity at runtime.
It needs to be a dynamic runtime call for correct vertical layering of
the drawer and the toolbar. -->
<android.support.v7.widget.Toolbar
- android:id="@+id/toolbar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" />
- <!-- Note layout_marginTop attribute with action bar height as the value.
- This "pushes" down the main content so that it doesn't overlap with
- the toolbar. -->
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize" />
+
<ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scrollbarStyle="outsideOverlay">
+ <TextView
+ android:id="@+id/content_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:layout_marginTop="?attr/actionBarSize"
- android:scrollbarStyle="outsideOverlay">
- <TextView
- android:id="@+id/content_text"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:text="@string/drawer_layout_summary"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:padding="16dp"/>
+ android:text="@string/drawer_layout_summary"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:padding="16dp"/>
</ScrollView>
- </FrameLayout>
+ </LinearLayout>
<!-- android:layout_gravity="start" tells DrawerLayout to treat
this as a sliding drawer on the starting side, which is
diff --git a/v7/appcompat/tests/res/layout/popup_test_activity.xml b/v7/appcompat/tests/res/layout/popup_test_activity.xml
index 152ddaf..9ea488f 100644
--- a/v7/appcompat/tests/res/layout/popup_test_activity.xml
+++ b/v7/appcompat/tests/res/layout/popup_test_activity.xml
@@ -24,6 +24,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
- android:text="@string/list_popup_window_show" />
+ android:text="@string/popup_show" />
</FrameLayout>
diff --git a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
index e2f6b3f..e167c9b 100644
--- a/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
+++ b/v7/appcompat/tests/res/layout/toolbar_decor_content.xml
@@ -24,4 +24,9 @@
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"/>
+ <android.support.v7.custom.FitWindowsContentLayout
+ android:id="@+id/test_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
</LinearLayout>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/layout/window_decor_content.xml b/v7/appcompat/tests/res/layout/window_decor_content.xml
index 657f51c..2d79660 100644
--- a/v7/appcompat/tests/res/layout/window_decor_content.xml
+++ b/v7/appcompat/tests/res/layout/window_decor_content.xml
@@ -19,4 +19,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ <android.support.v7.custom.FitWindowsContentLayout
+ android:id="@+id/test_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
</LinearLayout>
\ No newline at end of file
diff --git a/v7/appcompat/tests/res/menu/popup_menu.xml b/v7/appcompat/tests/res/menu/popup_menu.xml
new file mode 100644
index 0000000..f50efc5
--- /dev/null
+++ b/v7/appcompat/tests/res/menu/popup_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 Google Inc.
+
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/action_highlight"
+ android:title="@string/popup_menu_highlight" />
+ <item android:id="@+id/action_edit"
+ android:title="@string/popup_menu_edit" />
+ <item android:id="@+id/action_delete"
+ android:title="@string/popup_menu_delete" />
+ <item android:id="@+id/action_ignore"
+ android:title="@string/popup_menu_ignore" />
+ <item android:id="@+id/action_share"
+ android:title="@string/popup_menu_share">
+ <menu>
+ <item android:id="@+id/action_share_email"
+ android:title="@string/popup_menu_share_email" />
+ <item android:id="@+id/action_share_circles"
+ android:title="@string/popup_menu_share_circles" />
+ </menu>
+ </item>
+ <item android:id="@+id/action_print"
+ android:title="@string/popup_menu_print" />
+</menu>
diff --git a/v7/appcompat/tests/res/values/strings.xml b/v7/appcompat/tests/res/values/strings.xml
index b7347a2..e094078 100644
--- a/v7/appcompat/tests/res/values/strings.xml
+++ b/v7/appcompat/tests/res/values/strings.xml
@@ -21,8 +21,16 @@
<string name="drawer_open">Open navigation drawer</string>
<string name="drawer_close">Close navigation drawer</string>
- <string name="list_popup_window_activity">List popup window</string>
- <string name="list_popup_window_show">Show popup</string>
+ <string name="popup_activity">Popup activity</string>
+ <string name="popup_show">Show popup</string>
+ <string name="popup_menu_highlight">Highlight</string>
+ <string name="popup_menu_edit">Edit</string>
+ <string name="popup_menu_delete">Delete</string>
+ <string name="popup_menu_ignore">Ignore</string>
+ <string name="popup_menu_share">Share</string>
+ <string name="popup_menu_share_email">Via email</string>
+ <string name="popup_menu_share_circles">To my circles</string>
+ <string name="popup_menu_print">Print</string>
<string name="alert_dialog_activity">Alert dialog</string>
<string name="alert_dialog_show">Show alert dialog</string>
diff --git a/v7/appcompat/tests/src/android/support/v7/app/BaseBasicsTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/BaseBasicsTestCase.java
index ed421a9..d017d52 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/BaseBasicsTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/BaseBasicsTestCase.java
@@ -16,12 +16,24 @@
package android.support.v7.app;
-import android.support.v7.testutils.BaseTestActivity;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
-
import org.junit.Test;
+import android.os.Build;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.custom.FitWindowsContentLayout;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.testutils.TestUtils;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import android.view.WindowInsets;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
public abstract class BaseBasicsTestCase<A extends BaseTestActivity>
extends BaseInstrumentationTestCase<A> {
@@ -56,28 +68,53 @@
}
@Test
- @MediumTest
+ @SmallTest
public void testMenuInvalidationAfterDestroy() throws Throwable {
final A activity = getActivity();
+ // Reset to make sure that we don't have a menu currently
+ activity.reset();
+ assertNull(activity.getMenu());
- getInstrumentation().runOnMainSync(new Runnable() {
- @Override
- public void run() {
- // Reset to make sure that we don't have a menu currently
- activity.reset();
- assertNull(activity.getMenu());
+ // Now destroy the Activity
+ activity.finish();
+ TestUtils.waitForActivityDestroyed(activity);
- // Now dispatch a menu invalidation
- activity.supportInvalidateOptionsMenu();
- // Now destroy the Activity
- getInstrumentation().callActivityOnDestroy(activity);
- }
- });
-
- // Now wait for any delayed menu population
- Thread.sleep(100);
+ // Now dispatch a menu invalidation and wait for an idle sync
+ activity.supportInvalidateOptionsMenu();
+ getInstrumentation().waitForIdleSync();
// Make sure that we don't have a menu given to use after being destroyed
assertNull(activity.getMenu());
}
+
+ @Test
+ @SmallTest
+ public void testFitSystemWindowsReachesContent() {
+ final FitWindowsContentLayout content =
+ (FitWindowsContentLayout) getActivity().findViewById(R.id.test_content);
+ assertNotNull(content);
+ assertTrue(content.getFitsSystemWindowsCalled());
+ }
+
+ @Test
+ @SmallTest
+ public void testOnApplyWindowInsetsReachesContent() {
+ if (Build.VERSION.SDK_INT < 21) {
+ // OnApplyWindowInsetsListener is only available on API 21+
+ return;
+ }
+
+ final View content = getActivity().findViewById(R.id.test_content);
+ assertNotNull(content);
+
+ final AtomicBoolean applyWindowInsetsCalled = new AtomicBoolean();
+ content.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
+ @Override
+ public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
+ applyWindowInsetsCalled.set(true);
+ return windowInsets;
+ }
+ });
+ assertTrue(applyWindowInsetsCalled.get());
+ }
}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/BaseInstrumentationTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/BaseInstrumentationTestCase.java
index 6591ff7..76ca5dc 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/BaseInstrumentationTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/BaseInstrumentationTestCase.java
@@ -16,28 +16,50 @@
package android.support.v7.app;
-import org.junit.Before;
+import org.junit.Rule;
import org.junit.runner.RunWith;
import android.app.Activity;
+import android.app.Instrumentation;
import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
-import android.test.ActivityInstrumentationTestCase2;
@RunWith(AndroidJUnit4.class)
-public abstract class BaseInstrumentationTestCase<A extends Activity>
- extends ActivityInstrumentationTestCase2<A> {
+public abstract class BaseInstrumentationTestCase<A extends Activity> {
+
+ @Rule
+ public final ActivityTestRule<A> mActivityTestRule;
protected BaseInstrumentationTestCase(Class<A> activityClass) {
- super(activityClass);
+ mActivityTestRule = new ActivityTestRule<A>(activityClass);
}
- @Before
- @Override
- public void setUp() throws Exception {
- super.setUp();
- injectInstrumentation(InstrumentationRegistry.getInstrumentation());
- getActivity();
+ @Deprecated
+ public A getActivity() {
+ return mActivityTestRule.getActivity();
+ }
+
+ @Deprecated
+ public Instrumentation getInstrumentation() {
+ return InstrumentationRegistry.getInstrumentation();
+ }
+
+ @Deprecated
+ public void runTestOnUiThread(final Runnable r) throws Throwable {
+ final Throwable[] exceptions = new Throwable[1];
+ getInstrumentation().runOnMainSync(new Runnable() {
+ public void run() {
+ try {
+ r.run();
+ } catch (Throwable throwable) {
+ exceptions[0] = throwable;
+ }
+ }
+ });
+ if (exceptions[0] != null) {
+ throw exceptions[0];
+ }
}
}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
index d0beb59..debf278 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyEventsTestCase.java
@@ -16,10 +16,10 @@
package android.support.v7.app;
-import android.support.v7.testutils.BaseTestActivity;
import org.junit.Test;
import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
import android.support.v7.view.ActionMode;
import android.test.suitebuilder.annotation.MediumTest;
import android.test.suitebuilder.annotation.SmallTest;
@@ -29,6 +29,11 @@
import java.util.concurrent.atomic.AtomicBoolean;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
public abstract class BaseKeyEventsTestCase<A extends BaseTestActivity>
extends BaseInstrumentationTestCase<A> {
@@ -73,7 +78,7 @@
getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
getInstrumentation().waitForIdleSync();
- assertFalse("Activity was not destroyed", getActivity().isDestroyed());
+ assertFalse("Activity was not finished", getActivity().isFinishing());
assertTrue("ActionMode was destroyed", destroyed.get());
}
@@ -101,7 +106,7 @@
}
// Check that the Activity is still running and the SearchView is not expanded
- assertFalse("Activity was not destroyed", getActivity().isDestroyed());
+ assertFalse("Activity was not finished", getActivity().isFinishing());
assertFalse("SearchView was collapsed", getActivity().isSearchViewExpanded());
}
@@ -129,15 +134,15 @@
}
@Test
- @MediumTest
- public void testBackPressWithEmptyMenuDestroysActivity() throws InterruptedException {
+ @SmallTest
+ public void testBackPressWithEmptyMenuFinishesActivity() throws InterruptedException {
repopulateWithEmptyMenu();
getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_MENU);
getInstrumentation().waitForIdleSync();
getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
- waitAssertDestroyed();
+ assertTrue(getActivity().isFinishing());
}
@Test
@@ -171,25 +176,13 @@
assertEquals("onKeyDown event matches", KeyEvent.KEYCODE_MENU, upEvent.getKeyCode());
}
- private void waitAssertDestroyed() throws InterruptedException {
- int count = 0;
- while (count++ < 10) {
- if (!getActivity().isDestroyed()) {
- Thread.sleep(50);
- } else {
- break;
- }
- }
- assertTrue("Activity destroyed", getActivity().isDestroyed());
- }
-
private void repopulateWithEmptyMenu() throws InterruptedException {
int count = 0;
getActivity().setShouldPopulateOptionsMenu(false);
while (count++ < 10) {
Menu menu = getActivity().getMenu();
if (menu == null || menu.size() != 0) {
- Thread.sleep(50);
+ Thread.sleep(100);
} else {
return;
}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyboardShortcutsTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyboardShortcutsTestCase.java
index b118eec..f53b8e1 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/BaseKeyboardShortcutsTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/BaseKeyboardShortcutsTestCase.java
@@ -16,16 +16,19 @@
package android.support.v7.app;
-import android.support.v7.testutils.BaseTestActivity;
import org.junit.Test;
import android.os.SystemClock;
import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MenuItem;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
public abstract class BaseKeyboardShortcutsTestCase<A extends BaseTestActivity>
extends BaseInstrumentationTestCase<A> {
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DialogTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/DialogTestCase.java
index 053e971..69fdb5b 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/DialogTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/DialogTestCase.java
@@ -21,6 +21,9 @@
import android.app.Dialog;
import android.os.Bundle;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
public class DialogTestCase extends BaseInstrumentationTestCase<WindowDecorActionBarActivity> {
public DialogTestCase() {
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerDynamicLayoutTest.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerDynamicLayoutTest.java
index 90540d0..de49149 100755
--- a/v7/appcompat/tests/src/android/support/v7/app/DrawerDynamicLayoutTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerDynamicLayoutTest.java
@@ -15,6 +15,13 @@
*/
package android.support.v7.app;
+import android.support.test.InstrumentationRegistry;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.After;
+import org.junit.Test;
+
import android.support.annotation.LayoutRes;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
@@ -22,11 +29,6 @@
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.ViewStub;
-import org.hamcrest.Description;
-import org.hamcrest.Matcher;
-import org.hamcrest.TypeSafeMatcher;
-import org.junit.After;
-import org.junit.Test;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
@@ -45,23 +47,16 @@
}
@After
- @Override
public void tearDown() throws Exception {
// Now that the test is done, replace the activity content view with ViewStub so
// that it's ready to be replaced for the next test.
- try {
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- final DrawerDynamicLayoutActivity activity = getActivity();
- activity.setContentView(R.layout.drawer_dynamic_layout);
- }
- });
- } catch (Throwable t) {
- throw new Exception(t);
- }
-
- super.tearDown();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ final DrawerDynamicLayoutActivity activity = mActivityTestRule.getActivity();
+ activity.setContentView(R.layout.drawer_dynamic_layout);
+ }
+ });
}
/**
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutActions.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutActions.java
index 4d2b1be..dabc363 100755
--- a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutActions.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutActions.java
@@ -27,7 +27,7 @@
public class DrawerLayoutActions {
/**
- * Opens the drawer.
+ * Opens the drawer at the specified edge gravity.
*/
public static ViewAction openDrawer(final int drawerEdgeGravity) {
return new ViewAction() {
@@ -55,7 +55,35 @@
}
/**
- * Closes the drawer.
+ * Opens the drawer.
+ */
+ public static ViewAction openDrawer(final View drawerView) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Opens the drawer";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.openDrawer(drawerView);
+
+ // Wait for a full second to let the inner ViewDragHelper complete the operation
+ uiController.loopMainThreadForAtLeast(1000);
+ }
+ };
+ }
+
+ /**
+ * Closes the drawer at the specified edge gravity.
*/
public static ViewAction closeDrawer(final int drawerEdgeGravity) {
return new ViewAction() {
@@ -81,4 +109,86 @@
}
};
}
+
+ /**
+ * Closes the drawer.
+ */
+ public static ViewAction closeDrawer(final View drawerView) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Closes the drawer";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.closeDrawer(drawerView);
+
+ // Wait for a full second to let the inner ViewDragHelper complete the operation
+ uiController.loopMainThreadForAtLeast(1000);
+ }
+ };
+ }
+
+ /**
+ * Sets the lock mode for the drawer at the specified edge gravity.
+ */
+ public static ViewAction setDrawerLockMode(final int lockMode, final int drawerEdgeGravity) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Sets drawer lock mode";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.setDrawerLockMode(lockMode, drawerEdgeGravity);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets the lock mode for the drawer.
+ */
+ public static ViewAction setDrawerLockMode(final int lockMode, final View drawerView) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isAssignableFrom(DrawerLayout.class);
+ }
+
+ @Override
+ public String getDescription() {
+ return "Sets drawer lock mode";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ DrawerLayout drawerLayout = (DrawerLayout) view;
+ drawerLayout.setDrawerLockMode(lockMode, drawerView);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleActivity.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleActivity.java
new file mode 100644
index 0000000..b2e0abc
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleActivity.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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.support.v7.app;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.testutils.Shakespeare;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * Test activity for testing various APIs and interactions for DrawerLayout with start and end
+ * drawers.
+ */
+public class DrawerLayoutDoubleActivity extends BaseTestActivity {
+ private DrawerLayout mDrawerLayout;
+ private ListView mStartDrawer;
+ private View mEndDrawer;
+ private TextView mContent;
+
+ private Toolbar mToolbar;
+
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.drawer_double_layout;
+ }
+
+ @Override
+ protected void onContentViewSet() {
+ super.onContentViewSet();
+
+ mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ mStartDrawer = (ListView) findViewById(R.id.start_drawer);
+ mEndDrawer = findViewById(R.id.end_drawer);
+ mContent = (TextView) findViewById(R.id.content_text);
+
+ mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
+
+ // The drawer title must be set in order to announce state changes when
+ // accessibility is turned on. This is typically a simple description,
+ // e.g. "Navigation".
+ mDrawerLayout.setDrawerTitle(GravityCompat.START, getString(R.string.drawer_title));
+
+ mStartDrawer.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
+ Shakespeare.TITLES));
+ mStartDrawer.setOnItemClickListener(new DrawerItemClickListener());
+
+ // Find the toolbar in our layout and set it as the support action bar on the activity.
+ // This is required to have the drawer slide "over" the toolbar.
+ mToolbar = (Toolbar) findViewById(R.id.toolbar);
+ mToolbar.setTitle(R.string.drawer_title);
+ setSupportActionBar(mToolbar);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setDisplayShowHomeEnabled(false);
+
+ // Configure the background color fill of the system status bar (on supported platform
+ // versions) and the toolbar itself. We're using the same color, and android:statusBar
+ // from the theme makes the status bar slightly darker.
+ final int metalBlueColor = getResources().getColor(R.color.drawer_sample_metal_blue);
+ mDrawerLayout.setStatusBarBackgroundColor(metalBlueColor);
+ mToolbar.setBackgroundColor(metalBlueColor);
+ }
+
+ @Override
+ public void onBackPressed() {
+ // Is the start drawer open?
+ if (mDrawerLayout.isDrawerOpen(mStartDrawer)) {
+ // Close the drawer and return.
+ mDrawerLayout.closeDrawer(mStartDrawer);
+ return;
+ }
+
+ // Is the end drawer open?
+ if (mDrawerLayout.isDrawerOpen(mEndDrawer)) {
+ // Close the drawer and return.
+ mDrawerLayout.closeDrawer(mEndDrawer);
+ return;
+ }
+
+ super.onBackPressed();
+ }
+
+ /**
+ * This list item click listener implements very simple view switching by changing
+ * the primary content text. The drawer is closed when a selection is made.
+ */
+ private class DrawerItemClickListener implements ListView.OnItemClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ mContent.setText(Shakespeare.DIALOGUE[position]);
+ mToolbar.setTitle(Shakespeare.TITLES[position]);
+ mDrawerLayout.closeDrawer(mStartDrawer);
+ }
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleTest.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleTest.java
new file mode 100755
index 0000000..813d5fb
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutDoubleTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2016 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.support.v7.app;
+
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.custom.CustomDrawerLayout;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.View;
+import org.junit.Before;
+import org.junit.Test;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.v7.app.DrawerLayoutActions.closeDrawer;
+import static android.support.v7.app.DrawerLayoutActions.openDrawer;
+import static android.support.v7.app.DrawerLayoutActions.setDrawerLockMode;
+import static android.support.v7.testutils.TestUtilsActions.setLayoutDirection;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class DrawerLayoutDoubleTest
+ extends BaseInstrumentationTestCase<DrawerLayoutDoubleActivity> {
+ private CustomDrawerLayout mDrawerLayout;
+
+ private View mStartDrawer;
+
+ private View mEndDrawer;
+
+ private View mContentView;
+
+ public DrawerLayoutDoubleTest() {
+ super(DrawerLayoutDoubleActivity.class);
+ }
+
+ @Before
+ public void setUp() {
+ final DrawerLayoutDoubleActivity activity = mActivityTestRule.getActivity();
+ mDrawerLayout = (CustomDrawerLayout) activity.findViewById(R.id.drawer_layout);
+ mStartDrawer = mDrawerLayout.findViewById(R.id.start_drawer);
+ mEndDrawer = mDrawerLayout.findViewById(R.id.end_drawer);
+ mContentView = mDrawerLayout.findViewById(R.id.content);
+
+ // Close the drawers to reset the state for the next test
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mStartDrawer));
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mEndDrawer));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ @SmallTest
+ public void testQueryOpenStateOfNonExistentDrawer() {
+ // Note that we're expecting the isDrawerOpen API call to result in an exception being
+ // thrown since mContentView is not a drawer.
+ assertFalse("Querying open state of a view that is not a drawer",
+ mDrawerLayout.isDrawerOpen(mContentView));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ @SmallTest
+ public void testQueryVisibleStateOfNonExistentDrawer() {
+ // Note that we're expecting the isDrawerVisible API call to result in an exception being
+ // thrown since mContentView is not a drawer.
+ assertFalse("Querying visible state of a view that is not a drawer",
+ mDrawerLayout.isDrawerVisible(mContentView));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ @SmallTest
+ public void testOpenNonExistentDrawer() {
+ // Note that we're expecting the openDrawer action to result in an exception being
+ // thrown since mContentView is not a drawer.
+ onView(withId(R.id.drawer_layout)).perform(openDrawer(mContentView));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ @SmallTest
+ public void testCloseNonExistentDrawer() {
+ // Note that we're expecting the closeDrawer action to result in an exception being
+ // thrown since mContentView is not a drawer.
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mContentView));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ @SmallTest
+ public void testLockNonExistentDrawer() {
+ // Note that we're expecting the setDrawerLockMode action to result in an exception being
+ // thrown since mContentView is not a drawer.
+ onView(withId(R.id.drawer_layout)).perform(
+ setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN, mContentView));
+ }
+
+ private void verifyDrawerOpenClose() {
+ assertFalse("Start drawer is closed in initial state",
+ mDrawerLayout.isDrawerOpen(mStartDrawer));
+ assertFalse("Start drawer is not visible in initial state",
+ mDrawerLayout.isDrawerVisible(mStartDrawer));
+ assertFalse("End drawer is closed in initial state",
+ mDrawerLayout.isDrawerOpen(mEndDrawer));
+ assertFalse("End drawer is not visible in initial state",
+ mDrawerLayout.isDrawerVisible(mEndDrawer));
+
+ // Open the start drawer
+ onView(withId(R.id.drawer_layout)).perform(openDrawer(mStartDrawer));
+ // And check that it's open (with the end drawer closed)
+ assertTrue("Start drawer is now open", mDrawerLayout.isDrawerOpen(mStartDrawer));
+ assertTrue("Start drawer is now visible", mDrawerLayout.isDrawerVisible(mStartDrawer));
+ assertFalse("End drawer is still closed", mDrawerLayout.isDrawerOpen(mEndDrawer));
+ assertFalse("End drawer is still not visible", mDrawerLayout.isDrawerVisible(mEndDrawer));
+
+ // Close the start drawer
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mStartDrawer));
+ // And check that both drawers are closed
+ assertFalse("Start drawer is now closed", mDrawerLayout.isDrawerOpen(mStartDrawer));
+ assertFalse("Start drawer is now not visible", mDrawerLayout.isDrawerVisible(mStartDrawer));
+ assertFalse("End drawer is still closed", mDrawerLayout.isDrawerOpen(mEndDrawer));
+ assertFalse("End drawer is still not visible", mDrawerLayout.isDrawerVisible(mEndDrawer));
+
+ // Open the end drawer
+ onView(withId(R.id.drawer_layout)).perform(openDrawer(mEndDrawer));
+ // And check that it's open (with the start drawer closed)
+ assertFalse("Start drawer is still closed", mDrawerLayout.isDrawerOpen(mStartDrawer));
+ assertFalse("Start drawer is still not visible",
+ mDrawerLayout.isDrawerVisible(mStartDrawer));
+ assertTrue("End drawer is now open", mDrawerLayout.isDrawerOpen(mEndDrawer));
+ assertTrue("End drawer is now visible", mDrawerLayout.isDrawerVisible(mEndDrawer));
+
+ // Close the end drawer
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mEndDrawer));
+ // And check that both drawers are closed
+ assertFalse("Start drawer is still closed", mDrawerLayout.isDrawerOpen(mStartDrawer));
+ assertFalse("Start drawer is still not visible",
+ mDrawerLayout.isDrawerVisible(mStartDrawer));
+ assertFalse("End drawer is still closed", mDrawerLayout.isDrawerOpen(mEndDrawer));
+ assertFalse("End drawer is still not visible", mDrawerLayout.isDrawerVisible(mEndDrawer));
+ }
+
+ @Test
+ @SmallTest
+ public void testDrawerOpenCloseLtr() {
+ onView(withId(R.id.drawer_layout)).perform(
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_LTR));
+
+ verifyDrawerOpenClose();
+ }
+
+ @Test
+ @SmallTest
+ public void testDrawerOpenCloseRtl() {
+ onView(withId(R.id.drawer_layout)).perform(
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
+
+ verifyDrawerOpenClose();
+ }
+
+ private void verifyDrawerLockUnlock() {
+ assertEquals("Start drawer is unlocked in initial state",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mStartDrawer));
+ assertEquals("End drawer is unlocked in initial state",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mEndDrawer));
+
+ // Lock the start drawer open
+ onView(withId(R.id.drawer_layout)).perform(
+ setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN, mStartDrawer));
+ // And check that it's locked open (with the end drawer unlocked)
+ assertEquals("Start drawer is now locked open",
+ DrawerLayout.LOCK_MODE_LOCKED_OPEN, mDrawerLayout.getDrawerLockMode(mStartDrawer));
+ assertEquals("End drawer is still unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mEndDrawer));
+
+ // Unlock the start drawer and close it
+ onView(withId(R.id.drawer_layout)).perform(
+ setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, mStartDrawer));
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mStartDrawer));
+ // And check that both drawers are unlocked
+ assertEquals("Start drawer is now unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mStartDrawer));
+ assertEquals("End drawer is now unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mEndDrawer));
+
+ // Lock the end drawer open
+ onView(withId(R.id.drawer_layout)).perform(
+ setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_OPEN, mEndDrawer));
+ // And check that it's locked open (with the start drawer unlocked)
+ assertEquals("Start drawer is still unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mStartDrawer));
+ assertEquals("End drawer is now locked open",
+ DrawerLayout.LOCK_MODE_LOCKED_OPEN, mDrawerLayout.getDrawerLockMode(mEndDrawer));
+
+ // Unlock the end drawer and close it
+ onView(withId(R.id.drawer_layout)).perform(
+ setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, mEndDrawer));
+ onView(withId(R.id.drawer_layout)).perform(closeDrawer(mEndDrawer));
+ // And check that both drawers are unlocked
+ assertEquals("Start drawer is now unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mStartDrawer));
+ assertEquals("End drawer is now unlocked",
+ DrawerLayout.LOCK_MODE_UNLOCKED, mDrawerLayout.getDrawerLockMode(mEndDrawer));
+ }
+
+ @Test
+ @SmallTest
+ public void testDrawerLockUnlockLtr() {
+ onView(withId(R.id.drawer_layout)).perform(
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_LTR));
+
+ verifyDrawerLockUnlock();
+ }
+
+ @Test
+ @SmallTest
+ public void testDrawerLockUnlockRtl() {
+ onView(withId(R.id.drawer_layout)).perform(
+ setLayoutDirection(ViewCompat.LAYOUT_DIRECTION_RTL));
+
+ verifyDrawerLockUnlock();
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
index c5cc561..f0aef76 100755
--- a/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/DrawerLayoutTest.java
@@ -15,23 +15,26 @@
*/
package android.support.v7.app;
-import android.content.res.Resources;
-import android.os.Build;
-import android.view.View;
+import org.junit.Before;
+import org.junit.Test;
-import android.support.test.espresso.action.Press;
+import android.os.Build;
import android.support.test.espresso.action.GeneralLocation;
import android.support.test.espresso.action.GeneralSwipeAction;
+import android.support.test.espresso.action.Press;
import android.support.test.espresso.action.Swipe;
-
import android.support.v4.view.GravityCompat;
-import android.support.v7.custom.CustomDrawerLayout;
import android.support.v7.appcompat.test.R;
-
-import org.junit.Test;
+import android.support.v7.custom.CustomDrawerLayout;
+import android.view.View;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
public class DrawerLayoutTest extends BaseInstrumentationTestCase<DrawerLayoutActivity> {
private CustomDrawerLayout mDrawerLayout;
@@ -44,10 +47,10 @@
super(DrawerLayoutActivity.class);
}
- public void setUp() throws Exception {
- super.setUp();
+ @Before
+ public void setUp() {
- final DrawerLayoutActivity activity = getActivity();
+ final DrawerLayoutActivity activity = mActivityTestRule.getActivity();
mDrawerLayout = (CustomDrawerLayout) activity.findViewById(R.id.drawer_layout);
mStartDrawer = mDrawerLayout.findViewById(R.id.start_drawer);
mContentView = mDrawerLayout.findViewById(R.id.content);
@@ -58,6 +61,7 @@
}
@Test
+ @MediumTest
public void testDrawerOpenCloseViaAPI() {
assertFalse("Initial state", mDrawerLayout.isDrawerOpen(GravityCompat.START));
@@ -73,6 +77,7 @@
}
@Test
+ @MediumTest
public void testDrawerOpenCloseWithRedundancyViaAPI() {
assertFalse("Initial state", mDrawerLayout.isDrawerOpen(GravityCompat.START));
@@ -100,6 +105,7 @@
}
@Test
+ @MediumTest
public void testDrawerOpenCloseViaSwipes() {
assertFalse("Initial state", mDrawerLayout.isDrawerOpen(GravityCompat.START));
@@ -117,6 +123,7 @@
}
@Test
+ @MediumTest
public void testDrawerOpenCloseWithRedundancyViaSwipes() {
assertFalse("Initial state", mDrawerLayout.isDrawerOpen(GravityCompat.START));
@@ -148,6 +155,7 @@
}
@Test
+ @SmallTest
public void testDrawerHeight() {
// Open the drawer so it becomes visible
onView(withId(R.id.drawer_layout)).perform(
diff --git a/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java b/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
index 58a8f3f..1dcefcb 100644
--- a/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
+++ b/v7/appcompat/tests/src/android/support/v7/app/LayoutInflaterFactoryTestCase.java
@@ -35,6 +35,10 @@
import android.view.View;
import android.view.ViewGroup;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
public class LayoutInflaterFactoryTestCase
extends BaseInstrumentationTestCase<LayoutInflaterFactoryTestActivity> {
diff --git a/v7/appcompat/tests/src/android/support/v7/custom/FitWindowsContentLayout.java b/v7/appcompat/tests/src/android/support/v7/custom/FitWindowsContentLayout.java
new file mode 100644
index 0000000..dfad1c8
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/custom/FitWindowsContentLayout.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 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.support.v7.custom;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+public class FitWindowsContentLayout extends FrameLayout {
+
+ private final Rect mInsets = new Rect();
+ private boolean mFitSystemWindowsCalled = false;
+
+ public FitWindowsContentLayout(Context context) {
+ super(context);
+ }
+
+ public FitWindowsContentLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public FitWindowsContentLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected boolean fitSystemWindows(Rect insets) {
+ mFitSystemWindowsCalled = true;
+ mInsets.set(insets);
+
+ return super.fitSystemWindows(insets);
+ }
+
+ public boolean getFitsSystemWindowsCalled() {
+ return mFitSystemWindowsCalled;
+ }
+
+ public Rect getFitSystemWindowsInsets() {
+ return mInsets;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
index ae8e681..8bb452e 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtils.java
@@ -17,13 +17,15 @@
package android.support.v7.testutils;
+import junit.framework.Assert;
+
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
-import junit.framework.Assert;
public class TestUtils {
/**
@@ -93,4 +95,10 @@
bitmap.recycle();
}
}
+
+ public static void waitForActivityDestroyed(BaseTestActivity activity) {
+ while (!activity.isDestroyed()) {
+ SystemClock.sleep(30);
+ }
+ }
}
\ No newline at end of file
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsActions.java b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsActions.java
new file mode 100644
index 0000000..53971b2
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/TestUtilsActions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 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.support.v7.testutils;
+
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v4.view.ViewCompat;
+import android.view.View;
+import org.hamcrest.Matcher;
+
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+
+public class TestUtilsActions {
+ /**
+ * Sets layout direction on the view.
+ */
+ public static ViewAction setLayoutDirection(final int layoutDirection) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
+
+ @Override
+ public String getDescription() {
+ return "set layout direction";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ ViewCompat.setLayoutDirection(view, layoutDirection);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
index b517fc3..2ae4d64 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
@@ -24,17 +24,20 @@
import android.support.annotation.NonNull;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.graphics.ColorUtils;
+import android.support.v7.app.BaseInstrumentationTestCase;
import android.support.v7.appcompat.test.R;
import android.support.v7.testutils.AppCompatTintableViewActions;
import android.support.v7.testutils.BaseTestActivity;
import android.support.v7.testutils.TestUtils;
-import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.ViewGroup;
+import org.junit.Before;
+import org.junit.Test;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static org.junit.Assert.assertNull;
/**
* Base class for testing custom view extensions in appcompat-v7 that implement the
@@ -43,19 +46,20 @@
* base view class (such as <code>AppCompatTextView</code>'s all-caps support).
*/
public abstract class AppCompatBaseViewTest<A extends BaseTestActivity, T extends View>
- extends ActivityInstrumentationTestCase2<A> {
+ extends BaseInstrumentationTestCase<A> {
protected ViewGroup mContainer;
+ protected Resources mResources;
+
public AppCompatBaseViewTest(Class clazz) {
super(clazz);
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- final A activity = getActivity();
+ @Before
+ public void setUp() {
+ final A activity = mActivityTestRule.getActivity();
mContainer = (ViewGroup) activity.findViewById(R.id.container);
+ mResources = activity.getResources();
}
private void verifyBackgroundIsColoredAs(String description, @NonNull View view,
@@ -70,10 +74,10 @@
* This method tests that background tinting is not applied when the
* tintable view has no background.
*/
+ @Test
@SmallTest
public void testBackgroundTintingWithNoBackground() {
final @IdRes int viewId = R.id.view_tinted_no_background;
- final Resources res = getActivity().getResources();
final T view = (T) mContainer.findViewById(viewId);
// Note that all the asserts in this test check that the view background
@@ -93,7 +97,7 @@
// Load a new color state list, set it on the view and check that the background
// is still null.
final ColorStateList sandColor = ResourcesCompat.getColorStateList(
- res, R.color.color_state_list_sand, null);
+ mResources, R.color.color_state_list_sand, null);
onView(withId(viewId)).perform(
AppCompatTintableViewActions.setBackgroundTintList(sandColor));
@@ -111,18 +115,24 @@
* in enabled and disabled state across a number of <code>ColorStateList</code>s set as
* background tint lists on the same background.
*/
+ @Test
@SmallTest
public void testBackgroundTintingAcrossStateChange() {
final @IdRes int viewId = R.id.view_tinted_background;
- final Resources res = getActivity().getResources();
final T view = (T) mContainer.findViewById(viewId);
- @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
- @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
- @ColorInt int sandDefault = ResourcesCompat.getColor(res, R.color.sand_default, null);
- @ColorInt int sandDisabled = ResourcesCompat.getColor(res, R.color.sand_disabled, null);
- @ColorInt int oceanDefault = ResourcesCompat.getColor(res, R.color.ocean_default, null);
- @ColorInt int oceanDisabled = ResourcesCompat.getColor(res, R.color.ocean_disabled, null);
+ final @ColorInt int lilacDefault = ResourcesCompat.getColor(
+ mResources, R.color.lilac_default, null);
+ final @ColorInt int lilacDisabled = ResourcesCompat.getColor(
+ mResources, R.color.lilac_disabled, null);
+ final @ColorInt int sandDefault = ResourcesCompat.getColor(
+ mResources, R.color.sand_default, null);
+ final @ColorInt int sandDisabled = ResourcesCompat.getColor(
+ mResources, R.color.sand_disabled, null);
+ final @ColorInt int oceanDefault = ResourcesCompat.getColor(
+ mResources, R.color.ocean_default, null);
+ final @ColorInt int oceanDisabled = ResourcesCompat.getColor(
+ mResources, R.color.ocean_disabled, null);
// Test the default state for tinting set up in the layout XML file.
verifyBackgroundIsColoredAs("Default lilac tinting in enabled state", view,
@@ -143,7 +153,7 @@
// Load a new color state list, set it on the view and check that the background has
// switched to the matching entry in newly set color state list.
final ColorStateList sandColor = ResourcesCompat.getColorStateList(
- res, R.color.color_state_list_sand, null);
+ mResources, R.color.color_state_list_sand, null);
onView(withId(viewId)).perform(
AppCompatTintableViewActions.setBackgroundTintList(sandColor));
verifyBackgroundIsColoredAs("New sand tinting in enabled state", view,
@@ -164,7 +174,7 @@
// Load another color state list, set it on the view and check that the background has
// switched to the matching entry in newly set color state list.
final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
- res, R.color.color_state_list_ocean, null);
+ mResources, R.color.color_state_list_ocean, null);
onView(withId(viewId)).perform(
AppCompatTintableViewActions.setBackgroundTintList(oceanColor));
verifyBackgroundIsColoredAs("New ocean tinting in enabled state", view,
@@ -188,20 +198,20 @@
* in enabled and disabled state across the same background respects the currently set
* background tinting mode.
*/
+ @Test
@SmallTest
public void testBackgroundTintingAcrossModeChange() {
final @IdRes int viewId = R.id.view_untinted_background;
- final Resources res = getActivity().getResources();
final T view = (T) mContainer.findViewById(viewId);
- @ColorInt int emeraldDefault = ResourcesCompat.getColor(
- res, R.color.emerald_translucent_default, null);
- @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
- res, R.color.emerald_translucent_disabled, null);
+ final @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+ mResources, R.color.emerald_translucent_default, null);
+ final @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+ mResources, R.color.emerald_translucent_disabled, null);
// This is the fill color of R.drawable.test_background_green set on our view
// that we'll be testing in this method
- @ColorInt int backgroundColor = ResourcesCompat.getColor(
- res, R.color.test_green, null);
+ final @ColorInt int backgroundColor = ResourcesCompat.getColor(
+ mResources, R.color.test_green, null);
// Test the default state for tinting set up in the layout XML file.
verifyBackgroundIsColoredAs("Default no tinting in enabled state", view,
@@ -221,7 +231,7 @@
// Load a new color state list, set it on the view and check that the background has
// switched to the matching entry in newly set color state list.
final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
- res, R.color.color_state_list_emerald_translucent, null);
+ mResources, R.color.color_state_list_emerald_translucent, null);
onView(withId(viewId)).perform(
AppCompatTintableViewActions.setBackgroundTintList(emeraldColor));
verifyBackgroundIsColoredAs("New emerald tinting in enabled state under src_in", view,
@@ -259,20 +269,22 @@
* This method tests that opaque background tinting applied to tintable view
* is applied correctly after changing the background itself of the view.
*/
+ @Test
@SmallTest
public void testBackgroundOpaqueTintingAcrossBackgroundChange() {
final @IdRes int viewId = R.id.view_tinted_no_background;
- final Resources res = getActivity().getResources();
final T view = (T) mContainer.findViewById(viewId);
- @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
- @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+ final @ColorInt int lilacDefault = ResourcesCompat.getColor(
+ mResources, R.color.lilac_default, null);
+ final @ColorInt int lilacDisabled = ResourcesCompat.getColor(
+ mResources, R.color.lilac_disabled, null);
assertNull("No background after XML loading", view.getBackground());
// Set background on our view
onView(withId(viewId)).perform(AppCompatTintableViewActions.setBackgroundDrawable(
- ResourcesCompat.getDrawable(res, R.drawable.test_background_green, null)));
+ ResourcesCompat.getDrawable(mResources, R.drawable.test_background_green, null)));
// Test the default state for tinting set up in the layout XML file.
verifyBackgroundIsColoredAs("Default lilac tinting in enabled state on green background",
@@ -315,22 +327,22 @@
* This method tests that translucent background tinting applied to tintable view
* is applied correctly after changing the background itself of the view.
*/
+ @Test
@SmallTest
public void testBackgroundTranslucentTintingAcrossBackgroundChange() {
final @IdRes int viewId = R.id.view_untinted_no_background;
- final Resources res = getActivity().getResources();
final T view = (T) mContainer.findViewById(viewId);
- @ColorInt int emeraldDefault = ResourcesCompat.getColor(
- res, R.color.emerald_translucent_default, null);
- @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
- res, R.color.emerald_translucent_disabled, null);
+ final @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+ mResources, R.color.emerald_translucent_default, null);
+ final @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+ mResources, R.color.emerald_translucent_disabled, null);
// This is the fill color of R.drawable.test_background_green set on our view
// that we'll be testing in this method
- @ColorInt int backgroundColorGreen = ResourcesCompat.getColor(
- res, R.color.test_green, null);
- @ColorInt int backgroundColorRed = ResourcesCompat.getColor(
- res, R.color.test_red, null);
+ final @ColorInt int backgroundColorGreen = ResourcesCompat.getColor(
+ mResources, R.color.test_green, null);
+ final @ColorInt int backgroundColorRed = ResourcesCompat.getColor(
+ mResources, R.color.test_red, null);
assertNull("No background after XML loading", view.getBackground());
@@ -342,13 +354,13 @@
AppCompatTintableViewActions.setBackgroundTintMode(PorterDuff.Mode.SRC_OVER));
// Load and set a translucent color state list as the background tint list
final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
- res, R.color.color_state_list_emerald_translucent, null);
+ mResources, R.color.color_state_list_emerald_translucent, null);
onView(withId(viewId)).perform(
AppCompatTintableViewActions.setBackgroundTintList(emeraldColor));
// Set background on our view
onView(withId(viewId)).perform(AppCompatTintableViewActions.setBackgroundDrawable(
- ResourcesCompat.getDrawable(res, R.drawable.test_background_green, null)));
+ ResourcesCompat.getDrawable(mResources, R.drawable.test_background_green, null)));
// From this point on in this method we're allowing a margin of error in checking the
// color of the view background. This is due to both translucent colors being used
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
index d2abe95..8d6233d 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
@@ -15,14 +15,6 @@
*/
package android.support.v7.widget;
-import android.content.res.Resources;
-import android.support.v7.appcompat.test.R;
-import android.support.v7.testutils.AppCompatTextViewActions;
-import android.test.suitebuilder.annotation.SmallTest;
-
-import static android.support.test.espresso.Espresso.onView;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-
/**
* In addition to all tinting-related tests done by the base class, this class provides
* tests specific to <code>AppCompatImageView</code> class.
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
index 6448e7d..e2c6edf 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatTextViewTest.java
@@ -15,13 +15,14 @@
*/
package android.support.v7.widget;
-import android.content.res.Resources;
import android.support.v7.appcompat.test.R;
import android.support.v7.testutils.AppCompatTextViewActions;
import android.test.suitebuilder.annotation.SmallTest;
+import org.junit.Test;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static org.junit.Assert.assertEquals;
/**
* In addition to all tinting-related tests done by the base class, this class provides
@@ -33,11 +34,11 @@
super(AppCompatTextViewActivity.class);
}
+ @Test
@SmallTest
public void testAllCaps() throws Throwable {
- final Resources res = getActivity().getResources();
- final String text1 = res.getString(R.string.sample_text1);
- final String text2 = res.getString(R.string.sample_text2);
+ final String text1 = mResources.getString(R.string.sample_text1);
+ final String text2 = mResources.getString(R.string.sample_text2);
final AppCompatTextView textView1 =
(AppCompatTextView) mContainer.findViewById(R.id.text_view_caps1);
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
index 14e1bfe..9072100 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
@@ -18,8 +18,9 @@
import android.app.Instrumentation;
import android.graphics.Rect;
import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.v7.app.BaseInstrumentationTestCase;
import android.support.v7.appcompat.test.R;
-import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.SmallTest;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -32,17 +33,22 @@
import android.widget.PopupWindow;
import android.widget.TextView;
+import org.junit.Before;
+import org.junit.Test;
+
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.matcher.RootMatchers.withDecorView;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.support.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
-public class ListPopupWindowTest extends ActivityInstrumentationTestCase2<PopupTestActivity> {
+public class ListPopupWindowTest extends BaseInstrumentationTestCase<PopupTestActivity> {
private FrameLayout mContainer;
private Button mButton;
@@ -61,15 +67,14 @@
super(PopupTestActivity.class);
}
- @Override
- protected void setUp() throws Exception {
- super.setUp();
-
- final PopupTestActivity activity = getActivity();
+ @Before
+ public void setUp() throws Exception {
+ final PopupTestActivity activity = mActivityTestRule.getActivity();
mContainer = (FrameLayout) activity.findViewById(R.id.container);
mButton = (Button) mContainer.findViewById(R.id.test_button);
}
+ @Test
@SmallTest
public void testBasicContent() {
Builder popupBuilder = new Builder();
@@ -79,7 +84,7 @@
assertNotNull("Popup window created", mListPopupWindow);
assertTrue("Popup window showing", mListPopupWindow.isShowing());
- final View mainDecorView = getActivity().getWindow().getDecorView();
+ final View mainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView();
onView(withText("Alice"))
.inRoot(withDecorView(not(is(mainDecorView))))
.check(matches(isDisplayed()));
@@ -97,6 +102,7 @@
.check(matches(isDisplayed()));
}
+ @Test
@SmallTest
public void testAnchoring() {
Builder popupBuilder = new Builder();
@@ -121,15 +127,16 @@
popupOnScreenXY[1] + rect.top);
}
+ @Test
@SmallTest
- public void testDismissalViaAPI() throws Throwable {
+ public void testDismissalViaAPI() {
Builder popupBuilder = new Builder().withDismissListener();
popupBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
assertTrue("Popup window showing", mListPopupWindow.isShowing());
- runTestOnUiThread(new Runnable() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mListPopupWindow.dismiss();
@@ -178,7 +185,7 @@
// of view or data. Also, we don't want to use View.dispatchTouchEvent directly as
// that would require emulation of two separate sequences as well.
- Instrumentation instrumentation = getInstrumentation();
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
// Inject DOWN event
long downTime = SystemClock.uptimeMillis();
@@ -211,16 +218,19 @@
assertEquals("Click on underlying container", !setupAsModal, mIsContainerClicked);
}
+ @Test
@SmallTest
public void testDismissalOutsideNonModal() throws Throwable {
testDismissalViaTouch(false);
}
+ @Test
@SmallTest
public void testDismissalOutsideModal() throws Throwable {
testDismissalViaTouch(true);
}
+ @Test
@SmallTest
public void testItemClickViaEvent() {
Builder popupBuilder = new Builder().withItemClickListener();
@@ -231,16 +241,18 @@
assertEquals("Clicked item before click", -1, mListPopupClickedItem);
+ final View mainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView();
onView(withText("Charlie"))
- .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
+ .inRoot(withDecorView(not(is(mainDecorView))))
.perform(click());
assertEquals("Clicked item after click", 2, mListPopupClickedItem);
// Our item click listener also dismisses the popup
assertFalse("Popup window not showing after click", mListPopupWindow.isShowing());
}
+ @Test
@SmallTest
- public void testItemClickViaAPI() throws Throwable {
+ public void testItemClickViaAPI() {
Builder popupBuilder = new Builder().withItemClickListener();
popupBuilder.wireToActionButton();
@@ -248,7 +260,7 @@
assertTrue("Popup window showing", mListPopupWindow.isShowing());
assertEquals("Clicked item before click", -1, mListPopupClickedItem);
- runTestOnUiThread(new Runnable() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mListPopupWindow.performItemClick(1);
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
new file mode 100644
index 0000000..4218775
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
@@ -0,0 +1,578 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import android.support.test.InstrumentationRegistry;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+
+import android.app.Instrumentation;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.support.test.espresso.Root;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v7.app.BaseInstrumentationTestCase;
+import android.support.v7.appcompat.test.R;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewParent;
+import android.widget.Button;
+import android.widget.FrameLayout;
+
+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.doesNotExist;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
+import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class PopupMenuTest extends BaseInstrumentationTestCase<PopupTestActivity> {
+ // Since PopupMenu doesn't expose any access to the underlying
+ // implementation (like ListPopupWindow.getListView), we're relying on the
+ // class name of the list view from ListPopupWindow that is being used
+ // in PopupMenu. This is not the cleanest, but it's not making any assumptions
+ // on the platform-specific details of the popup windows.
+ private static final String DROP_DOWN_CLASS_NAME =
+ "android.support.v7.widget.ListPopupWindow$DropDownListView";
+ private FrameLayout mContainer;
+
+ private Button mButton;
+
+ private PopupMenu mPopupMenu;
+
+ private int mPopupClickedMenuItemId = -1;
+
+ private boolean mIsDismissedCalled = false;
+
+ private Resources mResources;
+
+ private View mMainDecorView;
+
+ public PopupMenuTest() {
+ super(PopupTestActivity.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ final PopupTestActivity activity = mActivityTestRule.getActivity();
+ mContainer = (FrameLayout) activity.findViewById(R.id.container);
+ mButton = (Button) mContainer.findViewById(R.id.test_button);
+ mResources = mActivityTestRule.getActivity().getResources();
+ mMainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView();
+ }
+
+ @Test
+ @SmallTest
+ public void testBasicContent() {
+ final Builder menuBuilder = new Builder();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+ assertNotNull("Popup menu created", mPopupMenu);
+ // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
+ // Use a custom matcher to check the visibility of the drop down list view instead.
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(isPlatformPopup()).check(matches(isDisplayed()));
+
+ // Note that MenuItem.isVisible() refers to the current "data" visibility state
+ // and not the "on screen" visibility state. This is why we're testing the display
+ // visibility of our main and sub menu items.
+
+ onView(withText(mResources.getString(R.string.popup_menu_highlight)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_edit)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_delete)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_ignore)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_share)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_print)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(matches(isDisplayed()));
+
+ // Share submenu items should not be visible
+ onView(withText(mResources.getString(R.string.popup_menu_share_email)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(doesNotExist());
+ onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .check(doesNotExist());
+ }
+
+ /**
+ * Returns the location of our popup menu in its window.
+ */
+ private int[] getPopupLocationInWindow() {
+ final int[] location = new int[2];
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(isPlatformPopup()).perform(new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
+
+ @Override
+ public String getDescription() {
+ return "Popup matcher";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ view.getLocationInWindow(location);
+ }
+ });
+ return location;
+ }
+
+ /**
+ * Returns the location of our popup menu on the screen.
+ */
+ private int[] getPopupLocationOnScreen() {
+ final int[] location = new int[2];
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(isPlatformPopup()).perform(new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
+
+ @Override
+ public String getDescription() {
+ return "Popup matcher";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ view.getLocationOnScreen(location);
+ }
+ });
+ return location;
+ }
+
+ /**
+ * Returns the combined padding around the content of our popup menu.
+ */
+ private Rect getPopupPadding() {
+ final Rect result = new Rect();
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(isPlatformPopup()).perform(new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return isDisplayed();
+ }
+
+ @Override
+ public String getDescription() {
+ return "Popup matcher";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ // Traverse the parent hierarchy and combine all their paddings
+ result.setEmpty();
+ final Rect current = new Rect();
+ while (true) {
+ ViewParent parent = view.getParent();
+ if (parent == null || !(parent instanceof View)) {
+ return;
+ }
+
+ view = (View) parent;
+ Drawable currentBackground = view.getBackground();
+ if (currentBackground != null) {
+ currentBackground.getPadding(current);
+ result.left += current.left;
+ result.right += current.right;
+ result.top += current.top;
+ result.bottom += current.bottom;
+ }
+ }
+ }
+ });
+ return result;
+ }
+
+ /**
+ * Returns a root matcher that matches roots that have window focus on their decor view.
+ */
+ private static Matcher<Root> hasWindowFocus() {
+ return new TypeSafeMatcher<Root>() {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("has window focus");
+ }
+
+ @Override
+ public boolean matchesSafely(Root root) {
+ View rootView = root.getDecorView();
+ return rootView.hasWindowFocus();
+ }
+ };
+ }
+
+ @Test
+ @SmallTest
+ public void testAnchoring() {
+ Builder menuBuilder = new Builder();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ final int[] anchorOnScreenXY = new int[2];
+ final int[] popupOnScreenXY = getPopupLocationOnScreen();
+ final int[] popupInWindowXY = getPopupLocationInWindow();
+ final Rect popupPadding = getPopupPadding();
+
+ mButton.getLocationOnScreen(anchorOnScreenXY);
+
+ // Allow for off-by-one mismatch in anchoring
+ assertEquals("Anchoring X", anchorOnScreenXY[0] + popupInWindowXY[0],
+ popupOnScreenXY[0], 1);
+ assertEquals("Anchoring Y", anchorOnScreenXY[1] + popupInWindowXY[1] + mButton.getHeight(),
+ popupOnScreenXY[1] + popupPadding.top, 1);
+ }
+
+ @Test
+ @SmallTest
+ public void testDismissalViaAPI() {
+ Builder menuBuilder = new Builder().withDismissListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ // Since PopupMenu is not a View, we can't use Espresso's view actions to invoke
+ // the dismiss() API
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mPopupMenu.dismiss();
+ }
+ });
+
+ assertTrue("Dismiss listener called", mIsDismissedCalled);
+ // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
+ // Use a custom matcher to check the visibility of the drop down list view instead.
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ @Test
+ @SmallTest
+ public void testDismissalViaTouch() throws Throwable {
+ Builder menuBuilder = new Builder().withDismissListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ // Determine the location of the popup on the screen so that we can emulate
+ // a tap outside of its bounds to dismiss it
+ final int[] popupOnScreenXY = getPopupLocationOnScreen();
+ final Rect popupPadding = getPopupPadding();
+
+
+ int emulatedTapX = popupOnScreenXY[0] - popupPadding.left - 20;
+ int emulatedTapY = popupOnScreenXY[1] + popupPadding.top + 20;
+
+ // The logic below uses Instrumentation to emulate a tap outside the bounds of the
+ // displayed popup menu. This tap is then treated by the framework to be "split" as
+ // the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying
+ // view root if the popup is not modal.
+ // It is not correct to emulate these two sequences separately in the test, as it
+ // wouldn't emulate the user-facing interaction for this test. Note that usage
+ // of Instrumentation is necessary here since Espresso's actions operate at the level
+ // of view or data. Also, we don't want to use View.dispatchTouchEvent directly as
+ // that would require emulation of two separate sequences as well.
+
+ Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+
+ // Inject DOWN event
+ long downTime = SystemClock.uptimeMillis();
+ MotionEvent eventDown = MotionEvent.obtain(
+ downTime, downTime, MotionEvent.ACTION_DOWN, emulatedTapX, emulatedTapY, 1);
+ instrumentation.sendPointerSync(eventDown);
+
+ // Inject MOVE event
+ long moveTime = SystemClock.uptimeMillis();
+ MotionEvent eventMove = MotionEvent.obtain(
+ moveTime, moveTime, MotionEvent.ACTION_MOVE, emulatedTapX, emulatedTapY, 1);
+ instrumentation.sendPointerSync(eventMove);
+
+ // Inject UP event
+ long upTime = SystemClock.uptimeMillis();
+ MotionEvent eventUp = MotionEvent.obtain(
+ upTime, upTime, MotionEvent.ACTION_UP, emulatedTapX, emulatedTapY, 1);
+ instrumentation.sendPointerSync(eventUp);
+
+ // Wait for the system to process all events in the queue
+ instrumentation.waitForIdleSync();
+
+ // At this point our popup should not be showing and should have notified its
+ // dismiss listener
+ assertTrue("Dismiss listener called", mIsDismissedCalled);
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ @Test
+ @SmallTest
+ public void testSimpleMenuItemClickViaEvent() {
+ Builder menuBuilder = new Builder().withMenuItemClickListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+
+ onView(withText(mResources.getString(R.string.popup_menu_delete)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .perform(click());
+ assertEquals("Clicked item after click", R.id.action_delete, mPopupClickedMenuItemId);
+
+ // Popup menu should be automatically dismissed on selecting an item
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ @Test
+ @SmallTest
+ public void testSimpleMenuItemClickViaAPI() {
+ Builder menuBuilder = new Builder().withMenuItemClickListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mPopupMenu.getMenu().performIdentifierAction(R.id.action_highlight, 0);
+ }
+ });
+
+ assertEquals("Clicked item after click", R.id.action_highlight, mPopupClickedMenuItemId);
+
+ // Popup menu should be automatically dismissed on selecting an item
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ @Test
+ @SmallTest
+ public void testSubMenuClicksViaEvent() throws Throwable {
+ Builder menuBuilder = new Builder().withMenuItemClickListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+
+ onView(withText(mResources.getString(R.string.popup_menu_share)))
+ .inRoot(withDecorView(not(is(mMainDecorView))))
+ .perform(click());
+ assertEquals("Clicked item after click", R.id.action_share, mPopupClickedMenuItemId);
+
+ // Sleep for a bit to allow the menu -> submenu transition to complete
+ Thread.sleep(1000);
+
+ // At this point we should now have our sub-menu displayed. At this point on newer
+ // platform versions (L+) we have two view roots on the screen - one for the main popup
+ // menu and one for the submenu that has just been activated. If we only use the
+ // logic based on decor view, Espresso will time out on waiting for the first root
+ // to acquire window focus. This is why from this point on in this test we are using
+ // two root conditions to detect the submenu - one with decor view not being the same
+ // as the decor view of our main activity window, and the other that checks for window
+ // focus.
+
+ // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
+ // Use a custom matcher to check the visibility of the drop down list view instead.
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+
+ // Note that MenuItem.isVisible() refers to the current "data" visibility state
+ // and not the "on screen" visibility state. This is why we're testing the display
+ // visibility of our main and sub menu items.
+
+ // Share submenu items should now be visible
+ onView(withText(mResources.getString(R.string.popup_menu_share_email)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+
+ // Now click an item in the sub-menu
+ onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .perform(click());
+ assertEquals("Clicked submenu item after click", R.id.action_share_circles,
+ mPopupClickedMenuItemId);
+
+ // Popup menu should be automatically dismissed on selecting an item in the submenu
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ @Test
+ @SmallTest
+ public void testSubMenuClicksViaAPI() throws Throwable {
+ Builder menuBuilder = new Builder().withMenuItemClickListener();
+ menuBuilder.wireToActionButton();
+
+ onView(withId(R.id.test_button)).perform(click());
+
+ assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0);
+ }
+ });
+
+ assertEquals("Clicked item after click", R.id.action_share, mPopupClickedMenuItemId);
+
+ // Sleep for a bit to allow the menu -> submenu transition to complete
+ Thread.sleep(1000);
+
+ // At this point we should now have our sub-menu displayed. At this point on newer
+ // platform versions (L+) we have two view roots on the screen - one for the main popup
+ // menu and one for the submenu that has just been activated. If we only use the
+ // logic based on decor view, Espresso will time out on waiting for the first root
+ // to acquire window focus. This is why from this point on in this test we are using
+ // two root conditions to detect the submenu - one with decor view not being the same
+ // as the decor view of our main activity window, and the other that checks for window
+ // focus.
+
+ // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
+ // Use a custom matcher to check the visibility of the drop down list view instead.
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+
+ // Note that MenuItem.isVisible() refers to the current "data" visibility state
+ // and not the "on screen" visibility state. This is why we're testing the display
+ // visibility of our main and sub menu items.
+
+ // Share submenu items should now be visible
+ onView(withText(mResources.getString(R.string.popup_menu_share_email)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+ onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
+ .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
+ .check(matches(isDisplayed()));
+
+ // Now ask the share submenu to perform an action on its specific menu item
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu().
+ performIdentifierAction(R.id.action_share_email, 0);
+ }
+ });
+ assertEquals("Clicked submenu item after click", R.id.action_share_email,
+ mPopupClickedMenuItemId);
+
+ // Popup menu should be automatically dismissed on selecting an item in the submenu
+ onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+ }
+
+ /**
+ * Inner helper class to configure an instance of <code>PopupMenu</code> for the
+ * specific test. The main reason for its existence is that once a popup menu is shown
+ * with the show() method, most of its configuration APIs are no-ops. This means that
+ * we can't add logic that is specific to a certain test once it's shown and we have a
+ * reference to a displayed PopupMenu.
+ */
+ private class Builder {
+ private boolean mHasDismissListener;
+ private boolean mHasMenuItemClickListener;
+
+ public Builder() {
+ }
+
+ public Builder withMenuItemClickListener() {
+ mHasMenuItemClickListener = true;
+ return this;
+ }
+
+ public Builder withDismissListener() {
+ mHasDismissListener = true;
+ return this;
+ }
+
+ private void show() {
+ mPopupMenu = new PopupMenu(mContainer.getContext(), mButton);
+ final MenuInflater menuInflater = mPopupMenu.getMenuInflater();
+ menuInflater.inflate(R.menu.popup_menu, mPopupMenu.getMenu());
+
+ if (mHasMenuItemClickListener) {
+ // Register a listener to be notified when a menu item in our popup menu has
+ // been clicked.
+ mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ mPopupClickedMenuItemId = item.getItemId();
+ return true;
+ }
+ });
+ }
+
+ if (mHasDismissListener) {
+ // Register a listener to be notified when our popup menu is dismissed.
+ mPopupMenu.setOnDismissListener(new PopupMenu.OnDismissListener() {
+ @Override
+ public void onDismiss(PopupMenu menu) {
+ mIsDismissedCalled = true;
+ }
+ });
+ }
+
+ // Show the popup menu
+ mPopupMenu.show();
+ }
+
+ public void wireToActionButton() {
+ mButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ show();
+ }
+ });
+ }
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
index f7ddba5..9a3736a 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
@@ -18,6 +18,10 @@
import android.support.v7.appcompat.test.R;
import android.support.v7.testutils.BaseTestActivity;
+/**
+ * This activity is used to test both {@link ListPopupWindow} and {@link PopupMenu} classes.
+ *
+ */
public class PopupTestActivity extends BaseTestActivity {
@Override
protected int getContentViewLayoutResId() {
diff --git a/v7/recyclerview/api/current.txt b/v7/recyclerview/api/current.txt
index d74c0e5..504e05d 100644
--- a/v7/recyclerview/api/current.txt
+++ b/v7/recyclerview/api/current.txt
@@ -235,6 +235,8 @@
method public abstract int getEnd();
method public abstract int getEndAfterPadding();
method public abstract int getEndPadding();
+ method public abstract int getMode();
+ method public abstract int getModeInOther();
method public abstract int getStartAfterPadding();
method public abstract int getTotalSpace();
method public int getTotalSpaceChange();
@@ -459,6 +461,7 @@
method public boolean canScrollHorizontally();
method public boolean canScrollVertically();
method public boolean checkLayoutParams(android.support.v7.widget.RecyclerView.LayoutParams);
+ method public static int chooseSize(int, int, int);
method public int computeHorizontalScrollExtent(android.support.v7.widget.RecyclerView.State);
method public int computeHorizontalScrollOffset(android.support.v7.widget.RecyclerView.State);
method public int computeHorizontalScrollRange(android.support.v7.widget.RecyclerView.State);
@@ -480,7 +483,8 @@
method public int getBottomDecorationHeight(android.view.View);
method public android.view.View getChildAt(int);
method public int getChildCount();
- method public static int getChildMeasureSpec(int, int, int, boolean);
+ method public static deprecated int getChildMeasureSpec(int, int, int, boolean);
+ method public static int getChildMeasureSpec(int, int, int, int, boolean);
method public boolean getClipToPadding();
method public int getColumnCountForAccessibility(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
method public int getDecoratedBottom(android.view.View);
@@ -491,6 +495,7 @@
method public int getDecoratedTop(android.view.View);
method public android.view.View getFocusedChild();
method public int getHeight();
+ method public int getHeightMode();
method public int getItemCount();
method public int getItemViewType(android.view.View);
method public int getLayoutDirection();
@@ -510,11 +515,14 @@
method public int getSelectionModeForAccessibility(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
method public int getTopDecorationHeight(android.view.View);
method public int getWidth();
+ method public int getWidthMode();
method public boolean hasFocus();
method public void ignoreView(android.view.View);
method public boolean isAttachedToWindow();
+ method public boolean isAutoMeasureEnabled();
method public boolean isFocused();
method public boolean isLayoutHierarchical(android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
+ method public boolean isMeasurementCacheEnabled();
method public boolean isSmoothScrolling();
method public void layoutDecorated(android.view.View, int, int, int, int);
method public void measureChild(android.view.View, int, int);
@@ -563,7 +571,10 @@
method public int scrollHorizontallyBy(int, android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
method public void scrollToPosition(int);
method public int scrollVerticallyBy(int, android.support.v7.widget.RecyclerView.Recycler, android.support.v7.widget.RecyclerView.State);
+ method public void setAutoMeasureEnabled(boolean);
+ method public void setMeasuredDimension(android.graphics.Rect, int, int);
method public void setMeasuredDimension(int, int);
+ method public void setMeasurementCacheEnabled(boolean);
method public void smoothScrollToPosition(android.support.v7.widget.RecyclerView, android.support.v7.widget.RecyclerView.State, int);
method public void startSmoothScroll(android.support.v7.widget.RecyclerView.SmoothScroller);
method public void stopIgnoringView(android.view.View);
@@ -684,6 +695,7 @@
method public int getItemCount();
method public int getTargetScrollPosition();
method public boolean hasTargetScrollPosition();
+ method public boolean isMeasuring();
method public boolean isPreLayout();
method public void put(int, java.lang.Object);
method public void remove(int);
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
index 09f7336..a5a8e33 100644
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/AdapterHelperTest.java
@@ -246,6 +246,18 @@
}
@Test
+ public void testNotifyAfterPre() {
+ setupBasic(10, 2, 3);
+ add(2, 1);
+ mAdapterHelper.preProcess();
+ add(3, 1);
+ mAdapterHelper.consumeUpdatesInOnePass();
+ mPreProcessClone.applyOps(mFirstPassUpdates, mTestAdapter);
+ mPreProcessClone.applyOps(mSecondPassUpdates, mTestAdapter);
+ assertAdaptersEqual(mTestAdapter, mPreProcessClone);
+ }
+
+ @Test
public void testSinglePass() {
setupBasic(10, 2, 3);
add(2, 1);
diff --git a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/ViewInfoStoreTest.java b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/ViewInfoStoreTest.java
index f460f79..c776f61 100644
--- a/v7/recyclerview/jvm-tests/src/android/support/v7/widget/ViewInfoStoreTest.java
+++ b/v7/recyclerview/jvm-tests/src/android/support/v7/widget/ViewInfoStoreTest.java
@@ -54,6 +54,37 @@
}
@Test
+ public void addOverridePre() {
+ RecyclerView.ViewHolder vh = new MockViewHolder();
+ MockInfo info = new MockInfo();
+ mStore.addToPreLayout(vh, info);
+ MockInfo info2 = new MockInfo();
+ mStore.addToPreLayout(vh, info2);
+ assertSame(info2, find(vh, FLAG_PRE));
+ }
+
+ @Test
+ public void addOverridePost() {
+ RecyclerView.ViewHolder vh = new MockViewHolder();
+ MockInfo info = new MockInfo();
+ mStore.addToPostLayout(vh, info);
+ MockInfo info2 = new MockInfo();
+ mStore.addToPostLayout(vh, info2);
+ assertSame(info2, find(vh, FLAG_POST));
+ }
+
+ @Test
+ public void addRemoveAndReAdd() {
+ RecyclerView.ViewHolder vh = new MockViewHolder();
+ MockInfo pre = new MockInfo();
+ mStore.addToPreLayout(vh, pre);
+ MockInfo post1 = new MockInfo();
+ mStore.addToPostLayout(vh, post1);
+ mStore.onViewDetached(vh);
+ mStore.addToDisappearedInLayout(vh);
+ }
+
+ @Test
public void addToPreLayout() {
RecyclerView.ViewHolder vh = new MockViewHolder();
MockInfo info = new MockInfo();
diff --git a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
index 9220c5e..b1dd91c 100644
--- a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
@@ -530,7 +530,7 @@
*/
boolean onItemRangeMoved(int from, int to, int itemCount) {
if (from == to) {
- return false;//no-op
+ return false; // no-op
}
if (itemCount != 1) {
throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
@@ -612,6 +612,10 @@
return position;
}
+ boolean hasUpdates() {
+ return !mPostponedList.isEmpty() && !mPendingUpdates.isEmpty();
+ }
+
/**
* Queued operation to happen when child views are updated.
*/
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index e182c93..c1c0d6d 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -38,11 +38,6 @@
private static final String TAG = "GridLayoutManager";
public static final int DEFAULT_SPAN_COUNT = -1;
/**
- * The measure spec for the scroll direction.
- */
- static final int MAIN_DIR_SPEC =
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- /**
* Span size have been changed but we've not done a new layout calculation.
*/
boolean mPendingSpanCountChange = false;
@@ -221,8 +216,13 @@
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
+ if (mOrientation == HORIZONTAL) {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.FILL_PARENT);
+ } else {
+ return new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
}
@Override
@@ -273,26 +273,61 @@
calculateItemBorders(totalSpace);
}
- private void calculateItemBorders(int totalSpace) {
- if (mCachedBorders == null || mCachedBorders.length != mSpanCount + 1
- || mCachedBorders[mCachedBorders.length - 1] != totalSpace) {
- mCachedBorders = new int[mSpanCount + 1];
+ @Override
+ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+ if (mCachedBorders == null) {
+ super.setMeasuredDimension(childrenBounds, wSpec, hSpec);
}
- mCachedBorders[0] = 0;
- int sizePerSpan = totalSpace / mSpanCount;
- int sizePerSpanRemainder = totalSpace % mSpanCount;
+ final int width, height;
+ if (mOrientation == VERTICAL) {
+ int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
+ height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+ width = chooseSize(wSpec, mCachedBorders[mCachedBorders.length - 1],
+ getMinimumWidth());
+ } else {
+ int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
+ width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+ height = chooseSize(hSpec, mCachedBorders[mCachedBorders.length - 1],
+ getMinimumHeight());
+ }
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * @param totalSpace Total available space after padding is removed
+ */
+ private void calculateItemBorders(int totalSpace) {
+ mCachedBorders = calculateItemBorders(mCachedBorders, mSpanCount, totalSpace);
+ }
+
+ /**
+ * @param cachedBorders The out array
+ * @param spanCount number of spans
+ * @param totalSpace total available space after padding is removed
+ * @return The updated array. Might be the same instance as the provided array if its size
+ * has not changed.
+ */
+ static int[] calculateItemBorders(int[] cachedBorders, int spanCount, int totalSpace) {
+ if (cachedBorders == null || cachedBorders.length != spanCount + 1
+ || cachedBorders[cachedBorders.length - 1] != totalSpace) {
+ cachedBorders = new int[spanCount + 1];
+ }
+ cachedBorders[0] = 0;
+ int sizePerSpan = totalSpace / spanCount;
+ int sizePerSpanRemainder = totalSpace % spanCount;
int consumedPixels = 0;
int additionalSize = 0;
- for (int i = 1; i <= mSpanCount; i++) {
+ for (int i = 1; i <= spanCount; i++) {
int itemSize = sizePerSpan;
additionalSize += sizePerSpanRemainder;
- if (additionalSize > 0 && (mSpanCount - additionalSize) < sizePerSpanRemainder) {
+ if (additionalSize > 0 && (spanCount - additionalSize) < sizePerSpanRemainder) {
itemSize += 1;
- additionalSize -= mSpanCount;
+ additionalSize -= spanCount;
}
consumedPixels += itemSize;
- mCachedBorders[i] = consumedPixels;
+ cachedBorders[i] = consumedPixels;
}
+ return cachedBorders;
}
@Override
@@ -433,6 +468,15 @@
@Override
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
+ final int otherDirSpecMode = mOrientationHelper.getModeInOther();
+ final boolean flexibleInOtherDir = otherDirSpecMode != View.MeasureSpec.EXACTLY;
+ final int currentOtherDirSize = getChildCount() > 0 ? mCachedBorders[mSpanCount] : 0;
+ // if grid layout's dimensions are not specified, let the new row change the measurements
+ // This is not perfect since we not covering all rows but still solves an important case
+ // where they may have a header row which should be laid out according to children.
+ if (flexibleInOtherDir) {
+ updateMeasurements(); // reset measurements
+ }
final boolean layingOutInPrimaryDirection =
layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_TAIL;
int count = 0;
@@ -470,6 +514,7 @@
}
int maxSize = 0;
+ float maxSizeInOther = 0; // use a float to get size per span
// we should assign spans before item decor offsets are calculated
assignSpans(recycler, state, count, consumedSpanCount, layingOutInPrimaryDirection);
@@ -490,35 +535,68 @@
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- final int spec = View.MeasureSpec.makeMeasureSpec(
- mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
- mCachedBorders[lp.mSpanIndex],
- View.MeasureSpec.EXACTLY);
+ final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
+ mCachedBorders[lp.mSpanIndex], otherDirSpecMode, 0,
+ mOrientation == HORIZONTAL ? lp.height : lp.width,
+ false);
+ final int mainSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(),
+ mOrientationHelper.getMode(), 0,
+ mOrientation == VERTICAL ? lp.height : lp.width, true);
if (mOrientation == VERTICAL) {
- measureChildWithDecorationsAndMargin(view, spec, getMainDirSpec(lp.height), false);
+ measureChildWithDecorationsAndMargin(view, spec, mainSpec, false, false);
} else {
- measureChildWithDecorationsAndMargin(view, getMainDirSpec(lp.width), spec, false);
+ measureChildWithDecorationsAndMargin(view, mainSpec, spec, false, false);
}
final int size = mOrientationHelper.getDecoratedMeasurement(view);
if (size > maxSize) {
maxSize = size;
}
+ final float otherSize = 1f * mOrientationHelper.getDecoratedMeasurementInOther(view) /
+ lp.mSpanSize;
+ if (otherSize > maxSizeInOther) {
+ maxSizeInOther = otherSize;
+ }
}
-
- // views that did not measure the maxSize has to be re-measured
- final int maxMeasureSpec = getMainDirSpec(maxSize);
+ if (flexibleInOtherDir) {
+ // re-distribute columns
+ guessMeasurement(maxSizeInOther, currentOtherDirSize);
+ // now we should re-measure any item that was match parent.
+ maxSize = 0;
+ for (int i = 0; i < count; i++) {
+ View view = mSet[i];
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
+ mCachedBorders[lp.mSpanIndex], View.MeasureSpec.EXACTLY, 0,
+ mOrientation == HORIZONTAL ? lp.height : lp.width, false);
+ final int mainSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(),
+ mOrientationHelper.getMode(), 0,
+ mOrientation == VERTICAL ? lp.height : lp.width, true);
+ if (mOrientation == VERTICAL) {
+ measureChildWithDecorationsAndMargin(view, spec, mainSpec, false, true);
+ } else {
+ measureChildWithDecorationsAndMargin(view, mainSpec, spec, false, true);
+ }
+ final int size = mOrientationHelper.getDecoratedMeasurement(view);
+ if (size > maxSize) {
+ maxSize = size;
+ }
+ }
+ }
+ // Views that did not measure the maxSize has to be re-measured
+ // We will stop doing this once we introduce Gravity in the GLM layout params
+ final int maxMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize,
+ View.MeasureSpec.EXACTLY);
for (int i = 0; i < count; i ++) {
final View view = mSet[i];
if (mOrientationHelper.getDecoratedMeasurement(view) != maxSize) {
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
- final int spec = View.MeasureSpec.makeMeasureSpec(
- mCachedBorders[lp.mSpanIndex + lp.mSpanSize] -
- mCachedBorders[lp.mSpanIndex],
- View.MeasureSpec.EXACTLY);
+ final int spec = getChildMeasureSpec(mCachedBorders[lp.mSpanIndex + lp.mSpanSize]
+ - mCachedBorders[lp.mSpanIndex], View.MeasureSpec.EXACTLY, 0,
+ mOrientation == HORIZONTAL ? lp.height : lp.width, false);
if (mOrientation == VERTICAL) {
- measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec, true);
+ measureChildWithDecorationsAndMargin(view, spec, maxMeasureSpec, true, true);
} else {
- measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec, true);
+ measureChildWithDecorationsAndMargin(view, maxMeasureSpec, spec, true, true);
}
}
}
@@ -547,8 +625,13 @@
View view = mSet[i];
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (mOrientation == VERTICAL) {
- left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
- right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ if (isLayoutRTL()) {
+ right = getPaddingLeft() + mCachedBorders[params.mSpanIndex + params.mSpanSize];
+ left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
+ } else {
+ left = getPaddingLeft() + mCachedBorders[params.mSpanIndex];
+ right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ }
} else {
top = getPaddingTop() + mCachedBorders[params.mSpanIndex];
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
@@ -572,16 +655,24 @@
Arrays.fill(mSet, null);
}
- private int getMainDirSpec(int dim) {
- if (dim < 0) {
- return MAIN_DIR_SPEC;
- } else {
- return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
- }
+ /**
+ * This is called after laying out a row (if vertical) or a column (if horizontal) when the
+ * RecyclerView does not have exact measurement specs.
+ * <p>
+ * Here we try to assign a best guess width or height and re-do the layout to update other
+ * views that wanted to FILL_PARENT in the non-scroll orientation.
+ *
+ * @param maxSizeInOther The maximum size per span ratio from the measurement of the children.
+ * @param currentOtherDirSize The size before this layout chunk. There is no reason to go below.
+ */
+ private void guessMeasurement(float maxSizeInOther, int currentOtherDirSize) {
+ final int contentSize = Math.round(maxSizeInOther * mSpanCount);
+ // always re-calculate because borders were stretched during the fill
+ calculateItemBorders(Math.max(contentSize, currentOtherDirSize));
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec,
- boolean capBothSpecs) {
+ boolean capBothSpecs, boolean alreadyMeasured) {
calculateItemDecorationsForChild(child, mDecorInsets);
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
if (capBothSpecs || mOrientation == VERTICAL) {
@@ -592,7 +683,16 @@
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mDecorInsets.top,
lp.bottomMargin + mDecorInsets.bottom);
}
- child.measure(widthSpec, heightSpec);
+ final boolean measure;
+ if (alreadyMeasured) {
+ measure = shouldReMeasureChild(child, widthSpec, heightSpec, lp);
+ } else {
+ measure = shouldMeasureChild(child, widthSpec, heightSpec, lp);
+ }
+ if (measure) {
+ child.measure(widthSpec, heightSpec);
+ }
+
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/LayoutState.java b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
index 2402313..bf730ad 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
@@ -15,6 +15,7 @@
*/
package android.support.v7.widget;
+
import android.view.View;
/**
@@ -35,8 +36,11 @@
final static int ITEM_DIRECTION_TAIL = 1;
- final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
-
+ /**
+ * We may not want to recycle children in some cases (e.g. layout)
+ */
+ boolean mRecycle = true;
+
/**
* Number of pixels that we should fill, in the layout direction.
*/
@@ -75,6 +79,11 @@
boolean mStopInFocusable;
/**
+ * If the content is not wrapped with any value
+ */
+ boolean mInfinite;
+
+ /**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index bef7d6a..fc8c523 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -23,13 +23,13 @@
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
-import android.util.AttributeSet;
+import android.support.v7.widget.RecyclerView.LayoutParams;
import android.support.v7.widget.helper.ItemTouchHelper;
+import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
-import android.support.v7.widget.RecyclerView.LayoutParams;
import java.util.List;
@@ -154,6 +154,7 @@
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
+ setAutoMeasureEnabled(true);
}
/**
@@ -170,6 +171,7 @@
setOrientation(properties.orientation);
setReverseLayout(properties.reverseLayout);
setStackFromEnd(properties.stackFromEnd);
+ setAutoMeasureEnabled(true);
}
/**
@@ -529,6 +531,7 @@
int endOffset;
onAnchorReady(recycler, state, mAnchorInfo);
detachAndScrapAttachedViews(recycler);
+ mLayoutState.mInfinite = mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED;
mLayoutState.mIsPreLayout = state.isPreLayout();
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
@@ -1115,9 +1118,10 @@
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
+ mLayoutState.mInfinite = mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED;
mLayoutState.mExtra = getExtraLayoutSpace(state);
mLayoutState.mLayoutDirection = layoutDirection;
- int fastScrollSpace;
+ int scrollingOffset;
if (layoutDirection == LayoutState.LAYOUT_END) {
mLayoutState.mExtra += mOrientationHelper.getEndPadding();
// get the first child in the direction we are going
@@ -1128,7 +1132,7 @@
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// calculate how much we can scroll without adding new children (independent of layout)
- fastScrollSpace = mOrientationHelper.getDecoratedEnd(child)
+ scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
@@ -1138,14 +1142,14 @@
: LayoutState.ITEM_DIRECTION_HEAD;
mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
- fastScrollSpace = -mOrientationHelper.getDecoratedStart(child)
+ scrollingOffset = -mOrientationHelper.getDecoratedStart(child)
+ mOrientationHelper.getStartAfterPadding();
}
mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
- mLayoutState.mAvailable -= fastScrollSpace;
+ mLayoutState.mAvailable -= scrollingOffset;
}
- mLayoutState.mScrollingOffset = fastScrollSpace;
+ mLayoutState.mScrollingOffset = scrollingOffset;
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
@@ -1157,8 +1161,8 @@
final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
updateLayoutState(layoutDirection, absDy, true, state);
- final int freeScroll = mLayoutState.mScrollingOffset;
- final int consumed = freeScroll + fill(recycler, mLayoutState, state, false);
+ final int consumed = mLayoutState.mScrollingOffset
+ + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
@@ -1294,7 +1298,7 @@
* @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection
*/
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
- if (!layoutState.mRecycle) {
+ if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
@@ -1328,7 +1332,7 @@
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
- while (remainingSpace > 0 && layoutState.hasMore(state)) {
+ while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
@@ -1439,6 +1443,13 @@
result.mFocusable = view.isFocusable();
}
+ @Override
+ boolean shouldMeasureTwice() {
+ return getHeightMode() != View.MeasureSpec.EXACTLY
+ && getWidthMode() != View.MeasureSpec.EXACTLY
+ && hasFlexibleChildInBothOrientations();
+ }
+
/**
* Converts a focusDirection to orientation.
*
@@ -1931,7 +1942,8 @@
boolean mIsPreLayout = false;
/**
- * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)} amount.
+ * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)}
+ * amount.
*/
int mLastScrollDelta;
@@ -1942,6 +1954,11 @@
List<RecyclerView.ViewHolder> mScrapList = null;
/**
+ * Used when there is no limit in how many views can be laid out.
+ */
+ boolean mInfinite;
+
+ /**
* @return true if there are more items in the data adapter
*/
boolean hasMore(RecyclerView.State state) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java b/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java
index 8ca9851..cd1f5e1 100644
--- a/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java
@@ -166,6 +166,28 @@
public abstract int getEndPadding();
/**
+ * Returns the MeasureSpec mode for the current orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ *
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getMode();
+
+ /**
+ * Returns the MeasureSpec mode for the perpendicular orientation from the LayoutManager.
+ *
+ * @return The current measure spec mode.
+ *
+ * @see View.MeasureSpec
+ * @see RecyclerView.LayoutManager#getWidthMode()
+ * @see RecyclerView.LayoutManager#getHeightMode()
+ */
+ public abstract int getModeInOther();
+
+ /**
* Creates an OrientationHelper for the given LayoutManager and orientation.
*
* @param layoutManager LayoutManager to attach to
@@ -257,6 +279,16 @@
public int getEndPadding() {
return mLayoutManager.getPaddingRight();
}
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getWidthMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getHeightMode();
+ }
};
}
@@ -333,6 +365,16 @@
public int getEndPadding() {
return mLayoutManager.getPaddingBottom();
}
+
+ @Override
+ public int getMode() {
+ return mLayoutManager.getHeightMode();
+ }
+
+ @Override
+ public int getModeInOther() {
+ return mLayoutManager.getWidthMode();
+ }
};
}
}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index edf9a45..ed12712 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.database.Observable;
@@ -31,6 +32,7 @@
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.os.TraceCompat;
import android.support.v4.view.InputDeviceCompat;
import android.support.v4.view.MotionEventCompat;
@@ -46,6 +48,7 @@
import android.support.v4.widget.EdgeEffectCompat;
import android.support.v4.widget.ScrollerCompat;
import android.support.v7.recyclerview.R;
+import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -55,6 +58,7 @@
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
+import android.view.View.MeasureSpec;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
@@ -72,7 +76,7 @@
import static android.support.v7.widget.AdapterHelper.Callback;
import static android.support.v7.widget.AdapterHelper.UpdateOp;
-import android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
+
/**
* A flexible view for providing a limited window into a large data set.
@@ -285,16 +289,19 @@
private final Rect mTempRect = new Rect();
private Adapter mAdapter;
- private LayoutManager mLayout;
+ @VisibleForTesting LayoutManager mLayout;
private RecyclerListener mRecyclerListener;
- private final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<ItemDecoration>();
+ private final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<>();
private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
- new ArrayList<OnItemTouchListener>();
+ new ArrayList<>();
private OnItemTouchListener mActiveOnItemTouchListener;
private boolean mIsAttached;
private boolean mHasFixedSize;
private boolean mFirstLayoutComplete;
- private boolean mEatRequestLayout;
+
+ // Counting lock to control whether we should ignore requestLayout calls from children or not.
+ private int mEatRequestLayout = 0;
+
private boolean mLayoutRequestEaten;
private boolean mLayoutFrozen;
private boolean mIgnoreMotionEventTillDown;
@@ -1094,7 +1101,8 @@
Log.d(TAG, "after removing animated view: " + view + ", " + this);
}
}
- resumeRequestLayout(false);
+ // only clear request eaten flag if we removed the view.
+ resumeRequestLayout(!removed);
return removed;
}
@@ -1674,26 +1682,42 @@
void eatRequestLayout() {
- if (!mEatRequestLayout) {
- mEatRequestLayout = true;
- if (!mLayoutFrozen) {
- mLayoutRequestEaten = false;
- }
+ mEatRequestLayout++;
+ if (mEatRequestLayout == 1 && !mLayoutFrozen) {
+ mLayoutRequestEaten = false;
}
}
void resumeRequestLayout(boolean performLayoutChildren) {
- if (mEatRequestLayout) {
+ if (mEatRequestLayout < 1) {
+ //noinspection PointlessBooleanExpression
+ if (DEBUG) {
+ throw new IllegalStateException("invalid eat request layout count");
+ }
+ mEatRequestLayout = 1;
+ }
+ if (!performLayoutChildren) {
+ // Reset the layout request eaten counter.
+ // This is necessary since eatRequest calls can be nested in which case the outher
+ // call will override the inner one.
+ // for instance:
+ // eat layout for process adapter updates
+ // eat layout for dispatchLayout
+ // a bunch of req layout calls arrive
+
+ mLayoutRequestEaten = false;
+ }
+ if (mEatRequestLayout == 1) {
// when layout is frozen we should delay dispatchLayout()
if (performLayoutChildren && mLayoutRequestEaten && !mLayoutFrozen &&
mLayout != null && mAdapter != null) {
dispatchLayout();
}
- mEatRequestLayout = false;
if (!mLayoutFrozen) {
mLayoutRequestEaten = false;
}
}
+ mEatRequestLayout--;
}
/**
@@ -2551,37 +2575,73 @@
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
- if (mAdapterUpdateDuringMeasure) {
- eatRequestLayout();
- processAdapterUpdatesAndSetAnimationFlags();
-
- if (mState.mRunPredictiveAnimations) {
- // TODO: try to provide a better approach.
- // When RV decides to run predictive animations, we need to measure in pre-layout
- // state so that pre-layout pass results in correct layout.
- // On the other hand, this will prevent the layout manager from resizing properly.
- mState.mInPreLayout = true;
- } else {
- // consume remaining updates to provide a consistent state with the layout pass.
- mAdapterHelper.consumeUpdatesInOnePass();
- mState.mInPreLayout = false;
- }
- mAdapterUpdateDuringMeasure = false;
- resumeRequestLayout(false);
- }
-
- if (mAdapter != null) {
- mState.mItemCount = mAdapter.getItemCount();
- } else {
- mState.mItemCount = 0;
- }
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
- } else {
- mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ return;
}
+ if (mLayout.mAutoMeasure) {
+ final int widthMode = MeasureSpec.getMode(widthSpec);
+ final int heightMode = MeasureSpec.getMode(heightSpec);
+ final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
+ && heightMode == MeasureSpec.EXACTLY;
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ if (skipMeasure || mAdapter == null) {
+ return;
+ }
+ if (mState.mLayoutStep == State.STEP_START) {
+ dispatchLayoutStep1();
+ }
+ // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
+ // consistency
+ mLayout.setMeasureSpecs(widthSpec, heightSpec);
+ mState.mIsMeasuring = true;
+ dispatchLayoutStep2();
- mState.mInPreLayout = false; // clear
+ // now we can get the width and height from the children.
+ mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+
+ // if RecyclerView has non-exact width and height and if there is at least one child
+ // which also has non-exact width & height, we have to re-measure.
+ if (mLayout.shouldMeasureTwice()) {
+ mLayout.setMeasureSpecs(
+ MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
+ mState.mIsMeasuring = true;
+ dispatchLayoutStep2();
+ // now we can get the width and height from the children.
+ mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
+ }
+ } else {
+ if (mHasFixedSize) {
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ return;
+ }
+ // custom onMeasure
+ if (mAdapterUpdateDuringMeasure) {
+ eatRequestLayout();
+ processAdapterUpdatesAndSetAnimationFlags();
+
+ if (mState.mRunPredictiveAnimations) {
+ mState.mInPreLayout = true;
+ } else {
+ // consume remaining updates to provide a consistent state with the layout pass.
+ mAdapterHelper.consumeUpdatesInOnePass();
+ mState.mInPreLayout = false;
+ }
+ mAdapterUpdateDuringMeasure = false;
+ resumeRequestLayout(false);
+ }
+
+ if (mAdapter != null) {
+ mState.mItemCount = mAdapter.getItemCount();
+ } else {
+ mState.mItemCount = 0;
+ }
+ eatRequestLayout();
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
+ resumeRequestLayout(false);
+ mState.mInPreLayout = false; // clear
+ }
}
/**
@@ -2626,6 +2686,7 @@
super.onSizeChanged(w, h, oldw, oldh);
if (w != oldw || h != oldh) {
invalidateGlows();
+ // layout's w/h are updated during measure/layout steps.
}
}
@@ -2824,14 +2885,46 @@
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
+ // leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
+ // leave the state in START
return;
}
- mViewInfoStore.clear();
+ mState.mIsMeasuring = false;
+ onEnterLayoutOrScroll();
+ if (mState.mLayoutStep == State.STEP_START) {
+ dispatchLayoutStep1();
+ mLayout.setExactMeasureSpecsFrom(this);
+ dispatchLayoutStep2();
+ } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth() ||
+ mLayout.getHeight() != getHeight()) {
+ // First 2 steps are done in onMeasure but looks like we have to run again due to
+ // changed size.
+ mLayout.setExactMeasureSpecsFrom(this);
+ dispatchLayoutStep2();
+ } else {
+ // always make sure we sync them (to ensure mode is exact)
+ mLayout.setExactMeasureSpecsFrom(this);
+ }
+ dispatchLayoutStep3();
+ onExitLayoutOrScroll();
+ }
+
+ /**
+ * The first step of a layout where we;
+ * - process adapter updates
+ * - decide which animation should run
+ * - save information about current views
+ * - If necessary, run predictive layout and save its information
+ */
+ private void dispatchLayoutStep1() {
+ mState.assertLayoutStep(State.STEP_START);
+ mState.mIsMeasuring = false;
eatRequestLayout();
+ mViewInfoStore.clear();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
@@ -2909,7 +3002,20 @@
} else {
clearOldPositions();
}
- mAdapterHelper.consumePostponedUpdates();
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ mState.mLayoutStep = State.STEP_LAYOUT;
+ }
+
+ /**
+ * The second layout step where we do the actual layout of the views for the final state.
+ * This step might be run multiple times if necessary (e.g. measure).
+ */
+ private void dispatchLayoutStep2() {
+ eatRequestLayout();
+ onEnterLayoutOrScroll();
+ mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
+ mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
@@ -2922,7 +3028,19 @@
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
+ mState.mLayoutStep = State.STEP_ANIMATIONS;
+ onExitLayoutOrScroll();
+ resumeRequestLayout(false);
+ }
+ /**
+ * The final step of the layout where we save the information about views for animations,
+ * trigger animations and do any necessary cleanup.
+ */
+ private void dispatchLayoutStep3() {
+ mState.assertLayoutStep(State.STEP_ANIMATIONS);
+ eatRequestLayout();
+ mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// Step 3: Find out where things are now, and process change animations.
int count = mChildHelper.getChildCount();
@@ -2952,18 +3070,18 @@
// Step 4: Process view info lists and trigger animations
mViewInfoStore.process(mViewInfoProcessCallback);
}
- resumeRequestLayout(false);
+
mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
- onExitLayoutOrScroll();
mLayout.mRequestedSimpleAnimations = false;
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
+ resumeRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
@@ -3133,17 +3251,15 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- eatRequestLayout();
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
- resumeRequestLayout(false);
mFirstLayoutComplete = true;
}
@Override
public void requestLayout() {
- if (!mEatRequestLayout && !mLayoutFrozen) {
+ if (mEatRequestLayout == 0 && !mLayoutFrozen) {
super.requestLayout();
} else {
mLayoutRequestEaten = true;
@@ -3473,7 +3589,7 @@
}
/**
- * Traverses the ascendants of the given view and returns the item view that contains it and
+ * Traverses the ancestors of the given view and returns the item view that contains it and
* also a direct child of the RecyclerView. This returned view can be used to get the
* ViewHolder by calling {@link #getChildViewHolder(View)}.
*
@@ -5807,7 +5923,6 @@
mOnChildAttachStateListeners.get(i).onChildViewAttachedToWindow(child);
}
}
-
}
/**
@@ -5835,17 +5950,119 @@
private boolean mRequestedSimpleAnimations = false;
- private boolean mIsAttachedToWindow = false;
+ boolean mIsAttachedToWindow = false;
+
+ private boolean mAutoMeasure = false;
+
+ /**
+ * LayoutManager has its own more strict measurement cache to avoid re-measuring a child
+ * if the space that will be given to it is already larger than what it has measured before.
+ */
+ private boolean mMeasurementCacheEnabled = true;
+
+
+ /**
+ * These measure specs might be the measure specs that were passed into RecyclerView's
+ * onMeasure method OR fake measure specs created by the RecyclerView.
+ * For example, when a layout is run, RecyclerView always sets these specs to be
+ * EXACTLY because a LayoutManager cannot resize RecyclerView during a layout pass.
+ */
+ private int mWidthSpec, mHeightSpec;
void setRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null) {
mRecyclerView = null;
mChildHelper = null;
+ mWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
+ mHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
} else {
mRecyclerView = recyclerView;
mChildHelper = recyclerView.mChildHelper;
+ mWidthSpec = MeasureSpec
+ .makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY);
+ mHeightSpec = MeasureSpec
+ .makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY);
}
+ }
+ void setMeasureSpecs(int wSpec, int hSpec) {
+ mWidthSpec = wSpec;
+ mHeightSpec = hSpec;
+ }
+
+ /**
+ * Called after a layout is calculated during a measure pass when using auto-measure.
+ * <p>
+ * It simply traverses all children to calculate a bounding box then calls
+ * {@link #setMeasuredDimension(Rect, int, int)}. LayoutManagers can override that method
+ * if they need to handle the bounding box differently.
+ * <p>
+ * For example, GridLayoutManager override that method to ensure that even if a column is
+ * empty, the GridLayoutManager still measures wide enough to include it.
+ *
+ * @param widthSpec The widthSpec that was passing into RecyclerView's onMeasure
+ * @param heightSpec The heightSpec that was passing into RecyclerView's onMeasure
+ */
+ void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
+ final int count = getChildCount();
+ if (count == 0) {
+ mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
+ return;
+ }
+ int minX = Integer.MAX_VALUE;
+ int minY = Integer.MAX_VALUE;
+ int maxX = Integer.MIN_VALUE;
+ int maxY = Integer.MIN_VALUE;
+
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ int left = getDecoratedLeft(child) - lp.leftMargin;
+ int right = getDecoratedRight(child) + lp.rightMargin;
+ int top = getDecoratedTop(child) - lp.topMargin;
+ int bottom = getDecoratedBottom(child) + lp.bottomMargin;
+ if (left < minX) {
+ minX = left;
+ }
+ if (right > maxX) {
+ maxX = right;
+ }
+ if (top < minY) {
+ minY = top;
+ }
+ if (bottom > maxY) {
+ maxY = bottom;
+ }
+ }
+ mRecyclerView.mTempRect.set(minX, minY, maxX, maxY);
+ setMeasuredDimension(mRecyclerView.mTempRect, widthSpec, heightSpec);
+ }
+
+ /**
+ * Sets the measured dimensions from the given bounding box of the children and the
+ * measurement specs that were passed into {@link RecyclerView#onMeasure(int, int)}. It is
+ * called after the RecyclerView calls
+ * {@link LayoutManager#onLayoutChildren(Recycler, State)} during a measurement pass.
+ * <p>
+ * This method should call {@link #setMeasuredDimension(int, int)}.
+ * <p>
+ * The default implementation adds the RecyclerView's padding to the given bounding box
+ * then caps the value to be within the given measurement specs.
+ * <p>
+ * This method is only called if the LayoutManager opted into the auto measurement API.
+ *
+ * @param childrenBounds The bounding box of all children
+ * @param wSpec The widthMeasureSpec that was passed into the RecyclerView.
+ * @param hSpec The heightMeasureSpec that was passed into the RecyclerView.
+ *
+ * @see #setAutoMeasureEnabled(boolean)
+ */
+ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+ int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
+ int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
+ int width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+ int height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+ setMeasuredDimension(width, height);
}
/**
@@ -5871,6 +6088,31 @@
}
/**
+ * Chooses a size from the given specs and parameters that is closest to the desired size
+ * and also complies with the spec.
+ *
+ * @param spec The measureSpec
+ * @param desired The preferred measurement
+ * @param min The minimum value
+ *
+ * @return A size that fits to the given specs
+ */
+ public static int chooseSize(int spec, int desired, int min) {
+ final int mode = View.MeasureSpec.getMode(spec);
+ final int size = View.MeasureSpec.getSize(spec);
+ switch (mode) {
+ case View.MeasureSpec.EXACTLY:
+ return size;
+ case View.MeasureSpec.AT_MOST:
+ desired = Math.min(size, desired);
+ // flow through
+ case View.MeasureSpec.UNSPECIFIED:
+ default:
+ return Math.max(desired, min);
+ }
+ }
+
+ /**
* Checks if RecyclerView is in the middle of a layout or scroll and throws an
* {@link IllegalStateException} if it <b>is</b>.
*
@@ -5884,6 +6126,86 @@
}
/**
+ * Defines whether the layout should be measured by the RecyclerView or the LayoutManager
+ * wants to handle the layout measurements itself.
+ * <p>
+ * This method is usually called by the LayoutManager with value {@code true} if it wants
+ * to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize
+ * the measurement logic, you can call this method with {@code false} and override
+ * {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic.
+ * <p>
+ * AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or
+ * handle various specs provided by the RecyclerView's parent.
+ * It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an
+ * {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based
+ * on children's positions. It does this while supporting all existing animation
+ * capabilities of the RecyclerView.
+ * <p>
+ * AutoMeasure works as follows:
+ * <ol>
+ * <li>LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of
+ * the framework LayoutManagers use {@code auto-measure}.</li>
+ * <li>When {@link RecyclerView#onMeasure(int, int)} is called, if the provided specs are
+ * exact, RecyclerView will only call LayoutManager's {@code onMeasure} and return without
+ * doing any layout calculation.</li>
+ * <li>If one of the layout specs is not {@code EXACT}, the RecyclerView will start the
+ * layout process in {@code onMeasure} call. It will process all pending Adapter updates and
+ * decide whether to run a predictive layout or not. If it decides to do so, it will first
+ * call {@link #onLayoutChildren(Recycler, State)} with {@link State#isPreLayout()} set to
+ * {@code true}. At this stage, {@link #getWidth()} and {@link #getHeight()} will still
+ * return the width and height of the RecyclerView as of the last layout calculation.
+ * <p>
+ * After handling the predictive case, RecyclerView will call
+ * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+ * {@code true} and {@link State#isPreLayout()} set to {@code false}. The LayoutManager can
+ * access the measurement specs via {@link #getHeight()}, {@link #getHeightMode()},
+ * {@link #getWidth()} and {@link #getWidthMode()}.</li>
+ * <li>After the layout calculation, RecyclerView sets the measured width & height by
+ * calculating the bounding box for the children (+ RecyclerView's padding). The
+ * LayoutManagers can override {@link #setMeasuredDimension(Rect, int, int)} to choose
+ * different values. For instance, GridLayoutManager overrides this value to handle the case
+ * where if it is vertical and has 3 columns but only 2 items, it should still measure its
+ * width to fit 3 items, not 2.</li>
+ * <li>Any following on measure call to the RecyclerView will run
+ * {@link #onLayoutChildren(Recycler, State)} with {@link State#isMeasuring()} set to
+ * {@code true} and {@link State#isPreLayout()} set to {@code false}. RecyclerView will
+ * take care of which views are actually added / removed / moved / changed for animations so
+ * that the LayoutManager should not worry about them and handle each
+ * {@link #onLayoutChildren(Recycler, State)} call as if it is the last one.
+ * </li>
+ * <li>When measure is complete and RecyclerView's
+ * {@link #onLayout(boolean, int, int, int, int)} method is called, RecyclerView checks
+ * whether it already did layout calculations during the measure pass and if so, it re-uses
+ * that information. It may still decide to call {@link #onLayoutChildren(Recycler, State)}
+ * if the last measure spec was different from the final dimensions or adapter contents
+ * have changed between the measure call and the layout call.</li>
+ * <li>Finally, animations are calculated and run as usual.</li>
+ * </ol>
+ *
+ * @param enabled <code>True</code> if the Layout should be measured by the
+ * RecyclerView, <code>false</code> if the LayoutManager wants
+ * to measure itself.
+ *
+ * @see #setMeasuredDimension(Rect, int, int)
+ * @see #isAutoMeasureEnabled()
+ */
+ public void setAutoMeasureEnabled(boolean enabled) {
+ mAutoMeasure = enabled;
+ }
+
+ /**
+ * Returns whether the LayoutManager uses the automatic measurement API or not.
+ *
+ * @return <code>True</code> if the LayoutManager is measured by the RecyclerView or
+ * <code>false</code> if it measures itself.
+ *
+ * @see #setAutoMeasureEnabled(boolean)
+ */
+ public boolean isAutoMeasureEnabled() {
+ return mAutoMeasure;
+ }
+
+ /**
* Returns whether this LayoutManager supports automatic item animations.
* A LayoutManager wishing to support item animations should obey certain
* rules as outlined in {@link #onLayoutChildren(Recycler, State)}.
@@ -6447,7 +6769,7 @@
}
/**
- * Traverses the ascendants of the given view and returns the item view that contains it
+ * Traverses the ancestors of the given view and returns the item view that contains it
* and also a direct child of the LayoutManager.
* <p>
* Note that this method may return null if the view is a child of the RecyclerView but
@@ -6695,12 +7017,48 @@
}
/**
+ * Return the width measurement spec mode of the RecyclerView.
+ * <p>
+ * This value is set only if the LayoutManager opts into the auto measure api via
+ * {@link #setAutoMeasureEnabled(boolean)}.
+ * <p>
+ * When RecyclerView is running a layout, this value is always set to
+ * {@link MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+ *
+ * @return Width measure spec mode.
+ *
+ * @see MeasureSpec#getMode(int)
+ * @see View#onMeasure(int, int)
+ */
+ public int getWidthMode() {
+ return MeasureSpec.getMode(mWidthSpec);
+ }
+
+ /**
+ * Return the height measurement spec mode of the RecyclerView.
+ * <p>
+ * This value is set only if the LayoutManager opts into the auto measure api via
+ * {@link #setAutoMeasureEnabled(boolean)}.
+ * <p>
+ * When RecyclerView is running a layout, this value is always set to
+ * {@link MeasureSpec#EXACTLY} even if it was measured with a different spec mode.
+ *
+ * @return Height measure spec mode.
+ *
+ * @see MeasureSpec#getMode(int)
+ * @see View#onMeasure(int, int)
+ */
+ public int getHeightMode() {
+ return MeasureSpec.getMode(mHeightSpec);
+ }
+
+ /**
* Return the width of the parent RecyclerView
*
* @return Width in pixels
*/
public int getWidth() {
- return mRecyclerView != null ? mRecyclerView.getWidth() : 0;
+ return MeasureSpec.getSize(mWidthSpec);
}
/**
@@ -6709,7 +7067,7 @@
* @return Height in pixels
*/
public int getHeight() {
- return mRecyclerView != null ? mRecyclerView.getHeight() : 0;
+ return MeasureSpec.getSize(mHeightSpec);
}
/**
@@ -6914,6 +7272,7 @@
} else {
detachViewAt(index);
recycler.scrapView(view);
+ mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}
@@ -6974,14 +7333,85 @@
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
-
- final int widthSpec = getChildMeasureSpec(getWidth(),
+ final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
canScrollHorizontally());
- final int heightSpec = getChildMeasureSpec(getHeight(),
+ final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
canScrollVertically());
- child.measure(widthSpec, heightSpec);
+ if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+ child.measure(widthSpec, heightSpec);
+ }
+ }
+
+ /**
+ * RecyclerView internally does its own View measurement caching which should help with
+ * WRAP_CONTENT.
+ * <p>
+ * Use this method if the View is already measured once in this layout pass.
+ */
+ boolean shouldReMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+ return !mMeasurementCacheEnabled
+ || !isMeasurementUpToDate(child.getMeasuredWidth(), widthSpec, lp.width)
+ || !isMeasurementUpToDate(child.getMeasuredHeight(), heightSpec, lp.height);
+ }
+
+ // we may consider making this public
+ /**
+ * RecyclerView internally does its own View measurement caching which should help with
+ * WRAP_CONTENT.
+ * <p>
+ * Use this method if the View is not yet measured and you need to decide whether to
+ * measure this View or not.
+ */
+ boolean shouldMeasureChild(View child, int widthSpec, int heightSpec, LayoutParams lp) {
+ return child.isLayoutRequested()
+ || !mMeasurementCacheEnabled
+ || !isMeasurementUpToDate(child.getWidth(), widthSpec, lp.width)
+ || !isMeasurementUpToDate(child.getHeight(), heightSpec, lp.height);
+ }
+
+ /**
+ * In addition to the View Framework's measurement cache, RecyclerView uses its own
+ * additional measurement cache for its children to avoid re-measuring them when not
+ * necessary. It is on by default but it can be turned off via
+ * {@link #setMeasurementCacheEnabled(boolean)}.
+ *
+ * @return True if measurement cache is enabled, false otherwise.
+ *
+ * @see #setMeasurementCacheEnabled(boolean)
+ */
+ public boolean isMeasurementCacheEnabled() {
+ return mMeasurementCacheEnabled;
+ }
+
+ /**
+ * Sets whether RecyclerView should use its own measurement cache for the children. This is
+ * a more aggressive cache than the framework uses.
+ *
+ * @param measurementCacheEnabled True to enable the measurement cache, false otherwise.
+ *
+ * @see #isMeasurementCacheEnabled()
+ */
+ public void setMeasurementCacheEnabled(boolean measurementCacheEnabled) {
+ mMeasurementCacheEnabled = measurementCacheEnabled;
+ }
+
+ private static boolean isMeasurementUpToDate(int childSize, int spec, int dimension) {
+ final int specMode = MeasureSpec.getMode(spec);
+ final int specSize = MeasureSpec.getSize(spec);
+ if (dimension > 0 && childSize != dimension) {
+ return false;
+ }
+ switch (specMode) {
+ case MeasureSpec.UNSPECIFIED:
+ return true;
+ case MeasureSpec.AT_MOST:
+ return specSize >= childSize;
+ case MeasureSpec.EXACTLY:
+ return specSize == childSize;
+ }
+ return false;
}
/**
@@ -7003,40 +7433,43 @@
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
- final int widthSpec = getChildMeasureSpec(getWidth(),
+ final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight() +
lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
- final int heightSpec = getChildMeasureSpec(getHeight(),
+ final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom() +
lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
- child.measure(widthSpec, heightSpec);
+ if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
+ child.measure(widthSpec, heightSpec);
+ }
}
/**
* Calculate a MeasureSpec value for measuring a child view in one dimension.
*
* @param parentSize Size of the parent view where the child will be placed
- * @param padding Total space currently consumed by other elements of parent
- * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+ * @param padding Total space currently consumed by other elements of the parent
+ * @param childDimension Desired size of the child view, or FILL_PARENT/WRAP_CONTENT.
* Generally obtained from the child view's LayoutParams
* @param canScroll true if the parent RecyclerView can scroll in this dimension
*
* @return a MeasureSpec value for the child view
+ * @deprecated use {@link #getChildMeasureSpec(int, int, int, int, boolean)}
*/
+ @Deprecated
public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
boolean canScroll) {
int size = Math.max(0, parentSize - padding);
int resultSize = 0;
int resultMode = 0;
-
if (canScroll) {
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else {
- // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
+ // FILL_PARENT can't be applied since we can scroll in this dimension, wrap
// instead using UNSPECIFIED.
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
@@ -7047,6 +7480,7 @@
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.FILL_PARENT) {
resultSize = size;
+ // TODO this should be my spec.
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
@@ -7057,6 +7491,63 @@
}
/**
+ * Calculate a MeasureSpec value for measuring a child view in one dimension.
+ *
+ * @param parentSize Size of the parent view where the child will be placed
+ * @param parentMode The measurement spec mode of the parent
+ * @param padding Total space currently consumed by other elements of parent
+ * @param childDimension Desired size of the child view, or FILL_PARENT/WRAP_CONTENT.
+ * Generally obtained from the child view's LayoutParams
+ * @param canScroll true if the parent RecyclerView can scroll in this dimension
+ *
+ * @return a MeasureSpec value for the child view
+ */
+ public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
+ int childDimension, boolean canScroll) {
+ int size = Math.max(0, parentSize - padding);
+ int resultSize = 0;
+ int resultMode = 0;
+ if (canScroll) {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT){
+ switch (parentMode) {
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ resultSize = size;
+ resultMode = parentMode;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ break;
+ }
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ } else {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT) {
+ resultSize = size;
+ resultMode = parentMode;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = size;
+ if (parentMode == MeasureSpec.AT_MOST || parentMode == MeasureSpec.EXACTLY) {
+ resultMode = MeasureSpec.AT_MOST;
+ } else {
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+
+ }
+ }
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+ /**
* Returns the measured width of the given child, plus the additional size of
* any insets applied by {@link ItemDecoration ItemDecorations}.
*
@@ -7993,6 +8484,39 @@
return properties;
}
+ void setExactMeasureSpecsFrom(RecyclerView recyclerView) {
+ setMeasureSpecs(
+ MeasureSpec.makeMeasureSpec(recyclerView.getWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(recyclerView.getHeight(), MeasureSpec.EXACTLY)
+ );
+ }
+
+ /**
+ * Internal API to allow LayoutManagers to be measured twice.
+ * <p>
+ * This is not public because LayoutManagers should be able to handle their layouts in one
+ * pass but it is very convenient to make existing LayoutManagers support wrapping content
+ * when both orientations are undefined.
+ * <p>
+ * This API will be removed after default LayoutManagers properly implement wrap content in
+ * non-scroll orientation.
+ */
+ boolean shouldMeasureTwice() {
+ return false;
+ }
+
+ boolean hasFlexibleChildInBothOrientations() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+ if (lp.width < 0 && lp.height < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Some general properties that a LayoutManager may want to use.
*/
@@ -9481,9 +10005,29 @@
* data between your components without needing to manage their lifecycles.</p>
*/
public static class State {
+ static final int STEP_START = 1;
+ static final int STEP_LAYOUT = 1 << 1;
+ static final int STEP_ANIMATIONS = 1 << 2;
+
+ void assertLayoutStep(int accepted) {
+ if ((accepted & mLayoutStep) == 0) {
+ throw new IllegalStateException("Layout state should be one of "
+ + Integer.toBinaryString(accepted) + " but it is "
+ + Integer.toBinaryString(mLayoutStep));
+ }
+ }
+
+ @IntDef(flag = true, value = {
+ STEP_START, STEP_LAYOUT, STEP_ANIMATIONS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface LayoutState {}
private int mTargetPosition = RecyclerView.NO_POSITION;
+ @LayoutState
+ private int mLayoutStep = STEP_START;
+
private SparseArray<Object> mData;
/**
@@ -9512,6 +10056,8 @@
private boolean mTrackOldChangeHolders = false;
+ private boolean mIsMeasuring = false;
+
State reset() {
mTargetPosition = RecyclerView.NO_POSITION;
if (mData != null) {
@@ -9519,9 +10065,32 @@
}
mItemCount = 0;
mStructureChanged = false;
+ mIsMeasuring = false;
return this;
}
+ /**
+ * Returns true if the RecyclerView is currently measuring the layout. This value is
+ * {@code true} only if the LayoutManager opted into the auto measure API and RecyclerView
+ * has non-exact measurement specs.
+ * <p>
+ * Note that if the LayoutManager supports predictive animations and it is calculating the
+ * pre-layout step, this value will be {@code false} even if the RecyclerView is in
+ * {@code onMeasure} call. This is because pre-layout means the previous state of the
+ * RecyclerView and measurements made for that state cannot change the RecyclerView's size.
+ * LayoutManager is always guaranteed to receive another call to
+ * {@link LayoutManager#onLayoutChildren(Recycler, State)} when this happens.
+ *
+ * @return True if the RecyclerView is currently calculating its bounds, false otherwise.
+ */
+ public boolean isMeasuring() {
+ return mIsMeasuring;
+ }
+
+ /**
+ * Returns true if
+ * @return
+ */
public boolean isPreLayout() {
return mInPreLayout;
}
@@ -9890,7 +10459,7 @@
*
* @see #recordPostLayoutInformation(State, ViewHolder)
* @see #animateAppearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
- * @see #animateDisappearance(ViewHolder, ItemHolderInfo)
+ * @see #animateDisappearance(ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animateChange(ViewHolder, ViewHolder, ItemHolderInfo, ItemHolderInfo)
* @see #animatePersistence(ViewHolder, ItemHolderInfo, ItemHolderInfo)
*/
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index 3147d24..a121ec9 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -30,7 +30,6 @@
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
import java.util.ArrayList;
@@ -38,10 +37,10 @@
import java.util.BitSet;
import java.util.List;
-import static android.support.v7.widget.LayoutState.LAYOUT_START;
-import static android.support.v7.widget.LayoutState.LAYOUT_END;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
+import static android.support.v7.widget.LayoutState.LAYOUT_END;
+import static android.support.v7.widget.LayoutState.LAYOUT_START;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
/**
@@ -177,7 +176,7 @@
/**
* Re-used measurement specs. updated by onLayout.
*/
- private int mFullSizeSpec, mWidthSpec, mHeightSpec;
+ private int mFullSizeSpec;
/**
* Re-used rectangle to get child decor offsets.
@@ -221,6 +220,7 @@
setOrientation(properties.orientation);
setSpanCount(properties.spanCount);
setReverseLayout(properties.reverseLayout);
+ setAutoMeasureEnabled(mGapStrategy != GAP_HANDLING_NONE);
}
/**
@@ -233,6 +233,7 @@
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
+ setAutoMeasureEnabled(mGapStrategy != GAP_HANDLING_NONE);
}
/**
@@ -373,10 +374,16 @@
private boolean checkSpanForGap(Span span) {
if (mShouldReverseLayout) {
if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) {
- return true;
+ // if it is full span, it is OK
+ final View endView = span.mViews.get(span.mViews.size() - 1);
+ final LayoutParams lp = span.getLayoutParams(endView);
+ return !lp.mFullSpan;
}
} else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) {
- return true;
+ // if it is full span, it is OK
+ final View startView = span.mViews.get(0);
+ final LayoutParams lp = span.getLayoutParams(startView);
+ return !lp.mFullSpan;
}
return false;
}
@@ -488,6 +495,7 @@
+ "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
}
mGapStrategy = gapStrategy;
+ setAutoMeasureEnabled(mGapStrategy != GAP_HANDLING_NONE);
requestLayout();
}
@@ -556,8 +564,31 @@
public boolean getReverseLayout() {
return mReverseLayout;
}
+
+ @Override
+ public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
+ // we don't like it to wrap content in our non-scroll direction.
+ final int width, height;
+ if (mOrientation == VERTICAL) {
+ int usedHeight = childrenBounds.height() + getPaddingTop() + getPaddingBottom();
+ height = chooseSize(hSpec, usedHeight, getMinimumHeight());
+ width = chooseSize(wSpec, mSizePerSpan * mSpanCount, getMinimumWidth());
+ } else {
+ int usedWidth = childrenBounds.width() + getPaddingLeft() + getPaddingRight();
+ width = chooseSize(wSpec, usedWidth, getMinimumWidth());
+ height = chooseSize(hSpec, mSizePerSpan * mSpanCount, getMinimumHeight());
+ }
+ setMeasuredDimension(width, height);
+ }
+
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ onLayoutChildren(recycler, state, true);
+ }
+
+
+ private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state,
+ boolean shouldCheckForGaps) {
ensureOrientationHelper();
final AnchorInfo anchorInfo = mAnchorInfo;
anchorInfo.reset();
@@ -603,8 +634,9 @@
}
}
detachAndScrapAttachedViews(recycler);
+ mLayoutState.mRecycle = false;
mLaidOutInvalidFullSpan = false;
- updateMeasureSpecs();
+ updateMeasureSpecs(mSecondaryOrientation.getTotalSpace());
updateLayoutState(anchorInfo.mPosition, state);
if (anchorInfo.mLayoutFromEnd) {
// Layout start.
@@ -624,6 +656,8 @@
fill(recycler, mLayoutState, state);
}
+ repositionToWrapContentIfNecessary();
+
if (getChildCount() > 0) {
if (mShouldReverseLayout) {
fixEndGap(recycler, state, true);
@@ -633,14 +667,16 @@
fixEndGap(recycler, state, false);
}
}
-
- if (!state.isPreLayout()) {
+ boolean hasGaps = false;
+ if (shouldCheckForGaps && !state.isPreLayout()) {
final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE
&& getChildCount() > 0
&& (mLaidOutInvalidFullSpan || hasGapsToFix() != null);
if (needToCheckForGaps) {
removeCallbacks(mCheckForGapsRunnable);
- postOnAnimation(mCheckForGapsRunnable);
+ if (checkForGaps()) {
+ hasGaps = true;
+ }
}
mPendingScrollPosition = NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
@@ -648,6 +684,58 @@
mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd;
mLastLayoutRTL = isLayoutRTL();
mPendingSavedState = null; // we don't need this anymore
+ if (hasGaps) {
+ onLayoutChildren(recycler, state, false);
+ }
+ }
+
+ private void repositionToWrapContentIfNecessary() {
+ if (mSecondaryOrientation.getMode() == View.MeasureSpec.EXACTLY) {
+ return; // nothing to do
+ }
+ float maxSize = 0;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i ++) {
+ View child = getChildAt(i);
+ float size = mSecondaryOrientation.getDecoratedMeasurement(child);
+ if (size < maxSize) {
+ continue;
+ }
+ LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+ if (layoutParams.isFullSpan()) {
+ size = 1f * size / mSpanCount;
+ }
+ maxSize = Math.max(maxSize, size);
+ }
+ int before = mSizePerSpan;
+ int desired = Math.round(maxSize * mSpanCount);
+ if (mSecondaryOrientation.getMode() == View.MeasureSpec.AT_MOST) {
+ desired = Math.min(desired, mSecondaryOrientation.getTotalSpace());
+ }
+ updateMeasureSpecs(desired);
+ if (mSizePerSpan == before) {
+ return; // nothing has changed
+ }
+ for (int i = 0; i < childCount; i ++) {
+ View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mFullSpan) {
+ continue;
+ }
+ if (isLayoutRTL() && mOrientation == VERTICAL) {
+ int newOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * mSizePerSpan;
+ int prevOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * before;
+ child.offsetLeftAndRight(newOffset - prevOffset);
+ } else {
+ int newOffset = lp.mSpan.mIndex * mSizePerSpan;
+ int prevOffset = lp.mSpan.mIndex * before;
+ if (mOrientation == VERTICAL) {
+ child.offsetLeftAndRight(newOffset - prevOffset);
+ } else {
+ child.offsetTopAndBottom(newOffset - prevOffset);
+ }
+ }
+ }
}
private void applyPendingSavedState(AnchorInfo anchorInfo) {
@@ -795,17 +883,11 @@
return true;
}
- void updateMeasureSpecs() {
- mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount;
+ void updateMeasureSpecs(int totalSpace) {
+ mSizePerSpan = totalSpace / mSpanCount;
+ //noinspection ResourceType
mFullSizeSpec = View.MeasureSpec.makeMeasureSpec(
- mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY);
- if (mOrientation == VERTICAL) {
- mWidthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
- mHeightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- } else {
- mHeightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
- mWidthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
- }
+ totalSpace, mSecondaryOrientation.getMode());
}
@Override
@@ -1004,43 +1086,48 @@
return computeScrollRange(state);
}
- private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp) {
+ private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp,
+ boolean alreadyMeasured) {
if (lp.mFullSpan) {
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(child, mFullSizeSpec,
- getSpecForDimension(lp.height, mHeightSpec));
+ getChildMeasureSpec(getHeight(), getHeightMode(), 0, lp.height, true),
+ alreadyMeasured);
} else {
measureChildWithDecorationsAndMargin(child,
- getSpecForDimension(lp.width, mWidthSpec), mFullSizeSpec);
+ getChildMeasureSpec(getWidth(), getWidthMode(), 0, lp.width, true),
+ mFullSizeSpec, alreadyMeasured);
}
} else {
if (mOrientation == VERTICAL) {
- measureChildWithDecorationsAndMargin(child, mWidthSpec,
- getSpecForDimension(lp.height, mHeightSpec));
+ measureChildWithDecorationsAndMargin(child,
+ getChildMeasureSpec(mSizePerSpan, getWidthMode(), 0, lp.width, false),
+ getChildMeasureSpec(getHeight(), getHeightMode(), 0, lp.height, true),
+ alreadyMeasured);
} else {
measureChildWithDecorationsAndMargin(child,
- getSpecForDimension(lp.width, mWidthSpec), mHeightSpec);
+ getChildMeasureSpec(getWidth(), getWidthMode(), 0, lp.width, true),
+ getChildMeasureSpec(mSizePerSpan, getHeightMode(), 0, lp.height, false),
+ alreadyMeasured);
}
}
}
- private int getSpecForDimension(int dim, int defaultSpec) {
- if (dim < 0) {
- return defaultSpec;
- } else {
- return View.MeasureSpec.makeMeasureSpec(dim, View.MeasureSpec.EXACTLY);
- }
- }
-
private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
- int heightSpec) {
+ int heightSpec, boolean alreadyMeasured) {
calculateItemDecorationsForChild(child, mTmpRect);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left,
lp.rightMargin + mTmpRect.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top,
lp.bottomMargin + mTmpRect.bottom);
- child.measure(widthSpec, heightSpec);
+ final boolean measure = alreadyMeasured
+ ? shouldReMeasureChild(child, widthSpec, heightSpec, lp)
+ : shouldMeasureChild(child, widthSpec, heightSpec, lp);
+ if (measure) {
+ child.measure(widthSpec, heightSpec);
+ }
+
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
@@ -1253,7 +1340,10 @@
private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
- final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
+ final int maxEndLine = getMaxEnd(Integer.MIN_VALUE);
+ if (maxEndLine == Integer.MIN_VALUE) {
+ return;
+ }
int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
int fixOffset;
if (gap > 0) {
@@ -1269,7 +1359,10 @@
private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
- final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding());
+ final int minStartLine = getMinStart(Integer.MAX_VALUE);
+ if (minStartLine == Integer.MAX_VALUE) {
+ return;
+ }
int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
int fixOffset;
if (gap > 0) {
@@ -1309,6 +1402,8 @@
mLayoutState.mStartLine = -startExtra;
}
mLayoutState.mStopInFocusable = false;
+ mLayoutState.mRecycle = true;
+ mLayoutState.mInfinite = mPrimaryOrientation.getMode() == View.MeasureSpec.UNSPECIFIED;
}
private void setLayoutStateDirection(int direction) {
@@ -1413,10 +1508,18 @@
final int targetLine;
// Line of the furthest row.
- if (layoutState.mLayoutDirection == LAYOUT_END) {
- targetLine = layoutState.mEndLine + layoutState.mAvailable;
- } else { // LAYOUT_START
- targetLine = layoutState.mStartLine - layoutState.mAvailable;
+ if (mLayoutState.mInfinite) {
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ targetLine = Integer.MAX_VALUE;
+ } else { // LAYOUT_START
+ targetLine = Integer.MIN_VALUE;
+ }
+ } else {
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ targetLine = layoutState.mEndLine + layoutState.mAvailable;
+ } else { // LAYOUT_START
+ targetLine = layoutState.mStartLine - layoutState.mAvailable;
+ }
}
updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine);
@@ -1430,7 +1533,8 @@
? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
boolean added = false;
- while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) {
+ while (layoutState.hasMore(state)
+ && (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) {
View view = layoutState.next(recycler);
LayoutParams lp = ((LayoutParams) view.getLayoutParams());
final int position = lp.getViewLayoutPosition();
@@ -1456,7 +1560,7 @@
} else {
addView(view, 0);
}
- measureChildWithDecorationsAndMargin(view, lp);
+ measureChildWithDecorationsAndMargin(view, lp, false);
final int start;
final int end;
@@ -1506,10 +1610,20 @@
}
}
attachViewToSpans(view, lp, layoutState);
- final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
- : currentSpan.mIndex * mSizePerSpan +
- mSecondaryOrientation.getStartAfterPadding();
- final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
+ final int otherStart;
+ final int otherEnd;
+ if (isLayoutRTL() && mOrientation == VERTICAL) {
+ otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() :
+ mSecondaryOrientation.getEndAfterPadding()
+ - (mSpanCount - 1 - currentSpan.mIndex) * mSizePerSpan;
+ otherStart = otherEnd - mSecondaryOrientation.getDecoratedMeasurement(view);
+ } else {
+ otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
+ : currentSpan.mIndex * mSizePerSpan +
+ mSecondaryOrientation.getStartAfterPadding();
+ otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
+ }
+
if (mOrientation == VERTICAL) {
layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
} else {
@@ -1580,7 +1694,7 @@
}
private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) {
- if (layoutState.mStopInFocusable) {
+ if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mAvailable == 0) {
@@ -1938,6 +2052,7 @@
layoutDir = LAYOUT_START;
referenceChildPosition = getFirstChildPosition();
}
+ mLayoutState.mRecycle = true;
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
@@ -2007,8 +2122,13 @@
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
+ if (mOrientation == HORIZONTAL) {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.FILL_PARENT);
+ } else {
+ return new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
}
@Override
@@ -2068,6 +2188,7 @@
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
mLayoutState.mAvailable = (int) (MAX_SCROLL_FACTOR * mPrimaryOrientation.getTotalSpace());
mLayoutState.mStopInFocusable = true;
+ mLayoutState.mRecycle = false;
fill(recycler, mLayoutState, state);
mLastLayoutFromEnd = mShouldReverseLayout;
if (!prevFocusFullSpan) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/ViewInfoStore.java b/v7/recyclerview/src/android/support/v7/widget/ViewInfoStore.java
index 0af8dfb..69529f0 100644
--- a/v7/recyclerview/src/android/support/v7/widget/ViewInfoStore.java
+++ b/v7/recyclerview/src/android/support/v7/widget/ViewInfoStore.java
@@ -21,6 +21,7 @@
import android.support.v4.util.ArrayMap;
import android.support.v4.util.LongSparseArray;
import android.support.v4.util.Pools;
+import android.view.View;
import static android.support.v7.widget.RecyclerView.ViewHolder;
import static android.support.v7.widget.RecyclerView.ItemAnimator.ItemHolderInfo;
@@ -241,6 +242,10 @@
InfoRecord.drainCache();
}
+ public void onViewDetached(ViewHolder viewHolder) {
+ removeFromDisappearedInLayout(viewHolder);
+ }
+
interface ProcessCallback {
void processDisappeared(ViewHolder viewHolder, ItemHolderInfo preInfo,
@Nullable ItemHolderInfo postInfo);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
new file mode 100644
index 0000000..cfe6fe1
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import android.content.Context;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+
+public class BaseGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
+
+ static final String TAG = "GridLayoutManagerTest";
+ static final boolean DEBUG = false;
+
+ WrappedGridLayoutManager mGlm;
+ GridTestAdapter mAdapter;
+
+ public RecyclerView setupBasic(Config config) throws Throwable {
+ return setupBasic(config, new GridTestAdapter(config.mItemCount));
+ }
+
+ public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ mAdapter = testAdapter;
+ mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
+ config.mReverseLayout);
+ mAdapter.assignSpanSizeLookup(mGlm);
+ recyclerView.setAdapter(mAdapter);
+ recyclerView.setLayoutManager(mGlm);
+ return recyclerView;
+ }
+
+ public static List<Config> createBaseVariations() {
+ List<Config> variations = new ArrayList<>();
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (int spanCount : new int[]{1, 3, 4}) {
+ variations.add(new Config(spanCount, orientation, reverseLayout));
+ }
+ }
+ }
+ return variations;
+ }
+
+ public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
+ mGlm.expectLayout(1);
+ setRecyclerView(recyclerView);
+ mGlm.waitForLayout(2);
+ }
+
+ protected int getSize(View view) {
+ if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
+ return view.getWidth();
+ }
+ return view.getHeight();
+ }
+
+ GridLayoutManager.LayoutParams getLp(View view) {
+ return (GridLayoutManager.LayoutParams) view.getLayoutParams();
+ }
+
+ static class Config implements Cloneable {
+
+ int mSpanCount;
+ int mOrientation = GridLayoutManager.VERTICAL;
+ int mItemCount = 1000;
+ int mSpanPerItem = 1;
+ boolean mReverseLayout = false;
+
+ Config(int spanCount, int itemCount) {
+ mSpanCount = spanCount;
+ mItemCount = itemCount;
+ }
+
+ public Config(int spanCount, int orientation, boolean reverseLayout) {
+ mSpanCount = spanCount;
+ mOrientation = orientation;
+ mReverseLayout = reverseLayout;
+ }
+
+ Config orientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "Config{" +
+ "mSpanCount=" + mSpanCount +
+ ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") +
+ ", mItemCount=" + mItemCount +
+ ", mReverseLayout=" + mReverseLayout +
+ '}';
+ }
+
+ public Config reverseLayout(boolean reverseLayout) {
+ mReverseLayout = reverseLayout;
+ return this;
+ }
+
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+ }
+
+ class WrappedGridLayoutManager extends GridLayoutManager {
+
+ CountDownLatch mLayoutLatch;
+
+ List<GridLayoutManagerTest.Callback>
+ mCallbacks = new ArrayList<GridLayoutManagerTest.Callback>();
+
+ Boolean mFakeRTL;
+
+ public WrappedGridLayoutManager(Context context, int spanCount) {
+ super(context, spanCount);
+ }
+
+ public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
+ boolean reverseLayout) {
+ super(context, spanCount, orientation, reverseLayout);
+ }
+
+ @Override
+ protected boolean isLayoutRTL() {
+ return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
+ }
+
+ public void setFakeRtl(Boolean fakeRtl) {
+ mFakeRTL = fakeRtl;
+ try {
+ requestLayoutOnUIThread(mRecyclerView);
+ } catch (Throwable throwable) {
+ postExceptionToInstrumentation(throwable);
+ }
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ try {
+ for (GridLayoutManagerTest.Callback callback : mCallbacks) {
+ callback.onBeforeLayout(recycler, state);
+ }
+ super.onLayoutChildren(recycler, state);
+ for (GridLayoutManagerTest.Callback callback : mCallbacks) {
+ callback.onAfterLayout(recycler, state);
+ }
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+ mLayoutLatch.countDown();
+ }
+
+ @Override
+ LayoutState createLayoutState() {
+ return new LayoutState() {
+ @Override
+ View next(RecyclerView.Recycler recycler) {
+ final boolean hadMore = hasMore(mRecyclerView.mState);
+ final int position = mCurrentPosition;
+ View next = super.next(recycler);
+ assertEquals("if has more, should return a view", hadMore, next != null);
+ assertEquals("position of the returned view must match current position",
+ position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
+ return next;
+ }
+ };
+ }
+
+ public void expectLayout(int layoutCount) {
+ mLayoutLatch = new CountDownLatch(layoutCount);
+ }
+
+ public void waitForLayout(int seconds) throws InterruptedException {
+ mLayoutLatch.await(seconds, SECONDS);
+ }
+ }
+
+ class GridTestAdapter extends TestAdapter {
+
+ Set<Integer> mFullSpanItems = new HashSet<Integer>();
+ int mSpanPerItem = 1;
+
+ GridTestAdapter(int count) {
+ super(count);
+ }
+
+ GridTestAdapter(int count, int spanPerItem) {
+ super(count);
+ mSpanPerItem = spanPerItem;
+ }
+
+ void setFullSpan(int... items) {
+ for (int i : items) {
+ mFullSpanItems.add(i);
+ }
+ }
+
+ void assignSpanSizeLookup(final GridLayoutManager glm) {
+ glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
+ }
+ });
+ }
+ }
+
+ class Callback {
+
+ public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ }
+
+ public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
index a8f554a..4e37369 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
@@ -19,6 +19,7 @@
import android.graphics.Rect;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -30,6 +31,8 @@
import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.FILL_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static org.junit.Assert.*;
public class BaseLinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
@@ -42,7 +45,11 @@
for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
for (boolean reverseLayout : new boolean[]{false, true}) {
for (boolean stackFromBottom : new boolean[]{false, true}) {
- variations.add(new Config(orientation, reverseLayout, stackFromBottom));
+ for (boolean wrap : new boolean[]{false, true}) {
+ variations.add(
+ new Config(orientation, reverseLayout, stackFromBottom).wrap(wrap));
+ }
+
}
}
}
@@ -79,6 +86,14 @@
mLayoutManager.setStackFromEnd(config.mStackFromEnd);
mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
mRecyclerView.setLayoutManager(mLayoutManager);
+ if (config.mWrap) {
+ mRecyclerView.setLayoutParams(
+ new ViewGroup.LayoutParams(
+ config.mOrientation == HORIZONTAL ? WRAP_CONTENT : FILL_PARENT,
+ config.mOrientation == VERTICAL ? WRAP_CONTENT : FILL_PARENT
+ )
+ );
+ }
if (waitForFirstLayout) {
waitForFirstLayout();
}
@@ -248,6 +263,8 @@
int mItemCount = DEFAULT_ITEM_COUNT;
+ boolean mWrap = false;
+
TestAdapter mTestAdapter;
Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
@@ -304,8 +321,14 @@
", mReverseLayout=" + mReverseLayout +
", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
", mItemCount=" + mItemCount +
+ ", wrap=" + mWrap +
'}';
}
+
+ public Config wrap(boolean wrap) {
+ mWrap = wrap;
+ return this;
+ }
}
class WrappedLinearLayoutManager extends LinearLayoutManager {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
index dfac00a..b8fb198 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewAnimationsTest.java
@@ -35,7 +35,7 @@
*/
public class BaseRecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
- protected static final boolean DEBUG = false;
+ protected static final boolean DEBUG = true;
protected static final String TAG = "RecyclerViewAnimationsTest";
@@ -645,6 +645,14 @@
}
setFrom(viewHolder);
}
+
+ @Override
+ public String toString() {
+ return "LoggingInfo{" +
+ "changeFlags=" + changeFlags +
+ ", payloads=" + payloads +
+ '}';
+ }
}
static class AnimateChange extends AnimateLogBase {
@@ -681,9 +689,9 @@
}
static class AnimateLogBase {
- final RecyclerView.ViewHolder viewHolder;
- final LoggingInfo preInfo;
- final LoggingInfo postInfo;
+ public final RecyclerView.ViewHolder viewHolder;
+ public final LoggingInfo preInfo;
+ public final LoggingInfo postInfo;
public AnimateLogBase(RecyclerView.ViewHolder viewHolder, LoggingInfo pre,
LoggingInfo postInfo) {
@@ -692,6 +700,14 @@
this.postInfo = postInfo;
}
+ public String log() {
+ return getClass().getSimpleName() + "[" + log(preInfo) + " - " + log(postInfo) + "]";
+ }
+
+ public String log(LoggingInfo info) {
+ return info == null ? "null" : info.toString();
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index a250f48..4acb3e5 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -541,7 +541,6 @@
assertEquals("getViewForPosition should return correct position",
i, getPosition(view));
addView(view);
-
measureChildWithMargins(view, 0, 0);
if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
layoutDecorated(view, getWidth() - getDecoratedMeasuredWidth(view), top,
@@ -672,7 +671,7 @@
assertNotNull(holder.mOwnerRecyclerView);
assertEquals(position, holder.getAdapterPosition());
final Item item = mItems.get(position);
- ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
+ ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")");
holder.mBoundItem = item;
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
index 6fe41f5..6519a32 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
@@ -1,7 +1,5 @@
package android.support.v7.widget;
-import org.junit.Before;
-
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.Log;
@@ -45,8 +43,11 @@
for (int spanCount : new int[]{1, 3}) {
for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
- variations.add(new Config(orientation, reverseLayout, spanCount,
- gapStrategy));
+ for (boolean wrap : new boolean[]{true, false}) {
+ variations.add(new Config(orientation, reverseLayout, spanCount,
+ gapStrategy).wrap(wrap));
+ }
+
}
}
}
@@ -391,6 +392,8 @@
int mItemCount = DEFAULT_ITEM_COUNT;
+ boolean mWrap = false;
+
Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
mOrientation = orientation;
mReverseLayout = reverseLayout;
@@ -427,6 +430,11 @@
return this;
}
+ public Config wrap(boolean wrap) {
+ mWrap = wrap;
+ return this;
+ }
+
@Override
public String toString() {
return "[CONFIG:" +
@@ -434,6 +442,7 @@
" orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
" reverse:" + (mReverseLayout ? "T" : "F") +
" itemCount:" + mItemCount +
+ " wrapContent:" + mWrap +
" gap strategy: " + gapStrategyName(mGapStrategy);
}
@@ -789,11 +798,12 @@
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
.getLayoutParams();
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
- ((StaggeredGridLayoutManager.LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
+ ((StaggeredGridLayoutManager.LayoutParams) lp)
+ .setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
} else {
- StaggeredGridLayoutManager.LayoutParams
- slp = new StaggeredGridLayoutManager.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT);
+ StaggeredGridLayoutManager.LayoutParams slp
+ = (StaggeredGridLayoutManager.LayoutParams) mLayoutManager
+ .generateDefaultLayoutParams();
holder.itemView.setLayoutParams(slp);
slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
lp = slp;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentTest.java
new file mode 100644
index 0000000..23aa5d5
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentTest.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (C) 2015 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.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.support.annotation.Nullable;
+import android.support.v4.util.LongSparseArray;
+import android.support.v7.widget.TestedFrameLayout.FullControlLayoutParams;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Class to test any generic wrap content behavior.
+ * It does so by running the same view scenario twice. Once with match parent setup to record all
+ * dimensions and once with wrap_content setup. Then compares all child locations & ids +
+ * RecyclerView size.
+ */
+abstract public class BaseWrapContentTest extends BaseRecyclerViewInstrumentationTest {
+
+ static final boolean DEBUG = false;
+ static final String TAG = "WrapContentTest";
+ RecyclerView.LayoutManager mLayoutManager;
+
+ TestAdapter mTestAdapter;
+
+ LoggingItemAnimator mLoggingItemAnimator;
+
+ boolean mIsWrapContent;
+
+ protected final WrapContentConfig mWrapContentConfig;
+
+ public BaseWrapContentTest(WrapContentConfig config) {
+ mWrapContentConfig = config;
+ }
+
+ abstract RecyclerView.LayoutManager createLayoutManager();
+
+ protected void testScenerio(Scenario scenario) throws Throwable {
+ FullControlLayoutParams matchParent = new FullControlLayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ FullControlLayoutParams wrapContent = new FullControlLayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ if (mWrapContentConfig.isUnlimitedHeight()) {
+ wrapContent.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+ if (mWrapContentConfig.isUnlimitedWidth()) {
+ wrapContent.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+
+ mIsWrapContent = false;
+ List<Snapshot> s1 = runScenario(scenario, matchParent, null);
+ mIsWrapContent = true;
+
+ List<Snapshot> s2 = runScenario(scenario, wrapContent, s1);
+ assertEquals("test sanity", s1.size(), s2.size());
+
+ for (int i = 0; i < s1.size(); i++) {
+ Snapshot step1 = s1.get(i);
+ Snapshot step2 = s2.get(i);
+ step1.assertSame(step2, i);
+ }
+ }
+
+ public List<Snapshot> runScenario(Scenario scenario, ViewGroup.LayoutParams lp,
+ @Nullable List<Snapshot> compareWith)
+ throws Throwable {
+ removeRecyclerView();
+ Item.idCounter.set(0);
+ List<Snapshot> result = new ArrayList<>();
+ RecyclerView.LayoutManager layoutManager = scenario.createLayoutManager();
+ WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity());
+ recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
+ recyclerView.setLayoutManager(layoutManager);
+ recyclerView.setLayoutParams(lp);
+ mLayoutManager = layoutManager;
+ mTestAdapter = new TestAdapter(scenario.getSeedAdapterSize());
+ recyclerView.setAdapter(mTestAdapter);
+ mLoggingItemAnimator = new LoggingItemAnimator();
+ recyclerView.setItemAnimator(mLoggingItemAnimator);
+ setRecyclerView(recyclerView);
+ recyclerView.waitUntilLayout();
+ int stepIndex = 0;
+ for (Step step : scenario.mStepList) {
+ mLoggingItemAnimator.reset();
+ step.onRun();
+ recyclerView.waitUntilLayout();
+ recyclerView.waitUntilAnimations();
+ Snapshot snapshot = takeSnapshot();
+ if (mIsWrapContent) {
+ snapshot.assertRvSize();
+ }
+ result.add(snapshot);
+ if (compareWith != null) {
+ compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
+ }
+ stepIndex++;
+ }
+ recyclerView.waitUntilLayout();
+ recyclerView.waitUntilAnimations();
+ Snapshot snapshot = takeSnapshot();
+ if (mIsWrapContent) {
+ snapshot.assertRvSize();
+ }
+ result.add(snapshot);
+ if (compareWith != null) {
+ compareWith.get(stepIndex).assertSame(snapshot, stepIndex);
+ }
+ return result;
+ }
+
+ void layoutAndCheck(TestedFrameLayout.FullControlLayoutParams lp,
+ BaseWrapContentWithAspectRatioTest.WrapContentAdapter adapter, Rect[] expected,
+ int width, int height) throws Throwable {
+ WrappedRecyclerView recyclerView = new WrappedRecyclerView(getActivity());
+ recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
+ recyclerView.setLayoutManager(createLayoutManager());
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutParams(lp);
+ setRecyclerView(recyclerView);
+ recyclerView.waitUntilLayout();
+ Snapshot snapshot = takeSnapshot();
+ int index = 0;
+ for (BaseWrapContentWithAspectRatioTest.MeasureBehavior behavior : adapter.behaviors) {
+ assertThat("behavior " + index, snapshot.mChildCoordinates.get(behavior.getId()),
+ is(expected[index]));
+ index ++;
+ }
+ Rect boundingBox = new Rect(0, 0, 0, 0);
+ for (Rect rect : expected) {
+ boundingBox.union(rect);
+ }
+ assertThat(recyclerView.getWidth(), is(width));
+ assertThat(recyclerView.getHeight(), is(height));
+ }
+
+
+ abstract protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager);
+
+ abstract protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager);
+
+ protected Snapshot takeSnapshot() throws Throwable {
+ Snapshot snapshot = new Snapshot(mRecyclerView, mLoggingItemAnimator,
+ getHorizontalGravity(mLayoutManager), getVerticalGravity(mLayoutManager));
+ return snapshot;
+ }
+
+ abstract class Scenario {
+
+ ArrayList<Step> mStepList = new ArrayList<>();
+
+ public Scenario(Step... steps) {
+ Collections.addAll(mStepList, steps);
+ }
+
+ public int getSeedAdapterSize() {
+ return 10;
+ }
+
+ public RecyclerView.LayoutManager createLayoutManager() {
+ return BaseWrapContentTest.this.createLayoutManager();
+ }
+ }
+
+ abstract static class Step {
+
+ abstract void onRun() throws Throwable;
+ }
+
+ class Snapshot {
+
+ Rect mRawChildrenBox = new Rect();
+
+ Rect mRvSize = new Rect();
+
+ Rect mRvPadding = new Rect();
+
+ Rect mRvParentSize = new Rect();
+
+ LongSparseArray<Rect> mChildCoordinates = new LongSparseArray<>();
+
+ LongSparseArray<String> mAppear = new LongSparseArray<>();
+
+ LongSparseArray<String> mDisappear = new LongSparseArray<>();
+
+ LongSparseArray<String> mPersistent = new LongSparseArray<>();
+
+ LongSparseArray<String> mChanged = new LongSparseArray<>();
+
+ int mVerticalGravity;
+
+ int mHorizontalGravity;
+
+ int mOffsetX, mOffsetY;// how much we should offset children
+
+ public Snapshot(RecyclerView recyclerView, LoggingItemAnimator loggingItemAnimator,
+ int horizontalGravity, int verticalGravity)
+ throws Throwable {
+ mRvSize = getViewBounds(recyclerView);
+ mRvParentSize = getViewBounds((View) recyclerView.getParent());
+ mRvPadding = new Rect(recyclerView.getPaddingLeft(), recyclerView.getPaddingTop(),
+ recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
+ mVerticalGravity = verticalGravity;
+ mHorizontalGravity = horizontalGravity;
+ if (mVerticalGravity == Gravity.TOP) {
+ mOffsetY = 0;
+ } else {
+ mOffsetY = mRvParentSize.bottom - mRvSize.bottom;
+ }
+
+ if (mHorizontalGravity == Gravity.LEFT) {
+ mOffsetX = 0;
+ } else {
+ mOffsetX = mRvParentSize.right - mRvSize.right;
+ }
+ collectChildCoordinates(recyclerView);
+ if (loggingItemAnimator != null) {
+ collectInto(mAppear, loggingItemAnimator.mAnimateAppearanceList);
+ collectInto(mDisappear, loggingItemAnimator.mAnimateDisappearanceList);
+ collectInto(mPersistent, loggingItemAnimator.mAnimatePersistenceList);
+ collectInto(mChanged, loggingItemAnimator.mAnimateChangeList);
+ }
+ }
+
+ public boolean doesChildrenFitVertically() {
+ return mRawChildrenBox.top >= mRvPadding.top
+ && mRawChildrenBox.bottom <= mRvSize.bottom - mRvPadding.bottom;
+ }
+
+ public boolean doesChildrenFitHorizontally() {
+ return mRawChildrenBox.left >= mRvPadding.left
+ && mRawChildrenBox.right <= mRvSize.right - mRvPadding.right;
+ }
+
+ public void assertSame(Snapshot other, int step) {
+ if (mWrapContentConfig.isUnlimitedHeight() &&
+ (!doesChildrenFitVertically() || !other.doesChildrenFitVertically())) {
+ if (DEBUG) {
+ Log.d(TAG, "cannot assert coordinates because it does not fit vertically");
+ }
+ return;
+ }
+ if (mWrapContentConfig.isUnlimitedWidth() &&
+ (!doesChildrenFitHorizontally() || !other.doesChildrenFitHorizontally())) {
+ if (DEBUG) {
+ Log.d(TAG, "cannot assert coordinates because it does not fit horizontally");
+ }
+ return;
+ }
+ assertMap("child coordinates. step:" + step, mChildCoordinates,
+ other.mChildCoordinates);
+ if (mWrapContentConfig.isUnlimitedHeight() || mWrapContentConfig.isUnlimitedWidth()) {
+ return;//cannot assert animatinos in unlimited size
+ }
+ assertMap("appearing step:" + step, mAppear, other.mAppear);
+ assertMap("disappearing step:" + step, mDisappear, other.mDisappear);
+ assertMap("persistent step:" + step, mPersistent, other.mPersistent);
+ assertMap("changed step:" + step, mChanged, other.mChanged);
+ }
+
+ private void assertMap(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2) {
+ StringBuilder logBuilder = new StringBuilder();
+ logBuilder.append(prefix).append("\n");
+ logBuilder.append("map1").append("\n");
+ logInto(map1, logBuilder);
+ logBuilder.append("map2").append("\n");
+ logInto(map2, logBuilder);
+ final String log = logBuilder.toString();
+ assertEquals(log + " same size", map1.size(), map2.size());
+ for (int i = 0; i < map1.size(); i++) {
+ assertAtIndex(log, map1, map2, i);
+ }
+ }
+
+ private void assertAtIndex(String prefix, LongSparseArray<?> map1, LongSparseArray<?> map2,
+ int index) {
+ long key1 = map1.keyAt(index);
+ long key2 = map2.keyAt(index);
+ assertEquals(prefix + "key mismatch at index " + index, key1, key2);
+ Object value1 = map1.valueAt(index);
+ Object value2 = map2.valueAt(index);
+ assertEquals(prefix + " value mismatch at index " + index, value1, value2);
+ }
+
+ private void logInto(LongSparseArray<?> map, StringBuilder sb) {
+ for (int i = 0; i < map.size(); i++) {
+ long key = map.keyAt(i);
+ Object value = map.valueAt(i);
+ sb.append(key).append(" : ").append(value).append("\n");
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("Snapshot{\n");
+ sb.append("child coordinates:\n");
+ logInto(mChildCoordinates, sb);
+ sb.append("appear animations:\n");
+ logInto(mAppear, sb);
+ sb.append("disappear animations:\n");
+ logInto(mDisappear, sb);
+ sb.append("change animations:\n");
+ logInto(mChanged, sb);
+ sb.append("persistent animations:\n");
+ logInto(mPersistent, sb);
+ sb.append("}");
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mChildCoordinates.hashCode();
+ result = 31 * result + mAppear.hashCode();
+ result = 31 * result + mDisappear.hashCode();
+ result = 31 * result + mPersistent.hashCode();
+ result = 31 * result + mChanged.hashCode();
+ return result;
+ }
+
+ private void collectInto(
+ LongSparseArray<String> target,
+ List<? extends BaseRecyclerViewAnimationsTest.AnimateLogBase> list) {
+ for (BaseRecyclerViewAnimationsTest.AnimateLogBase base : list) {
+ long id = getItemId(base.viewHolder);
+ assertNull(target.get(id));
+ target.put(id, log(base));
+ }
+ }
+
+ private String log(BaseRecyclerViewAnimationsTest.AnimateLogBase base) {
+ return base.getClass().getSimpleName() +
+ ((TextView) base.viewHolder.itemView).getText() + ": " +
+ "[pre:" + log(base.postInfo) +
+ ", post:" + log(base.postInfo) + "]";
+ }
+
+ private String log(BaseRecyclerViewAnimationsTest.LoggingInfo postInfo) {
+ if (postInfo == null) {
+ return "?";
+ }
+ return "PI[flags: " + postInfo.changeFlags
+ + ",l:" + (postInfo.left + mOffsetX)
+ + ",t:" + (postInfo.top + mOffsetY)
+ + ",r:" + (postInfo.right + mOffsetX)
+ + ",b:" + (postInfo.bottom + mOffsetY) + "]";
+ }
+
+ void collectChildCoordinates(RecyclerView recyclerView) throws Throwable {
+ mRawChildrenBox = new Rect(0, 0, 0, 0);
+ final int childCount = recyclerView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = recyclerView.getChildAt(i);
+ Rect childBounds = getChildBounds(recyclerView, child, true);
+ mRawChildrenBox.union(getChildBounds(recyclerView, child, false));
+ RecyclerView.ViewHolder childViewHolder = recyclerView.getChildViewHolder(child);
+ mChildCoordinates.put(getItemId(childViewHolder), childBounds);
+ }
+ }
+
+ private Rect getViewBounds(View view) {
+ return new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
+ }
+
+ private Rect getChildBounds(RecyclerView recyclerView, View child, boolean offset) {
+ RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+ RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
+ Rect rect = new Rect(layoutManager.getDecoratedLeft(child) - lp.leftMargin,
+ layoutManager.getDecoratedTop(child) - lp.topMargin,
+ layoutManager.getDecoratedRight(child) + lp.rightMargin,
+ layoutManager.getDecoratedBottom(child) + lp.bottomMargin);
+ if (offset) {
+ rect.offset(mOffsetX, mOffsetY);
+ }
+ return rect;
+ }
+
+ private long getItemId(RecyclerView.ViewHolder vh) {
+ if (vh instanceof TestViewHolder) {
+ return ((TestViewHolder) vh).mBoundItem.mId;
+ } else if (vh instanceof BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) {
+ BaseWrapContentWithAspectRatioTest.WrapContentViewHolder casted =
+ (BaseWrapContentWithAspectRatioTest.WrapContentViewHolder) vh;
+ return casted.mView.mBehavior.getId();
+ } else {
+ throw new IllegalArgumentException("i don't support any VH");
+ }
+ }
+
+ public void assertRvSize() {
+ if (shouldWrapContentHorizontally()) {
+ int expectedW = mRawChildrenBox.width() + mRvPadding.left + mRvPadding.right;
+ assertTrue(mRvSize.width() + " <= " + expectedW, mRvSize.width() <= expectedW);
+ }
+ if (shouldWrapContentVertically()) {
+ int expectedH = mRawChildrenBox.height() + mRvPadding.top + mRvPadding.bottom;
+ assertTrue(mRvSize.height() + "<=" + expectedH, mRvSize.height() <= expectedH);
+ }
+ }
+ }
+
+ protected boolean shouldWrapContentHorizontally() {
+ return true;
+ }
+
+ protected boolean shouldWrapContentVertically() {
+ return true;
+ }
+
+ static class WrappedRecyclerView extends RecyclerView {
+
+ public WrappedRecyclerView(Context context) {
+ super(context);
+ }
+
+ public void waitUntilLayout() {
+ while (isLayoutRequested()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void waitUntilAnimations() {
+ while (mItemAnimator != null && mItemAnimator.isRunning()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ }
+ }
+
+ static class WrapContentConfig {
+
+ private boolean unlimitedWidth;
+ private boolean unlimitedHeight;
+
+ public WrapContentConfig(boolean unlimitedWidth, boolean unlimitedHeight) {
+ this.unlimitedWidth = unlimitedWidth;
+ this.unlimitedHeight = unlimitedHeight;
+ }
+
+ public boolean isUnlimitedWidth() {
+ return unlimitedWidth;
+ }
+
+ public WrapContentConfig setUnlimitedWidth(boolean unlimitedWidth) {
+ this.unlimitedWidth = unlimitedWidth;
+ return this;
+ }
+
+ public boolean isUnlimitedHeight() {
+ return unlimitedHeight;
+ }
+
+ public WrapContentConfig setUnlimitedHeight(boolean unlimitedHeight) {
+ this.unlimitedHeight = unlimitedHeight;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "WrapContentConfig{" +
+ "unlimitedWidth=" + unlimitedWidth +
+ ", unlimitedHeight=" + unlimitedHeight +
+ '}';
+ }
+
+ public TestedFrameLayout.FullControlLayoutParams toLayoutParams(int wDim, int hDim) {
+ TestedFrameLayout.FullControlLayoutParams
+ lp = new TestedFrameLayout.FullControlLayoutParams(
+ wDim, hDim);
+ if (unlimitedWidth) {
+ lp.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+ if (unlimitedHeight) {
+ lp.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+ return lp;
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentWithAspectRatioTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentWithAspectRatioTest.java
new file mode 100644
index 0000000..f022eee
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseWrapContentWithAspectRatioTest.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.v4.util.Pair;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+
+abstract public class BaseWrapContentWithAspectRatioTest extends BaseRecyclerViewInstrumentationTest {
+ final BaseWrapContentTest.WrapContentConfig mWrapContentConfig;
+
+ protected BaseWrapContentWithAspectRatioTest(
+ BaseWrapContentTest.WrapContentConfig wrapContentConfig) {
+ mWrapContentConfig = wrapContentConfig;
+ }
+
+ int getSize(View view, int orientation) {
+ if (orientation == VERTICAL) {
+ return view.getHeight();
+ }
+ return view.getWidth();
+ }
+
+ static class LoggingView extends View {
+
+ MeasureBehavior mBehavior;
+
+ public void setBehavior(MeasureBehavior behavior) {
+ mBehavior = behavior;
+ }
+
+ public LoggingView(Context context) {
+ super(context);
+ }
+
+ public LoggingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public LoggingView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public LoggingView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mBehavior.onMeasure(this, widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mBehavior.onLayout(changed, left, top, right, bottom);
+ }
+
+ public void setMeasured(int w, int h) {
+ setMeasuredDimension(w, h);
+ }
+
+ public void prepareLayoutParams() {
+ mBehavior.setLayoutParams(this);
+ }
+ }
+
+ static class AspectRatioMeasureBehavior extends MeasureBehavior {
+
+ Float ratio;
+ int control;
+
+ public AspectRatioMeasureBehavior(int desiredW, int desiredH, int wMode, int hMode) {
+ super(desiredW, desiredH, wMode, hMode);
+ }
+
+ public AspectRatioMeasureBehavior aspectRatio(int control, float ratio) {
+ this.control = control;
+ this.ratio = ratio;
+ return this;
+ }
+
+ @Override
+ public void onMeasure(LoggingView view, int wSpec,
+ int hSpec) {
+ super.onMeasure(view, wSpec, hSpec);
+ if (control == VERTICAL) {
+ view.setMeasured(getSecondary(view.getMeasuredHeight()),
+ view.getMeasuredHeight());
+ } else if (control == HORIZONTAL) {
+ view.setMeasured(view.getMeasuredWidth(),
+ getSecondary(view.getMeasuredWidth()));
+ }
+ }
+
+ public int getSecondary(int controlSize) {
+ return (int) (controlSize * ratio);
+ }
+ }
+
+ static class MeasureBehavior {
+ private static final AtomicLong idCounter = new AtomicLong(0);
+ public List<Pair<Integer, Integer>> measureSpecs = new ArrayList<>();
+ public List<Pair<Integer, Integer>> layouts = new ArrayList<>();
+ int desiredW, desiredH;
+ final long mId = idCounter.incrementAndGet();
+
+ ViewGroup.LayoutParams layoutParams;
+
+ public MeasureBehavior(int desiredW, int desiredH, int wMode, int hMode) {
+ this.desiredW = desiredW;
+ this.desiredH = desiredH;
+ layoutParams = new ViewGroup.LayoutParams(
+ wMode, hMode
+ );
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public void onMeasure(LoggingView view, int wSpec, int hSpec) {
+ measureSpecs.add(new Pair<>(wSpec, hSpec));
+ view.setMeasured(
+ RecyclerView.LayoutManager.chooseSize(wSpec, desiredW, 0),
+ RecyclerView.LayoutManager.chooseSize(hSpec, desiredH, 0));
+ }
+
+ public int getSpec(int position, int orientation) {
+ if (orientation == VERTICAL) {
+ return measureSpecs.get(position).second;
+ } else {
+ return measureSpecs.get(position).first;
+ }
+ }
+
+ public void setLayoutParams(LoggingView view) {
+ view.setLayoutParams(layoutParams);
+ }
+
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ if (changed) {
+ layouts.add(new Pair<>(right - left, bottom - top));
+ }
+ }
+ }
+
+
+ static class WrapContentViewHolder extends RecyclerView.ViewHolder {
+
+ LoggingView mView;
+
+ public WrapContentViewHolder(ViewGroup parent) {
+ super(new LoggingView(parent.getContext()));
+ mView = (LoggingView) itemView;
+ mView.setBackgroundColor(Color.GREEN);
+ }
+ }
+
+ static class WrapContentAdapter extends RecyclerView.Adapter<WrapContentViewHolder> {
+
+ List<MeasureBehavior> behaviors = new ArrayList<>();
+
+ public WrapContentAdapter(MeasureBehavior... behaviors) {
+ Collections.addAll(this.behaviors, behaviors);
+ }
+
+ @Override
+ public WrapContentViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new WrapContentViewHolder(parent);
+ }
+
+ @Override
+ public void onBindViewHolder(WrapContentViewHolder holder, int position) {
+ holder.mView.setBehavior(behaviors.get(position));
+ holder.mView.prepareLayoutParams();
+ }
+
+ @Override
+ public int getItemCount() {
+ return behaviors.size();
+ }
+ }
+
+ static class MeasureSpecMatcher extends BaseMatcher<Integer> {
+
+ private boolean checkSize = false;
+ private boolean checkMode = false;
+ private int mSize;
+ private int mMode;
+
+ public static MeasureSpecMatcher is(int size, int mode) {
+ MeasureSpecMatcher matcher = new MeasureSpecMatcher(size, mode);
+ matcher.checkSize = true;
+ matcher.checkMode = true;
+ return matcher;
+ }
+
+ public static MeasureSpecMatcher size(int size) {
+ MeasureSpecMatcher matcher = new MeasureSpecMatcher(size, 0);
+ matcher.checkSize = true;
+ matcher.checkMode = false;
+ return matcher;
+ }
+
+ public static MeasureSpecMatcher mode(int mode) {
+ MeasureSpecMatcher matcher = new MeasureSpecMatcher(0, mode);
+ matcher.checkSize = false;
+ matcher.checkMode = true;
+ return matcher;
+ }
+
+ private MeasureSpecMatcher(int size, int mode) {
+ mSize = size;
+ mMode = mode;
+
+ }
+
+ @Override
+ public boolean matches(Object item) {
+ if (item == null) {
+ return false;
+ }
+ Integer intValue = (Integer) item;
+ final int size = View.MeasureSpec.getSize(intValue);
+ final int mode = View.MeasureSpec.getMode(intValue);
+ if (checkSize && size != mSize) {
+ return false;
+ }
+ if (checkMode && mode != mMode) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void describeMismatch(Object item, Description description) {
+ Integer intValue = (Integer) item;
+ final int size = View.MeasureSpec.getSize(intValue);
+ final int mode = View.MeasureSpec.getMode(intValue);
+ if (checkSize && size != mSize) {
+ description.appendText(" Expected size was ").appendValue(mSize)
+ .appendText(" but received size is ").appendValue(size);
+ }
+ if (checkMode && mode != mMode) {
+ description.appendText(" Expected mode was ").appendValue(modeName(mMode))
+ .appendText(" but received mode is ").appendValue(modeName(mode));
+ }
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ if (checkSize) {
+ description.appendText(" Measure spec size:").appendValue(mSize);
+ }
+ if (checkMode) {
+ description.appendText(" Measure spec mode:").appendValue(modeName(mMode));
+ }
+ }
+
+ private static String modeName(int mode) {
+ switch (mode) {
+ case View.MeasureSpec.AT_MOST:
+ return "at most";
+ case View.MeasureSpec.EXACTLY:
+ return "exactly";
+ default:
+ return "unspecified";
+ }
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerBaseConfigSetTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerBaseConfigSetTest.java
new file mode 100644
index 0000000..2cd4fb6
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerBaseConfigSetTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2014 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.support.v7.widget;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import android.support.v7.widget.BaseGridLayoutManagerTest.Config;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class GridLayoutManagerBaseConfigSetTest extends BaseGridLayoutManagerTest {
+ @Parameterized.Parameters(name = "{0}")
+ public static List<Config> params() {
+ return createBaseVariations();
+ }
+
+ private final Config mConfig;
+
+ public GridLayoutManagerBaseConfigSetTest(Config config) {
+ mConfig = config;
+ }
+
+ @Test
+ public void scrollBackAndPreservePositions() throws Throwable {
+ Config config = (Config) mConfig.clone();
+ config.mItemCount = 150;
+ scrollBackAndPreservePositionsTest(config);
+ }
+
+ public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable {
+ final RecyclerView rv = setupBasic(config);
+ for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) {
+ mAdapter.setFullSpan(i);
+ }
+ waitForFirstLayout(rv);
+ final int[] globalPositions = new int[mAdapter.getItemCount()];
+ Arrays.fill(globalPositions, Integer.MIN_VALUE);
+ final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20)
+ * (config.mReverseLayout ? -1 : 1);
+ final String logPrefix = config.toString();
+ final int[] globalPos = new int[1];
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ assertSame("test sanity", mRecyclerView, rv);
+ int globalScrollPosition = 0;
+ int visited = 0;
+ while (visited < mAdapter.getItemCount()) {
+ for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
+ View child = mRecyclerView.getChildAt(i);
+ final int pos = mRecyclerView.getChildLayoutPosition(child);
+ if (globalPositions[pos] != Integer.MIN_VALUE) {
+ continue;
+ }
+ visited++;
+ GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
+ child.getLayoutParams();
+ if (config.mReverseLayout) {
+ globalPositions[pos] = globalScrollPosition +
+ mGlm.mOrientationHelper.getDecoratedEnd(child);
+ } else {
+ globalPositions[pos] = globalScrollPosition +
+ mGlm.mOrientationHelper.getDecoratedStart(child);
+ }
+ assertEquals(logPrefix + " span index should match",
+ mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
+ lp.getSpanIndex());
+ }
+ int scrolled = mGlm.scrollBy(scrollStep,
+ mRecyclerView.mRecycler, mRecyclerView.mState);
+ globalScrollPosition += scrolled;
+ if (scrolled == 0) {
+ assertEquals(
+ logPrefix + " If scroll is complete, all views should be visited",
+ visited, mAdapter.getItemCount());
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
+ }
+ globalPos[0] = globalScrollPosition;
+ }
+ });
+ checkForMainThreadException();
+ // test sanity, ensure scroll happened
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int childCount = mGlm.getChildCount();
+ final BitSet expectedPositions = new BitSet();
+ for (int i = 0; i < childCount; i ++) {
+ expectedPositions.set(mAdapter.getItemCount() - i - 1);
+ }
+ for (int i = 0; i <childCount; i ++) {
+ final View view = mGlm.getChildAt(i);
+ int position = mGlm.getPosition(view);
+ assertTrue("child position should be in last page", expectedPositions.get(position));
+ }
+ }
+ });
+ getInstrumentation().waitForIdleSync();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ int globalScrollPosition = globalPos[0];
+ // now scroll back and make sure global positions match
+ BitSet shouldTest = new BitSet(mAdapter.getItemCount());
+ shouldTest.set(0, mAdapter.getItemCount() - 1, true);
+ String assertPrefix = config
+ + " global pos must match when scrolling in reverse for position ";
+ int scrollAmount = Integer.MAX_VALUE;
+ while (!shouldTest.isEmpty() && scrollAmount != 0) {
+ for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
+ View child = mRecyclerView.getChildAt(i);
+ int pos = mRecyclerView.getChildLayoutPosition(child);
+ if (!shouldTest.get(pos)) {
+ continue;
+ }
+ GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
+ child.getLayoutParams();
+ shouldTest.clear(pos);
+ int globalPos;
+ if (config.mReverseLayout) {
+ globalPos = globalScrollPosition +
+ mGlm.mOrientationHelper.getDecoratedEnd(child);
+ } else {
+ globalPos = globalScrollPosition +
+ mGlm.mOrientationHelper.getDecoratedStart(child);
+ }
+ assertEquals(assertPrefix + pos,
+ globalPositions[pos], globalPos);
+ assertEquals("span index should match",
+ mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
+ lp.getSpanIndex());
+ }
+ scrollAmount = mGlm.scrollBy(-scrollStep,
+ mRecyclerView.mRecycler, mRecyclerView.mState);
+ globalScrollPosition += scrollAmount;
+ }
+ assertTrue("all views should be seen", shouldTest.isEmpty());
+ }
+ });
+ checkForMainThreadException();
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java
new file mode 100644
index 0000000..2fc5015
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static org.junit.Assert.assertEquals;
+
+@RunWith(Parameterized.class)
+public class GridLayoutManagerCachedBordersTest extends BaseGridLayoutManagerTest {
+
+ @Parameterized.Parameters(name = "{0}")
+ public static List<Config> params() {
+ List<Config> testConfigurations = createBaseVariations();
+ testConfigurations.addAll(cachedBordersTestConfigs());
+ return testConfigurations;
+ }
+
+ private final Config mConfig;
+
+ public GridLayoutManagerCachedBordersTest(Config config) {
+ mConfig = config;
+ }
+
+
+ @Test
+ public void gridCachedBorderstTest() throws Throwable {
+ RecyclerView recyclerView = setupBasic(mConfig);
+ waitForFirstLayout(recyclerView);
+ final boolean vertical = mConfig.mOrientation == GridLayoutManager.VERTICAL;
+ final int expectedSizeSum = vertical ? recyclerView.getWidth() : recyclerView.getHeight();
+ final int lastVisible = mGlm.findLastVisibleItemPosition();
+ for (int i = 0; i < lastVisible; i += mConfig.mSpanCount) {
+ if ((i + 1) * mConfig.mSpanCount - 1 < lastVisible) {
+ int childrenSizeSum = 0;
+ for (int j = 0; j < mConfig.mSpanCount; j++) {
+ View child = recyclerView.getChildAt(i * mConfig.mSpanCount + j);
+ childrenSizeSum += vertical ? child.getWidth() : child.getHeight();
+ }
+ assertEquals(expectedSizeSum, childrenSizeSum);
+ }
+ }
+ }
+
+ private static List<Config> cachedBordersTestConfigs() {
+ ArrayList<Config> configs = new ArrayList<Config>();
+ final int[] spanCounts = new int[]{88, 279, 741};
+ final int[] spanPerItem = new int[]{11, 9, 13};
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (int i = 0; i < spanCounts.length; i++) {
+ Config config = new Config(spanCounts[i], orientation, reverseLayout);
+ config.mSpanPerItem = spanPerItem[i];
+ configs.add(config);
+ }
+ }
+ }
+ return configs;
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerRtlTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerRtlTest.java
new file mode 100644
index 0000000..78f1576
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerRtlTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(Parameterized.class)
+public class GridLayoutManagerRtlTest extends BaseGridLayoutManagerTest {
+
+ public GridLayoutManagerRtlTest(Config config, boolean changeRtlAfter, boolean oneLine,
+ boolean itemsWrapContent) {
+ mConfig = config;
+ mChangeRtlAfter = changeRtlAfter;
+ mOneLine = oneLine;
+ mItemsWrapContent = itemsWrapContent;
+ }
+
+ @Parameterized.Parameters(name = "conf: {0} changeRl:{1} oneLine: {2} itemsWrap: {3}")
+ public static List<Object[]> params() {
+ List<Object[]> result = new ArrayList<>();
+ for (boolean changeRtlAfter : new boolean[]{false, true}) {
+ for (boolean oneLine : new boolean[]{false, true}) {
+ for (boolean itemsWrapContent : new boolean[]{false, true}) {
+ for (Config config : createBaseVariations()) {
+ result.add(new Object[] {
+ config,
+ changeRtlAfter,
+ oneLine,
+ itemsWrapContent
+ });
+ }
+ }
+ }
+ }
+ return result;
+ }
+ final Config mConfig;
+ final boolean mChangeRtlAfter;
+ final boolean mOneLine;
+ final boolean mItemsWrapContent;
+
+
+ @Test
+ public void rtlTest() throws Throwable {
+ if (mOneLine && mConfig.mOrientation != VERTICAL) {
+ return;// nothing to test
+ }
+ if (mConfig.mSpanCount == 1) {
+ mConfig.mSpanCount = 2;
+ }
+ String logPrefix = mConfig + ", changeRtlAfterLayout:" + mChangeRtlAfter + ","
+ + "oneLine:" + mOneLine + " itemsWrap:" + mItemsWrapContent;
+ mConfig.mItemCount = 5;
+ if (mOneLine) {
+ mConfig.mSpanCount = mConfig.mItemCount + 1;
+ } else {
+ mConfig.mSpanCount = Math.min(mConfig.mItemCount - 1, mConfig.mSpanCount);
+ }
+
+ RecyclerView rv = setupBasic(mConfig, new GridTestAdapter(mConfig.mItemCount) {
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ if (mItemsWrapContent) {
+ ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+ if (lp == null) {
+ lp = mGlm.generateDefaultLayoutParams();
+ }
+ if (mConfig.mOrientation == HORIZONTAL) {
+ lp.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ lp.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ }
+ }
+ }
+ });
+ if (mChangeRtlAfter) {
+ waitForFirstLayout(rv);
+ mGlm.expectLayout(1);
+ mGlm.setFakeRtl(true);
+ mGlm.waitForLayout(2);
+ } else {
+ mGlm.mFakeRTL = true;
+ waitForFirstLayout(rv);
+ }
+
+ assertEquals("view should become rtl", true, mGlm.isLayoutRTL());
+ OrientationHelper helper = OrientationHelper.createHorizontalHelper(mGlm);
+ View child0 = mGlm.findViewByPosition(0);
+ final int secondChildPos = mConfig.mOrientation == VERTICAL ? 1
+ : mConfig.mSpanCount;
+ View child1 = mGlm.findViewByPosition(secondChildPos);
+ assertNotNull(logPrefix + " child position 0 should be laid out", child0);
+ assertNotNull(
+ logPrefix + " second child position " + (secondChildPos) + " should be laid out",
+ child1);
+ if (mConfig.mOrientation == VERTICAL || !mConfig.mReverseLayout) {
+ assertTrue(logPrefix + " second child should be to the left of first child",
+ helper.getDecoratedStart(child0) >= helper.getDecoratedEnd(child1));
+ assertEquals(logPrefix + " first child should be right aligned",
+ helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
+ } else {
+ assertTrue(logPrefix + " first child should be to the left of second child",
+ helper.getDecoratedStart(child1) >= helper.getDecoratedEnd(child0));
+ assertEquals(logPrefix + " first child should be left aligned",
+ helper.getDecoratedStart(child0), helper.getStartAfterPadding());
+ }
+ checkForMainThreadException();
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index 2cd562f..ee0e0db 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -55,49 +55,7 @@
@MediumTest
@RunWith(AndroidJUnit4.class)
-public class GridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
-
- static final String TAG = "GridLayoutManagerTest";
-
- static final boolean DEBUG = false;
-
- WrappedGridLayoutManager mGlm;
-
- GridTestAdapter mAdapter;
-
- final List<Config> mBaseVariations = new ArrayList<Config>();
-
- @Before
- public void setUp() throws Exception {
- for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
- for (boolean reverseLayout : new boolean[]{false, true}) {
- for (int spanCount : new int[]{1, 3, 4}) {
- mBaseVariations.add(new Config(spanCount, orientation, reverseLayout));
- }
- }
- }
- }
-
- public RecyclerView setupBasic(Config config) throws Throwable {
- return setupBasic(config, new GridTestAdapter(config.mItemCount));
- }
-
- public RecyclerView setupBasic(Config config, GridTestAdapter testAdapter) throws Throwable {
- RecyclerView recyclerView = new RecyclerView(getActivity());
- mAdapter = testAdapter;
- mGlm = new WrappedGridLayoutManager(getActivity(), config.mSpanCount, config.mOrientation,
- config.mReverseLayout);
- mAdapter.assignSpanSizeLookup(mGlm);
- recyclerView.setAdapter(mAdapter);
- recyclerView.setLayoutManager(mGlm);
- return recyclerView;
- }
-
- public void waitForFirstLayout(RecyclerView recyclerView) throws Throwable {
- mGlm.expectLayout(1);
- setRecyclerView(recyclerView);
- mGlm.waitForLayout(2);
- }
+public class GridLayoutManagerTest extends BaseGridLayoutManagerTest {
@Test
public void focusSearchFailureUp() throws Throwable {
@@ -109,7 +67,7 @@
focusSearchFailure(true);
}
- public void focusSearchFailure(boolean scrollDown) throws Throwable {
+ private void focusSearchFailure(boolean scrollDown) throws Throwable {
final RecyclerView recyclerView = setupBasic(new Config(3, 31).reverseLayout(!scrollDown)
, new GridTestAdapter(31, 1) {
RecyclerView mAttachedRv;
@@ -317,68 +275,6 @@
}
@Test
- public void rTL() throws Throwable {
- for (boolean changeRtlAfter : new boolean[]{false, true}) {
- for (boolean oneLine : new boolean[]{false, true}) {
- for (Config config : mBaseVariations) {
- rtlTest(config, changeRtlAfter, oneLine);
- removeRecyclerView();
- }
- }
- }
- }
-
- void rtlTest(Config config, boolean changeRtlAfter, boolean oneLine) throws Throwable {
- if (oneLine && config.mOrientation != VERTICAL) {
- return;// nothing to test
- }
- if (config.mSpanCount == 1) {
- config.mSpanCount = 2;
- }
- String logPrefix = config + ", changeRtlAfterLayout:" + changeRtlAfter + ", oneLine:" + oneLine;
- config.mItemCount = 5;
- if (oneLine) {
- config.mSpanCount = config.mItemCount + 1;
- } else {
- config.mSpanCount = Math.min(config.mItemCount - 1, config.mSpanCount);
- }
-
- RecyclerView rv = setupBasic(config);
- if (changeRtlAfter) {
- waitForFirstLayout(rv);
- mGlm.expectLayout(1);
- mGlm.setFakeRtl(true);
- mGlm.waitForLayout(2);
- } else {
- mGlm.mFakeRTL = true;
- waitForFirstLayout(rv);
- }
-
- assertEquals("view should become rtl", true, mGlm.isLayoutRTL());
- OrientationHelper helper = OrientationHelper.createHorizontalHelper(mGlm);
- View child0 = mGlm.findViewByPosition(0);
- final int secondChildPos = config.mOrientation == VERTICAL ? 1
- : config.mSpanCount;
- View child1 = mGlm.findViewByPosition(secondChildPos);
- assertNotNull(logPrefix + " child position 0 should be laid out", child0);
- assertNotNull(
- logPrefix + " second child position " + (secondChildPos) + " should be laid out",
- child1);
- if (config.mOrientation == VERTICAL || !config.mReverseLayout) {
- assertTrue(logPrefix + " second child should be to the left of first child",
- helper.getDecoratedStart(child0) >= helper.getDecoratedEnd(child1));
- assertEquals(logPrefix + " first child should be right aligned",
- helper.getDecoratedEnd(child0), helper.getEndAfterPadding());
- } else {
- assertTrue(logPrefix + " first child should be to the left of second child",
- helper.getDecoratedStart(child1) >= helper.getDecoratedEnd(child0));
- assertEquals(logPrefix + " first child should be left aligned",
- helper.getDecoratedStart(child0), helper.getStartAfterPadding());
- }
- checkForMainThreadException();
- }
-
- @Test
public void movingAGroupOffScreenForAddedItems() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
final int[] maxId = new int[1];
@@ -414,50 +310,6 @@
}
@Test
- public void cachedBorders() throws Throwable {
- List<Config> testConfigurations = new ArrayList<Config>(mBaseVariations);
- testConfigurations.addAll(cachedBordersTestConfigs());
- for (Config config : testConfigurations) {
- gridCachedBorderstTest(config);
- }
- }
-
- private void gridCachedBorderstTest(Config config) throws Throwable {
- RecyclerView recyclerView = setupBasic(config);
- waitForFirstLayout(recyclerView);
- final boolean vertical = config.mOrientation == GridLayoutManager.VERTICAL;
- final int expectedSizeSum = vertical ? recyclerView.getWidth() : recyclerView.getHeight();
- final int lastVisible = mGlm.findLastVisibleItemPosition();
- for (int i = 0; i < lastVisible; i += config.mSpanCount) {
- if ((i+1)*config.mSpanCount - 1 < lastVisible) {
- int childrenSizeSum = 0;
- for (int j = 0; j < config.mSpanCount; j++) {
- View child = recyclerView.getChildAt(i * config.mSpanCount + j);
- childrenSizeSum += vertical ? child.getWidth() : child.getHeight();
- }
- assertEquals(expectedSizeSum, childrenSizeSum);
- }
- }
- removeRecyclerView();
- }
-
- private List<Config> cachedBordersTestConfigs() {
- ArrayList<Config> configs = new ArrayList<Config>();
- final int [] spanCounts = new int[]{88, 279, 741};
- final int [] spanPerItem = new int[]{11, 9, 13};
- for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
- for (boolean reverseLayout : new boolean[]{false, true}) {
- for (int i = 0 ; i < spanCounts.length; i++) {
- Config config = new Config(spanCounts[i], orientation, reverseLayout);
- config.mSpanPerItem = spanPerItem[i];
- configs.add(config);
- }
- }
- }
- return configs;
- }
-
- @Test
public void layoutParams() throws Throwable {
layoutParamsTest(GridLayoutManager.HORIZONTAL);
removeRecyclerView();
@@ -578,13 +430,6 @@
assertEquals(secondRowSize, getSize(mGlm.findViewByPosition(5)));
}
- private int getSize(View view) {
- if (mGlm.getOrientation() == GridLayoutManager.HORIZONTAL) {
- return view.getWidth();
- }
- return view.getHeight();
- }
-
@Test
public void anchorUpdate() throws InterruptedException {
GridLayoutManager glm = new GridLayoutManager(getActivity(), 11);
@@ -922,15 +767,6 @@
}
@Test
- public void scrollBackAndPreservePositions() throws Throwable {
- for (Config config : mBaseVariations) {
- config.mItemCount = 150;
- scrollBackAndPreservePositionsTest(config);
- removeRecyclerView();
- }
- }
-
- @Test
public void spanSizeChange() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
waitForFirstLayout(rv);
@@ -967,277 +803,4 @@
assertEquals("item index 5 should be in span 2", 0,
getLp(mGlm.findViewByPosition(5)).getSpanIndex());
}
-
- GridLayoutManager.LayoutParams getLp(View view) {
- return (GridLayoutManager.LayoutParams) view.getLayoutParams();
- }
-
- public void scrollBackAndPreservePositionsTest(final Config config) throws Throwable {
- final RecyclerView rv = setupBasic(config);
- for (int i = 1; i < mAdapter.getItemCount(); i += config.mSpanCount + 2) {
- mAdapter.setFullSpan(i);
- }
- waitForFirstLayout(rv);
- final int[] globalPositions = new int[mAdapter.getItemCount()];
- Arrays.fill(globalPositions, Integer.MIN_VALUE);
- final int scrollStep = (mGlm.mOrientationHelper.getTotalSpace() / 20)
- * (config.mReverseLayout ? -1 : 1);
- final String logPrefix = config.toString();
- final int[] globalPos = new int[1];
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- assertSame("test sanity", mRecyclerView, rv);
- int globalScrollPosition = 0;
- int visited = 0;
- while (visited < mAdapter.getItemCount()) {
- for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
- View child = mRecyclerView.getChildAt(i);
- final int pos = mRecyclerView.getChildLayoutPosition(child);
- if (globalPositions[pos] != Integer.MIN_VALUE) {
- continue;
- }
- visited++;
- GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
- child.getLayoutParams();
- if (config.mReverseLayout) {
- globalPositions[pos] = globalScrollPosition +
- mGlm.mOrientationHelper.getDecoratedEnd(child);
- } else {
- globalPositions[pos] = globalScrollPosition +
- mGlm.mOrientationHelper.getDecoratedStart(child);
- }
- assertEquals(logPrefix + " span index should match",
- mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
- lp.getSpanIndex());
- }
- int scrolled = mGlm.scrollBy(scrollStep,
- mRecyclerView.mRecycler, mRecyclerView.mState);
- globalScrollPosition += scrolled;
- if (scrolled == 0) {
- assertEquals(
- logPrefix + " If scroll is complete, all views should be visited",
- visited, mAdapter.getItemCount());
- }
- }
- if (DEBUG) {
- Log.d(TAG, "done recording positions " + Arrays.toString(globalPositions));
- }
- globalPos[0] = globalScrollPosition;
- }
- });
- checkForMainThreadException();
- // test sanity, ensure scroll happened
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- final int childCount = mGlm.getChildCount();
- final BitSet expectedPositions = new BitSet();
- for (int i = 0; i < childCount; i ++) {
- expectedPositions.set(mAdapter.getItemCount() - i - 1);
- }
- for (int i = 0; i <childCount; i ++) {
- final View view = mGlm.getChildAt(i);
- int position = mGlm.getPosition(view);
- assertTrue("child position should be in last page", expectedPositions.get(position));
- }
- }
- });
- getInstrumentation().waitForIdleSync();
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- int globalScrollPosition = globalPos[0];
- // now scroll back and make sure global positions match
- BitSet shouldTest = new BitSet(mAdapter.getItemCount());
- shouldTest.set(0, mAdapter.getItemCount() - 1, true);
- String assertPrefix = config
- + " global pos must match when scrolling in reverse for position ";
- int scrollAmount = Integer.MAX_VALUE;
- while (!shouldTest.isEmpty() && scrollAmount != 0) {
- for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
- View child = mRecyclerView.getChildAt(i);
- int pos = mRecyclerView.getChildLayoutPosition(child);
- if (!shouldTest.get(pos)) {
- continue;
- }
- GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
- child.getLayoutParams();
- shouldTest.clear(pos);
- int globalPos;
- if (config.mReverseLayout) {
- globalPos = globalScrollPosition +
- mGlm.mOrientationHelper.getDecoratedEnd(child);
- } else {
- globalPos = globalScrollPosition +
- mGlm.mOrientationHelper.getDecoratedStart(child);
- }
- assertEquals(assertPrefix + pos,
- globalPositions[pos], globalPos);
- assertEquals("span index should match",
- mGlm.getSpanSizeLookup().getSpanIndex(pos, mGlm.getSpanCount()),
- lp.getSpanIndex());
- }
- scrollAmount = mGlm.scrollBy(-scrollStep,
- mRecyclerView.mRecycler, mRecyclerView.mState);
- globalScrollPosition += scrollAmount;
- }
- assertTrue("all views should be seen", shouldTest.isEmpty());
- }
- });
- checkForMainThreadException();
- }
-
- class WrappedGridLayoutManager extends GridLayoutManager {
-
- CountDownLatch mLayoutLatch;
-
- List<Callback> mCallbacks = new ArrayList<Callback>();
-
- Boolean mFakeRTL;
-
- public WrappedGridLayoutManager(Context context, int spanCount) {
- super(context, spanCount);
- }
-
- public WrappedGridLayoutManager(Context context, int spanCount, int orientation,
- boolean reverseLayout) {
- super(context, spanCount, orientation, reverseLayout);
- }
-
- @Override
- protected boolean isLayoutRTL() {
- return mFakeRTL == null ? super.isLayoutRTL() : mFakeRTL;
- }
-
- public void setFakeRtl(Boolean fakeRtl) {
- mFakeRTL = fakeRtl;
- try {
- requestLayoutOnUIThread(mRecyclerView);
- } catch (Throwable throwable) {
- postExceptionToInstrumentation(throwable);
- }
- }
-
- @Override
- public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
- try {
- for (Callback callback : mCallbacks) {
- callback.onBeforeLayout(recycler, state);
- }
- super.onLayoutChildren(recycler, state);
- for (Callback callback : mCallbacks) {
- callback.onAfterLayout(recycler, state);
- }
- } catch (Throwable t) {
- postExceptionToInstrumentation(t);
- }
- mLayoutLatch.countDown();
- }
-
- @Override
- LayoutState createLayoutState() {
- return new LayoutState() {
- @Override
- View next(RecyclerView.Recycler recycler) {
- final boolean hadMore = hasMore(mRecyclerView.mState);
- final int position = mCurrentPosition;
- View next = super.next(recycler);
- assertEquals("if has more, should return a view", hadMore, next != null);
- assertEquals("position of the returned view must match current position",
- position, RecyclerView.getChildViewHolderInt(next).getLayoutPosition());
- return next;
- }
- };
- }
-
- public void expectLayout(int layoutCount) {
- mLayoutLatch = new CountDownLatch(layoutCount);
- }
-
- public void waitForLayout(int seconds) throws InterruptedException {
- mLayoutLatch.await(seconds, SECONDS);
- }
- }
-
- class Config {
-
- int mSpanCount;
- int mOrientation = GridLayoutManager.VERTICAL;
- int mItemCount = 1000;
- int mSpanPerItem = 1;
- boolean mReverseLayout = false;
-
- Config(int spanCount, int itemCount) {
- mSpanCount = spanCount;
- mItemCount = itemCount;
- }
-
- public Config(int spanCount, int orientation, boolean reverseLayout) {
- mSpanCount = spanCount;
- mOrientation = orientation;
- mReverseLayout = reverseLayout;
- }
-
- Config orientation(int orientation) {
- mOrientation = orientation;
- return this;
- }
-
- @Override
- public String toString() {
- return "Config{" +
- "mSpanCount=" + mSpanCount +
- ", mOrientation=" + (mOrientation == GridLayoutManager.HORIZONTAL ? "h" : "v") +
- ", mItemCount=" + mItemCount +
- ", mReverseLayout=" + mReverseLayout +
- '}';
- }
-
- public Config reverseLayout(boolean reverseLayout) {
- mReverseLayout = reverseLayout;
- return this;
- }
-
-
- }
-
- class GridTestAdapter extends TestAdapter {
-
- Set<Integer> mFullSpanItems = new HashSet<Integer>();
- int mSpanPerItem = 1;
-
- GridTestAdapter(int count) {
- super(count);
- }
-
- GridTestAdapter(int count, int spanPerItem) {
- super(count);
- mSpanPerItem = spanPerItem;
- }
-
- void setFullSpan(int... items) {
- for (int i : items) {
- mFullSpanItems.add(i);
- }
- }
-
- void assignSpanSizeLookup(final GridLayoutManager glm) {
- glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
- @Override
- public int getSpanSize(int position) {
- return mFullSpanItems.contains(position) ? glm.getSpanCount() : mSpanPerItem;
- }
- });
- }
- }
-
- class Callback {
-
- public void onBeforeLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
- }
-
- public void onAfterLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
- }
- }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentTest.java
new file mode 100644
index 0000000..c8fdbeb
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.view.Gravity;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.support.v7.widget.BaseWrapContentWithAspectRatioTest.*;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@RunWith(JUnit4.class)
+public class GridLayoutManagerWrapContentTest extends BaseWrapContentTest {
+
+ public GridLayoutManagerWrapContentTest() {
+ super(new WrapContentConfig(false, false));
+ }
+
+ @Override
+ RecyclerView.LayoutManager createLayoutManager() {
+ return new GridLayoutManager(getActivity(), 3);
+ }
+
+ @Test
+ public void testHandleSecondLineChangingBorders() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ new MeasureBehavior(10, 10, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 10, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 10, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(20, 10, WRAP_CONTENT, WRAP_CONTENT)
+ );
+ Rect[] expected = new Rect[] {
+ new Rect(0, 0, 10, 10),
+ new Rect(20, 0, 30, 10),
+ new Rect(40, 0, 50, 10),
+ new Rect(0, 10, 20, 20)
+ };
+ layoutAndCheck(lp, adapter, expected, 60, 20);
+ }
+
+ @Test
+ public void testSecondLineAffectingBordersWithAspectRatio() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ new AspectRatioMeasureBehavior(10, 5, MATCH_PARENT, WRAP_CONTENT)
+ .aspectRatio(HORIZONTAL, .5f),
+ new MeasureBehavior(10, 5, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 5, MATCH_PARENT, WRAP_CONTENT),
+ new MeasureBehavior(20, 10, WRAP_CONTENT, WRAP_CONTENT)
+ );
+ Rect[] expected = new Rect[] {
+ new Rect(0, 0, 20, 10),
+ new Rect(20, 0, 30, 10),
+ new Rect(40, 0, 60, 10),
+ new Rect(0, 10, 20, 20)
+ };
+ layoutAndCheck(lp, adapter, expected, 60, 20);
+ }
+
+ @Override
+ protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager) {
+ return Gravity.TOP;
+ }
+
+ @Override
+ protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager) {
+ return Gravity.LEFT;
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentWithAspectRatioTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentWithAspectRatioTest.java
new file mode 100644
index 0000000..d33611c
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerWrapContentWithAspectRatioTest.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.graphics.Color;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static android.support.v7.widget.BaseWrapContentTest.WrapContentConfig;
+import static android.support.v7.widget.GridLayoutManagerTest.Config;
+import static org.hamcrest.CoreMatchers.*;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+
+@RunWith(Parameterized.class)
+@MediumTest
+public class GridLayoutManagerWrapContentWithAspectRatioTest
+ extends BaseWrapContentWithAspectRatioTest {
+
+ @Parameterized.Parameters(name = "{0} {1} {2}")
+ public static List<Object[]> params() {
+ List<Object[]> params = new ArrayList<>();
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (boolean unlimitedW : new boolean[]{true, false}) {
+ for (boolean unlimitedH : new boolean[]{true, false}) {
+ for (int behavior1Size : new int[]{8, 10, 12}) {
+ params.add(new Object[]{
+ new WrapContentConfig(unlimitedW, unlimitedH),
+ new Config(3, orientation, reverseLayout),
+ behavior1Size
+ });
+ }
+
+ }
+ }
+
+ }
+ }
+ return params;
+ }
+
+ private GridLayoutManagerTest.Config mConfig;
+
+ private int mBehavior1Size;
+
+ int mTestOrientation;
+
+ boolean mUnlimited;
+
+ RecyclerView.LayoutManager mLayoutManager;
+
+ BaseWrapContentTest.WrappedRecyclerView mRecyclerView;
+
+ OrientationHelper mHelper;
+
+ public GridLayoutManagerWrapContentWithAspectRatioTest(WrapContentConfig wrapContentConfig,
+ GridLayoutManagerTest.Config config, int behavior1Size) {
+ super(wrapContentConfig);
+ mConfig = config;
+ mBehavior1Size = behavior1Size;
+ }
+
+ @Before
+ public final void init() {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ if (mConfig.mOrientation == VERTICAL) {
+ mTestOrientation = HORIZONTAL;
+ mUnlimited = lp.wSpec != null;
+ } else {
+ mTestOrientation = VERTICAL;
+ mUnlimited = lp.hSpec != null;
+ }
+ mLayoutManager = createFromConfig();
+
+ mRecyclerView = new BaseWrapContentTest.WrappedRecyclerView(getActivity());
+ mHelper = OrientationHelper.createOrientationHelper(
+ mLayoutManager, 1 - mConfig.mOrientation);
+
+ mRecyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setLayoutParams(lp);
+ }
+
+ @Test
+ public void testChildWithMultipleSpans() throws Throwable {
+ MeasureBehavior behavior1;
+ behavior1 = new MeasureBehavior(mBehavior1Size, mBehavior1Size,
+ mTestOrientation == HORIZONTAL ? MATCH_PARENT : WRAP_CONTENT,
+ mTestOrientation == VERTICAL ? MATCH_PARENT : WRAP_CONTENT);
+
+ MeasureBehavior behavior2 = new MeasureBehavior(
+ mTestOrientation == HORIZONTAL ? 30 : 10,
+ mTestOrientation == VERTICAL ? 30 : 10, WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(behavior1, behavior2);
+ ((GridLayoutManager) mLayoutManager).setSpanSizeLookup(
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return position == 1 ? 2 : 1;
+ }
+ });
+ mRecyclerView.setAdapter(adapter);
+ setRecyclerView(mRecyclerView);
+ mRecyclerView.waitUntilLayout();
+
+ final int parentSize = getSize((View) mRecyclerView.getParent(), mTestOrientation);
+
+ if (mUnlimited) {
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ } else {
+ int[] borders = GridLayoutManager.calculateItemBorders(null,
+ mConfig.mSpanCount, parentSize);
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[1] - borders[0], AT_MOST));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[3] - borders[1], AT_MOST));
+ }
+ // child0 should be measured again because it measured its size as 10
+ assertThat(behavior1.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(15, EXACTLY));
+ assertThat(behavior1.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ switch (mBehavior1Size) {
+ case 10:
+ assertThat(behavior1.measureSpecs.size(), is(2));
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ break;
+ case 8:
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ assertThat(behavior1.measureSpecs.size(), is(3));
+ assertThat(behavior1.getSpec(2, mTestOrientation),
+ MeasureSpecMatcher.is(15, EXACTLY));
+ assertThat(behavior1.getSpec(2, mConfig.mOrientation),
+ MeasureSpecMatcher.is(10, EXACTLY));
+ break;
+ case 12:
+ assertThat(behavior1.measureSpecs.size(), is(2));
+ assertThat(behavior2.measureSpecs.size(), is(2));
+ assertThat(behavior2.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(30, AT_MOST));
+ assertThat(behavior2.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.is(12, EXACTLY));
+ break;
+ }
+
+ View child0 = mRecyclerView.getChildAt(0);
+ assertThat(getSize(child0, mTestOrientation), is(15));
+
+ View child1 = mRecyclerView.getChildAt(1);
+ assertThat(getSize(child1, mTestOrientation), is(30));
+
+ assertThat(mHelper.getDecoratedStart(child0), is(0));
+ assertThat(mHelper.getDecoratedStart(child1), is(15));
+
+ assertThat(mHelper.getDecoratedEnd(child0), is(15));
+ assertThat(mHelper.getDecoratedEnd(child1), is(45));
+
+ assertThat(mHelper.getDecoratedMeasurementInOther(child0),
+ is(Math.max(10, mBehavior1Size)));
+ assertThat(mHelper.getDecoratedMeasurementInOther(child1),
+ is(Math.max(10, mBehavior1Size)));
+
+ assertThat(getSize(mRecyclerView, mTestOrientation), is(45));
+ assertThat(getSize(mRecyclerView, 1 - mTestOrientation), is(Math.max(10, mBehavior1Size)));
+ }
+
+ @Test
+ public void testChildWithMatchParentInOtherDirection() throws Throwable {
+ MeasureBehavior behavior1;
+ behavior1 = new MeasureBehavior(mBehavior1Size, mBehavior1Size,
+ mTestOrientation == HORIZONTAL ? MATCH_PARENT : WRAP_CONTENT,
+ mTestOrientation == VERTICAL ? MATCH_PARENT : WRAP_CONTENT);
+
+ MeasureBehavior behavior2 = new MeasureBehavior(
+ mTestOrientation == HORIZONTAL ? 15 : 10,
+ mTestOrientation == VERTICAL ? 15 : 10, WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(behavior1, behavior2);
+
+ mRecyclerView.setAdapter(adapter);
+ setRecyclerView(mRecyclerView);
+ mRecyclerView.waitUntilLayout();
+ final int parentSize = getSize((View) mRecyclerView.getParent(), mTestOrientation);
+ if (mUnlimited) {
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ } else {
+ int[] borders = GridLayoutManager.calculateItemBorders(null, mConfig.mSpanCount,
+ parentSize);
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[1] - borders[0], AT_MOST));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[2] - borders[1], AT_MOST));
+ }
+ // child0 should be measured again because it measured its size as 10
+ assertThat(behavior1.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(15, EXACTLY));
+ assertThat(behavior1.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ switch (mBehavior1Size) {
+ case 10:
+ assertThat(behavior1.measureSpecs.size(), is(2));
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ break;
+ case 8:
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ assertThat(behavior1.measureSpecs.size(), is(3));
+ assertThat(behavior1.getSpec(2, mTestOrientation),
+ MeasureSpecMatcher.is(15, EXACTLY));
+ assertThat(behavior1.getSpec(2, mConfig.mOrientation),
+ MeasureSpecMatcher.is(10, EXACTLY));
+ break;
+ case 12:
+ assertThat(behavior1.measureSpecs.size(), is(2));
+ assertThat(behavior2.measureSpecs.size(), is(2));
+ assertThat(behavior2.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(15, AT_MOST));
+ assertThat(behavior2.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.is(12, EXACTLY));
+ break;
+ }
+
+ View child0 = mRecyclerView.getChildAt(0);
+ assertThat(getSize(child0, mTestOrientation), is(15));
+
+ View child1 = mRecyclerView.getChildAt(1);
+ assertThat(getSize(child1, mTestOrientation), is(15));
+
+ assertThat(mHelper.getDecoratedStart(child0), is(0));
+ assertThat(mHelper.getDecoratedStart(child1), is(15));
+
+ assertThat(mHelper.getDecoratedEnd(child0), is(15));
+ assertThat(mHelper.getDecoratedEnd(child1), is(30));
+
+ assertThat(mHelper.getDecoratedMeasurementInOther(child0),
+ is(Math.max(10, mBehavior1Size)));
+ assertThat(mHelper.getDecoratedMeasurementInOther(child1),
+ is(Math.max(10, mBehavior1Size)));
+
+ assertThat(getSize(mRecyclerView, mTestOrientation), is(45));
+ assertThat(getSize(mRecyclerView, 1 - mTestOrientation), is(Math.max(10, mBehavior1Size)));
+ }
+
+ @Test
+ public void testAllChildrenWrapContentInOtherDirection() throws Throwable {
+ MeasureBehavior behavior1;
+ behavior1 = new MeasureBehavior(mBehavior1Size, mBehavior1Size, WRAP_CONTENT, WRAP_CONTENT);
+
+ MeasureBehavior behavior2 = new MeasureBehavior(
+ mTestOrientation == HORIZONTAL ? 15 : 10,
+ mTestOrientation == VERTICAL ? 15 : 10, WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(behavior1, behavior2);
+
+ mRecyclerView.setAdapter(adapter);
+ setRecyclerView(mRecyclerView);
+ mRecyclerView.waitUntilLayout();
+ final int parentSize = getSize((View) mRecyclerView.getParent(), mTestOrientation);
+ if (mUnlimited) {
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.mode(UNSPECIFIED));
+ } else {
+ int[] borders = GridLayoutManager.calculateItemBorders(null,
+ mConfig.mSpanCount, parentSize);
+ assertThat(behavior1.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[1] - borders[0], AT_MOST));
+ assertThat(behavior2.getSpec(0, mTestOrientation),
+ MeasureSpecMatcher.is(borders[2] - borders[1], AT_MOST));
+ }
+
+ switch (mBehavior1Size) {
+ case 10:
+ assertThat(behavior1.measureSpecs.size(), is(1));
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ break;
+ case 8:
+ assertThat(behavior2.measureSpecs.size(), is(1));
+ assertThat(behavior1.measureSpecs.size(), is(2));
+ assertThat(behavior1.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(15, AT_MOST));
+ assertThat(behavior1.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.is(10, EXACTLY));
+ break;
+ case 12:
+ assertThat(behavior1.measureSpecs.size(), is(1));
+ assertThat(behavior2.measureSpecs.size(), is(2));
+ assertThat(behavior2.getSpec(1, mTestOrientation),
+ MeasureSpecMatcher.is(15, AT_MOST));
+ assertThat(behavior2.getSpec(1, mConfig.mOrientation),
+ MeasureSpecMatcher.is(12, EXACTLY));
+ break;
+ }
+
+ View child0 = mRecyclerView.getChildAt(0);
+ assertThat(getSize(child0, mTestOrientation), is(mBehavior1Size));
+
+ View child1 = mRecyclerView.getChildAt(1);
+ assertThat(getSize(child1, mTestOrientation), is(15));
+
+ assertThat(mHelper.getDecoratedStart(child0), is(0));
+ assertThat(mHelper.getDecoratedStart(child1), is(15));
+
+ assertThat(mHelper.getDecoratedEnd(child0), is(mBehavior1Size));
+ assertThat(mHelper.getDecoratedEnd(child1), is(30));
+
+ assertThat(mHelper.getDecoratedMeasurementInOther(child0),
+ is(Math.max(10, mBehavior1Size)));
+ assertThat(mHelper.getDecoratedMeasurementInOther(child1),
+ is(Math.max(10, mBehavior1Size)));
+
+ assertThat(getSize(mRecyclerView, mTestOrientation), is(45));
+ assertThat(getSize(mRecyclerView, 1 - mTestOrientation), is(Math.max(10, mBehavior1Size)));
+ }
+
+ private RecyclerView.LayoutManager createFromConfig() {
+ return new GridLayoutManager(getActivity(), mConfig.mSpanCount,
+ mConfig.mOrientation, mConfig.mReverseLayout);
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerBaseConfigSetTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerBaseConfigSetTest.java
index 37b9821..73e913c 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerBaseConfigSetTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerBaseConfigSetTest.java
@@ -17,25 +17,28 @@
package android.support.v7.widget;
-import org.junit.After;
-import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import android.graphics.Rect;
-import android.support.test.InstrumentationRegistry;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static android.support.v7.widget.LayoutState.LAYOUT_END;
import static android.support.v7.widget.LayoutState.LAYOUT_START;
import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
-import static org.junit.Assert.*;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.ViewGroup.LayoutParams.FILL_PARENT;
/**
* Tests that rely on the basic configuration and does not do any additions / removals
@@ -52,7 +55,11 @@
@Parameterized.Parameters(name = "{0}")
public static List<Config> configs() throws CloneNotSupportedException {
- return createBaseVariations();
+ List<Config> result = new ArrayList<>();
+ for (Config config : createBaseVariations()) {
+ result.add(config);
+ }
+ return result;
}
@Test
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerPrepareForDropTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerPrepareForDropTest.java
index 773b253..27c93fc 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerPrepareForDropTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerPrepareForDropTest.java
@@ -105,11 +105,11 @@
int position) {
super.onBindViewHolder(holder, position);
if (config.mOrientation == HORIZONTAL) {
- final int base = mRecyclerView.getWidth() / 5;
+ final int base = mLayoutManager.getWidth() / 5;
final int itemRand = holder.mBoundItem.mText.hashCode() % base;
holder.itemView.setMinimumWidth(base + itemRand);
} else {
- final int base = mRecyclerView.getHeight() / 5;
+ final int base = mLayoutManager.getHeight() / 5;
final int itemRand = holder.mBoundItem.mText.hashCode() % base;
holder.itemView.setMinimumHeight(base + itemRand);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentTest.java
new file mode 100644
index 0000000..52c27bb
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2015 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.support.v7.widget;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.support.v4.view.ViewCompat;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.Gravity;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v7.widget.BaseLinearLayoutManagerTest.Config;
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+
+@RunWith(Parameterized.class)
+@MediumTest
+public class LinearLayoutManagerWrapContentTest extends BaseWrapContentTest {
+
+ Config mConfig;
+
+ public LinearLayoutManagerWrapContentTest(Config config,
+ WrapContentConfig wrapContentConfig) {
+ super(wrapContentConfig);
+ mConfig = config;
+ }
+
+ @Test
+ public void deletion() throws Throwable {
+ testScenerio(new Scenario(
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.deleteAndNotify(3, 3);
+ }
+ },
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.deleteAndNotify(3, 3);
+ }
+ },
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.deleteAndNotify(1, 2);
+ }
+ }) {
+ });
+ }
+
+ @Test
+ public void addition() throws Throwable {
+ testScenerio(new Scenario(
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.addAndNotify(1, 2);
+ }
+ }
+ ,
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.addAndNotify(0, 2);
+ }
+ },
+ new Step() {
+ @Override
+ void onRun() throws Throwable {
+ mTestAdapter.addAndNotify(6, 3);
+ }
+ }
+ ) {
+ @Override
+ public int getSeedAdapterSize() {
+ return 2;
+ }
+ });
+ }
+
+ @Parameterized.Parameters(name = "{0} {1}")
+ public static Iterable<Object[]> data() {
+ List<Object[]> params = new ArrayList<>();
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (boolean stackFromBottom : new boolean[]{false, true}) {
+ params.add(
+ new Object[]{
+ new Config(orientation, reverseLayout, stackFromBottom),
+ new WrapContentConfig(false, false)
+ }
+ );
+ params.add(
+ new Object[]{
+ new Config(orientation, reverseLayout, stackFromBottom),
+ new WrapContentConfig(HORIZONTAL == orientation,
+ VERTICAL == orientation)
+ }
+ );
+ }
+ }
+ }
+ return params;
+ }
+
+ @Override
+ RecyclerView.LayoutManager createLayoutManager() {
+ return createFromConfig();
+ }
+
+ private LinearLayoutManager createFromConfig() {
+ LinearLayoutManager llm = new LinearLayoutManager(getActivity(), mConfig.mOrientation,
+ mConfig.mReverseLayout);
+ llm.setStackFromEnd(mConfig.mStackFromEnd);
+ return llm;
+ }
+
+ @Override
+ protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager) {
+ if (mConfig.mOrientation == HORIZONTAL) {
+ return Gravity.TOP;
+ }
+ if (mConfig.mReverseLayout ^ mConfig.mStackFromEnd) {
+ return Gravity.BOTTOM;
+ }
+ return Gravity.TOP;
+ }
+
+ @Override
+ protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager) {
+ boolean rtl = layoutManager.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
+ if (mConfig.mOrientation == VERTICAL) {
+ if (rtl) {
+ return Gravity.RIGHT;
+ }
+ return Gravity.LEFT;
+ }
+ boolean end = mConfig.mReverseLayout ^ mConfig.mStackFromEnd;
+ if (rtl ^ end) {
+ return Gravity.RIGHT;
+ }
+ return Gravity.LEFT;
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentWithAspectRatioTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentWithAspectRatioTest.java
new file mode 100644
index 0000000..2bb0750
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerWrapContentWithAspectRatioTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import android.graphics.Color;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.v7.widget.LinearLayoutManager.HORIZONTAL;
+import static android.support.v7.widget.LinearLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+@RunWith(Parameterized.class)
+@MediumTest
+public class LinearLayoutManagerWrapContentWithAspectRatioTest
+ extends BaseWrapContentWithAspectRatioTest {
+
+ final LinearLayoutManagerTest.Config mConfig;
+ final float mRatio;
+
+ public LinearLayoutManagerWrapContentWithAspectRatioTest(
+ BaseLinearLayoutManagerTest.Config config,
+ BaseWrapContentTest.WrapContentConfig wrapContentConfig,
+ float ratio) {
+ super(wrapContentConfig);
+ mConfig = config;
+ mRatio = ratio;
+ }
+
+ @Parameterized.Parameters(name = "{0} {1} ratio:{2}")
+ public static Iterable<Object[]> data() {
+ List<Object[]> params = new ArrayList<>();
+ for (float ratio : new float[]{.5f, 1f, 2f}) {
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (boolean stackFromBottom : new boolean[]{false, true}) {
+ params.add(
+ new Object[]{
+ new BaseLinearLayoutManagerTest.Config(orientation,
+ reverseLayout, stackFromBottom),
+ new BaseWrapContentTest.WrapContentConfig(
+ false, false),
+ ratio
+ }
+ );
+ params.add(
+ new Object[]{
+ new BaseLinearLayoutManagerTest.Config(orientation,
+ reverseLayout, stackFromBottom),
+ new BaseWrapContentTest.WrapContentConfig(
+ HORIZONTAL == orientation,
+ VERTICAL == orientation),
+ ratio
+ }
+ );
+ params.add(
+ new Object[]{
+ new BaseLinearLayoutManagerTest.Config(orientation,
+ reverseLayout, stackFromBottom),
+ new BaseWrapContentTest.WrapContentConfig(
+ VERTICAL == orientation,
+ HORIZONTAL == orientation),
+ ratio
+ }
+ );
+ params.add(
+ new Object[]{
+ new BaseLinearLayoutManagerTest.Config(orientation,
+ reverseLayout, stackFromBottom),
+ new BaseWrapContentTest.WrapContentConfig(
+ true, true),
+ ratio
+ }
+ );
+ }
+ }
+ }
+ }
+ return params;
+ }
+
+ @Test
+ public void wrapContentAffectsOtherOrientation() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams
+ wrapContent = new TestedFrameLayout.FullControlLayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ int testOrientation = mConfig.mOrientation == VERTICAL ? HORIZONTAL : VERTICAL;
+ boolean unlimitedSize = false;
+ if (mWrapContentConfig.isUnlimitedHeight()) {
+ wrapContent.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ unlimitedSize = testOrientation == VERTICAL;
+ }
+ if (mWrapContentConfig.isUnlimitedWidth()) {
+ wrapContent.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ unlimitedSize |= testOrientation == HORIZONTAL;
+ }
+
+ RecyclerView.LayoutManager layoutManager = createFromConfig();
+ BaseWrapContentTest.WrappedRecyclerView
+ recyclerView = new BaseWrapContentTest.WrappedRecyclerView(getActivity());
+ recyclerView.setBackgroundColor(Color.rgb(0, 0, 255));
+ recyclerView.setLayoutManager(layoutManager);
+ recyclerView.setLayoutParams(wrapContent);
+
+ AspectRatioMeasureBehavior behavior1 = new AspectRatioMeasureBehavior(10, 10,
+ WRAP_CONTENT, WRAP_CONTENT).aspectRatio(testOrientation, mRatio);
+ AspectRatioMeasureBehavior behavior2 = new AspectRatioMeasureBehavior(15, 15,
+ testOrientation == HORIZONTAL ? MATCH_PARENT : WRAP_CONTENT,
+ testOrientation == VERTICAL ? MATCH_PARENT : WRAP_CONTENT)
+ .aspectRatio(testOrientation, mRatio);
+ AspectRatioMeasureBehavior behavior3 = new AspectRatioMeasureBehavior(8, 8,
+ testOrientation == HORIZONTAL ? MATCH_PARENT : WRAP_CONTENT,
+ testOrientation == VERTICAL ? MATCH_PARENT : WRAP_CONTENT)
+ .aspectRatio(testOrientation, mRatio);
+
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ behavior1, behavior2, behavior3
+ );
+
+ recyclerView.setAdapter(adapter);
+ setRecyclerView(recyclerView);
+ recyclerView.waitUntilLayout();
+
+ int parentDim = getSize((View) recyclerView.getParent(), testOrientation);
+
+ View itemView1 = recyclerView.findViewHolderForAdapterPosition(0).itemView;
+ assertThat("first child test size", getSize(itemView1, testOrientation),
+ CoreMatchers.is(10));
+ assertThat("first child dependant size", getSize(itemView1, mConfig.mOrientation),
+ CoreMatchers.is(behavior1.getSecondary(10)));
+
+ View itemView2 = recyclerView.findViewHolderForAdapterPosition(1).itemView;
+ assertThat("second child test size", getSize(itemView2, testOrientation),
+ CoreMatchers.is(15));
+ assertThat("second child dependant size", getSize(itemView2, mConfig.mOrientation),
+ CoreMatchers.is(behavior2.getSecondary(15)));
+
+ View itemView3 = recyclerView.findViewHolderForAdapterPosition(2).itemView;
+ assertThat("third child test size", getSize(itemView3, testOrientation),
+ CoreMatchers.is(15));
+ assertThat("third child dependant size", getSize(itemView3, mConfig.mOrientation),
+ CoreMatchers.is(behavior3.getSecondary(15)));
+
+ assertThat("it should be measured only once", behavior1.measureSpecs.size(),
+ CoreMatchers.is(1));
+ if (unlimitedSize) {
+ assertThat(behavior1.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(0, View.MeasureSpec.UNSPECIFIED));
+ } else {
+ assertThat(behavior1.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(parentDim, View.MeasureSpec.AT_MOST));
+ }
+
+ assertThat("it should be measured once", behavior2.measureSpecs.size(), CoreMatchers.is(1));
+ if (unlimitedSize) {
+ assertThat(behavior2.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(0, View.MeasureSpec.UNSPECIFIED));
+ } else {
+ assertThat(behavior2.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(parentDim, View.MeasureSpec.AT_MOST));
+ }
+
+ assertThat("it should be measured twice", behavior3.measureSpecs.size(),
+ CoreMatchers.is(2));
+ if (unlimitedSize) {
+ assertThat(behavior3.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(0, View.MeasureSpec.UNSPECIFIED));
+ } else {
+ assertThat(behavior3.getSpec(0, testOrientation),
+ MeasureSpecMatcher.is(parentDim, View.MeasureSpec.AT_MOST));
+ }
+
+ assertThat(behavior3.getSpec(1, testOrientation),
+ MeasureSpecMatcher.is(15, View.MeasureSpec.EXACTLY));
+ final int totalScrollSize = getSize(itemView1, mConfig.mOrientation)
+ + getSize(itemView2, mConfig.mOrientation)
+ + getSize(itemView3, mConfig.mOrientation);
+ assertThat("RecyclerView should wrap its content in the scroll direction",
+ getSize(mRecyclerView, mConfig.mOrientation), CoreMatchers.is(totalScrollSize));
+ assertThat("RecyclerView should wrap its content in the scroll direction",
+ getSize(mRecyclerView, testOrientation), CoreMatchers.is(15));
+ }
+
+ private LinearLayoutManager createFromConfig() {
+ LinearLayoutManager llm = new LinearLayoutManager(getActivity(), mConfig.mOrientation,
+ mConfig.mReverseLayout);
+ llm.setStackFromEnd(mConfig.mStackFromEnd);
+ return llm;
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
index 9bbdd4b..b1bc21d 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LoggingItemAnimator.java
@@ -53,11 +53,33 @@
return false;
}
+ @NonNull
+ @Override
+ public ItemHolderInfo recordPreLayoutInformation(@NonNull RecyclerView.State state,
+ @NonNull RecyclerView.ViewHolder viewHolder,
+ @AdapterChanges int changeFlags, @NonNull List<Object> payloads) {
+ BaseRecyclerViewAnimationsTest.LoggingInfo
+ loggingInfo = new BaseRecyclerViewAnimationsTest.LoggingInfo(viewHolder, changeFlags, payloads);
+ return loggingInfo;
+ }
+
+ @NonNull
+ @Override
+ public ItemHolderInfo recordPostLayoutInformation(@NonNull RecyclerView.State state,
+ @NonNull RecyclerView.ViewHolder viewHolder) {
+ BaseRecyclerViewAnimationsTest.LoggingInfo
+ loggingInfo = new BaseRecyclerViewAnimationsTest.LoggingInfo(viewHolder, 0, null);
+ return loggingInfo;
+ }
+
+
@Override
public boolean animateDisappearance(@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull ItemHolderInfo preLayoutInfo, ItemHolderInfo postLayoutInfo) {
mAnimateDisappearanceList
- .add(new AnimateDisappearance(viewHolder, null, null));
+ .add(new AnimateDisappearance(viewHolder,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) preLayoutInfo,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) postLayoutInfo));
return super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo);
}
@@ -66,7 +88,9 @@
ItemHolderInfo preLayoutInfo,
@NonNull ItemHolderInfo postLayoutInfo) {
mAnimateAppearanceList
- .add(new AnimateAppearance(viewHolder, null, null));
+ .add(new AnimateAppearance(viewHolder,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) preLayoutInfo,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) postLayoutInfo));
return super.animateAppearance(viewHolder, preLayoutInfo, postLayoutInfo);
}
@@ -75,7 +99,9 @@
@NonNull ItemHolderInfo preInfo,
@NonNull ItemHolderInfo postInfo) {
mAnimatePersistenceList
- .add(new AnimatePersistence(viewHolder, null, null));
+ .add(new AnimatePersistence(viewHolder,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) preInfo,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) postInfo));
return super.animatePersistence(viewHolder, preInfo, postInfo);
}
@@ -84,7 +110,9 @@
@NonNull RecyclerView.ViewHolder newHolder, @NonNull ItemHolderInfo preInfo,
@NonNull ItemHolderInfo postInfo) {
mAnimateChangeList
- .add(new AnimateChange(oldHolder, newHolder, null, null));
+ .add(new AnimateChange(oldHolder, newHolder,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) preInfo,
+ (BaseRecyclerViewAnimationsTest.LoggingInfo) postInfo));
return super.animateChange(oldHolder, newHolder, preInfo, postInfo);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 25bb8ae..9ea61fd 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -20,6 +20,7 @@
import org.junit.runner.RunWith;
import android.graphics.Rect;
+import android.support.annotation.NonNull;
import android.os.Debug;
import android.support.annotation.NonNull;
import android.support.test.runner.AndroidJUnit4;
@@ -797,8 +798,8 @@
mLayoutManager.waitForLayout(2);
}
- private void testChangeWithPayload(final boolean supportsChangeAnim, final boolean canReUse,
- Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
+ private void testChangeWithPayload(final boolean supportsChangeAnim,
+ final boolean canReUse, Object[][] notifyPayloads, Object[][] expectedPayloadsInOnBind)
throws Throwable {
final List<Object> expectedPayloads = new ArrayList<Object>();
final int changedIndex = 3;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 15a9012..bedc506 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -27,12 +27,13 @@
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.SystemClock;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
import android.support.v4.view.ViewCompat;
import android.support.v7.util.TouchUtils;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import android.view.Gravity;
-import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -50,11 +51,10 @@
import java.util.concurrent.atomic.AtomicInteger;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
-import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_DRAGGING;
+import static android.support.v7.widget.RecyclerView.SCROLL_STATE_IDLE;
import static android.support.v7.widget.RecyclerView.SCROLL_STATE_SETTLING;
import static android.support.v7.widget.RecyclerView.getChildViewHolderInt;
-import android.support.test.runner.AndroidJUnit4;
import static org.junit.Assert.*;
@RunWith(AndroidJUnit4.class)
@@ -208,15 +208,25 @@
});
assertTrue(c.hasFocus());
freezeLayout(true);
- getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ focusSearch(recyclerView, c, View.FOCUS_DOWN);
assertEquals("onFocusSearchFailed should not be called when layout is frozen",
0, focusSearchCalled.get());
freezeLayout(false);
- getInstrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_DOWN);
+ focusSearch(recyclerView, c, View.FOCUS_DOWN);
assertTrue(focusLatch.await(2, TimeUnit.SECONDS));
assertEquals(1, focusSearchCalled.get());
}
+ public void focusSearch(final ViewGroup parent, final View focused, final int direction)
+ throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ parent.focusSearch(focused, direction);
+ }
+ });
+ }
+
@Test
public void frozenAndChangeAdapter() throws Throwable {
RecyclerView recyclerView = new RecyclerView(getActivity());
@@ -1261,13 +1271,16 @@
}
@Test
- public void accessRecyclerOnOnMeasure() throws Throwable {
- accessRecyclerOnOnMeasureTest(false);
- removeRecyclerView();
+ public void aAccessRecyclerOnOnMeasureWithPredictive() throws Throwable {
accessRecyclerOnOnMeasureTest(true);
}
@Test
+ public void accessRecyclerOnOnMeasureWithoutPredictive() throws Throwable {
+ accessRecyclerOnOnMeasureTest(false);
+ }
+
+ @Test
public void smoothScrollWithRemovedItemsAndRemoveItem() throws Throwable {
smoothScrollTest(true);
}
@@ -1481,8 +1494,10 @@
assertNotNull(view);
assertEquals(i, getPosition(view));
}
- assertEquals(state.toString(),
- expectedOnMeasureStateCount.get(), state.getItemCount());
+ if (!state.isPreLayout()) {
+ assertEquals(state.toString(),
+ expectedOnMeasureStateCount.get(), state.getItemCount());
+ }
} catch (Throwable t) {
postExceptionToInstrumentation(t);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
index 7047cde..15331b5 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerBaseConfigSetTest.java
@@ -61,20 +61,46 @@
@Test
public void rTL() throws Throwable {
- rtlTest(false);
+ rtlTest(false, false);
}
@Test
public void rTLChangeAfter() throws Throwable {
- rtlTest(true);
+ rtlTest(true, false);
}
- void rtlTest(boolean changeRtlAfter) throws Throwable {
+ @Test
+ public void rTLItemWrapContent() throws Throwable {
+ rtlTest(false, true);
+ }
+
+ @Test
+ public void rTLChangeAfterItemWrapContent() throws Throwable {
+ rtlTest(true, true);
+ }
+
+ void rtlTest(boolean changeRtlAfter, final boolean wrapContent) throws Throwable {
if (mConfig.mSpanCount == 1) {
mConfig.mSpanCount = 2;
}
String logPrefix = mConfig + ", changeRtlAfterLayout:" + changeRtlAfter;
- setupByConfig(mConfig.itemCount(5));
+ setupByConfig(mConfig.itemCount(5),
+ new GridTestAdapter(mConfig.mItemCount, mConfig.mOrientation) {
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ if (wrapContent) {
+ if (mOrientation == HORIZONTAL) {
+ holder.itemView.getLayoutParams().height
+ = RecyclerView.LayoutParams.WRAP_CONTENT;
+ } else {
+ holder.itemView.getLayoutParams().width
+ = RecyclerView.LayoutParams.MATCH_PARENT;
+ }
+ }
+ }
+ });
if (changeRtlAfter) {
waitFirstLayout();
mLayoutManager.expectLayouts(1);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
index 006ab54..07d036a 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
@@ -61,6 +61,7 @@
import static android.support.v7.widget.StaggeredGridLayoutManager.LayoutParams;
import static org.junit.Assert.*;
+
@MediumTest
public class StaggeredGridLayoutManagerTest extends BaseStaggeredGridLayoutManagerTest {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerWrapContentTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerWrapContentTest.java
new file mode 100644
index 0000000..5b04302
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerWrapContentTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 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.support.v7.widget;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import android.graphics.Rect;
+import android.view.Gravity;
+import android.view.View;
+
+import static android.support.v7.widget.StaggeredGridLayoutManager.VERTICAL;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.support.v7.widget.BaseWrapContentWithAspectRatioTest.*;
+
+@RunWith(JUnit4.class)
+public class StaggeredGridLayoutManagerWrapContentTest extends BaseWrapContentTest {
+
+ public StaggeredGridLayoutManagerWrapContentTest() {
+ super(new WrapContentConfig(false, false));
+ }
+
+ @Test
+ public void testSimple() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ new MeasureBehavior(10, 10, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 15, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 20, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(20, 10, WRAP_CONTENT, WRAP_CONTENT)
+ );
+ Rect[] expected = new Rect[] {
+ new Rect(0, 0, 10, 10),
+ new Rect(20, 0, 30, 15),
+ new Rect(40, 0, 50, 20),
+ new Rect(0, 10, 20, 20)
+ };
+ layoutAndCheck(lp, adapter, expected, 60, 20);
+ }
+
+ @Test
+ public void testUnspecifiedWidth() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ lp.wSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ new MeasureBehavior(2000, 10, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(500, 15, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(400, 20, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(50, 10, MATCH_PARENT, WRAP_CONTENT)
+ );
+ Rect[] expected = new Rect[] {
+ new Rect(0, 0, 2000, 10),
+ new Rect(2000, 0, 2500, 15),
+ new Rect(4000, 0, 4400, 20),
+ new Rect(0, 10, 2000, 20)
+ };
+ layoutAndCheck(lp, adapter, expected, 6000, 20);
+ }
+
+ @Test
+ public void testUnspecifiedHeight() throws Throwable {
+ TestedFrameLayout.FullControlLayoutParams lp =
+ mWrapContentConfig.toLayoutParams(WRAP_CONTENT, WRAP_CONTENT);
+ lp.hSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ WrapContentAdapter adapter = new WrapContentAdapter(
+ new MeasureBehavior(10, 4000, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 5500, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(10, 3000, WRAP_CONTENT, WRAP_CONTENT),
+ new MeasureBehavior(20, 100, WRAP_CONTENT, WRAP_CONTENT)
+ );
+ Rect[] expected = new Rect[] {
+ new Rect(0, 0, 10, 4000),
+ new Rect(20, 0, 30, 5500),
+ new Rect(40, 0, 50, 3000),
+ new Rect(40, 3000, 60, 3100)
+ };
+ layoutAndCheck(lp, adapter, expected, 60, 5500);
+ }
+
+ @Override
+ RecyclerView.LayoutManager createLayoutManager() {
+ return new StaggeredGridLayoutManager(3, VERTICAL);
+ }
+
+ @Override
+ protected int getVerticalGravity(RecyclerView.LayoutManager layoutManager) {
+ return Gravity.TOP;
+ }
+
+ @Override
+ protected int getHorizontalGravity(RecyclerView.LayoutManager layoutManager) {
+ return Gravity.LEFT;
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java b/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
index 18aeba4..040d001 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/TestedFrameLayout.java
@@ -19,8 +19,10 @@
import android.content.Context;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
public class TestedFrameLayout extends FrameLayout implements NestedScrollingParent {
@@ -37,6 +39,103 @@
}
@Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ RecyclerView recyclerView = getRvChild();
+ if (recyclerView == null) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ FullControlLayoutParams lp = (FullControlLayoutParams) recyclerView.getLayoutParams();
+ if (lp.wSpec == null && lp.hSpec == null) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ return;
+ }
+ final int childWidthMeasureSpec;
+ if (lp.wSpec != null) {
+ childWidthMeasureSpec = lp.wSpec;
+ } else if (lp.width == LayoutParams.MATCH_PARENT) {
+ final int width = Math.max(0, getMeasuredWidth()
+ - lp.leftMargin - lp.rightMargin);
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+ lp.leftMargin + lp.rightMargin, lp.width);
+ }
+
+ final int childHeightMeasureSpec;
+ if (lp.hSpec != null) {
+ childHeightMeasureSpec = lp.hSpec;
+ } else if (lp.height == LayoutParams.MATCH_PARENT) {
+ final int height = Math.max(0, getMeasuredHeight()
+ - lp.topMargin - lp.bottomMargin);
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+ lp.topMargin + lp.bottomMargin, lp.height);
+ }
+ recyclerView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY &&
+ MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ MeasureSpec.getSize(heightMeasureSpec)
+ );
+ } else {
+ setMeasuredDimension(
+ chooseSize(widthMeasureSpec,
+ recyclerView.getWidth() + getPaddingLeft() + getPaddingRight(),
+ getMinimumWidth()),
+ chooseSize(heightMeasureSpec,
+ recyclerView.getHeight() + getPaddingTop() + getPaddingBottom(),
+ getMinimumHeight()));
+ }
+ }
+
+ public static int chooseSize(int spec, int desired, int min) {
+ final int mode = View.MeasureSpec.getMode(spec);
+ final int size = View.MeasureSpec.getSize(spec);
+ switch (mode) {
+ case View.MeasureSpec.EXACTLY:
+ return size;
+ case View.MeasureSpec.AT_MOST:
+ return Math.min(size, desired);
+ case View.MeasureSpec.UNSPECIFIED:
+ default:
+ return Math.max(desired, min);
+ }
+ }
+
+
+ private RecyclerView getRvChild() {
+ for (int i = 0; i < getChildCount(); i++) {
+ if (getChildAt(i) instanceof RecyclerView) {
+ return (RecyclerView) getChildAt(i);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof FullControlLayoutParams;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return new FullControlLayoutParams(p);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new FullControlLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new FullControlLayoutParams(getWidth(), getHeight());
+ }
+
+ @Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// Always start nested scroll
return mNestedFlingMode == TEST_NESTED_SCROLL_MODE_CONSUME
@@ -98,4 +197,30 @@
public void setNestedFlingMode(int mode) {
mNestedFlingMode = mode;
}
+
+ public static class FullControlLayoutParams extends FrameLayout.LayoutParams {
+
+ Integer wSpec;
+ Integer hSpec;
+
+ public FullControlLayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public FullControlLayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public FullControlLayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public FullControlLayoutParams(FrameLayout.LayoutParams source) {
+ super(source);
+ }
+
+ public FullControlLayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/WrapContentBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/WrapContentBasicTest.java
new file mode 100644
index 0000000..b485fa6
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/WrapContentBasicTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2015 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.support.v7.widget;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.AndroidTestCase;
+import android.view.Display;
+import android.view.View;
+import android.view.ViewGroup;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(AndroidJUnit4.class)
+public class WrapContentBasicTest extends AndroidTestCase {
+
+ private WrapContentLayoutManager mLayoutManager;
+ private RecyclerView mRecyclerView;
+ private WrapAdapter mAdapter;
+ private static int WRAP = View.MeasureSpec.makeMeasureSpec(10, View.MeasureSpec.AT_MOST);
+ private static int EXACT = View.MeasureSpec.makeMeasureSpec(10, View.MeasureSpec.EXACTLY);
+ private static int UNSPECIFIED = View.MeasureSpec
+ .makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ setContext(InstrumentationRegistry.getContext());
+ RecyclerView rv = new RecyclerView(getContext());
+ mRecyclerView = spy(rv);
+ mLayoutManager = spy(new WrapContentLayoutManager());
+ // working around a mockito issue
+ rv.mLayout = mLayoutManager;
+ mAdapter = spy(new WrapAdapter());
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mRecyclerView.setAdapter(mAdapter);
+ }
+
+ @After
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @Test
+ public void testLayoutInOnMeasureWithoutPredictive() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ when(mLayoutManager.supportsPredictiveItemAnimations()).thenReturn(false);
+ mRecyclerView.onMeasure(WRAP, WRAP);
+ mRecyclerView.onMeasure(WRAP, WRAP);
+ mRecyclerView.onLayout(true, 0, 10, 10, 10);
+ verify(mLayoutManager, times(3))
+ .onLayoutChildren(mRecyclerView.mRecycler, mRecyclerView.mState);
+ }
+
+ @Test
+ public void dataChangeAfterMeasure() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ mRecyclerView.onMeasure(WRAP, WRAP);
+ mRecyclerView.onMeasure(WRAP, WRAP);
+ mAdapter.notifyItemChanged(1);
+ mRecyclerView.onLayout(true, 0, 10, 10, 10);
+ verify(mLayoutManager, times(3))
+ .onLayoutChildren(mRecyclerView.mRecycler, mRecyclerView.mState);
+ }
+
+ @Test
+ public void setDimensionsFromChildren() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ mLayoutManager.setMeasuredDimensionFromChildren(WRAP, WRAP);
+ verify(mLayoutManager).setMeasuredDimension(children[0].getWidth(),
+ children[0].getHeight());
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec1() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ int hSpec = View.MeasureSpec.makeMeasureSpec(111, View.MeasureSpec.EXACTLY);
+ mLayoutManager.setMeasuredDimensionFromChildren(WRAP, hSpec);
+ verify(mLayoutManager).setMeasuredDimension(children[0].getWidth(), 111);
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec2() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ int wSpec = View.MeasureSpec.makeMeasureSpec(111, View.MeasureSpec.EXACTLY);
+ mLayoutManager.setMeasuredDimensionFromChildren(wSpec, WRAP);
+ verify(mLayoutManager).setMeasuredDimension(111, children[0].getHeight());
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec3() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ children[0].layout(0, 0, 100, 100);
+ children[1].layout(-5, 0, 100, 100);
+ children[2].layout(-5, -10, 100, 100);
+ mLayoutManager.setMeasuredDimensionFromChildren(UNSPECIFIED, UNSPECIFIED);
+ verify(mLayoutManager).setMeasuredDimension(105, 110);
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec4() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ children[0].layout(0, 0, 100, 100);
+ children[1].layout(-5, 0, 100, 100);
+ children[2].layout(-5, -10, 100, 100);
+ int atMost = View.MeasureSpec.makeMeasureSpec(95, View.MeasureSpec.AT_MOST);
+ mLayoutManager.setMeasuredDimensionFromChildren(atMost, atMost);
+ verify(mLayoutManager).setMeasuredDimension(95, 95);
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec5() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ children[0].layout(0, 0, 100, 100);
+ children[1].layout(-5, 0, 100, 100);
+ children[2].layout(-5, -10, 100, 100);
+ when(mRecyclerView.getMinimumWidth()).thenReturn(250);
+ mLayoutManager.setMeasuredDimensionFromChildren(UNSPECIFIED, UNSPECIFIED);
+ verify(mLayoutManager).setMeasuredDimension(250, 110);
+
+ when(mRecyclerView.getMinimumWidth()).thenReturn(5);
+ mLayoutManager.setMeasuredDimensionFromChildren(UNSPECIFIED, UNSPECIFIED);
+ verify(mLayoutManager).setMeasuredDimension(105, 110);
+ }
+
+ @Test
+ public void setDimensionsFromChildrenAnsSpec6() {
+ mLayoutManager.setAutoMeasureEnabled(true);
+ View[] children = createMockChildren(3);
+ children[0].layout(0, 0, 100, 100);
+ children[1].layout(-5, 0, 100, 100);
+ children[2].layout(-5, -10, 100, 100);
+ when(mRecyclerView.getMinimumHeight()).thenReturn(250);
+ mLayoutManager.setMeasuredDimensionFromChildren(UNSPECIFIED, UNSPECIFIED);
+ verify(mLayoutManager).setMeasuredDimension(105, 250);
+
+ when(mRecyclerView.getMinimumHeight()).thenReturn(50);
+ mLayoutManager.setMeasuredDimensionFromChildren(UNSPECIFIED, UNSPECIFIED);
+ verify(mLayoutManager).setMeasuredDimension(105, 110);
+ }
+
+ private View[] createMockChildren(int count) {
+ View[] views = new View[count];
+ for (int i = 0; i < count; i++) {
+ View v = new View(getContext());
+ v.setLayoutParams(new RecyclerView.LayoutParams(1, 1));
+ views[i] = v;
+ when(mLayoutManager.getChildAt(i)).thenReturn(v);
+ }
+ when(mLayoutManager.getChildCount()).thenReturn(3);
+ return views;
+ }
+
+ public class WrapContentLayoutManager extends RecyclerView.LayoutManager {
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ public class WrapAdapter extends RecyclerView.Adapter {
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return null;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return 10;
+ }
+ }
+}