Merge "Support custom views on snackbars"
diff --git a/annotations/AndroidManifest.xml b/annotations/AndroidManifest.xml
index 6f24ecb..2593e0e 100644
--- a/annotations/AndroidManifest.xml
+++ b/annotations/AndroidManifest.xml
@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
-<manifest package="android.support.annotations" />
+<manifest package="android.support.annotation" />
diff --git a/api/current.txt b/api/current.txt
index 23e790c..ccc0abd 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -9201,6 +9201,21 @@
method public void setDividerAllowedBelow(boolean);
}
+ public class SeekBarPreference extends android.support.v7.preference.Preference {
+ ctor public SeekBarPreference(android.content.Context, android.util.AttributeSet, int, int);
+ ctor public SeekBarPreference(android.content.Context, android.util.AttributeSet, int);
+ ctor public SeekBarPreference(android.content.Context, android.util.AttributeSet);
+ ctor public SeekBarPreference(android.content.Context);
+ method public int getMax();
+ method public int getMin();
+ method public int getValue();
+ method public boolean isAdjustable();
+ method public void setAdjustable(boolean);
+ method public void setMax(int);
+ method public void setMin(int);
+ method public void setValue(int);
+ }
+
public class SwitchPreferenceCompat extends android.support.v7.preference.TwoStatePreference {
ctor public SwitchPreferenceCompat(android.content.Context, android.util.AttributeSet, int, int);
ctor public SwitchPreferenceCompat(android.content.Context, android.util.AttributeSet, int);
diff --git a/design/src/android/support/design/internal/BottomNavigationMenuView.java b/design/src/android/support/design/internal/BottomNavigationMenuView.java
index 158dda7..df361e6 100644
--- a/design/src/android/support/design/internal/BottomNavigationMenuView.java
+++ b/design/src/android/support/design/internal/BottomNavigationMenuView.java
@@ -16,6 +16,8 @@
package android.support.design.internal;
+import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
+
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
@@ -32,8 +34,6 @@
import android.view.View;
import android.view.ViewGroup;
-import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
-
/**
* @hide For internal use only.
*/
@@ -55,6 +55,7 @@
private ColorStateList mItemIconTint;
private ColorStateList mItemTextColor;
private int mItemBackgroundRes;
+ private int[] mTempChildWidths;
private BottomNavigationPresenter mPresenter;
private MenuBuilder mMenu;
@@ -89,6 +90,7 @@
mMenu.performItemAction(itemView.getItemData(), mPresenter, 0);
}
};
+ mTempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
}
@Override
@@ -105,10 +107,8 @@
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int count = getChildCount();
- final int childState = 0;
final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
- final int[] childWidths = new int[count];
if (mShiftingMode) {
final int inactiveCount = count - 1;
final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
@@ -117,9 +117,9 @@
final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
int extra = width - activeWidth - inactiveWidth * inactiveCount;
for (int i = 0; i < count; i++) {
- childWidths[i] = (i == mActiveButton) ? activeWidth : inactiveWidth;
+ mTempChildWidths[i] = (i == mActiveButton) ? activeWidth : inactiveWidth;
if (extra > 0) {
- childWidths[i]++;
+ mTempChildWidths[i]++;
extra--;
}
}
@@ -128,9 +128,9 @@
final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
int extra = width - childWidth * count;
for (int i = 0; i < count; i++) {
- childWidths[i] = childWidth;
+ mTempChildWidths[i] = childWidth;
if (extra > 0) {
- childWidths[i]++;
+ mTempChildWidths[i]++;
extra--;
}
}
@@ -142,7 +142,7 @@
if (child.getVisibility() == GONE) {
continue;
}
- child.measure(MeasureSpec.makeMeasureSpec(childWidths[i], MeasureSpec.EXACTLY),
+ child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
heightSpec);
ViewGroup.LayoutParams params = child.getLayoutParams();
params.width = child.getMeasuredWidth();
@@ -150,9 +150,8 @@
}
setMeasuredDimension(
ViewCompat.resolveSizeAndState(totalWidth,
- MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), childState),
- ViewCompat.resolveSizeAndState(mItemHeight, heightSpec,
- childState << MEASURED_HEIGHT_STATE_SHIFT));
+ MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
+ ViewCompat.resolveSizeAndState(mItemHeight, heightSpec, 0));
}
@Override
@@ -180,19 +179,34 @@
return 0;
}
- public void setIconTintList(ColorStateList color) {
- mItemIconTint = color;
+ /**
+ * Set the tint which is applied to the menu items' icons.
+ *
+ * @param tint the tint to apply.
+ */
+ public void setIconTintList(ColorStateList tint) {
+ mItemIconTint = tint;
if (mButtons == null) return;
for (BottomNavigationItemView item : mButtons) {
- item.setIconTintList(color);
+ item.setIconTintList(tint);
}
}
+ /**
+ * Returns the tint which is applied to menu items' icons.
+ *
+ * @return The ColorStateList that is used to tint menu items' icons.
+ */
@Nullable
public ColorStateList getIconTintList() {
return mItemIconTint;
}
+ /**
+ * Set the text color to be used on menu items.
+ *
+ * @param color the ColorStateList used for menu items' text.
+ */
public void setItemTextColor(ColorStateList color) {
mItemTextColor = color;
if (mButtons == null) return;
@@ -201,10 +215,19 @@
}
}
+ /**
+ * Returns the text color used on menu items.
+ *
+ * @return the ColorStateList used for menu items' text.
+ */
public ColorStateList getItemTextColor() {
return mItemTextColor;
}
+ /**
+ * Sets the resource id to be used for item background.
+ * @param background the resource id of the background.
+ */
public void setItemBackgroundRes(int background) {
mItemBackgroundRes = background;
if (mButtons == null) return;
@@ -213,6 +236,11 @@
}
}
+ /**
+ * Returns the background resource of the menu items.
+ *
+ * @return the resource id of the background.
+ */
public int getItemBackgroundRes() {
return mItemBackgroundRes;
}
diff --git a/design/src/android/support/design/widget/BottomNavigationView.java b/design/src/android/support/design/widget/BottomNavigationView.java
index 476889f..1d36aa8 100644
--- a/design/src/android/support/design/widget/BottomNavigationView.java
+++ b/design/src/android/support/design/widget/BottomNavigationView.java
@@ -210,7 +210,7 @@
}
/**
- * Returns the tint which is applied to menu items' icons.
+ * Returns the text color used on menu items.
*
* @see #setItemTextColor(ColorStateList)
*
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
index 3355956..c97d6e1 100644
--- a/design/src/android/support/design/widget/TextInputLayout.java
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -58,6 +58,7 @@
import android.text.method.PasswordTransformationMethod;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.SparseArray;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -159,6 +160,8 @@
private boolean mHasReconstructedEditTextBackground;
private boolean mInDrawableStateChanged;
+ private boolean mRestoringSavedState;
+
public TextInputLayout(Context context) {
this(context, null);
}
@@ -316,7 +319,7 @@
mEditText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
- updateLabelState(true);
+ updateLabelState(!mRestoringSavedState);
if (mCounterEnabled) {
updateCounter(s.length());
}
@@ -398,10 +401,14 @@
if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
// We should be showing the label so do so if it isn't already
- collapseHint(animate);
+ if (mHintExpanded) {
+ collapseHint(animate);
+ }
} else {
// We should not be showing the label so hide it
- expandHint(animate);
+ if (!mHintExpanded) {
+ expandHint(animate);
+ }
}
}
@@ -797,8 +804,8 @@
} else {
mCounterOverflowed = length > mCounterMaxLength;
if (wasCounterOverflowed != mCounterOverflowed) {
- TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed ?
- mCounterOverflowTextAppearance : mCounterTextAppearance);
+ TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed
+ ? mCounterOverflowTextAppearance : mCounterTextAppearance);
}
mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
length, mCounterMaxLength));
@@ -887,7 +894,7 @@
super(superState);
}
- public SavedState(Parcel source, ClassLoader loader) {
+ SavedState(Parcel source, ClassLoader loader) {
super(source, loader);
error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
@@ -942,6 +949,13 @@
requestLayout();
}
+ @Override
+ protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+ mRestoringSavedState = true;
+ super.dispatchRestoreInstanceState(container);
+ mRestoringSavedState = false;
+ }
+
/**
* Returns the error message that was set to be displayed with
* {@link #setError(CharSequence)}, or <code>null</code> if no error was set
@@ -1211,24 +1225,24 @@
applyPasswordToggleTint();
}
- void passwordVisibilityToggleRequested() {
- if (mPasswordToggleEnabled) {
- // Store the current cursor position
- final int selection = mEditText.getSelectionEnd();
+ void passwordVisibilityToggleRequested() {
+ if (mPasswordToggleEnabled) {
+ // Store the current cursor position
+ final int selection = mEditText.getSelectionEnd();
- if (hasPasswordTransformation()) {
- mEditText.setTransformationMethod(null);
- mPasswordToggledVisible = true;
- } else {
- mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- mPasswordToggledVisible = false;
- }
+ if (hasPasswordTransformation()) {
+ mEditText.setTransformationMethod(null);
+ mPasswordToggledVisible = true;
+ } else {
+ mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
+ mPasswordToggledVisible = false;
+ }
- mPasswordToggleView.setChecked(mPasswordToggledVisible);
+ mPasswordToggleView.setChecked(mPasswordToggledVisible);
- // And restore the cursor position
- mEditText.setSelection(selection);
- }
+ // And restore the cursor position
+ mEditText.setSelection(selection);
+ }
}
private boolean hasPasswordTransformation() {
@@ -1339,7 +1353,8 @@
mHintExpanded = true;
}
- private void animateToExpansionFraction(final float target) {
+ @VisibleForTesting
+ void animateToExpansionFraction(final float target) {
if (mCollapsingTextHelper.getExpansionFraction() == target) {
return;
}
@@ -1411,4 +1426,4 @@
}
return false;
}
-}
\ No newline at end of file
+}
diff --git a/design/tests/res/values/ids.xml b/design/tests/res/values/ids.xml
index e5fcf63..52b8356 100644
--- a/design/tests/res/values/ids.xml
+++ b/design/tests/res/values/ids.xml
@@ -24,4 +24,6 @@
<item name="page_7" type="id"/>
<item name="page_8" type="id"/>
<item name="page_9" type="id"/>
+ <item name="textinputlayout" type="id"/>
+ <item name="textinputedittext" type="id"/>
</resources>
\ No newline at end of file
diff --git a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
index 7ac3102..3923ee9 100755
--- a/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
+++ b/design/tests/src/android/support/design/widget/TextInputLayoutTest.java
@@ -21,7 +21,8 @@
import static android.support.design.testutils.TestUtilsMatchers.withCompoundDrawable;
import static android.support.design.testutils.TextInputLayoutActions.setError;
import static android.support.design.testutils.TextInputLayoutActions.setErrorEnabled;
-import static android.support.design.testutils.TextInputLayoutActions.setPasswordVisibilityToggleEnabled;
+import static android.support.design.testutils.TextInputLayoutActions
+ .setPasswordVisibilityToggleEnabled;
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.typeText;
@@ -38,14 +39,18 @@
import static org.junit.Assert.assertTrue;
import android.app.Activity;
+import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
+import android.os.Parcelable;
import android.support.design.test.R;
import android.support.test.annotation.UiThreadTest;
import android.support.test.espresso.NoMatchingViewException;
import android.support.test.espresso.ViewAssertion;
import android.support.test.filters.SmallTest;
+import android.util.AttributeSet;
+import android.util.SparseArray;
import android.view.View;
import android.widget.EditText;
@@ -59,6 +64,30 @@
private static final String INPUT_TEXT = "Random input text";
+ public class TestTextInputLayout extends TextInputLayout {
+ public int animateToExpansionFractionCount = 0;
+ public float animateToExpansionFractionRecentValue = -1;
+
+ public TestTextInputLayout(Context context) {
+ super(context);
+ }
+
+ public TestTextInputLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public TestTextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void animateToExpansionFraction(float target) {
+ super.animateToExpansionFraction(target);
+ animateToExpansionFractionRecentValue = target;
+ animateToExpansionFractionCount++;
+ }
+ }
+
public TextInputLayoutTest() {
super(TextInputLayoutActivity.class);
}
@@ -229,6 +258,35 @@
layout.drawableStateChanged();
}
+ @UiThreadTest
+ @Test
+ public void testSaveRestoreStateAnimation() {
+ final Activity activity = mActivityTestRule.getActivity();
+ final TestTextInputLayout layout = new TestTextInputLayout(activity);
+ layout.setId(R.id.textinputlayout);
+ final TextInputEditText editText = new TextInputEditText(activity);
+ editText.setText(INPUT_TEXT);
+ editText.setId(R.id.textinputedittext);
+ layout.addView(editText);
+
+ SparseArray<Parcelable> container = new SparseArray<>();
+ layout.saveHierarchyState(container);
+ layout.restoreHierarchyState(container);
+ assertEquals("Expected no animations since we simply saved/restored state",
+ 0, layout.animateToExpansionFractionCount);
+
+ editText.setText("");
+ assertEquals("Expected one call to animate because we cleared text in editText",
+ 1, layout.animateToExpansionFractionCount);
+ assertEquals(0f, layout.animateToExpansionFractionRecentValue, 0f);
+
+ container = new SparseArray<>();
+ layout.saveHierarchyState(container);
+ layout.restoreHierarchyState(container);
+ assertEquals("Expected no additional animations since we simply saved/restored state",
+ 1, layout.animateToExpansionFractionCount);
+ }
+
static ViewAssertion isHintExpanded(final boolean expanded) {
return new ViewAssertion() {
@Override
diff --git a/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java b/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
index 639e906..26a8444 100644
--- a/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
+++ b/graphics/drawable/animated/src/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
@@ -42,6 +42,7 @@
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
+
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -341,7 +342,11 @@
return;
}
int eventType = parser.getEventType();
- while (eventType != XmlPullParser.END_DOCUMENT) {
+ final int innerDepth = parser.getDepth() + 1;
+
+ // Parse everything until the end of the animated-vector element.
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
if (eventType == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
if (DBG_ANIMATION_VECTOR_DRAWABLE) {
diff --git a/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java b/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
index f4d41b5..57c8c93 100644
--- a/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
+++ b/graphics/drawable/static/src/android/support/graphics/drawable/VectorDrawableCompat.java
@@ -14,11 +14,7 @@
package android.support.graphics.drawable;
-import android.support.annotation.RestrictTo;
-import android.support.v4.content.res.ResourcesCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
+import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
import android.annotation.TargetApi;
import android.content.res.ColorStateList;
@@ -38,24 +34,27 @@
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
-import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Build;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Xml;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
import java.io.IOException;
import java.util.ArrayList;
import java.util.Stack;
-import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
-
/**
* For API 24 and above, this class is delegating to the framework's {@link VectorDrawable}.
* For older API version, this class lets you create a drawable based on an XML vector graphic.
@@ -738,7 +737,11 @@
groupStack.push(pathRenderer.mRootGroup);
int eventType = parser.getEventType();
- while (eventType != XmlPullParser.END_DOCUMENT) {
+ final int innerDepth = parser.getDepth() + 1;
+
+ // Parse everything until the end of the vector element.
+ while (eventType != XmlPullParser.END_DOCUMENT
+ && (parser.getDepth() >= innerDepth || eventType != XmlPullParser.END_TAG)) {
if (eventType == XmlPullParser.START_TAG) {
final String tagName = parser.getName();
final VGroup currentGroup = groupStack.peek();
diff --git a/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentNestingStatePagerSupport.java b/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentNestingStatePagerSupport.java
index 5863852..36a8144 100644
--- a/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentNestingStatePagerSupport.java
+++ b/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentNestingStatePagerSupport.java
@@ -15,10 +15,6 @@
*/
package com.example.android.supportv13.app;
-import java.util.ArrayList;
-
-import com.example.android.supportv13.R;
-
import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.app.Activity;
@@ -29,6 +25,10 @@
import android.support.v13.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
+import com.example.android.supportv13.R;
+
+import java.util.ArrayList;
+
//BEGIN_INCLUDE(complete)
public class FragmentNestingStatePagerSupport extends Activity {
ViewPager mViewPager;
@@ -101,7 +101,7 @@
mActionBar = activity.getActionBar();
mViewPager = pager;
mViewPager.setAdapter(this);
- mViewPager.setOnPageChangeListener(this);
+ mViewPager.addOnPageChangeListener(this);
}
public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args) {
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentCustomAnimationSupport.java b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentCustomAnimationSupport.java
index 5893521..478cb7d 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentCustomAnimationSupport.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentCustomAnimationSupport.java
@@ -16,20 +16,21 @@
package com.example.android.supportv4.app;
-import com.example.android.supportv4.R;
-
+import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
-
-import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewCompat;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import com.example.android.supportv4.R;
+
public class FragmentCustomAnimationSupport extends FragmentActivity {
int mStackLevel = 1;
@@ -39,7 +40,7 @@
setContentView(R.layout.fragment_stack);
// Watch for button clicks.
- Button button = (Button)findViewById(R.id.new_fragment);
+ Button button = (Button) findViewById(R.id.new_fragment);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
addFragmentToStack();
@@ -119,8 +120,9 @@
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.hello_world, container, false);
View tv = v.findViewById(R.id.text);
- ((TextView)tv).setText("Fragment #" + mNum);
- tv.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
+ ((TextView) tv).setText("Fragment #" + mNum);
+ ViewCompat.setBackground(tv,
+ ContextCompat.getDrawable(getContext(), android.R.drawable.gallery_thumb));
return v;
}
}
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStackSupport.java b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStackSupport.java
index 337f2c1..4115d5e 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStackSupport.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStackSupport.java
@@ -16,21 +16,22 @@
package com.example.android.supportv4.app;
-import com.example.android.supportv4.R;
-
+import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
-
-import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewCompat;
import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
import android.view.View.OnClickListener;
+import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import com.example.android.supportv4.R;
+
public class FragmentStackSupport extends FragmentActivity {
int mStackLevel = 1;
@@ -40,13 +41,13 @@
setContentView(R.layout.fragment_stack);
// Watch for button clicks.
- Button button = (Button)findViewById(R.id.new_fragment);
+ Button button = (Button) findViewById(R.id.new_fragment);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
addFragmentToStack();
}
});
- button = (Button)findViewById(R.id.home);
+ button = (Button) findViewById(R.id.home);
button.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
// If there is a back stack, pop it all.
@@ -128,8 +129,9 @@
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.hello_world, container, false);
View tv = v.findViewById(R.id.text);
- ((TextView)tv).setText("Fragment #" + mNum);
- tv.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
+ ((TextView) tv).setText("Fragment #" + mNum);
+ ViewCompat.setBackground(tv,
+ ContextCompat.getDrawable(getContext(), android.R.drawable.gallery_thumb));
return v;
}
}
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/app/LoaderCustomSupport.java b/samples/Support4Demos/src/com/example/android/supportv4/app/LoaderCustomSupport.java
index d858fae..d9689d2 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/app/LoaderCustomSupport.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/app/LoaderCustomSupport.java
@@ -32,6 +32,7 @@
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.AsyncTaskLoader;
+import android.support.v4.content.ContextCompat;
import android.support.v4.content.IntentCompat;
import android.support.v4.content.Loader;
import android.support.v4.content.pm.ActivityInfoCompat;
@@ -116,8 +117,8 @@
return mIcon;
}
- return mLoader.getContext().getResources().getDrawable(
- android.R.drawable.sym_def_app_icon);
+ return ContextCompat.getDrawable(
+ mLoader.getContext(), android.R.drawable.sym_def_app_icon);
}
@Override public String toString() {
@@ -230,8 +231,8 @@
@Override public List<AppEntry> loadInBackground() {
// Retrieve all known applications.
List<ApplicationInfo> apps = mPm.getInstalledApplications(
- PackageManager.GET_UNINSTALLED_PACKAGES |
- PackageManager.GET_DISABLED_COMPONENTS);
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS);
if (apps == null) {
apps = new ArrayList<ApplicationInfo>();
}
@@ -240,7 +241,7 @@
// Create corresponding array of entries and load their labels.
List<AppEntry> entries = new ArrayList<AppEntry>(apps.size());
- for (int i=0; i<apps.size(); i++) {
+ for (int i = 0; i < apps.size(); i++) {
AppEntry entry = new AppEntry(this, apps.get(i));
entry.loadLabel(context);
entries.add(entry);
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
index 765dc88..9fac4ab 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/BrowseFragment.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
+import android.support.v4.content.ContextCompat;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.util.Log;
@@ -277,8 +278,9 @@
holder.mTitleView.setText(item.getDescription().getTitle());
holder.mDescriptionView.setText(item.getDescription().getDescription());
if (item.isPlayable()) {
- holder.mImageView.setImageDrawable(getContext().getResources()
- .getDrawable(R.drawable.ic_play_arrow_white_24dp));
+
+ holder.mImageView.setImageDrawable(ContextCompat.getDrawable(
+ getContext(), R.drawable.ic_play_arrow_white_24dp));
holder.mImageView.setVisibility(View.VISIBLE);
} else {
holder.mImageView.setVisibility(View.GONE);
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/QueueAdapter.java b/samples/Support4Demos/src/com/example/android/supportv4/media/QueueAdapter.java
index 0702e2b..3d327ff 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/QueueAdapter.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/QueueAdapter.java
@@ -16,8 +16,8 @@
package com.example.android.supportv4.media;
-import com.example.android.supportv4.R;
import android.app.Activity;
+import android.support.v4.content.ContextCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.LayoutInflater;
import android.view.View;
@@ -26,6 +26,8 @@
import android.widget.ImageView;
import android.widget.TextView;
+import com.example.android.supportv4.R;
+
import java.util.ArrayList;
/**
@@ -74,10 +76,10 @@
// If the itemId matches the active Id then use a different icon
if (mActiveQueueItemId == item.getQueueId()) {
holder.mImageView.setImageDrawable(
- getContext().getResources().getDrawable(R.drawable.ic_equalizer_white_24dp));
+ ContextCompat.getDrawable(getContext(), R.drawable.ic_equalizer_white_24dp));
} else {
holder.mImageView.setImageDrawable(
- getContext().getResources().getDrawable(R.drawable.ic_play_arrow_white_24dp));
+ ContextCompat.getDrawable(getContext(), R.drawable.ic_play_arrow_white_24dp));
}
return convertView;
}
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/media/QueueFragment.java b/samples/Support4Demos/src/com/example/android/supportv4/media/QueueFragment.java
index f66447d..6ec5477 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/media/QueueFragment.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/media/QueueFragment.java
@@ -20,6 +20,7 @@
import android.content.ComponentName;
import android.os.Bundle;
import android.os.RemoteException;
+import android.support.v4.content.ContextCompat;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
@@ -236,10 +237,10 @@
if (enablePlay) {
mPlayPause.setImageDrawable(
- getActivity().getResources().getDrawable(R.drawable.ic_play_arrow_white_24dp));
+ ContextCompat.getDrawable(getActivity(), R.drawable.ic_play_arrow_white_24dp));
} else {
mPlayPause.setImageDrawable(
- getActivity().getResources().getDrawable(R.drawable.ic_pause_white_24dp));
+ ContextCompat.getDrawable(getActivity(), R.drawable.ic_pause_white_24dp));
}
mSkipPrevious.setEnabled((state.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)
diff --git a/samples/Support4Demos/src/com/example/android/supportv4/view/CheckableFrameLayout.java b/samples/Support4Demos/src/com/example/android/supportv4/view/CheckableFrameLayout.java
index c559cf6..1ca8840 100644
--- a/samples/Support4Demos/src/com/example/android/supportv4/view/CheckableFrameLayout.java
+++ b/samples/Support4Demos/src/com/example/android/supportv4/view/CheckableFrameLayout.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
+import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.widget.Checkable;
import android.widget.FrameLayout;
@@ -35,7 +36,7 @@
public void setChecked(boolean checked) {
mChecked = checked;
- setBackgroundDrawable(checked ? new ColorDrawable(0xff0000a0) : null);
+ ViewCompat.setBackground(this, checked ? new ColorDrawable(0xff0000a0) : null);
}
public boolean isChecked() {
diff --git a/v17/leanback/generatev4.py b/v17/leanback/generatev4.py
index 01c1cd9..7ba498f 100755
--- a/v17/leanback/generatev4.py
+++ b/v17/leanback/generatev4.py
@@ -19,7 +19,7 @@
print "Generate v4 fragment related code for leanback"
-cls = ['Background', 'Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
+cls = ['Base', 'BaseRow', 'Browse', 'Details', 'Error', 'Headers',
'PlaybackOverlay', 'Playback', 'Rows', 'Search', 'VerticalGrid', 'Branded', 'GuidedStep', 'Onboarding']
for w in cls:
diff --git a/v17/leanback/jbmr2/android/support/v17/leanback/os/TraceHelperJbmr2.java b/v17/leanback/jbmr2/android/support/v17/leanback/os/TraceHelperJbmr2.java
deleted file mode 100644
index 70b8ce9..0000000
--- a/v17/leanback/jbmr2/android/support/v17/leanback/os/TraceHelperJbmr2.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * 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.v17.leanback.os;
-
-import android.os.Trace;
-
-class TraceHelperJbmr2 {
-
- public static void beginSection(String section) {
- Trace.beginSection(section);
- }
-
- public static void endSection() {
- Trace.endSection();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
index 76cfd64..f63371b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
@@ -35,7 +35,6 @@
import android.support.annotation.ColorInt;
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.BackgroundHelper;
-import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.util.Log;
@@ -589,9 +588,6 @@
* for this Activity.
*/
public static BackgroundManager getInstance(Activity activity) {
- if (activity instanceof FragmentActivity) {
- return getSupportInstance((FragmentActivity) activity);
- }
BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
.findFragmentByTag(FRAGMENT_TAG);
if (fragment != null) {
@@ -602,24 +598,10 @@
// manager is null: this is a fragment restored by FragmentManager,
// fall through to create a BackgroundManager attach to it.
}
- return new BackgroundManager(activity, false);
+ return new BackgroundManager(activity);
}
- private static BackgroundManager getSupportInstance(FragmentActivity activity) {
- BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity
- .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
- if (fragment != null) {
- BackgroundManager manager = fragment.getBackgroundManager();
- if (manager != null) {
- return manager;
- }
- // manager is null: this is a fragment restored by FragmentManager,
- // fall through to create a BackgroundManager attach to it.
- }
- return new BackgroundManager(activity, true);
- }
-
- private BackgroundManager(Activity activity, boolean isSupportFragmentActivity) {
+ private BackgroundManager(Activity activity) {
mContext = activity;
mService = BackgroundContinuityService.getInstance();
mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
@@ -648,11 +630,7 @@
}
ta.recycle();
- if (isSupportFragmentActivity) {
- createSupportFragment((FragmentActivity) activity);
- } else {
- createFragment(activity);
- }
+ createFragment(activity);
}
private void createFragment(Activity activity) {
@@ -672,24 +650,6 @@
mFragmentState = fragment;
}
- private void createSupportFragment(FragmentActivity activity) {
- // Use a fragment to ensure the background manager gets detached properly.
- BackgroundSupportFragment fragment = (BackgroundSupportFragment) activity
- .getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
- if (fragment == null) {
- fragment = new BackgroundSupportFragment();
- activity.getSupportFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG)
- .commit();
- } else {
- if (fragment.getBackgroundManager() != null) {
- throw new IllegalStateException("Created duplicated BackgroundManager for same "
- + "activity, please use getInstance() instead");
- }
- }
- fragment.setBackgroundManager(this);
- mFragmentState = fragment;
- }
-
DrawableWrapper getImageInWrapper() {
return mLayerDrawable == null
? null : mLayerDrawable.findWrapperById(R.id.background_imagein);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundSupportFragment.java
deleted file mode 100644
index aef9678..0000000
--- a/v17/leanback/src/android/support/v17/leanback/app/BackgroundSupportFragment.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* This file is auto-generated from BackgroundFragment.java. DO NOT MODIFY. */
-
-/*
- * 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.v17.leanback.app;
-
-import android.support.v4.app.Fragment;
-import android.support.annotation.RestrictTo;
-
-import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
-
-/**
- * Fragment used by the background manager.
- * @hide
- */
-@RestrictTo(GROUP_ID)
-public final class BackgroundSupportFragment extends Fragment implements
- BackgroundManager.FragmentStateQueriable {
- private BackgroundManager mBackgroundManager;
-
- void setBackgroundManager(BackgroundManager backgroundManager) {
- mBackgroundManager = backgroundManager;
- }
-
- BackgroundManager getBackgroundManager() {
- return mBackgroundManager;
- }
-
- @Override
- public void onStart() {
- super.onStart();
- // mBackgroundManager might be null:
- // if BackgroundSupportFragment is just restored by FragmentManager,
- // and user does not call BackgroundManager.getInstance() yet.
- if (mBackgroundManager != null) {
- mBackgroundManager.onActivityStart();
- }
- }
-
- @Override
- public void onResume() {
- super.onResume();
- // mBackgroundManager might be null:
- // if BackgroundSupportFragment is just restored by FragmentManager,
- // and user does not call BackgroundManager.getInstance() yet.
- if (mBackgroundManager != null) {
- mBackgroundManager.onResume();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- // mBackgroundManager might be null:
- // if BackgroundSupportFragment is just restored by FragmentManager,
- // and user does not call BackgroundManager.getInstance() yet.
- if (mBackgroundManager != null) {
- mBackgroundManager.detach();
- }
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/os/TraceHelper.java b/v17/leanback/src/android/support/v17/leanback/os/TraceHelper.java
deleted file mode 100644
index 4f1cd33..0000000
--- a/v17/leanback/src/android/support/v17/leanback/os/TraceHelper.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.v17.leanback.os;
-
-import android.os.Build;
-import android.support.annotation.RestrictTo;
-import android.support.v17.leanback.os.TraceHelperJbmr2;
-
-import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
-
-
-/**
- * Helper for systrace events.
- * @hide
- */
-@RestrictTo(GROUP_ID)
-public final class TraceHelper {
-
- final static TraceHelperVersionImpl sImpl;
-
- interface TraceHelperVersionImpl {
- void beginSection(String section);
- void endSection();
- }
-
- private static final class TraceHelperStubImpl implements TraceHelperVersionImpl {
- TraceHelperStubImpl() {
- }
-
- @Override
- public void beginSection(String section) {
- }
-
- @Override
- public void endSection() {
- }
- }
-
- private static final class TraceHelperJbmr2Impl implements TraceHelperVersionImpl {
- TraceHelperJbmr2Impl() {
- }
-
- @Override
- public void beginSection(String section) {
- TraceHelperJbmr2.beginSection(section);
- }
-
- @Override
- public void endSection() {
- TraceHelperJbmr2.endSection();
- }
- }
-
- private TraceHelper() {
- }
-
- static {
- if (Build.VERSION.SDK_INT >= 18) {
- sImpl = new TraceHelperJbmr2Impl();
- } else {
- sImpl = new TraceHelperStubImpl();
- }
- }
-
- public static void beginSection(String section) {
- sImpl.beginSection(section);
- }
-
- public static void endSection() {
- sImpl.endSection();
- }
-}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index a52555b..1414d4a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -24,7 +24,7 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.support.v17.leanback.os.TraceHelper;
+import android.support.v4.os.TraceCompat;
import android.support.v4.util.CircularIntArray;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
@@ -903,7 +903,7 @@
return;
}
- if (TRACE) TraceHelper.beginSection("onChildSelected");
+ if (TRACE) TraceCompat.beginSection("onChildSelected");
View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
if (view != null) {
RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
@@ -918,7 +918,7 @@
}
fireOnChildViewHolderSelected(mBaseGridView, null, NO_POSITION, 0);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
// Children may request layout when a child selection event occurs (such as a change of
// padding on the current and previously selected rows).
@@ -945,7 +945,7 @@
return;
}
- if (TRACE) TraceHelper.beginSection("onChildSelectedAndPositioned");
+ if (TRACE) TraceCompat.beginSection("onChildSelectedAndPositioned");
View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
if (view != null) {
RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
@@ -957,7 +957,7 @@
}
fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, null, NO_POSITION, 0);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
@@ -1147,9 +1147,6 @@
mGrid.setSpacing(mSpacingPrimary);
detachAndScrapAttachedViews(mRecycler);
mGrid.resetVisibleIndex();
- if (mFocusPosition == NO_POSITION) {
- mBaseGridView.clearFocus();
- }
mWindowAlignment.mainAxis().invalidateScrollMin();
mWindowAlignment.mainAxis().invalidateScrollMax();
if (focusViewWasInTree && firstVisibleIndex <= mFocusPosition) {
@@ -1231,7 +1228,7 @@
return false;
}
- if (TRACE) TraceHelper.beginSection("processRowSizeSecondary");
+ if (TRACE) TraceCompat.beginSection("processRowSizeSecondary");
CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
boolean changed = false;
int scrapChildWidth = -1;
@@ -1299,7 +1296,7 @@
}
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
return changed;
}
@@ -1441,7 +1438,7 @@
}
void measureChild(View child) {
- if (TRACE) TraceHelper.beginSection("measureChild");
+ if (TRACE) TraceCompat.beginSection("measureChild");
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
calculateItemDecorationsForChild(child, sTempRect);
int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right;
@@ -1471,7 +1468,7 @@
+ " measuredHeight " + child.getMeasuredHeight());
}
if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
/**
@@ -1500,22 +1497,22 @@
@Override
public int createItem(int index, boolean append, Object[] item) {
- if (TRACE) TraceHelper.beginSection("createItem");
- if (TRACE) TraceHelper.beginSection("getview");
+ if (TRACE) TraceCompat.beginSection("createItem");
+ if (TRACE) TraceCompat.beginSection("getview");
View v = getViewForPosition(index);
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
LayoutParams lp = (LayoutParams) v.getLayoutParams();
RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
lp.setItemAlignmentFacet((ItemAlignmentFacet)getFacet(vh, ItemAlignmentFacet.class));
// See recyclerView docs: we don't need re-add scraped view if it was removed.
if (!lp.isItemRemoved()) {
- if (TRACE) TraceHelper.beginSection("addView");
+ if (TRACE) TraceCompat.beginSection("addView");
if (append) {
addView(v);
} else {
addView(v, 0);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
if (mChildVisibility != -1) {
v.setVisibility(mChildVisibility);
}
@@ -1582,7 +1579,7 @@
if (DEBUG) {
Log.d(getTag(), "addView " + index + " " + v);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
if (index == mGrid.getFirstVisibleIndex()) {
if (!mGrid.isReversedFlow()) {
@@ -1610,14 +1607,14 @@
@Override
public void removeItem(int index) {
- if (TRACE) TraceHelper.beginSection("removeItem");
+ if (TRACE) TraceCompat.beginSection("removeItem");
View v = findViewByPosition(index);
if (mInLayout) {
detachAndScrapView(v, mRecycler);
} else {
removeAndRecycleView(v, mRecycler);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
@Override
@@ -1636,7 +1633,7 @@
};
void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
- if (TRACE) TraceHelper.beginSection("layoutChild");
+ if (TRACE) TraceCompat.beginSection("layoutChild");
int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v)
: getDecoratedMeasuredWidthWithMargin(v);
if (mFixedRowSizeSecondary > 0) {
@@ -1678,7 +1675,7 @@
params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top,
sTempRect.right - right, sTempRect.bottom - bottom);
updateChildAlignments(v);
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
private void updateChildAlignments(View v) {
@@ -1824,12 +1821,12 @@
@Override
public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
- if (TRACE) TraceHelper.beginSection("removeAndRecycleAllViews");
+ if (TRACE) TraceCompat.beginSection("removeAndRecycleAllViews");
if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
for (int i = getChildCount() - 1; i >= 0; i--) {
removeAndRecycleViewAt(i, recycler);
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
// called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable
@@ -2061,7 +2058,7 @@
// scroll in main direction may add/prune views
private int scrollDirectionPrimary(int da) {
- if (TRACE) TraceHelper.beginSection("scrollPrimary");
+ if (TRACE) TraceCompat.beginSection("scrollPrimary");
boolean isMaxUnknown = false, isMinUnknown = false;
int minScroll = 0, maxScroll = 0;
if (da > 0) {
@@ -2082,13 +2079,13 @@
}
}
if (da == 0) {
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
return 0;
}
offsetChildrenPrimary(-da);
mScrollOffsetPrimary += da;
if (mInLayout) {
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
return da;
}
@@ -2103,20 +2100,20 @@
updated = getChildCount() > childCount;
childCount = getChildCount();
- if (TRACE) TraceHelper.beginSection("remove");
+ if (TRACE) TraceCompat.beginSection("remove");
if (mReverseFlowPrimary ? da > 0 : da < 0) {
removeInvisibleViewsAtEnd();
} else {
removeInvisibleViewsAtFront();
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
updated |= getChildCount() < childCount;
if (updated) {
updateRowSecondarySizeRefresh();
}
mBaseGridView.invalidate();
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
return da;
}
@@ -2296,7 +2293,7 @@
void scrollToSelection(int position, int subposition,
boolean smooth, int primaryScrollExtra) {
- if (TRACE) TraceHelper.beginSection("scrollToSelection");
+ if (TRACE) TraceCompat.beginSection("scrollToSelection");
mPrimaryScrollExtra = primaryScrollExtra;
View view = findViewByPosition(position);
if (view != null) {
@@ -2322,7 +2319,7 @@
requestLayout();
}
}
- if (TRACE) TraceHelper.endSection();
+ if (TRACE) TraceCompat.endSection();
}
void startPositionSmoothScroller(int position) {
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java
index 87050c1..9225ade 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTestActivity.java
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.test.R;
+import android.view.View;
/**
* Activity containing {@link DetailsFragmentTest} used for testing.
@@ -33,6 +34,8 @@
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ final int uiOptions = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
+ getWindow().getDecorView().setSystemUiVisibility(uiOptions);
setContentView(R.layout.details);
mFragment = new DetailsTestFragment();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/ChildDrawableTest.java b/v17/leanback/tests/java/android/support/v17/leanback/graphics/ChildDrawableTest.java
index aff21ff..0b0430e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/graphics/ChildDrawableTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/graphics/ChildDrawableTest.java
@@ -21,7 +21,9 @@
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
+import android.os.Build;
import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -141,6 +143,7 @@
assertEquals(expectedBounds, adjustedBounds);
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
@Test
public void constantState() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java b/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
index 0b8b5a8..a91c221 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/graphics/FitWidthBitmapDrawableTest.java
@@ -24,6 +24,8 @@
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.os.Build;
+import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
@@ -64,6 +66,7 @@
verify(canvas).drawBitmap(eq(bitmap), eq(bitmapBounds), eq(expectedDest), any(Paint.class));
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
@Test
public void constantState() {
FitWidthBitmapDrawable drawable = new FitWidthBitmapDrawable();
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
index 8296c83..0795e16 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -27,9 +27,11 @@
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
+import android.os.Build;
import android.os.Parcelable;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
+import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.support.v17.leanback.test.R;
@@ -116,6 +118,15 @@
Thread.sleep(500);
}
+ static String dumpGridView(BaseGridView gridView) {
+ return "findFocus:" + gridView.getRootView().findFocus()
+ + " isLayoutRequested:" + gridView.isLayoutRequested()
+ + " selectedPosition:" + gridView.getSelectedPosition()
+ + " adapter.itemCount:" + gridView.getAdapter().getItemCount()
+ + " itemAnimator.isRunning:" + gridView.getItemAnimator().isRunning()
+ + " scrollState:" + gridView.getScrollState();
+ }
+
/**
* Change selected position.
*/
@@ -210,6 +221,17 @@
protected void scrollToBegin(Runnable verify) throws Throwable {
int key;
+ // first move to first column/row
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ key = KeyEvent.KEYCODE_DPAD_UP;
+ } else {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ }
+ scroll(key, null);
if (mOrientation == BaseGridView.HORIZONTAL) {
if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
key = KeyEvent.KEYCODE_DPAD_RIGHT;
@@ -224,6 +246,17 @@
protected void scrollToEnd(Runnable verify) throws Throwable {
int key;
+ // first move to first column/row
+ if (mOrientation == BaseGridView.HORIZONTAL) {
+ key = KeyEvent.KEYCODE_DPAD_UP;
+ } else {
+ if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
+ key = KeyEvent.KEYCODE_DPAD_RIGHT;
+ } else {
+ key = KeyEvent.KEYCODE_DPAD_LEFT;
+ }
+ }
+ scroll(key, null);
if (mOrientation == BaseGridView.HORIZONTAL) {
if (mGridView.getLayoutDirection() == ViewGroup.LAYOUT_DIRECTION_RTL) {
key = KeyEvent.KEYCODE_DPAD_LEFT;
@@ -500,6 +533,7 @@
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
@Test
public void testItemDecorationAndMarginsAndOpticalBounds() throws Throwable {
final int leftMargin = 3;
@@ -529,7 +563,8 @@
final int decorationBottom = 2;
final Rect opticalPaddings = new Rect();
- mGridView.getContext().getDrawable(ninePatchDrawableResourceId).getPadding(opticalPaddings);
+ mGridView.getResources().getDrawable(ninePatchDrawableResourceId)
+ .getPadding(opticalPaddings);
final int opticalInsetsLeft = opticalPaddings.left;
final int opticalInsetsTop = opticalPaddings.top;
final int opticalInsetsRight = opticalPaddings.right;
@@ -1031,7 +1066,8 @@
}
});
- final int removeIndex = mGridView.getChildCount() - 1;
+ final int removeIndex = mGridView.getChildViewHolder(
+ mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
mActivityTestRule.runOnUiThread(new Runnable() {
public void run() {
mActivity.removeItems(removeIndex, 1);
@@ -1081,10 +1117,10 @@
for (int i = 0; i < 20; i++) {
sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
}
- Thread.sleep(100);
assertTrue(mGridView.getLayoutManager().isSmoothScrolling());
- final int removeIndex = mGridView.getChildCount() - 1;
+ final int removeIndex = mGridView.getChildViewHolder(
+ mGridView.getChildAt(mGridView.getChildCount() - 1)).getAdapterPosition();
mActivityTestRule.runOnUiThread(new Runnable() {
public void run() {
mActivity.removeItems(removeIndex, 1);
@@ -1098,7 +1134,7 @@
waitForTransientStateGone(null);
waitForScrollIdle();
int focusIndex = mGridView.getSelectedPosition();
- int leftEdge = mGridView.getLayoutManager().findViewByPosition(focusIndex).getLeft();
+ int topEdge = mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop();
mActivityTestRule.runOnUiThread(new Runnable() {
public void run() {
@@ -1107,8 +1143,8 @@
});
waitForTransientStateGone(null);
waitForScrollIdle();
- assertEquals(leftEdge,
- mGridView.getLayoutManager().findViewByPosition(focusIndex).getLeft());
+ assertEquals(topEdge,
+ mGridView.getLayoutManager().findViewByPosition(focusIndex).getTop());
}
@Test
@@ -1305,6 +1341,88 @@
}
@Test
+ public void removeFocusableItemAndFocusableRecyclerViewGetsFocus() throws Throwable {
+ final int numItems = 100;
+ final int numColumns = 3;
+ final int focusableIndex = 2;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = false;
+ }
+ focusable[focusableIndex] = true;
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(focusableIndex);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(focusableIndex, mGridView.getSelectedPosition());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusableIndex, 1);
+ }
+ });
+ waitForTransientStateGone(null);
+ assertTrue(dumpGridView(mGridView), mGridView.isFocused());
+ }
+
+ @Test
+ public void removeFocusableItemAndUnFocusableRecyclerViewLosesFocus() throws Throwable {
+ final int numItems = 100;
+ final int numColumns = 3;
+ final int focusableIndex = 2;
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, numItems);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = numColumns;
+ boolean[] focusable = new boolean[numItems];
+ for (int i = 0; i < focusable.length; i++) {
+ focusable[i] = false;
+ }
+ focusable[focusableIndex] = true;
+ intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
+ initActivity(intent);
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setFocusableInTouchMode(false);
+ mGridView.setFocusable(false);
+ mGridView.setSelectedPositionSmooth(focusableIndex);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ assertEquals(focusableIndex, mGridView.getSelectedPosition());
+
+ mActivityTestRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.removeItems(focusableIndex, 1);
+ }
+ });
+ waitForTransientStateGone(null);
+ assertFalse(dumpGridView(mGridView), mGridView.hasFocus());
+ }
+
+ @Test
public void testNonFocusableVertical() throws Throwable {
final int numItems = 200;
final int startPos = 44;
@@ -1428,7 +1546,7 @@
public void testTransferFocusable2() throws Throwable {
final int numItems = 200;
final int numColumns = 3;
- final int startPos = 10;
+ final int startPos = 3; // make sure view at startPos is in visible area.
Intent intent = new Intent();
intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
@@ -1447,6 +1565,8 @@
intent.putExtra(GridActivity.EXTRA_ITEMS_FOCUSABLE, focusable);
initActivity(intent);
+ assertTrue(mGridView.getLayoutManager().findViewByPosition(startPos).hasFocus());
+
changeArraySize(0);
assertTrue(mGridView.isFocused());
@@ -1904,24 +2024,11 @@
for (int i = 0; i < 20; i++) {
sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
}
- Thread.sleep(100);
- int total = 0;
while (mGridView.getLayoutManager().isSmoothScrolling()
|| mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
- if ((total += 10) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
- throw new RuntimeException("waitForScrollIdle Timeout");
- }
- try {
- // Repeatedly pressing to make sure pending keys does not drop to zero.
- Thread.sleep(10);
- sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
- } catch (InterruptedException ex) {
- break;
- }
+ // Repeatedly pressing to make sure pending keys does not drop to zero.
+ sendKey(KeyEvent.KEYCODE_DPAD_DOWN);
}
-
- assertTrue("LinearSmoothScroller would not use many RV.smoothScrollBy() calls",
- ((VerticalGridViewEx) mGridView).mSmoothScrollByCalled < 10);
}
@Test
diff --git a/v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml b/v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
new file mode 100644
index 0000000..c121d77
--- /dev/null
+++ b/v17/preference-leanback/res/layout/leanback_preference_widget_seekbar.xml
@@ -0,0 +1,100 @@
+<?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.
+-->
+
+<!-- Leanback-styled Layout used for a SeekBar preference -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingStart="@dimen/lb_preference_item_padding_start"
+ android:paddingEnd="@dimen/lb_preference_item_padding_end"
+ android:clickable="true"
+ android:focusable="true"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <ImageView
+ android:id="@+android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:minWidth="@dimen/preference_icon_minWidth"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <TextView android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:layout_marginBottom="@dimen/lb_preference_item_primary_text_margin_bottom"
+ android:fontFamily="sans-serif-condensed"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:textColor="@color/lb_preference_item_primary_text_color"
+ android:textSize="@dimen/lb_preference_item_primary_text_size"/>
+
+ <TextView android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:fontFamily="sans-serif-condensed"
+ android:textColor="@color/lb_preference_item_secondary_text_color"
+ android:textSize="@dimen/lb_preference_item_secondary_text_size"
+ android:maxLines="4" />
+
+ <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
+ to the children of this container layout. Otherwise, the animated pressed state will also
+ play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
+ The background of the SeekBar is also set to null to disable the ripple background -->
+ <android.support.v7.preference.UnPressableLinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/summary"
+ android:layout_alignStart="@android:id/title"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ <SeekBar
+ android:id="@+id/seekbar"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/lb_preference_seekbar_padding_start"
+ android:paddingEnd="@dimen/lb_preference_seekbar_padding_end"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" />
+
+ <TextView android:id="@+id/seekbar_value"
+ android:layout_width="@dimen/lb_preference_seekbar_value_width"
+ android:layout_height="match_parent"
+ android:gravity="right|center_vertical"
+ android:fontFamily="sans-serif-condensed"
+ android:textColor="@color/lb_preference_item_primary_text_color"
+ android:textSize="@dimen/lb_preference_item_primary_text_size"/>
+ </android.support.v7.preference.UnPressableLinearLayout>
+
+ </RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/v17/preference-leanback/res/values/dimens.xml b/v17/preference-leanback/res/values/dimens.xml
index f3d36af..ed662a1 100644
--- a/v17/preference-leanback/res/values/dimens.xml
+++ b/v17/preference-leanback/res/values/dimens.xml
@@ -38,4 +38,7 @@
<dimen name="lb_preference_category_height">40dp</dimen>
<dimen name="lb_settings_pane_width">360dp</dimen>
+ <dimen name="lb_preference_seekbar_padding_start">0dp</dimen>
+ <dimen name="lb_preference_seekbar_padding_end">22dp</dimen>
+ <dimen name="lb_preference_seekbar_value_width">36dp</dimen>
</resources>
diff --git a/v17/preference-leanback/res/values/styles.xml b/v17/preference-leanback/res/values/styles.xml
index fbb1ad0..cb12b3a 100644
--- a/v17/preference-leanback/res/values/styles.xml
+++ b/v17/preference-leanback/res/values/styles.xml
@@ -50,6 +50,12 @@
<item name="android:switchTextOff">@string/v7_preference_off</item>
</style>
+ <style name="LeanbackPreference.SeekBarPreference">
+ <item name="android:layout">@layout/leanback_preference_widget_seekbar</item>
+ <item name="adjustable">true</item>
+ <item name="showSeekBarValue">true</item>
+ </style>
+
<style name="LeanbackPreference.PreferenceScreen">
</style>
diff --git a/v17/preference-leanback/res/values/themes.xml b/v17/preference-leanback/res/values/themes.xml
index 591fdfa..d173b30 100644
--- a/v17/preference-leanback/res/values/themes.xml
+++ b/v17/preference-leanback/res/values/themes.xml
@@ -25,6 +25,7 @@
<item name="preferenceInformationStyle">@style/LeanbackPreference.Information</item>
<item name="checkBoxPreferenceStyle">@style/LeanbackPreference.CheckBoxPreference</item>
<item name="switchPreferenceCompatStyle">@style/LeanbackPreference.SwitchPreferenceCompat</item>
+ <item name="seekBarPreferenceStyle">@style/LeanbackPreference.SeekBarPreference</item>
<item name="switchPreferenceStyle">@style/LeanbackPreference.SwitchPreference</item>
<item name="dialogPreferenceStyle">@style/LeanbackPreference.DialogPreference</item>
<item name="editTextPreferenceStyle">@style/LeanbackPreference.DialogPreference.EditTextPreference</item>
diff --git a/v7/preference/res/layout/preference_widget_seekbar.xml b/v7/preference/res/layout/preference_widget_seekbar.xml
new file mode 100644
index 0000000..30bc5ff
--- /dev/null
+++ b/v7/preference/res/layout/preference_widget_seekbar.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.
+-->
+
+<!-- Layout used by SeekBarPreference for the seekbar widget style. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:gravity="center_vertical"
+ android:paddingEnd="?android:attr/scrollbarSize"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <ImageView
+ android:id="@+android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:minWidth="@dimen/preference_icon_minWidth"/>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dip"
+ android:layout_marginEnd="8dip"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip"
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+ <TextView android:id="@+android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"/>
+
+ <TextView android:id="@+android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="4"/>
+
+ <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
+ to the children of this container layout. Otherwise, the animated pressed state will also
+ play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
+ The background of the SeekBar is also set to null to disable the ripple background -->
+ <android.support.v7.preference.UnPressableLinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/summary"
+ android:layout_alignStart="@android:id/title"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+ <SeekBar
+ android:id="@+id/seekbar"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/preference_seekbar_padding_start"
+ android:paddingEnd="@dimen/preference_seekbar_padding_end"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" />
+
+ <TextView android:id="@+id/seekbar_value"
+ android:layout_width="@dimen/preference_seekbar_value_width"
+ android:layout_height="match_parent"
+ android:gravity="right|center_vertical"
+ android:fontFamily="sans-serif-condensed"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"/>
+ </android.support.v7.preference.UnPressableLinearLayout>
+
+ </RelativeLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/v7/preference/res/values/attrs.xml b/v7/preference/res/values/attrs.xml
index bd64109..baf2a03 100644
--- a/v7/preference/res/values/attrs.xml
+++ b/v7/preference/res/values/attrs.xml
@@ -254,4 +254,20 @@
<attr name="selectableItemBackground" />
</declare-styleable>
+ <declare-styleable name="SeekBarPreference">
+ <attr name="min" format="integer"/>
+ <attr name="android:max"/>
+ <attr name="android:layout" />
+ <!-- Attribute indicating whether the slider within this preference can be adjusted, that is
+ pressing left/right keys when this preference is focused will move the slider accordingly
+ (e.g. inline adjustable preferences). False, if the slider within the preference is
+ read-only and cannot be adjusted. By default, the seekbar is adjustable. -->
+ <attr name="adjustable" format="boolean" />
+ <!-- Flag indicating whether the TextView next to the seekbar that shows the current seekbar
+ value will be displayed. If true, the view is VISIBLE; if false, the view will be GONE.
+ By default, this view is VISIBLE. -->
+ <attr name="showSeekBarValue" format="boolean" />
+ </declare-styleable>
+
+
</resources>
diff --git a/v7/preference/res/values/dimens.xml b/v7/preference/res/values/dimens.xml
new file mode 100644
index 0000000..4816e36
--- /dev/null
+++ b/v7/preference/res/values/dimens.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Minimum space to allocate to the left of a preference item for an icon.
+ This helps in aligning titles when some items have icons and some don't. When space is
+ at a premium, we don't pre-allocate any space. -->
+ <dimen name="preference_icon_minWidth">0dp</dimen>
+ <!-- The padding to the left of the seekbar view within a SeekBarPreference -->
+ <dimen name="preference_seekbar_padding_start">0dp</dimen>
+ <!-- The padding to the right of the seekbar view within a SeekBarPreference -->
+ <dimen name="preference_seekbar_padding_end">22dp</dimen>
+ <!-- The width of the TextView indicating the current value of the SeekBarPreference -->
+ <dimen name="preference_seekbar_value_width">36dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/v7/preference/res/values/styles.xml b/v7/preference/res/values/styles.xml
index 06b24fa..e61a361 100644
--- a/v7/preference/res/values/styles.xml
+++ b/v7/preference/res/values/styles.xml
@@ -49,6 +49,12 @@
<item name="android:switchTextOff">@string/v7_preference_off</item>
</style>
+ <style name="Preference.SeekBarPreference">
+ <item name="android:layout">@layout/preference_widget_seekbar</item>
+ <item name="adjustable">true</item>
+ <item name="showSeekBarValue">true</item>
+ </style>
+
<style name="Preference.PreferenceScreen">
</style>
diff --git a/v7/preference/res/values/themes.xml b/v7/preference/res/values/themes.xml
index bb7f496..af5f469 100644
--- a/v7/preference/res/values/themes.xml
+++ b/v7/preference/res/values/themes.xml
@@ -24,6 +24,7 @@
<item name="preferenceInformationStyle">@style/Preference.Information</item>
<item name="checkBoxPreferenceStyle">@style/Preference.CheckBoxPreference</item>
<item name="switchPreferenceCompatStyle">@style/Preference.SwitchPreferenceCompat</item>
+ <item name="seekBarPreferenceStyle">@style/Preference.SeekBarPreference</item>
<item name="dialogPreferenceStyle">@style/Preference.DialogPreference</item>
<item name="editTextPreferenceStyle">@style/Preference.DialogPreference.EditTextPreference</item>
<item name="preferenceFragmentListStyle">@style/PreferenceFragmentList</item>
diff --git a/v7/preference/src/android/support/v7/preference/SeekBarPreference.java b/v7/preference/src/android/support/v7/preference/SeekBarPreference.java
new file mode 100644
index 0000000..6bfb523
--- /dev/null
+++ b/v7/preference/src/android/support/v7/preference/SeekBarPreference.java
@@ -0,0 +1,339 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+/**
+ * Preference based on android.preference.SeekBarPreference but uses support v7 preference as base.
+ * It contains a title and a seekbar and an optional seekbar value TextView. The actual preference
+ * layout is customizable by setting {@code android:layout} on the preference widget layout or
+ * {@code seekBarPreferenceStyle} attribute.
+ * The seekbar within the preference can be defined adjustable or not by setting {@code
+ * adjustable} attribute. If adjustable, the preference will be responsive to DPAD left/right keys.
+ * Otherwise, it skips those keys.
+ * The seekbar value view can be shown or disabled by setting {@code showSeekBarValue} attribute
+ * to true or false, respectively.
+ * Other SeekBar specific attributes (e.g. {@code title, summary, defaultValue, min, max}) can be
+ * set directly on the preference widget layout.
+ */
+public class SeekBarPreference extends Preference {
+
+ private int mSeekBarValue;
+ private int mMin;
+ private int mMax;
+ private boolean mTrackingTouch;
+ private SeekBar mSeekBar;
+ private TextView mSeekBarValueTextView;
+ private boolean mAdjustable; // whether the seekbar should respond to the left/right keys
+ private boolean mShowSeekBarValue; // whether to show the seekbar value TextView next to the bar
+
+ private static final String TAG = "SeekBarPreference";
+
+ /**
+ * Listener reacting to the SeekBar changing value by the user
+ */
+ private OnSeekBarChangeListener mSeekBarChangeListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser && !mTrackingTouch) {
+ syncValueInternal(seekBar);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ mTrackingTouch = true;
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ mTrackingTouch = false;
+ if (seekBar.getProgress() + mMin != mSeekBarValue) {
+ syncValueInternal(seekBar);
+ }
+ }
+ };
+
+ /**
+ * Listener reacting to the user pressing DPAD left/right keys if {@code
+ * adjustable} attribute is set to true; it transfers the key presses to the SeekBar
+ * to be handled accordingly.
+ */
+ private View.OnKeyListener mSeekBarKeyListener = new View.OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) {
+ return false;
+ }
+ if (!mAdjustable && (keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_RIGHT)) {
+ // Right or left keys are pressed when in non-adjustable mode; Skip the keys.
+ return false;
+ }
+
+ // We don't want to propagate the click keys down to the seekbar view since it will create
+ // the ripple effect for the thumb.
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
+ return false;
+ }
+
+ if (mSeekBar == null) {
+ Log.e(TAG, "SeekBar view is null and hence cannot be adjusted.");
+ return false;
+ }
+ return mSeekBar.onKeyDown(keyCode, event);
+ }
+ };
+
+ public SeekBarPreference(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.SeekBarPreference, defStyleAttr, defStyleRes);
+
+ /**
+ * The ordering of these two statements are important. If we want to set max first, we need
+ * to perform the same steps by changing min/max to max/min as following:
+ * mMax = a.getInt(...) and setMin(...).
+ */
+ mMin = a.getInt(R.styleable.SeekBarPreference_min, 0);
+ setMax(a.getInt(R.styleable.SeekBarPreference_android_max, 100));
+ mAdjustable = a.getBoolean(R.styleable.SeekBarPreference_adjustable, true);
+ mShowSeekBarValue = a.getBoolean(R.styleable.SeekBarPreference_showSeekBarValue, true);
+ a.recycle();
+ }
+
+ public SeekBarPreference(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SeekBarPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.seekBarPreferenceStyle);
+ }
+
+ public SeekBarPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder view) {
+ super.onBindViewHolder(view);
+ view.itemView.setOnKeyListener(mSeekBarKeyListener);
+ mSeekBar = (SeekBar) view.findViewById(R.id.seekbar);
+ mSeekBarValueTextView = (TextView) view.findViewById(R.id.seekbar_value);
+ if (mShowSeekBarValue) {
+ mSeekBarValueTextView.setVisibility(View.VISIBLE);
+ } else {
+ mSeekBarValueTextView.setVisibility(View.GONE);
+ mSeekBarValueTextView = null;
+ }
+
+ if (mSeekBar == null) {
+ Log.e(TAG, "SeekBar view is null in onBindViewHolder.");
+ return;
+ }
+ mSeekBar.setOnSeekBarChangeListener(mSeekBarChangeListener);
+ mSeekBar.setMax(mMax - mMin);
+ mSeekBar.setProgress(mSeekBarValue - mMin);
+ if (mSeekBarValueTextView != null) {
+ mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
+ }
+ mSeekBar.setEnabled(isEnabled());
+ }
+
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValue(restoreValue ? getPersistedInt(mSeekBarValue)
+ : (Integer) defaultValue);
+ }
+
+ @Override
+ protected Object onGetDefaultValue(TypedArray a, int index) {
+ return a.getInt(index, 0);
+ }
+
+ public void setMin(int min) {
+ if (min > mMax) {
+ min = mMax;
+ }
+ if (min != mMin) {
+ mMin = min;
+ notifyChanged();
+ }
+ }
+
+ public int getMin() {
+ return mMin;
+ }
+
+ public void setMax(int max) {
+ if (max < mMin) {
+ max = mMin;
+ }
+ if (max != mMax) {
+ mMax = max;
+ notifyChanged();
+ }
+ }
+
+ public int getMax() {
+ return mMax;
+ }
+
+ public void setAdjustable(boolean adjustable) {
+ mAdjustable = adjustable;
+ }
+
+ public boolean isAdjustable() {
+ return mAdjustable;
+ }
+
+ public void setValue(int seekBarValue) {
+ setValueInternal(seekBarValue, true);
+ }
+
+ private void setValueInternal(int seekBarValue, boolean notifyChanged) {
+ if (seekBarValue < mMin) {
+ seekBarValue = mMin;
+ }
+ if (seekBarValue > mMax) {
+ seekBarValue = mMax;
+ }
+
+ if (seekBarValue != mSeekBarValue) {
+ mSeekBarValue = seekBarValue;
+ if (mSeekBarValueTextView != null) {
+ mSeekBarValueTextView.setText(String.valueOf(mSeekBarValue));
+ }
+ persistInt(seekBarValue);
+ if (notifyChanged) {
+ notifyChanged();
+ }
+ }
+ }
+
+ public int getValue() {
+ return mSeekBarValue;
+ }
+
+ /**
+ * Persist the seekBar's seekbar value if callChangeListener
+ * returns true, otherwise set the seekBar's value to the stored value
+ */
+ private void syncValueInternal(SeekBar seekBar) {
+ int seekBarValue = mMin + seekBar.getProgress();
+ if (seekBarValue != mSeekBarValue) {
+ if (callChangeListener(seekBarValue)) {
+ setValueInternal(seekBarValue, false);
+ } else {
+ seekBar.setProgress(mSeekBarValue - mMin);
+ }
+ }
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ if (isPersistent()) {
+ // No need to save instance state since it's persistent
+ return superState;
+ }
+
+ // Save the instance state
+ final SavedState myState = new SavedState(superState);
+ myState.seekBarValue = mSeekBarValue;
+ myState.min = mMin;
+ myState.max = mMax;
+ return myState;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (!state.getClass().equals(SavedState.class)) {
+ // Didn't save state for us in onSaveInstanceState
+ super.onRestoreInstanceState(state);
+ return;
+ }
+
+ // Restore the instance state
+ SavedState myState = (SavedState) state;
+ super.onRestoreInstanceState(myState.getSuperState());
+ mSeekBarValue = myState.seekBarValue;
+ mMin = myState.min;
+ mMax = myState.max;
+ notifyChanged();
+ }
+
+ /**
+ * SavedState, a subclass of {@link BaseSavedState}, will store the state
+ * of MyPreference, a subclass of Preference.
+ * <p>
+ * It is important to always call through to super methods.
+ */
+ private static class SavedState extends BaseSavedState {
+ int seekBarValue;
+ int min;
+ int max;
+
+ public SavedState(Parcel source) {
+ super(source);
+
+ // Restore the click counter
+ seekBarValue = source.readInt();
+ min = source.readInt();
+ max = source.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ // Save the click counter
+ dest.writeInt(seekBarValue);
+ dest.writeInt(min);
+ dest.writeInt(max);
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<SavedState> CREATOR =
+ new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/v7/preference/src/android/support/v7/preference/UnPressableLinearLayout.java b/v7/preference/src/android/support/v7/preference/UnPressableLinearLayout.java
new file mode 100644
index 0000000..7129cd7
--- /dev/null
+++ b/v7/preference/src/android/support/v7/preference/UnPressableLinearLayout.java
@@ -0,0 +1,43 @@
+/*
+ * 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.preference;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+import android.support.annotation.RestrictTo;
+import static android.support.annotation.RestrictTo.Scope.GROUP_ID;
+
+/**
+ * Custom LinearLayout that does not propagate the pressed state down to its children.
+ * By default, the pressed state is propagated to all the children that are not clickable
+ * or long-clickable.
+ * @hide
+ */
+@RestrictTo(GROUP_ID)
+public class UnPressableLinearLayout extends LinearLayout {
+ public UnPressableLinearLayout(Context context) {
+ this(context, null);
+ }
+
+ public UnPressableLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void dispatchSetPressed(boolean pressed) {
+ // Skip dispatching the pressed key state to the children so that they don't trigger any
+ // pressed state animation on their stateful drawables.
+ }
+}
diff --git a/v7/preference/tests/NO_DOCS b/v7/preference/tests/NO_DOCS
new file mode 100644
index 0000000..db956bc
--- /dev/null
+++ b/v7/preference/tests/NO_DOCS
@@ -0,0 +1,17 @@
+# 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.
+
+Having this file, named NO_DOCS, in a directory will prevent
+Android javadocs from being generated for java files under
+the directory. This is especially useful for test projects.
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
index cf43129..eee1995 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
@@ -24,10 +24,88 @@
class GapWorker implements Runnable {
static final ThreadLocal<GapWorker> sGapWorker = new ThreadLocal<>();
- ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
+ private ArrayList<RecyclerView> mRecyclerViews = new ArrayList<>();
long mPostTimeNs;
long mFrameIntervalNs;
+ /**
+ * Prefetch information associated with a specfic RecyclerView.
+ */
+ static class PrefetchRegistryImpl implements RecyclerView.PrefetchRegistry {
+ private int mPrefetchDx;
+ private int mPrefetchDy;
+ int[] mPrefetchArray;
+
+ int mCount;
+
+ void setPrefetchVector(int dx, int dy) {
+ mPrefetchDx = dx;
+ mPrefetchDy = dy;
+ }
+
+ void collectPrefetchPositionsFromView(RecyclerView view) {
+ mCount = 0;
+ if (mPrefetchArray != null) {
+ Arrays.fill(mPrefetchArray, -1);
+ }
+
+ final RecyclerView.LayoutManager layout = view.mLayout;
+ if (view.mAdapter != null
+ && layout != null
+ && layout.isItemPrefetchEnabled()
+ && !view.hasPendingAdapterUpdates()) {
+ layout.collectPrefetchPositions(mPrefetchDx, mPrefetchDy, view.mState, this);
+ if (mCount > layout.mPrefetchMaxCountObserved) {
+ layout.mPrefetchMaxCountObserved = mCount;
+ view.mRecycler.updateViewCacheSize();
+ }
+ }
+ }
+
+ @Override
+ public void addPosition(int layoutPosition, int pixelDistance) {
+ if (pixelDistance < 0) {
+ throw new IllegalArgumentException("Pixel distance must be non-negative");
+ }
+
+ // allocate or expand array as needed, doubling when needed
+ final int storagePosition = mCount * 2;
+ if (mPrefetchArray == null) {
+ mPrefetchArray = new int[4];
+ Arrays.fill(mPrefetchArray, -1);
+ } else if (storagePosition >= mPrefetchArray.length) {
+ final int[] oldArray = mPrefetchArray;
+ mPrefetchArray = new int[storagePosition * 2];
+ System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
+ }
+
+ // add position
+ mPrefetchArray[storagePosition] = layoutPosition;
+ mPrefetchArray[storagePosition + 1] = pixelDistance;
+
+ mCount++;
+ }
+
+ boolean lastPrefetchIncludedPosition(int position) {
+ if (mPrefetchArray != null) {
+ final int count = mCount * 2;
+ for (int i = 0; i < count; i += 2) {
+ if (mPrefetchArray[i] == position) return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called when prefetch indices are no longer valid for cache prioritization.
+ */
+ void clearPrefetchPositions() {
+ if (mPrefetchArray != null) {
+ Arrays.fill(mPrefetchArray, -1);
+ }
+ }
+ }
+
public void add(RecyclerView recyclerView) {
if (RecyclerView.DEBUG && mRecyclerViews.contains(recyclerView)) {
throw new IllegalStateException("RecyclerView already present in worker list!");
@@ -56,74 +134,35 @@
}
}
- recyclerView.mPrefetchDx = prefetchDx;
- recyclerView.mPrefetchDy = prefetchDy;
- }
-
- static boolean lastPrefetchIncludedPosition(RecyclerView recyclerView, int position) {
- if (recyclerView.mPrefetchArray != null) {
- for (int i = 0; i < recyclerView.mPrefetchArray.length; i++) {
- if (recyclerView.mPrefetchArray[i] == position) return true;
- }
- }
- return false;
- }
-
- /**
- * Called when prefetch indices are no longer valid for cache prioritization.
- */
- static void clearPrefetchPositions(RecyclerView recyclerView) {
- if (recyclerView.mPrefetchArray != null) {
- Arrays.fill(recyclerView.mPrefetchArray, -1);
- }
+ recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
static void layoutPrefetch(long deadlineNs, RecyclerView view) {
- final int prefetchCount = view.mLayout.getItemPrefetchCount();
- if (view.mAdapter == null
- || view.mLayout == null
- || !view.mLayout.isItemPrefetchEnabled()
- || prefetchCount < 1
- || view.hasPendingAdapterUpdates()) {
- // abort - no work
- return;
- }
+ final RecyclerView.Recycler recycler = view.mRecycler;
+ final PrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
- if (view.mPrefetchArray == null
- || view.mPrefetchArray.length < prefetchCount) {
- view.mPrefetchArray = new int[prefetchCount];
- }
- Arrays.fill(view.mPrefetchArray, -1);
- int viewCount = view.mLayout.gatherPrefetchIndices(
- view.mPrefetchDx, view.mPrefetchDy,
- view.mState, view.mPrefetchArray);
- layoutPrefetchImpl(deadlineNs, view.mRecycler, view.mPrefetchArray, viewCount);
- }
+ prefetchRegistry.collectPrefetchPositionsFromView(view);
- static void layoutPrefetchImpl(long deadlineNs, RecyclerView.Recycler recycler,
- int[] prefetchArray, int viewCount) {
- if (viewCount == 0) return;
+ final int count = prefetchRegistry.mCount;
+ for (int i = 0; i < count; i++) {
+ int pos = prefetchRegistry.mPrefetchArray[i * 2];
+ if (pos < 0) {
+ throw new IllegalArgumentException("Invalid prefetch position requested: " + pos);
+ }
- int childPosition = prefetchArray[viewCount - 1];
- if (childPosition < 0) {
- throw new IllegalArgumentException("Invalid prefetch position requested: "
- + childPosition);
- }
- RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
- childPosition, false, deadlineNs);
- if (viewCount > 1) {
- layoutPrefetchImpl(deadlineNs, recycler, prefetchArray, viewCount - 1);
- }
- if (holder != null) {
- if (holder.isBound()) {
- // Only give the view a chance to go into the cache if binding succeeded
- recycler.recycleViewHolderInternal(holder);
- } else {
- // Didn't bind, so we can't cache the view, but it will stay in the pool until next
- // prefetch/traversal. If a View fails to bind, it means we didn't have enough time
- // prior to the deadline (and won't for other instances of this type, during this
- // GapWorker pass).
- recycler.addViewHolderToRecycledViewPool(holder);
+ RecyclerView.ViewHolder holder = recycler.tryGetViewHolderForPositionByDeadline(
+ pos, false, deadlineNs);
+ if (holder != null) {
+ if (holder.isBound()) {
+ // Only give the view a chance to go into the cache if binding succeeded
+ recycler.recycleViewHolderInternal(holder);
+ } else {
+ // Didn't bind, so we can't cache the view, but it will stay in the pool until
+ // next prefetch/traversal. If a View fails to bind, it means we didn't have
+ // enough time prior to the deadline (and won't for other instances of this
+ // type, during this GapWorker prefetch pass).
+ recycler.addViewHolderToRecycledViewPool(holder);
+ }
}
}
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index 6365017..c4a72d3 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -505,24 +505,18 @@
}
@Override
- int getItemPrefetchCount() {
- return mSpanCount;
- }
-
- @Override
- int gatherPrefetchIndicesForLayoutState(RecyclerView.State state, LayoutState layoutState,
- int[] outIndices) {
+ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
int remainingSpan = mSpanCount;
int count = 0;
while (count < mSpanCount && layoutState.hasMore(state) && remainingSpan > 0) {
final int pos = layoutState.mCurrentPosition;
- outIndices[count] = pos;
+ prefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
final int spanSize = mSpanSizeLookup.getSpanSize(pos);
remainingSpan -= spanSize;
layoutState.mCurrentPosition += layoutState.mItemDirection;
count++;
}
- return count;
}
@Override
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index d21406c..1f4682f 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -1182,34 +1182,28 @@
&& mOrientationHelper.getEnd() == 0;
}
- @Override
- int getItemPrefetchCount() {
- return 1;
- }
-
- int gatherPrefetchIndicesForLayoutState(RecyclerView.State state, LayoutState layoutState,
- int[] outIndices) {
+ void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
final int pos = layoutState.mCurrentPosition;
if (pos >= 0 && pos < state.getItemCount()) {
- outIndices[0] = pos;
- return 1;
+ prefetchRegistry.addPosition(pos, layoutState.mScrollingOffset);
}
- return 0;
}
+ /** @hide */
@Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
// can't support this scroll, so don't bother prefetching
- return 0;
+ return;
}
-
final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(delta);
updateLayoutState(layoutDirection, absDy, true, state);
- return gatherPrefetchIndicesForLayoutState(state, mLayoutState, outIndices);
+ collectPrefetchPositionsForLayoutState(state, mLayoutState, prefetchRegistry);
}
int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index c72feec..d9a3046 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -419,11 +419,8 @@
final ViewFlinger mViewFlinger = new ViewFlinger();
GapWorker mGapWorker;
-
- // Following mPrefetchXXX fields are all owned by mGapWorker
- int mPrefetchDx;
- int mPrefetchDy;
- int[] mPrefetchArray;
+ GapWorker.PrefetchRegistryImpl mPrefetchRegistry =
+ ALLOW_THREAD_GAP_WORK ? new GapWorker.PrefetchRegistryImpl() : null;
final State mState = new State();
@@ -2791,7 +2788,7 @@
vtev)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
- if (ALLOW_THREAD_GAP_WORK) {
+ if (mGapWorker != null) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
@@ -4587,11 +4584,11 @@
if (scroller.isFinished() || !fullyConsumedAny) {
setScrollState(SCROLL_STATE_IDLE); // setting state to idle will stop this.
if (ALLOW_THREAD_GAP_WORK) {
- GapWorker.clearPrefetchPositions(RecyclerView.this);
+ mPrefetchRegistry.clearPrefetchPositions();
}
} else {
postOnAnimation();
- if (ALLOW_THREAD_GAP_WORK) {
+ if (mGapWorker != null) {
mGapWorker.postFromTraversal(RecyclerView.this, dx, dy);
}
}
@@ -5009,11 +5006,9 @@
}
void updateViewCacheSize() {
- int extraCache = 0;
- if (mLayout != null && ALLOW_THREAD_GAP_WORK) {
- extraCache = mLayout.isItemPrefetchEnabled() ? mLayout.getItemPrefetchCount() : 0;
- }
+ int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
mViewCacheMax = mRequestedCacheMax + extraCache;
+
// first, try the views that can be recycled
for (int i = mCachedViews.size() - 1;
i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
@@ -5444,7 +5439,7 @@
}
mCachedViews.clear();
if (ALLOW_THREAD_GAP_WORK) {
- GapWorker.clearPrefetchPositions(RecyclerView.this);
+ mPrefetchRegistry.clearPrefetchPositions();
}
}
@@ -5519,14 +5514,12 @@
int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
- && !GapWorker.lastPrefetchIncludedPosition(
- RecyclerView.this, holder.mPosition)) {
+ && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
- if (!GapWorker.lastPrefetchIncludedPosition(
- RecyclerView.this, cachedPos)) {
+ if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
@@ -6617,6 +6610,12 @@
private boolean mItemPrefetchEnabled = true;
/**
+ * Written by {@link GapWorker} when prefetches occur to track largest number of view ever
+ * requested by a {@link #collectPrefetchPositions(int, int, State, PrefetchRegistry)} call.
+ */
+ int mPrefetchMaxCountObserved;
+
+ /**
* 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
@@ -6918,6 +6917,7 @@
public final void setItemPrefetchEnabled(boolean enabled) {
if (enabled != mItemPrefetchEnabled) {
mItemPrefetchEnabled = enabled;
+ mPrefetchMaxCountObserved = 0;
if (mRecyclerView != null) {
mRecyclerView.mRecycler.updateViewCacheSize();
}
@@ -6936,11 +6936,28 @@
return mItemPrefetchEnabled;
}
- int getItemPrefetchCount() { return 0; }
-
- int gatherPrefetchIndices(int dx, int dy, State state, int[] outIndices) {
- return 0;
- }
+ /**
+ * Gather all positions from the LayoutManager to be prefetched.
+ *
+ * <p>If item prefetch is enabled, this method is called in between traversals to gather
+ * which positions the LayoutManager will soon need, given upcoming movement in subsequent
+ * traversals.</p>
+ *
+ * <p>The LayoutManager should call {@link PrefetchRegistry#addPosition(int, int)} for each
+ * item to be prepared, and these positions will have their ViewHolders created and bound
+ * in advance of being needed by a scroll or layout.</p>
+ *
+ * @param dx X movement component.
+ * @param dy Y movement component.
+ * @param state State of RecyclerView
+ * @param prefetchRegistry PrefetchRegistry to add prefetch entries into.
+ *
+ * @see #isItemPrefetchEnabled()
+ *
+ * @hide
+ */
+ public void collectPrefetchPositions(int dx, int dy, State state,
+ PrefetchRegistry prefetchRegistry) {}
void dispatchAttachedToWindow(RecyclerView view) {
mIsAttachedToWindow = true;
@@ -9367,6 +9384,24 @@
}
/**
+ * Interface for LayoutManagers to request items to be prefetched, based on position, with
+ * specified distance from viewport, which indicates priority.
+ *
+ * @hide
+ */
+ public interface PrefetchRegistry {
+ /**
+ * Requests an an item to be prefetched, based on position, with a specified distance,
+ * indicating priority.
+ *
+ * @param layoutPosition Position of the item to prefetch.
+ * @param pixelDistance Distance from the current viewport to the bounds of the item,
+ * must be non-negative.
+ */
+ void addPosition(int layoutPosition, int pixelDistance);
+ }
+
+ /**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
index eba10ca..7040bcc 100644
--- a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -212,6 +212,12 @@
*/
private boolean mSmoothScrollbarEnabled = true;
+ /**
+ * Temporary array used (solely in {@link #collectPrefetchPositions}) for stashing and sorting
+ * distances to views being prefetched.
+ */
+ private int[] mPrefetchDistances;
+
private final Runnable mCheckForGapsRunnable = new Runnable() {
@Override
public void run() {
@@ -2065,29 +2071,44 @@
requestLayout();
}
+ /** @hide */
@Override
- int getItemPrefetchCount() {
- return mSpanCount;
- }
-
- @Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
+ /* This method uses the simplifying assumption that the next N items (where N = span count)
+ * will be assigned, one-to-one, to spans, where ordering is based on which span extends
+ * least beyond the viewport.
+ *
+ * While this simplified model will be incorrect in some cases, it's difficult to know
+ * item heights, or whether individual items will be full span prior to construction.
+ *
+ * While this greedy estimation approach may underestimate the distance to prefetch items,
+ * it's very unlikely to overestimate them, so distances can be conservatively used to know
+ * the soonest (in terms of scroll distance) a prefetched view may come on screen.
+ */
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
// can't support this scroll, so don't bother prefetching
- return 0;
+ return;
}
prepareLayoutStateForDelta(delta, state);
- int remainingSpan = mSpanCount;
- int count = 0;
- while (count < mSpanCount && mLayoutState.hasMore(state) && remainingSpan > 0) {
- final int pos = mLayoutState.mCurrentPosition;
- outIndices[count] = pos;
- remainingSpan--;
- mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
- count++;
+
+ // build sorted list of distances to end of each span (though we don't care which is which)
+ if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) {
+ mPrefetchDistances = new int[mSpanCount];
}
- return count;
+ for (int i = 0; i < mSpanCount; i++) {
+ mPrefetchDistances[i] = mLayoutState.mItemDirection == LAYOUT_START
+ ? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine)
+ : mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine;
+ }
+ Arrays.sort(mPrefetchDistances, 0, mSpanCount);
+
+ // then assign them in order to the next N views (where N = span count)
+ for (int i = 0; i < mSpanCount && mLayoutState.hasMore(state); i++) {
+ prefetchRegistry.addPosition(mLayoutState.mCurrentPosition, mPrefetchDistances[i]);
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ }
}
void prepareLayoutStateForDelta(int delta, RecyclerView.State state) {
@@ -2104,8 +2125,7 @@
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
- final int absDt = Math.abs(delta);
- mLayoutState.mAvailable = absDt;
+ mLayoutState.mAvailable = Math.abs(delta);
}
int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
@@ -2136,12 +2156,12 @@
return totalScroll;
}
- private int getLastChildPosition() {
+ int getLastChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
}
- private int getFirstChildPosition() {
+ int getFirstChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(0));
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
index 1d05852..8d93553 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -318,9 +318,10 @@
}
@Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- return super.gatherPrefetchIndices(dx, dy, state, outIndices);
+ super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
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 c9c2e7e..9bea02c 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
@@ -635,9 +635,10 @@
}
@Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- return super.gatherPrefetchIndices(dx, dy, state, outIndices);
+ super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
}
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 9915981..ea88391 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseStaggeredGridLayoutManagerTest.java
@@ -805,9 +805,10 @@
}
@Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
if (prefetchLatch != null) prefetchLatch.countDown();
- return super.gatherPrefetchIndices(dx, dy, state, outIndices);
+ super.collectPrefetchPositions(dx, dy, state, prefetchRegistry);
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java
index ec6ba23..71f93e6 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerCachedBordersTest.java
@@ -52,7 +52,9 @@
@Test
public void gridCachedBorderstTest() throws Throwable {
RecyclerView recyclerView = setupBasic(mConfig);
- waitForFirstLayout(recyclerView);
+ mGlm.expectLayout(1);
+ setRecyclerView(recyclerView);
+ mGlm.waitForLayout(10);
final boolean vertical = mConfig.mOrientation == GridLayoutManager.VERTICAL;
final int expectedSizeSum = vertical ? recyclerView.getWidth() : recyclerView.getHeight();
final int lastVisible = mGlm.findLastVisibleItemPosition();
@@ -69,7 +71,7 @@
}
private static List<Config> cachedBordersTestConfigs() {
- ArrayList<Config> configs = new ArrayList<Config>();
+ ArrayList<Config> configs = new ArrayList<>();
final int[] spanCounts = new int[]{88, 279, 741};
final int[] spanPerItem = new int[]{11, 9, 13};
for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
index 2c5244b..73e1753 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewBasicTest.java
@@ -30,7 +30,6 @@
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
-import android.support.test.filters.SdkSuppress;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.AttributeSet;
@@ -363,19 +362,42 @@
assertSame(RecyclerView.sQuinticInterpolator, mRecyclerView.mViewFlinger.mInterpolator);
}
- @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
@Test
public void prefetchChangesCacheSize() {
+ mRecyclerView.setAdapter(new MockAdapter(20));
MockLayoutManager mlm = new MockLayoutManager() {
@Override
- int getItemPrefetchCount() {
- return 3;
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchManager) {
+ prefetchManager.addPosition(0, 0);
+ prefetchManager.addPosition(1, 0);
+ prefetchManager.addPosition(2, 0);
}
};
+
RecyclerView.Recycler recycler = mRecyclerView.mRecycler;
assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
mRecyclerView.setLayoutManager(mlm);
- assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
+ assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ // layout, so prefetches can occur
+ mRecyclerView.measure(View.MeasureSpec.EXACTLY | 100, View.MeasureSpec.EXACTLY | 100);
+ mRecyclerView.layout(0, 0, 100, 100);
+
+ // prefetch gets 3 items, so expands cache by 3
+ mRecyclerView.mPrefetchRegistry.collectPrefetchPositionsFromView(mRecyclerView);
+ assertEquals(3, mRecyclerView.mPrefetchRegistry.mCount);
+ assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
+
+ // Reset to default by removing layout
+ mRecyclerView.setLayoutManager(null);
+ assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE, recycler.mViewCacheMax);
+
+ // And restore by restoring layout
+ mRecyclerView.setLayoutManager(mlm);
+ assertEquals(RecyclerView.Recycler.DEFAULT_CACHE_SIZE + 3, recycler.mViewCacheMax);
+ }
}
static class MockLayoutManager extends RecyclerView.LayoutManager {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
index 45bcf13..8712e22 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
@@ -88,16 +89,11 @@
}
@Override
- int getItemPrefetchCount() {
- return 3;
- }
-
- @Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
- outIndices[0] = 0;
- outIndices[1] = 1;
- outIndices[2] = 2;
- return 3;
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchManager) {
+ prefetchManager.addPosition(0, 0);
+ prefetchManager.addPosition(1, 0);
+ prefetchManager.addPosition(2, 0);
}
@Override
@@ -155,7 +151,15 @@
@Test
public void prefetchItemsNotEvictedWithInserts() {
- mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3));
+ mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 3) {
+ @Override
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
+ prefetchRegistry.addPosition(0, 0);
+ prefetchRegistry.addPosition(1, 0);
+ prefetchRegistry.addPosition(2, 0);
+ }
+ });
RecyclerView.Adapter mockAdapter = mock(RecyclerView.Adapter.class);
when(mockAdapter.onCreateViewHolder(any(ViewGroup.class), anyInt()))
@@ -173,8 +177,10 @@
mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 320);
mRecyclerView.layout(0, 0, 320, 320);
- mRecyclerView.mPrefetchArray = new int[] { 0, 1, 2 };
- GapWorker.layoutPrefetchImpl(Long.MAX_VALUE, mRecycler, mRecyclerView.mPrefetchArray, 3);
+ assertEquals(2, mRecyclerView.mRecycler.mViewCacheMax);
+ GapWorker.layoutPrefetch(RecyclerView.FOREVER_NS, mRecyclerView);
+ assertEquals(5, mRecyclerView.mRecycler.mViewCacheMax);
+
verifyCacheContainsPositions(0, 1, 2);
// further views recycled, as though from scrolling, shouldn't evict prefetched views:
@@ -231,8 +237,7 @@
assertTrue(mRecycler.mCachedViews.isEmpty());
// rows 0, 1, and 2 are all attached and visible. Prefetch row 3:
- mRecyclerView.mPrefetchDx = 0;
- mRecyclerView.mPrefetchDy = 1;
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
GapWorker.layoutPrefetch(RecyclerView.FOREVER_NS, mRecyclerView);
// row 3 is cached:
@@ -259,8 +264,7 @@
View view = new View(getContext());
view.setMinimumWidth(100);
view.setMinimumHeight(100);
- RecyclerView.ViewHolder holder = new RecyclerView.ViewHolder(view) {};
- return holder;
+ return new RecyclerView.ViewHolder(view) {};
}
@Override
@@ -285,14 +289,13 @@
final long deadlineNs = mRecyclerView.getNanoTime() + TimeUnit.MILLISECONDS.toNanos(19);
// Timed prefetch
- mRecyclerView.mPrefetchDx = 0;
- mRecyclerView.mPrefetchDy = 1;
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
GapWorker.layoutPrefetch(deadlineNs, mRecyclerView);
// will have enough time to inflate/bind one view, and inflate another
assertTrue(mRecycler.mCachedViews.size() == 1);
assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 1);
- verifyCacheContainsPositions(11); // Note: order/view here is an implementation detail
+ verifyCacheContainsPositions(9); // Note: order/view here is an implementation detail
// Unbounded prefetch this time
@@ -303,4 +306,92 @@
assertTrue(mRecyclerView.getRecycledViewPool().getRecycledViewCount(0) == 0);
verifyCacheContainsPositions(9, 10, 11);
}
-}
\ No newline at end of file
+
+ private void verifyPositionsPrefetched(RecyclerView.LayoutManager lm, int dx, int dy,
+ Integer[] ... positionData) {
+ RecyclerView.PrefetchRegistry prefetchRegistry = mock(RecyclerView.PrefetchRegistry.class);
+ lm.collectPrefetchPositions(dx, dy, mRecyclerView.mState, prefetchRegistry);
+
+ verify(prefetchRegistry, times(positionData.length)).addPosition(anyInt(), anyInt());
+ for (Integer[] aPositionData : positionData) {
+ verify(prefetchRegistry).addPosition(aPositionData[0], aPositionData[1]);
+ }
+ }
+
+ @Test
+ public void prefetchStaggeredItemsPriority() {
+ StaggeredGridLayoutManager sglm =
+ new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
+ mRecyclerView.setLayoutManager(sglm);
+
+ // first view 50x100 pixels, rest are 100x100 so second column is offset
+ mRecyclerView.setAdapter(new RecyclerView.Adapter() {
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new RecyclerView.ViewHolder(new View(getContext())) {};
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ holder.itemView.setMinimumWidth(100);
+ holder.itemView.setMinimumHeight(position == 0 ? 50 : 100);
+ }
+
+ @Override
+ public int getItemCount() {
+ return 100;
+ }
+ });
+
+ mRecyclerView.measure(View.MeasureSpec.AT_MOST | 200, View.MeasureSpec.AT_MOST | 200);
+ mRecyclerView.layout(0, 0, 200, 200);
+
+ /* Each row is 50 pixels:
+ * ------------- *
+ * 0 | 1 *
+ * 2 | 1 *
+ * 2 | 3 *
+ *___4___|___3___*
+ * 4 | 5 *
+ * 6 | 5 *
+ * ... *
+ */
+ assertEquals(5, mRecyclerView.getChildCount());
+ assertEquals(0, sglm.getFirstChildPosition());
+ assertEquals(4, sglm.getLastChildPosition());
+
+ // prefetching down shows 5 at 0 pixels away, 6 at 50 pixels away
+ verifyPositionsPrefetched(sglm, 0, 10,
+ new Integer[] {5, 0}, new Integer[] {6, 50});
+
+ // Prefetch upward shows nothing
+ verifyPositionsPrefetched(sglm, 0, -10);
+
+ mRecyclerView.scrollBy(0, 100);
+
+ /* Each row is 50 pixels:
+ * ------------- *
+ * 0 | 1 *
+ *___2___|___1___*
+ * 2 | 3 *
+ * 4 | 3 *
+ * 4 | 5 *
+ *___6___|___5___*
+ * 6 | 7 *
+ * 8 | 7 *
+ * ... *
+ */
+
+ assertEquals(5, mRecyclerView.getChildCount());
+ assertEquals(2, sglm.getFirstChildPosition());
+ assertEquals(6, sglm.getLastChildPosition());
+
+ // prefetching down shows 7 at 0 pixels away, 8 at 50 pixels away
+ verifyPositionsPrefetched(sglm, 0, 10,
+ new Integer[] {7, 0}, new Integer[] {8, 50});
+
+ // prefetching up shows 1 is 0 pixels away, 0 at 50 pixels away
+ verifyPositionsPrefetched(sglm, 0, -10,
+ new Integer[] {1, 0}, new Integer[] {0, 50});
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
index 820b205..b3f44a8 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewPrefetchTest.java
@@ -57,15 +57,10 @@
}
@Override
- int getItemPrefetchCount() {
- return 1;
- }
-
- @Override
- int gatherPrefetchIndices(int dx, int dy, RecyclerView.State state, int[] outIndices) {
+ public void collectPrefetchPositions(int dx, int dy, RecyclerView.State state,
+ RecyclerView.PrefetchRegistry prefetchRegistry) {
prefetchLatch.countDown();
- outIndices[0] = 6;
- return 1;
+ prefetchRegistry.addPosition(6, 0);
}
void waitForPrefetch(int time) throws InterruptedException {