Merge "Revert "Add EXTRA_STREAM_TYPE"" into nyc-support-25.4-dev
diff --git a/api/current.txt b/api/current.txt
index 665bc78..eb66483 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -2636,7 +2636,7 @@
ctor public PlaybackControlGlue(android.content.Context, int[], int[]);
method public void enableProgressUpdating(boolean);
method public android.support.v17.leanback.widget.PlaybackControlsRow getControlsRow();
- method public android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
+ method public deprecated android.support.v17.leanback.widget.PlaybackControlsRowPresenter getControlsRowPresenter();
method public abstract int getCurrentPosition();
method public abstract int getCurrentSpeedId();
method public int[] getFastForwardSpeeds();
@@ -2644,6 +2644,7 @@
method public abstract int getMediaDuration();
method public abstract java.lang.CharSequence getMediaSubtitle();
method public abstract java.lang.CharSequence getMediaTitle();
+ method public android.support.v17.leanback.widget.PlaybackRowPresenter getPlaybackRowPresenter();
method public int[] getRewindSpeeds();
method public abstract long getSupportedActions();
method public int getUpdatePeriod();
@@ -2660,8 +2661,9 @@
method public void play(int);
method public final void play();
method public void setControlsRow(android.support.v17.leanback.widget.PlaybackControlsRow);
- method public void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
+ method public deprecated void setControlsRowPresenter(android.support.v17.leanback.widget.PlaybackControlsRowPresenter);
method public void setFadingEnabled(boolean);
+ method public void setPlaybackRowPresenter(android.support.v17.leanback.widget.PlaybackRowPresenter);
method public void updateProgress();
field public static final int ACTION_CUSTOM_LEFT_FIRST = 1; // 0x1
field public static final int ACTION_CUSTOM_RIGHT_FIRST = 4096; // 0x1000
@@ -4575,7 +4577,6 @@
method public final android.support.v4.app.FragmentManager getFragmentManager();
method public final java.lang.Object getHost();
method public final int getId();
- method public android.view.LayoutInflater getLayoutInflater(android.os.Bundle);
method public android.support.v4.app.LoaderManager getLoaderManager();
method public final android.support.v4.app.Fragment getParentFragment();
method public java.lang.Object getReenterTransition();
@@ -4618,6 +4619,7 @@
method public void onDestroyOptionsMenu();
method public void onDestroyView();
method public void onDetach();
+ method public android.view.LayoutInflater onGetLayoutInflater(android.os.Bundle);
method public void onHiddenChanged(boolean);
method public void onInflate(android.content.Context, android.util.AttributeSet, android.os.Bundle);
method public deprecated void onInflate(android.app.Activity, android.util.AttributeSet, android.os.Bundle);
@@ -4778,7 +4780,7 @@
method public abstract android.support.v4.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
method public abstract int getBackStackEntryCount();
method public abstract android.support.v4.app.Fragment getFragment(android.os.Bundle, java.lang.String);
- method public abstract java.util.List<android.support.v4.app.Fragment> getFragments();
+ method public abstract java.util.Collection<android.support.v4.app.Fragment> getFragments();
method public abstract boolean isDestroyed();
method public abstract void popBackStack();
method public abstract void popBackStack(java.lang.String, int);
@@ -8109,6 +8111,13 @@
field public static final int INVALID_ID = -2147483648; // 0x80000000
}
+ public class ImageViewCompat {
+ method public static android.content.res.ColorStateList getImageTintList(android.widget.ImageView);
+ method public static android.graphics.PorterDuff.Mode getImageTintMode(android.widget.ImageView);
+ method public static void setImageTintList(android.widget.ImageView, android.content.res.ColorStateList);
+ method public static void setImageTintMode(android.widget.ImageView, android.graphics.PorterDuff.Mode);
+ }
+
public final class ListPopupWindowCompat {
method public static android.view.View.OnTouchListener createDragToOpenListener(java.lang.Object, android.view.View);
}
diff --git a/compat/api21/android/support/v4/view/ViewCompatLollipop.java b/compat/api21/android/support/v4/view/ViewCompatLollipop.java
index 26c462a..f38fec0 100644
--- a/compat/api21/android/support/v4/view/ViewCompatLollipop.java
+++ b/compat/api21/android/support/v4/view/ViewCompatLollipop.java
@@ -16,13 +16,13 @@
package android.support.v4.view;
+import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.RequiresApi;
-import android.annotation.TargetApi;
import android.view.View;
import android.view.ViewParent;
import android.view.WindowInsets;
diff --git a/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java b/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java
new file mode 100644
index 0000000..c5279d0
--- /dev/null
+++ b/compat/api21/android/support/v4/widget/ImageViewCompatLollipop.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.widget.ImageView;
+
+@RequiresApi(21)
+class ImageViewCompatLollipop {
+ static ColorStateList getImageTintList(ImageView view) {
+ return view.getImageTintList();
+ }
+
+ static void setImageTintList(ImageView view, ColorStateList tintList) {
+ view.setImageTintList(tintList);
+
+ if (Build.VERSION.SDK_INT == 21) {
+ // Work around a bug in L that did not update the state of the image source
+ // after applying the tint
+ Drawable imageViewDrawable = view.getDrawable();
+ boolean hasTint = (view.getImageTintList() != null)
+ && (view.getImageTintMode() != null);
+ if ((imageViewDrawable != null) && hasTint) {
+ if (imageViewDrawable.isStateful()) {
+ imageViewDrawable.setState(view.getDrawableState());
+ }
+ view.setImageDrawable(imageViewDrawable);
+ }
+ }
+ }
+
+ static PorterDuff.Mode getImageTintMode(ImageView view) {
+ return view.getImageTintMode();
+ }
+
+ static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ view.setImageTintMode(mode);
+
+ if (Build.VERSION.SDK_INT == 21) {
+ // Work around a bug in L that did not update the state of the image source
+ // after applying the tint
+ Drawable imageViewDrawable = view.getDrawable();
+ boolean hasTint = (view.getImageTintList() != null)
+ && (view.getImageTintMode() != null);
+ if ((imageViewDrawable != null) && hasTint) {
+ if (imageViewDrawable.isStateful()) {
+ imageViewDrawable.setState(view.getDrawableState());
+ }
+ view.setImageDrawable(imageViewDrawable);
+ }
+ }
+ }
+}
diff --git a/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java b/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
index 83c014b..9a22d7e 100644
--- a/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
+++ b/compat/gingerbread/android/support/v4/view/TintableBackgroundView.java
@@ -22,7 +22,7 @@
/**
* Interface which allows a {@link android.view.View} to receive background tinting calls from
- * {@code ViewCompat} when running on API v20 devices or lower.
+ * {@link ViewCompat} when running on API v20 devices or lower.
*/
public interface TintableBackgroundView {
diff --git a/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java b/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java
new file mode 100644
index 0000000..1963340
--- /dev/null
+++ b/compat/gingerbread/android/support/v4/widget/ImageViewCompatBase.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.widget.ImageView;
+
+class ImageViewCompatBase {
+ static ColorStateList getImageTintList(ImageView view) {
+ return (view instanceof TintableImageSourceView)
+ ? ((TintableImageSourceView) view).getSupportImageTintList()
+ : null;
+ }
+
+ static void setImageTintList(ImageView view, ColorStateList tintList) {
+ if (view instanceof TintableImageSourceView) {
+ ((TintableImageSourceView) view).setSupportImageTintList(tintList);
+ }
+ }
+
+ static PorterDuff.Mode getImageTintMode(ImageView view) {
+ return (view instanceof TintableImageSourceView)
+ ? ((TintableImageSourceView) view).getSupportImageTintMode()
+ : null;
+ }
+
+ static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ if (view instanceof TintableImageSourceView) {
+ ((TintableImageSourceView) view).setSupportImageTintMode(mode);
+ }
+ }
+}
diff --git a/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java b/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java
new file mode 100644
index 0000000..0c3d436
--- /dev/null
+++ b/compat/gingerbread/android/support/v4/widget/TintableImageSourceView.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Interface which allows an {@link android.widget.ImageView} to receive image tinting calls
+ * from {@link ImageViewCompat} when running on API v20 devices or lower.
+ *
+ * @hide Internal use only
+ */
+@RestrictTo(LIBRARY_GROUP)
+public interface TintableImageSourceView {
+
+ /**
+ * Applies a tint to the image drawable. Does not modify the current tint
+ * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+ * <p>
+ * Subsequent calls to the source's image will automatically
+ * mutate the drawable and apply the specified tint and tint mode.
+ *
+ * @param tint the tint to apply, may be {@code null} to clear tint
+ *
+ * @see #getSupportImageTintList()
+ */
+ void setSupportImageTintList(@Nullable ColorStateList tint);
+
+ /**
+ * Return the tint applied to the image drawable, if specified.
+ *
+ * @return the tint applied to the image drawable
+ */
+ @Nullable
+ ColorStateList getSupportImageTintList();
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setSupportImageTintList(ColorStateList)}} to the image
+ * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ *
+ * @param tintMode the blending mode used to apply the tint, may be
+ * {@code null} to clear tint
+ * @see #getSupportImageTintMode()
+ */
+ void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode);
+
+ /**
+ * Return the blending mode used to apply the tint to the image
+ * drawable, if specified.
+ *
+ * @return the blending mode used to apply the tint to the image drawable
+ */
+ @Nullable
+ PorterDuff.Mode getSupportImageTintMode();
+}
diff --git a/compat/java/android/support/v4/view/ViewCompat.java b/compat/java/android/support/v4/view/ViewCompat.java
index 434b850..82ac508 100644
--- a/compat/java/android/support/v4/view/ViewCompat.java
+++ b/compat/java/android/support/v4/view/ViewCompat.java
@@ -3130,7 +3130,7 @@
* Applies a tint to the background drawable.
* <p>
* This will always take effect when running on API v21 or newer. When running on platforms
- * previous to API v21, it will only take effect if {@code view} implement the
+ * previous to API v21, it will only take effect if {@code view} implements the
* {@code TintableBackgroundView} interface.
*/
public static void setBackgroundTintList(View view, ColorStateList tintList) {
@@ -3160,6 +3160,7 @@
public static void setBackgroundTintMode(View view, PorterDuff.Mode mode) {
IMPL.setBackgroundTintMode(view, mode);
}
+
// TODO: getters for various view properties (rotation, etc)
/**
diff --git a/compat/java/android/support/v4/widget/ImageViewCompat.java b/compat/java/android/support/v4/widget/ImageViewCompat.java
new file mode 100644
index 0000000..3c0b357
--- /dev/null
+++ b/compat/java/android/support/v4/widget/ImageViewCompat.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.widget;
+
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.widget.ImageView;
+
+/**
+ * Helper for accessing features in {@link ImageView} introduced in later platform releases
+ * in a backwards compatible fashion.
+ */
+public class ImageViewCompat {
+ interface ImageViewCompatImpl {
+ ColorStateList getImageTintList(ImageView view);
+
+ void setImageTintList(ImageView view, ColorStateList tintList);
+
+ PorterDuff.Mode getImageTintMode(ImageView view);
+
+ void setImageTintMode(ImageView view, PorterDuff.Mode mode);
+ }
+
+ static class BaseViewCompatImpl implements ImageViewCompatImpl {
+ @Override
+ public ColorStateList getImageTintList(ImageView view) {
+ return ImageViewCompatBase.getImageTintList(view);
+ }
+
+ @Override
+ public void setImageTintList(ImageView view, ColorStateList tintList) {
+ ImageViewCompatBase.setImageTintList(view, tintList);
+ }
+
+ @Override
+ public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ ImageViewCompatBase.setImageTintMode(view, mode);
+ }
+
+ @Override
+ public PorterDuff.Mode getImageTintMode(ImageView view) {
+ return ImageViewCompatBase.getImageTintMode(view);
+ }
+ }
+
+ static class LollipopViewCompatImpl extends BaseViewCompatImpl {
+ @Override
+ public ColorStateList getImageTintList(ImageView view) {
+ return ImageViewCompatLollipop.getImageTintList(view);
+ }
+
+ @Override
+ public void setImageTintList(ImageView view, ColorStateList tintList) {
+ ImageViewCompatLollipop.setImageTintList(view, tintList);
+ }
+
+ @Override
+ public void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ ImageViewCompatLollipop.setImageTintMode(view, mode);
+ }
+
+ @Override
+ public PorterDuff.Mode getImageTintMode(ImageView view) {
+ return ImageViewCompatLollipop.getImageTintMode(view);
+ }
+ }
+
+ static final ImageViewCompatImpl IMPL;
+ static {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ IMPL = new LollipopViewCompatImpl();
+ } else {
+ IMPL = new BaseViewCompatImpl();
+ }
+ }
+
+ /**
+ * Return the tint applied to the image drawable, if specified.
+ */
+ public static ColorStateList getImageTintList(ImageView view) {
+ return IMPL.getImageTintList(view);
+ }
+
+ /**
+ * Applies a tint to the image drawable.
+ */
+ public static void setImageTintList(ImageView view, ColorStateList tintList) {
+ IMPL.setImageTintList(view, tintList);
+ }
+
+ /**
+ * Return the blending mode used to apply the tint to the image drawable, if specified.
+ */
+ public static PorterDuff.Mode getImageTintMode(ImageView view) {
+ return IMPL.getImageTintMode(view);
+ }
+
+ /**
+ * Specifies the blending mode used to apply the tint specified by
+ * {@link #setImageTintList(android.widget.ImageView, android.content.res.ColorStateList)}
+ * to the image drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+ */
+ public static void setImageTintMode(ImageView view, PorterDuff.Mode mode) {
+ IMPL.setImageTintMode(view, mode);
+ }
+
+ private ImageViewCompat() {}
+}
diff --git a/fragment/java/android/support/v4/app/DialogFragment.java b/fragment/java/android/support/v4/app/DialogFragment.java
index 6a6456b..dfd0416 100644
--- a/fragment/java/android/support/v4/app/DialogFragment.java
+++ b/fragment/java/android/support/v4/app/DialogFragment.java
@@ -302,9 +302,9 @@
}
@Override
- public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+ public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
if (!mShowsDialog) {
- return super.getLayoutInflater(savedInstanceState);
+ return super.onGetLayoutInflater(savedInstanceState);
}
mDialog = onCreateDialog(savedInstanceState);
diff --git a/fragment/java/android/support/v4/app/Fragment.java b/fragment/java/android/support/v4/app/Fragment.java
index 698db20..f827cfb 100644
--- a/fragment/java/android/support/v4/app/Fragment.java
+++ b/fragment/java/android/support/v4/app/Fragment.java
@@ -1127,9 +1127,21 @@
* a previous saved state, this is the state.
* @return The LayoutInflater used to inflate Views of this Fragment.
*/
- public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
+ public LayoutInflater onGetLayoutInflater(Bundle savedInstanceState) {
+ // TODO: move the implementation in getLayoutInflater to here
+ return getLayoutInflater(savedInstanceState);
+ }
+
+ /**
+ * Use {@link #onGetLayoutInflater(Bundle)} instead
+ * @hide
+ * @deprecated Use {@link #onGetLayoutInflater(Bundle)} instead.
+ */
+ @Deprecated
+ @RestrictTo(LIBRARY_GROUP)
+ public LayoutInflater getLayoutInflater(Bundle savedFragmentState) {
if (mHost == null) {
- throw new IllegalStateException("getLayoutInflater() cannot be executed until the "
+ throw new IllegalStateException("onGetLayoutInflater() cannot be executed until the "
+ "Fragment is attached to the FragmentManager.");
}
LayoutInflater result = mHost.onGetLayoutInflater();
diff --git a/fragment/java/android/support/v4/app/FragmentController.java b/fragment/java/android/support/v4/app/FragmentController.java
index 7894437..48b3602 100644
--- a/fragment/java/android/support/v4/app/FragmentController.java
+++ b/fragment/java/android/support/v4/app/FragmentController.java
@@ -29,7 +29,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -78,22 +77,14 @@
* Returns the number of active fragments.
*/
public int getActiveFragmentsCount() {
- final List<Fragment> actives = mHost.mFragmentManager.mActive;
- return actives == null ? 0 : actives.size();
+ return mHost.mFragmentManager.getActiveFragmentCount();
}
/**
* Returns the list of active fragments.
*/
public List<Fragment> getActiveFragments(List<Fragment> actives) {
- if (mHost.mFragmentManager.mActive == null) {
- return null;
- }
- if (actives == null) {
- actives = new ArrayList<Fragment>(getActiveFragmentsCount());
- }
- actives.addAll(mHost.mFragmentManager.mActive);
- return actives;
+ return mHost.mFragmentManager.getActiveFragments();
}
/**
diff --git a/fragment/java/android/support/v4/app/FragmentManager.java b/fragment/java/android/support/v4/app/FragmentManager.java
index f8839a7..bc6e842 100644
--- a/fragment/java/android/support/v4/app/FragmentManager.java
+++ b/fragment/java/android/support/v4/app/FragmentManager.java
@@ -62,6 +62,7 @@
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -328,14 +329,14 @@
public abstract Fragment getFragment(Bundle bundle, String key);
/**
- * Get a list of all fragments that are currently added to the FragmentManager.
+ * Get a collection of all fragments that are currently added to the FragmentManager.
* This may include those that are hidden as well as those that are shown.
* This will not include any fragments only in the back stack, or fragments that
* are detached or removed.
*
- * @return A list of all fragments that are added to the FragmentManager.
+ * @return A collection of all fragments that are added to the FragmentManager.
*/
- public abstract List<Fragment> getFragments();
+ public abstract Collection<Fragment> getFragments();
/**
* Save the current instance state of the given Fragment. This can be
@@ -546,6 +547,7 @@
FragmentState[] mActive;
int[] mAdded;
BackStackState[] mBackStack;
+ int mNextFragmentIndex;
public FragmentManagerState() {
}
@@ -554,6 +556,7 @@
mActive = in.createTypedArray(FragmentState.CREATOR);
mAdded = in.createIntArray();
mBackStack = in.createTypedArray(BackStackState.CREATOR);
+ mNextFragmentIndex = in.readInt();
}
@Override
@@ -566,6 +569,7 @@
dest.writeTypedArray(mActive, flags);
dest.writeIntArray(mAdded);
dest.writeTypedArray(mBackStack, flags);
+ dest.writeInt(mNextFragmentIndex);
}
public static final Parcelable.Creator<FragmentManagerState> CREATOR
@@ -665,12 +669,12 @@
}
ArrayList<OpGenerator> mPendingActions;
- Runnable[] mTmpActions;
boolean mExecutingActions;
- ArrayList<Fragment> mActive;
+ int mNextFragmentIndex = 0;
+
ArrayList<Fragment> mAdded;
- ArrayList<Integer> mAvailIndices;
+ SparseArray<Fragment> mActive;
ArrayList<BackStackRecord> mBackStack;
ArrayList<Fragment> mCreatedMenus;
@@ -829,6 +833,7 @@
}
doPendingDeferredStart();
+ burpActive();
return executePop;
}
@@ -872,10 +877,6 @@
if (index == -1) {
return null;
}
- if (index >= mActive.size()) {
- throwException(new IllegalStateException("Fragment no longer exists for key "
- + key + ": index " + index));
- }
Fragment f = mActive.get(index);
if (f == null) {
throwException(new IllegalStateException("Fragment no longer exists for key "
@@ -885,15 +886,45 @@
}
@Override
- public List<Fragment> getFragments() {
+ public Collection<Fragment> getFragments() {
if (mAdded == null) {
return Collections.EMPTY_LIST;
}
synchronized (mAdded) {
- return (List<Fragment>) mAdded.clone();
+ return (Collection<Fragment>) mAdded.clone();
}
}
+ /**
+ * This is used by FragmentController to get the Active fragments.
+ *
+ * @return A list of active fragments in the fragment manager, including those that are in the
+ * back stack.
+ */
+ List<Fragment> getActiveFragments() {
+ if (mActive == null) {
+ return null;
+ }
+ final int count = mActive.size();
+ ArrayList<Fragment> fragments = new ArrayList<>(count);
+ for (int i = 0; i < count; i++) {
+ fragments.add(mActive.valueAt(i));
+ }
+ return fragments;
+ }
+
+ /**
+ * Used by FragmentController to get the number of Active Fragments.
+ *
+ * @return The number of active fragments.
+ */
+ int getActiveFragmentCount() {
+ if (mActive == null) {
+ return 0;
+ }
+ return mActive.size();
+ }
+
@Override
public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
if (fragment.mIndex < 0) {
@@ -939,7 +970,7 @@
writer.print(Integer.toHexString(System.identityHashCode(this)));
writer.println(":");
for (int i=0; i<N; i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
writer.print(prefix); writer.print(" #"); writer.print(i);
writer.print(": "); writer.println(f);
if (f != null) {
@@ -1034,10 +1065,6 @@
writer.print(prefix); writer.print(" mNoTransactionsBecause=");
writer.println(mNoTransactionsBecause);
}
- if (mAvailIndices != null && mAvailIndices.size() > 0) {
- writer.print(prefix); writer.print(" mAvailIndices: ");
- writer.println(Arrays.toString(mAvailIndices.toArray()));
- }
}
static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f);
@@ -1259,7 +1286,7 @@
// For fragments that are part of the content view
// layout, we need to instantiate the view immediately
// and the inflater will take care of adding it.
- f.mView = f.performCreateView(f.getLayoutInflater(
+ f.mView = f.performCreateView(f.onGetLayoutInflater(
f.mSavedFragmentState), null, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
@@ -1303,7 +1330,7 @@
}
}
f.mContainer = container;
- f.mView = f.performCreateView(f.getLayoutInflater(
+ f.mView = f.performCreateView(f.onGetLayoutInflater(
f.mSavedFragmentState), container, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
@@ -1610,7 +1637,7 @@
// and detached.
final int numActive = mActive.size();
for (int i = 0; i < numActive; i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null && (f.mRemoving || f.mDetached) && !f.mIsNewlyAdded) {
moveFragmentToExpectedState(f);
if (f.mLoaderManager != null) {
@@ -1634,7 +1661,7 @@
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
@@ -1646,17 +1673,11 @@
return;
}
- if (mAvailIndices == null || mAvailIndices.size() <= 0) {
- if (mActive == null) {
- mActive = new ArrayList<Fragment>();
- }
- f.setIndex(mActive.size(), mParent);
- mActive.add(f);
-
- } else {
- f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
- mActive.set(f.mIndex, f);
+ f.setIndex(mNextFragmentIndex++, mParent);
+ if (mActive == null) {
+ mActive = new SparseArray<>();
}
+ mActive.put(f.mIndex, f);
if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
}
@@ -1666,11 +1687,10 @@
}
if (DEBUG) Log.v(TAG, "Freeing fragment index " + f);
- mActive.set(f.mIndex, null);
- if (mAvailIndices == null) {
- mAvailIndices = new ArrayList<Integer>();
- }
- mAvailIndices.add(f.mIndex);
+ // Don't remove yet. That happens in burpActive(). This prevents
+ // concurrent modification while iterating over mActive
+ mActive.put(f.mIndex, null);
+
mHost.inactivateFragment(f.mWho);
f.initState();
}
@@ -1808,7 +1828,7 @@
if (mActive != null) {
// Now for any known fragment.
for (int i=mActive.size()-1; i>=0; i--) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null && f.mFragmentId == id) {
return f;
}
@@ -1831,7 +1851,7 @@
if (mActive != null && tag != null) {
// Now for any known fragment.
for (int i=mActive.size()-1; i>=0; i--) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null && tag.equals(f.mTag)) {
return f;
}
@@ -1843,7 +1863,7 @@
public Fragment findFragmentByWho(String who) {
if (mActive != null && who != null) {
for (int i=mActive.size()-1; i>=0; i--) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null && (f=f.findFragmentByWho(who)) != null) {
return f;
}
@@ -1876,6 +1896,10 @@
}
synchronized (this) {
if (mDestroyed || mHost == null) {
+ if (allowStateLoss) {
+ // This FragmentManager isn't attached, so drop the entire transaction.
+ return;
+ }
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
@@ -1992,6 +2016,10 @@
}
public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
+ if (allowStateLoss && (mHost == null || mDestroyed)) {
+ // This FragmentManager isn't attached, so drop the entire transaction.
+ return;
+ }
ensureExecReady(allowStateLoss);
if (action.generateOps(mTmpRecords, mTmpIsPop)) {
mExecutingActions = true;
@@ -2003,6 +2031,7 @@
}
doPendingDeferredStart();
+ burpActive();
}
/**
@@ -2033,6 +2062,7 @@
}
doPendingDeferredStart();
+ burpActive();
return didSomething;
}
@@ -2301,7 +2331,7 @@
for (int i = 0; i < numActive; i++) {
// Allow added fragments to be removed during the pop since we aren't going
// to move them to the final state with moveToState(mCurState).
- Fragment fragment = mActive.get(i);
+ Fragment fragment = mActive.valueAt(i);
if (fragment != null && fragment.mView != null && fragment.mIsNewlyAdded
&& record.interactsWith(fragment.mContainerId)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB
@@ -2423,7 +2453,7 @@
private void endAnimatingAwayFragments() {
final int numFragments = mActive == null ? 0 : mActive.size();
for (int i = 0; i < numFragments; i++) {
- Fragment fragment = mActive.get(i);
+ Fragment fragment = mActive.valueAt(i);
if (fragment != null && fragment.getAnimatingAway() != null) {
// Give up waiting for the animation and just end it.
final int stateAfterAnimating = fragment.getStateAfterAnimating();
@@ -2470,7 +2500,7 @@
if (mHavePendingDeferredStart) {
boolean loadersRunning = false;
for (int i = 0; i < mActive.size(); i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null && f.mLoaderManager != null) {
loadersRunning |= f.mLoaderManager.hasRunningLoaders();
}
@@ -2560,7 +2590,7 @@
ArrayList<FragmentManagerNonConfig> childFragments = null;
if (mActive != null) {
for (int i=0; i<mActive.size(); i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null) {
if (f.mRetainInstance) {
if (fragments == null) {
@@ -2676,7 +2706,7 @@
FragmentState[] active = new FragmentState[N];
boolean haveFragments = false;
for (int i=0; i<N; i++) {
- Fragment f = mActive.get(i);
+ Fragment f = mActive.valueAt(i);
if (f != null) {
if (f.mIndex < 0) {
throwException(new IllegalStateException(
@@ -2762,6 +2792,7 @@
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
+ fms.mNextFragmentIndex = mNextFragmentIndex;
return fms;
}
@@ -2783,7 +2814,15 @@
for (int i = 0; i < count; i++) {
Fragment f = nonConfigFragments.get(i);
if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
- FragmentState fs = fms.mActive[f.mIndex];
+ int index = 0; // index into fms.mActive
+ while (index < fms.mActive.length && fms.mActive[index].mIndex != f.mIndex) {
+ index++;
+ }
+ if (index == fms.mActive.length) {
+ throwException(new IllegalStateException("Could not find active fragment "
+ + "with index " + f.mIndex));
+ }
+ FragmentState fs = fms.mActive[index];
fs.mInstance = f;
f.mSavedViewState = null;
f.mBackStackNesting = 0;
@@ -2801,10 +2840,7 @@
// Build the full list of active fragments, instantiating them from
// their saved state.
- mActive = new ArrayList<>(fms.mActive.length);
- if (mAvailIndices != null) {
- mAvailIndices.clear();
- }
+ mActive = new SparseArray<>(fms.mActive.length);
for (int i=0; i<fms.mActive.length; i++) {
FragmentState fs = fms.mActive[i];
if (fs != null) {
@@ -2814,18 +2850,11 @@
}
Fragment f = fs.instantiate(mHost, mParent, childNonConfig);
if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
- mActive.add(f);
+ mActive.put(f.mIndex, f);
// Now that the fragment is instantiated (or came from being
// retained above), clear mInstance in case we end up re-restoring
// from this FragmentState again.
fs.mInstance = null;
- } else {
- mActive.add(null);
- if (mAvailIndices == null) {
- mAvailIndices = new ArrayList<Integer>();
- }
- if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i);
- mAvailIndices.add(i);
}
}
@@ -2836,12 +2865,10 @@
for (int i = 0; i < count; i++) {
Fragment f = nonConfigFragments.get(i);
if (f.mTargetIndex >= 0) {
- if (f.mTargetIndex < mActive.size()) {
- f.mTarget = mActive.get(f.mTargetIndex);
- } else {
+ f.mTarget = mActive.get(f.mTargetIndex);
+ if (f.mTarget == null) {
Log.w(TAG, "Re-attaching retained fragment " + f
+ " target no longer exists: " + f.mTargetIndex);
- f.mTarget = null;
}
}
}
@@ -2890,6 +2917,23 @@
} else {
mBackStack = null;
}
+
+ this.mNextFragmentIndex = fms.mNextFragmentIndex;
+ }
+
+ /**
+ * To prevent list modification errors, mActive sets values to null instead of
+ * removing them when the Fragment becomes inactive. This cleans up the list at the
+ * end of executing the transactions.
+ */
+ private void burpActive() {
+ if (mActive != null) {
+ for (int i = mActive.size() - 1; i >= 0; i--) {
+ if (mActive.valueAt(i) == null) {
+ mActive.delete(mActive.keyAt(i));
+ }
+ }
+ }
}
public void attachController(FragmentHostCallback host,
diff --git a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
index c9d7351..c6db19b 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentLifecycleTest.java
@@ -684,6 +684,52 @@
assertTrue(activity.onDestroyLatch.await(1000, TimeUnit.MILLISECONDS));
}
+ /**
+ * When a fragment is saved in non-config, it should be restored to the same index.
+ */
+ @Test
+ @UiThreadTest
+ public void restoreNonConfig() throws Throwable {
+ FragmentController fc = FragmentTestUtil.createController(mActivityRule);
+ FragmentTestUtil.resume(mActivityRule, fc, null);
+ FragmentManager fm = fc.getSupportFragmentManager();
+
+ Fragment fragment1 = new StrictFragment();
+ fm.beginTransaction()
+ .add(fragment1, "1")
+ .addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+ Fragment fragment2 = new StrictFragment();
+ fragment2.setRetainInstance(true);
+ fragment2.setTargetFragment(fragment1, 0);
+ Fragment fragment3 = new StrictFragment();
+ fm.beginTransaction()
+ .remove(fragment1)
+ .add(fragment2, "2")
+ .add(fragment3, "3")
+ .addToBackStack(null)
+ .commit();
+ fm.executePendingTransactions();
+
+ Pair<Parcelable, FragmentManagerNonConfig> savedState =
+ FragmentTestUtil.destroy(mActivityRule, fc);
+
+ fc = FragmentTestUtil.createController(mActivityRule);
+ FragmentTestUtil.resume(mActivityRule, fc, savedState);
+ boolean foundFragment2 = false;
+ for (Fragment fragment : fc.getSupportFragmentManager().getFragments()) {
+ if (fragment == fragment2) {
+ foundFragment2 = true;
+ assertNotNull(fragment.getTargetFragment());
+ assertEquals("1", fragment.getTargetFragment().getTag());
+ } else {
+ assertNotEquals("2", fragment.getTag());
+ }
+ }
+ assertTrue(foundFragment2);
+ }
+
private void assertAnimationsMatch(FragmentManager fm, int enter, int exit, int popEnter,
int popExit) {
FragmentManagerImpl fmImpl = (FragmentManagerImpl) fm;
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
index ff48420..9ee0c48 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTestUtil.java
@@ -145,9 +145,10 @@
}
}
- public static FragmentController createController(ActivityTestRule<FragmentTestActivity> rule) {
+ public static FragmentController createController(
+ ActivityTestRule<? extends FragmentActivity> rule) {
final FragmentController[] controller = new FragmentController[1];
- final FragmentTestActivity activity = rule.getActivity();
+ final FragmentActivity activity = rule.getActivity();
runOnUiThreadRethrow(rule, new Runnable() {
@Override
public void run() {
diff --git a/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java b/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
index 932aba5..7179fd3 100644
--- a/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
+++ b/fragment/tests/java/android/support/v4/app/FragmentTransactionTest.java
@@ -33,7 +33,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.List;
+import java.util.Collection;
/**
* Tests usage of the {@link FragmentTransaction} class.
@@ -179,9 +179,9 @@
.commit();
FragmentTestUtil.executePendingTransactions(mActivityRule);
- List<Fragment> fragments = fm.getFragments();
+ Collection<Fragment> fragments = fm.getFragments();
assertEquals(1, fragments.size());
- assertEquals(fragment, fragments.get(0));
+ assertTrue(fragments.contains(fragment));
// Removed fragments shouldn't show
fm.beginTransaction()
@@ -209,13 +209,13 @@
FragmentTestUtil.executePendingTransactions(mActivityRule);
fragments = fm.getFragments();
assertEquals(1, fragments.size());
- assertEquals(fragment, fragments.get(0));
+ assertTrue(fragments.contains(fragment));
// And showing it again shouldn't change anything:
FragmentTestUtil.popBackStackImmediate(mActivityRule);
fragments = fm.getFragments();
assertEquals(1, fragments.size());
- assertEquals(fragment, fragments.get(0));
+ assertTrue(fragments.contains(fragment));
// Now pop back to the start state
FragmentTestUtil.popBackStackImmediate(mActivityRule);
@@ -223,8 +223,9 @@
// We can't force concurrency, but we can do it lots of times and hope that
// we hit it.
for (int i = 0; i < 100; i++) {
+ Fragment fragment2 = new CorrectFragment();
fm.beginTransaction()
- .add(R.id.content, fragment)
+ .add(R.id.content, fragment2)
.addToBackStack(null)
.commit();
getFragmentsUntilSize(1);
@@ -234,6 +235,49 @@
}
}
+ /**
+ * When a FragmentManager is detached, it should allow commitAllowingStateLoss()
+ * and commitNowAllowingStateLoss() by just dropping the transaction.
+ */
+ @Test
+ public void commitAllowStateLossDetached() throws Throwable {
+ Fragment fragment1 = new CorrectFragment();
+ mActivity.getSupportFragmentManager()
+ .beginTransaction()
+ .add(fragment1, "1")
+ .commit();
+ FragmentTestUtil.executePendingTransactions(mActivityRule);
+ final FragmentManager fm = fragment1.getChildFragmentManager();
+ mActivity.getSupportFragmentManager()
+ .beginTransaction()
+ .remove(fragment1)
+ .commit();
+ FragmentTestUtil.executePendingTransactions(mActivityRule);
+ assertEquals(0, mActivity.getSupportFragmentManager().getFragments().size());
+ assertEquals(0, fm.getFragments().size());
+
+ // Now the fragment1's fragment manager should allow commitAllowingStateLoss
+ // by doing nothing since it has been detached.
+ Fragment fragment2 = new CorrectFragment();
+ fm.beginTransaction()
+ .add(fragment2, "2")
+ .commitAllowingStateLoss();
+ FragmentTestUtil.executePendingTransactions(mActivityRule);
+ assertEquals(0, fm.getFragments().size());
+
+ // It should also allow commitNowAllowingStateLoss by doing nothing
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Fragment fragment3 = new CorrectFragment();
+ fm.beginTransaction()
+ .add(fragment3, "3")
+ .commitNowAllowingStateLoss();
+ assertEquals(0, fm.getFragments().size());
+ }
+ });
+ }
+
private void getFragmentsUntilSize(int expectedSize) {
final long endTime = SystemClock.uptimeMillis() + 3000;
diff --git a/fragment/tests/java/android/support/v4/app/HostCallbacks.java b/fragment/tests/java/android/support/v4/app/HostCallbacks.java
index 9a0ef1c..15698a6 100644
--- a/fragment/tests/java/android/support/v4/app/HostCallbacks.java
+++ b/fragment/tests/java/android/support/v4/app/HostCallbacks.java
@@ -16,20 +16,19 @@
package android.support.v4.app;
import android.os.Handler;
-import android.support.v4.app.test.FragmentTestActivity;
import android.view.LayoutInflater;
import android.view.View;
-class HostCallbacks extends FragmentHostCallback<FragmentTestActivity> {
- private final FragmentTestActivity mActivity;
+class HostCallbacks extends FragmentHostCallback<FragmentActivity> {
+ private final FragmentActivity mActivity;
- HostCallbacks(FragmentTestActivity activity, Handler handler, int windowAnimations) {
+ HostCallbacks(FragmentActivity activity, Handler handler, int windowAnimations) {
super(activity, handler, windowAnimations);
mActivity = activity;
}
@Override
- public FragmentTestActivity onGetHost() {
+ public FragmentActivity onGetHost() {
return mActivity;
}
diff --git a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
index eef97f1..8fa2a30 100644
--- a/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
+++ b/media-compat/java/android/support/v4/media/MediaBrowserCompat.java
@@ -849,10 +849,11 @@
static class MediaBrowserImplBase
implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl {
- static final int CONNECT_STATE_DISCONNECTED = 0;
- static final int CONNECT_STATE_CONNECTING = 1;
- private static final int CONNECT_STATE_CONNECTED = 2;
- static final int CONNECT_STATE_SUSPENDED = 3;
+ static final int CONNECT_STATE_DISCONNECTING = 0;
+ static final int CONNECT_STATE_DISCONNECTED = 1;
+ static final int CONNECT_STATE_CONNECTING = 2;
+ static final int CONNECT_STATE_CONNECTED = 3;
+ static final int CONNECT_STATE_SUSPENDED = 4;
final Context mContext;
final ComponentName mServiceComponent;
@@ -952,21 +953,26 @@
// It's ok to call this any state, because allowing this lets apps not have
// to check isConnected() unnecessarily. They won't appreciate the extra
// assertions for this. We do everything we can here to go back to a sane state.
- if (mCallbacksMessenger != null) {
- try {
- mServiceBinderWrapper.disconnect(mCallbacksMessenger);
- } catch (RemoteException ex) {
- // We are disconnecting anyway. Log, just for posterity but it's not
- // a big problem.
- Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ mState = CONNECT_STATE_DISCONNECTING;
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mCallbacksMessenger != null) {
+ try {
+ mServiceBinderWrapper.disconnect(mCallbacksMessenger);
+ } catch (RemoteException ex) {
+ // We are disconnecting anyway. Log, just for posterity but it's not
+ // a big problem.
+ Log.w(TAG, "RemoteException during connect for " + mServiceComponent);
+ }
+ }
+ forceCloseConnection();
+ if (DEBUG) {
+ Log.d(TAG, "disconnect...");
+ dump();
+ }
}
- }
- forceCloseConnection();
-
- if (DEBUG) {
- Log.d(TAG, "disconnect...");
- dump();
- }
+ });
}
/**
@@ -1047,7 +1053,7 @@
// If we are connected, tell the service that we are watching. If we aren't
// connected, the service will be told when we connect.
- if (mState == CONNECT_STATE_CONNECTED) {
+ if (isConnected()) {
try {
mServiceBinderWrapper.addSubscription(parentId, callback.mToken, copiedOptions,
mCallbacksMessenger);
@@ -1069,7 +1075,7 @@
// Tell the service if necessary.
try {
if (callback == null) {
- if (mState == CONNECT_STATE_CONNECTED) {
+ if (isConnected()) {
mServiceBinderWrapper.removeSubscription(parentId, null,
mCallbacksMessenger);
}
@@ -1078,7 +1084,7 @@
final List<Bundle> optionsList = sub.getOptionsList();
for (int i = callbacks.size() - 1; i >= 0; --i) {
if (callbacks.get(i) == callback) {
- if (mState == CONNECT_STATE_CONNECTED) {
+ if (isConnected()) {
mServiceBinderWrapper.removeSubscription(
parentId, callback.mToken, mCallbacksMessenger);
}
@@ -1106,7 +1112,7 @@
if (cb == null) {
throw new IllegalArgumentException("cb is null");
}
- if (mState != CONNECT_STATE_CONNECTED) {
+ if (!isConnected()) {
Log.i(TAG, "Not connected, unable to retrieve the MediaItem.");
mHandler.post(new Runnable() {
@Override
@@ -1264,6 +1270,8 @@
*/
private static String getStateLabel(int state) {
switch (state) {
+ case CONNECT_STATE_DISCONNECTING:
+ return "CONNECT_STATE_DISCONNECTING";
case CONNECT_STATE_DISCONNECTED:
return "CONNECT_STATE_DISCONNECTED";
case CONNECT_STATE_CONNECTING:
diff --git a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
index b64d7e1..b7581f0 100644
--- a/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
+++ b/media-compat/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -1219,7 +1219,6 @@
public void writeToParcel(Parcel dest, int flags) {
if (android.os.Build.VERSION.SDK_INT >= 21) {
dest.writeParcelable((Parcelable) mInner, flags);
- dest.writeStrongBinder(mExtraBinder == null ? null : mExtraBinder.asBinder());
} else {
dest.writeStrongBinder((IBinder) mInner);
}
@@ -1278,14 +1277,12 @@
@Override
public Token createFromParcel(Parcel in) {
Object inner;
- IMediaSession extraBinder = null;
if (android.os.Build.VERSION.SDK_INT >= 21) {
inner = in.readParcelable(null);
- extraBinder = IMediaSession.Stub.asInterface(in.readStrongBinder());
} else {
inner = in.readStrongBinder();
}
- return new Token(inner, extraBinder);
+ return new Token(inner);
}
@Override
diff --git a/media-compat/tests/AndroidManifest.xml b/media-compat/tests/AndroidManifest.xml
index 93ead1e..e58817a 100644
--- a/media-compat/tests/AndroidManifest.xml
+++ b/media-compat/tests/AndroidManifest.xml
@@ -36,6 +36,12 @@
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
+ <service android:name="android.support.v4.media.StubRemoteMediaBrowserServiceCompat"
+ android:process=":remote">
+ <intent-filter>
+ <action android:name="android.media.browse.MediaBrowserService"/>
+ </intent-filter>
+ </service>
</application>
<instrumentation
diff --git a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
index fd87a76..ad3383a 100644
--- a/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
+++ b/media-compat/tests/src/android/support/v4/media/MediaBrowserCompatTest.java
@@ -19,7 +19,9 @@
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -58,6 +60,10 @@
private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
"android.support.mediacompat.test",
"android.support.v4.media.StubMediaBrowserServiceCompat");
+ private static final ComponentName TEST_REMOTE_BROWSER_SERVICE = new ComponentName(
+ "android.support.mediacompat.test",
+ "android.support.v4.media.StubRemoteMediaBrowserServiceCompat");
+
private static final ComponentName TEST_INVALID_BROWSER_SERVICE = new ComponentName(
"invalid.package", "invalid.ServiceClassName");
private final StubConnectionCallback mConnectionCallback = new StubConnectionCallback();
@@ -71,10 +77,10 @@
public void testMediaBrowser() {
resetCallbacks();
createMediaBrowser(TEST_BROWSER_SERVICE);
- assertEquals(false, mMediaBrowser.isConnected());
+ assertFalse(mMediaBrowser.isConnected());
connectMediaBrowserService();
- assertEquals(true, mMediaBrowser.isConnected());
+ assertTrue(mMediaBrowser.isConnected());
assertEquals(TEST_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
assertEquals(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
@@ -90,6 +96,33 @@
return !mMediaBrowser.isConnected();
}
}.run();
+ assertFalse(mMediaBrowser.isConnected());
+ }
+
+ @Test
+ @SmallTest
+ public void testMediaBrowserWithRemoteService() {
+ resetCallbacks();
+ createMediaBrowser(TEST_REMOTE_BROWSER_SERVICE);
+ assertFalse(mMediaBrowser.isConnected());
+
+ connectMediaBrowserService();
+ assertTrue(mMediaBrowser.isConnected());
+
+ assertEquals(TEST_REMOTE_BROWSER_SERVICE, mMediaBrowser.getServiceComponent());
+ assertEquals(StubRemoteMediaBrowserServiceCompat.MEDIA_ID_ROOT, mMediaBrowser.getRoot());
+ assertEquals(StubRemoteMediaBrowserServiceCompat.EXTRAS_VALUE,
+ mMediaBrowser.getExtras().getString(
+ StubRemoteMediaBrowserServiceCompat.EXTRAS_KEY));
+
+ mMediaBrowser.disconnect();
+ new PollingCheck(TIME_OUT_MS) {
+ @Override
+ protected boolean check() {
+ return !mMediaBrowser.isConnected();
+ }
+ }.run();
+ assertFalse(mMediaBrowser.isConnected());
}
@Test
diff --git a/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
new file mode 100644
index 0000000..9d7e73d
--- /dev/null
+++ b/media-compat/tests/src/android/support/v4/media/StubRemoteMediaBrowserServiceCompat.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.os.Bundle;
+import android.support.v4.media.MediaBrowserCompat.MediaItem;
+import android.support.v4.media.session.MediaSessionCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Stub implementation of {@link android.support.v4.media.MediaBrowserServiceCompat}.
+ */
+public class StubRemoteMediaBrowserServiceCompat extends MediaBrowserServiceCompat {
+ static final String EXTRAS_KEY = "test_extras_key";
+ static final String EXTRAS_VALUE = "test_extras_value";
+
+ static final String MEDIA_ID_ROOT = "test_media_id_root";
+
+ static final String[] MEDIA_ID_CHILDREN = new String[]{
+ "test_media_id_children_0", "test_media_id_children_1",
+ "test_media_id_children_2", "test_media_id_children_3"
+ };
+
+ private static MediaSessionCompat mSession;
+ private Bundle mExtras;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mSession = new MediaSessionCompat(this, "StubRemoteMediaBrowserServiceCompat");
+ setSessionToken(mSession.getSessionToken());
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
+ mExtras = new Bundle();
+ mExtras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+ return new BrowserRoot(MEDIA_ID_ROOT, mExtras);
+ }
+
+ @Override
+ public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) {
+ List<MediaItem> mediaItems = new ArrayList<>();
+ if (MEDIA_ID_ROOT.equals(parentMediaId)) {
+ Bundle rootHints = getBrowserRootHints();
+ for (String id : MEDIA_ID_CHILDREN) {
+ mediaItems.add(createMediaItem(id));
+ }
+ result.sendResult(mediaItems);
+ }
+ }
+
+ private MediaItem createMediaItem(String id) {
+ return new MediaItem(new MediaDescriptionCompat.Builder()
+ .setMediaId(id).setExtras(getBrowserRootHints()).build(),
+ MediaItem.FLAG_BROWSABLE);
+ }
+}
diff --git a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java b/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
index 1fe0874..2d0ab62 100644
--- a/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
+++ b/v17/leanback/api21/android/support/v17/leanback/transition/TransitionHelperApi21.java
@@ -55,10 +55,18 @@
return window.getSharedElementEnterTransition();
}
+ public static void setSharedElementEnterTransition(Window window, Object transition) {
+ window.setSharedElementEnterTransition((Transition) transition);
+ }
+
public static Object getSharedElementReturnTransition(Window window) {
return window.getSharedElementReturnTransition();
}
+ public static void setSharedElementReturnTransition(Window window, Object transition) {
+ window.setSharedElementReturnTransition((Transition) transition);
+ }
+
public static Object getSharedElementExitTransition(Window window) {
return window.getSharedElementExitTransition();
}
@@ -71,10 +79,18 @@
return window.getEnterTransition();
}
+ public static void setEnterTransition(Window window, Object transition) {
+ window.setEnterTransition((Transition) transition);
+ }
+
public static Object getReturnTransition(Window window) {
return window.getReturnTransition();
}
+ public static void setReturnTransition(Window window, Object transition) {
+ window.setReturnTransition((Transition) transition);
+ }
+
public static Object getExitTransition(Window window) {
return window.getExitTransition();
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
index ff5ef2e..b123b9c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseFragment.java
@@ -13,12 +13,12 @@
*/
package android.support.v17.leanback.app;
-import static android.support.v17.leanback.util.StateMachine.STATUS_EXECUTED;
-
import android.os.Bundle;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.util.StateMachine.Condition;
+import android.support.v17.leanback.util.StateMachine.Event;
import android.support.v17.leanback.util.StateMachine.State;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -30,15 +30,20 @@
class BaseFragment extends BrandedFragment {
/**
- * Condition: {@link TransitionHelper#systemSupportsEntranceTransitions()} is true
- * Action: none
+ * The start state for all
*/
- private final State STATE_ALLOWED = new State() {
- @Override
- public boolean canRun() {
- return TransitionHelper.systemSupportsEntranceTransitions();
- }
+ final State STATE_START = new State("START", true, false);
+ /**
+ * Initial State for ENTRNACE transition.
+ */
+ final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
+
+ /**
+ * prepareEntranceTransition is just called, but view not ready yet. We can enable the
+ * busy spinner.
+ */
+ final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
@Override
public void run() {
mProgressBarManager.show();
@@ -46,15 +51,13 @@
};
/**
- * Condition: {@link #isReadyForPrepareEntranceTransition()} is true
- * Action: {@link #onEntranceTransitionPrepare()} }
+ * prepareEntranceTransition is called and main content view to slide in was created, so we can
+ * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
+ * in this State, the process is very different in subclass, e.g. BrowseFragment hide header
+ * views and hide main fragment view in two steps.
*/
- private final State STATE_PREPARE = new State() {
- @Override
- public boolean canRun() {
- return isReadyForPrepareEntranceTransition();
- }
-
+ final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
+ "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
@Override
public void run() {
onEntranceTransitionPrepare();
@@ -62,15 +65,9 @@
};
/**
- * Condition: {@link #isReadyForStartEntranceTransition()} is true
- * Action: {@link #onExecuteEntranceTransition()} }
+ * execute the entrance transition.
*/
- private final State STATE_START = new State() {
- @Override
- public boolean canRun() {
- return isReadyForStartEntranceTransition();
- }
-
+ final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
@Override
public void run() {
mProgressBarManager.hide();
@@ -78,26 +75,109 @@
}
};
- final StateMachine mEnterTransitionStates;
+ /**
+ * execute onEntranceTransitionEnd.
+ */
+ final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
+ @Override
+ public void run() {
+ onEntranceTransitionEnd();
+ }
+ };
+
+ /**
+ * either entrance transition completed or skipped
+ */
+ final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
+
+ /**
+ * Event fragment.onCreate()
+ */
+ final Event EVT_ON_CREATE = new Event("onCreate");
+
+ /**
+ * Event fragment.onViewCreated()
+ */
+ final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
+
+ /**
+ * Event for {@link #prepareEntranceTransition()} is called.
+ */
+ final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
+
+ /**
+ * Event for {@link #startEntranceTransition()} is called.
+ */
+ final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
+
+ /**
+ * Event for entrance transition is ended through Transition listener.
+ */
+ final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
+
+ /**
+ * Event for skipping entrance transition if not supported.
+ */
+ final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
+ @Override
+ public boolean canProceed() {
+ return !TransitionHelper.systemSupportsEntranceTransitions();
+ }
+ };
+
+ final StateMachine mStateMachine = new StateMachine();
Object mEntranceTransition;
final ProgressBarManager mProgressBarManager = new ProgressBarManager();
BaseFragment() {
- mEnterTransitionStates = new StateMachine();
- mEnterTransitionStates.addState(STATE_ALLOWED);
- mEnterTransitionStates.addState(STATE_PREPARE);
- mEnterTransitionStates.addState(STATE_START);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ createStateMachineStates();
+ createStateMachineTransitions();
+ mStateMachine.start();
+ super.onCreate(savedInstanceState);
+ mStateMachine.fireEvent(EVT_ON_CREATE);
+ }
+
+ void createStateMachineStates() {
+ mStateMachine.addState(STATE_START);
+ mStateMachine.addState(STATE_ENTRANCE_INIT);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
+ mStateMachine.addState(STATE_ENTRANCE_PERFORM);
+ mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
+ mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
+ }
+
+ void createStateMachineTransitions() {
+ mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
+ EVT_PREPARE_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_PERFORM,
+ EVT_START_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ STATE_ENTRANCE_PERFORM);
+ mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
+ STATE_ENTRANCE_ON_ENDED,
+ EVT_ENTRANCE_END);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- performPendingStates();
- }
-
- final void performPendingStates() {
- mEnterTransitionStates.runPendingStates();
+ mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
}
/**
@@ -127,18 +207,7 @@
* override the default transition that browse and details provides.
*/
public void prepareEntranceTransition() {
- mEnterTransitionStates.runState(STATE_ALLOWED);
- mEnterTransitionStates.runState(STATE_PREPARE);
- }
-
- /**
- * Return true if entrance transition is enabled and not started yet.
- * Entrance transition can only be executed once and isEntranceTransitionEnabled()
- * is reset to false after entrance transition is started.
- */
- boolean isEntranceTransitionEnabled() {
- // Enabled when passed STATE_ALLOWED in prepareEntranceTransition call.
- return STATE_ALLOWED.getStatus() == STATUS_EXECUTED;
+ mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
}
/**
@@ -179,26 +248,6 @@
}
/**
- * Returns true if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- * @return True if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- */
- boolean isReadyForPrepareEntranceTransition() {
- return getView() != null;
- }
-
- /**
- * Returns true if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- * @return True if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- */
- boolean isReadyForStartEntranceTransition() {
- return getView() != null;
- }
-
- /**
* When fragment finishes loading data, it should call startEntranceTransition()
* to execute the entrance transition.
* startEntranceTransition() will start transition only if both two conditions
@@ -210,7 +259,7 @@
* and executed when view is created.
*/
public void startEntranceTransition() {
- mEnterTransitionStates.runState(STATE_START);
+ mStateMachine.fireEvent(EVT_START_ENTRANCE);
}
void onExecuteEntranceTransition() {
@@ -225,9 +274,11 @@
return true;
}
internalCreateEntranceTransition();
+ onEntranceTransitionStart();
if (mEntranceTransition != null) {
- onEntranceTransitionStart();
runEntranceTransition(mEntranceTransition);
+ } else {
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
}
return false;
}
@@ -244,8 +295,7 @@
@Override
public void onTransitionEnd(Object transition) {
mEntranceTransition = null;
- onEntranceTransitionEnd();
- mEnterTransitionStates.resetStatus();
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
}
});
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
index 62ee0d4..0db81b3 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseSupportFragment.java
@@ -16,12 +16,12 @@
*/
package android.support.v17.leanback.app;
-import static android.support.v17.leanback.util.StateMachine.STATUS_EXECUTED;
-
import android.os.Bundle;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
import android.support.v17.leanback.util.StateMachine;
+import android.support.v17.leanback.util.StateMachine.Condition;
+import android.support.v17.leanback.util.StateMachine.Event;
import android.support.v17.leanback.util.StateMachine.State;
import android.view.View;
import android.view.ViewTreeObserver;
@@ -33,15 +33,20 @@
class BaseSupportFragment extends BrandedSupportFragment {
/**
- * Condition: {@link TransitionHelper#systemSupportsEntranceTransitions()} is true
- * Action: none
+ * The start state for all
*/
- private final State STATE_ALLOWED = new State() {
- @Override
- public boolean canRun() {
- return TransitionHelper.systemSupportsEntranceTransitions();
- }
+ final State STATE_START = new State("START", true, false);
+ /**
+ * Initial State for ENTRNACE transition.
+ */
+ final State STATE_ENTRANCE_INIT = new State("ENTRANCE_INIT");
+
+ /**
+ * prepareEntranceTransition is just called, but view not ready yet. We can enable the
+ * busy spinner.
+ */
+ final State STATE_ENTRANCE_ON_PREPARED = new State("ENTRANCE_ON_PREPARED", true, false) {
@Override
public void run() {
mProgressBarManager.show();
@@ -49,15 +54,13 @@
};
/**
- * Condition: {@link #isReadyForPrepareEntranceTransition()} is true
- * Action: {@link #onEntranceTransitionPrepare()} }
+ * prepareEntranceTransition is called and main content view to slide in was created, so we can
+ * call {@link #onEntranceTransitionPrepare}. Note that we dont set initial content to invisible
+ * in this State, the process is very different in subclass, e.g. BrowseSupportFragment hide header
+ * views and hide main fragment view in two steps.
*/
- private final State STATE_PREPARE = new State() {
- @Override
- public boolean canRun() {
- return isReadyForPrepareEntranceTransition();
- }
-
+ final State STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW = new State(
+ "ENTRANCE_ON_PREPARED_ON_CREATEVIEW") {
@Override
public void run() {
onEntranceTransitionPrepare();
@@ -65,15 +68,9 @@
};
/**
- * Condition: {@link #isReadyForStartEntranceTransition()} is true
- * Action: {@link #onExecuteEntranceTransition()} }
+ * execute the entrance transition.
*/
- private final State STATE_START = new State() {
- @Override
- public boolean canRun() {
- return isReadyForStartEntranceTransition();
- }
-
+ final State STATE_ENTRANCE_PERFORM = new State("STATE_ENTRANCE_PERFORM") {
@Override
public void run() {
mProgressBarManager.hide();
@@ -81,26 +78,109 @@
}
};
- final StateMachine mEnterTransitionStates;
+ /**
+ * execute onEntranceTransitionEnd.
+ */
+ final State STATE_ENTRANCE_ON_ENDED = new State("ENTRANCE_ON_ENDED") {
+ @Override
+ public void run() {
+ onEntranceTransitionEnd();
+ }
+ };
+
+ /**
+ * either entrance transition completed or skipped
+ */
+ final State STATE_ENTRANCE_COMPLETE = new State("ENTRANCE_COMPLETE", true, false);
+
+ /**
+ * Event fragment.onCreate()
+ */
+ final Event EVT_ON_CREATE = new Event("onCreate");
+
+ /**
+ * Event fragment.onViewCreated()
+ */
+ final Event EVT_ON_CREATEVIEW = new Event("onCreateView");
+
+ /**
+ * Event for {@link #prepareEntranceTransition()} is called.
+ */
+ final Event EVT_PREPARE_ENTRANCE = new Event("prepareEntranceTransition");
+
+ /**
+ * Event for {@link #startEntranceTransition()} is called.
+ */
+ final Event EVT_START_ENTRANCE = new Event("startEntranceTransition");
+
+ /**
+ * Event for entrance transition is ended through Transition listener.
+ */
+ final Event EVT_ENTRANCE_END = new Event("onEntranceTransitionEnd");
+
+ /**
+ * Event for skipping entrance transition if not supported.
+ */
+ final Condition COND_TRANSITION_NOT_SUPPORTED = new Condition("EntranceTransitionNotSupport") {
+ @Override
+ public boolean canProceed() {
+ return !TransitionHelper.systemSupportsEntranceTransitions();
+ }
+ };
+
+ final StateMachine mStateMachine = new StateMachine();
Object mEntranceTransition;
final ProgressBarManager mProgressBarManager = new ProgressBarManager();
BaseSupportFragment() {
- mEnterTransitionStates = new StateMachine();
- mEnterTransitionStates.addState(STATE_ALLOWED);
- mEnterTransitionStates.addState(STATE_PREPARE);
- mEnterTransitionStates.addState(STATE_START);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ createStateMachineStates();
+ createStateMachineTransitions();
+ mStateMachine.start();
+ super.onCreate(savedInstanceState);
+ mStateMachine.fireEvent(EVT_ON_CREATE);
+ }
+
+ void createStateMachineStates() {
+ mStateMachine.addState(STATE_START);
+ mStateMachine.addState(STATE_ENTRANCE_INIT);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED);
+ mStateMachine.addState(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW);
+ mStateMachine.addState(STATE_ENTRANCE_PERFORM);
+ mStateMachine.addState(STATE_ENTRANCE_ON_ENDED);
+ mStateMachine.addState(STATE_ENTRANCE_COMPLETE);
+ }
+
+ void createStateMachineTransitions() {
+ mStateMachine.addTransition(STATE_START, STATE_ENTRANCE_INIT, EVT_ON_CREATE);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_COMPLETE,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_ENTRANCE_ON_PREPARED,
+ EVT_PREPARE_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_ON_CREATEVIEW);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_PERFORM,
+ EVT_START_ENTRANCE);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ STATE_ENTRANCE_PERFORM);
+ mStateMachine.addTransition(STATE_ENTRANCE_PERFORM,
+ STATE_ENTRANCE_ON_ENDED,
+ EVT_ENTRANCE_END);
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_ENDED, STATE_ENTRANCE_COMPLETE);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- performPendingStates();
- }
-
- final void performPendingStates() {
- mEnterTransitionStates.runPendingStates();
+ mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
}
/**
@@ -130,18 +210,7 @@
* override the default transition that browse and details provides.
*/
public void prepareEntranceTransition() {
- mEnterTransitionStates.runState(STATE_ALLOWED);
- mEnterTransitionStates.runState(STATE_PREPARE);
- }
-
- /**
- * Return true if entrance transition is enabled and not started yet.
- * Entrance transition can only be executed once and isEntranceTransitionEnabled()
- * is reset to false after entrance transition is started.
- */
- boolean isEntranceTransitionEnabled() {
- // Enabled when passed STATE_ALLOWED in prepareEntranceTransition call.
- return STATE_ALLOWED.getStatus() == STATUS_EXECUTED;
+ mStateMachine.fireEvent(EVT_PREPARE_ENTRANCE);
}
/**
@@ -182,26 +251,6 @@
}
/**
- * Returns true if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- * @return True if it is ready to perform {@link #prepareEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- */
- boolean isReadyForPrepareEntranceTransition() {
- return getView() != null;
- }
-
- /**
- * Returns true if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- * @return True if it is ready to perform {@link #startEntranceTransition()}, false otherwise.
- * Subclass may override and add additional conditions.
- */
- boolean isReadyForStartEntranceTransition() {
- return getView() != null;
- }
-
- /**
* When fragment finishes loading data, it should call startEntranceTransition()
* to execute the entrance transition.
* startEntranceTransition() will start transition only if both two conditions
@@ -213,7 +262,7 @@
* and executed when view is created.
*/
public void startEntranceTransition() {
- mEnterTransitionStates.runState(STATE_START);
+ mStateMachine.fireEvent(EVT_START_ENTRANCE);
}
void onExecuteEntranceTransition() {
@@ -228,9 +277,11 @@
return true;
}
internalCreateEntranceTransition();
+ onEntranceTransitionStart();
if (mEntranceTransition != null) {
- onEntranceTransitionStart();
runEntranceTransition(mEntranceTransition);
+ } else {
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
}
return false;
}
@@ -247,8 +298,7 @@
@Override
public void onTransitionEnd(Object transition) {
mEntranceTransition = null;
- onEntranceTransitionEnd();
- mEnterTransitionStates.resetStatus();
+ mStateMachine.fireEvent(EVT_ENTRANCE_END);
}
});
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
index f16d569..35350e4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrandedFragment.java
@@ -150,6 +150,7 @@
}
if (mTitleView != null && view instanceof ViewGroup) {
mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
+ mTitleHelper.showTitle(mShowingTitle);
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
index 1a0d81f..9c42780 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrandedSupportFragment.java
@@ -153,6 +153,7 @@
}
if (mTitleView != null && view instanceof ViewGroup) {
mTitleHelper = new TitleHelper((ViewGroup) view, mTitleView);
+ mTitleHelper.showTitle(mShowingTitle);
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index 8bbe601..c64673b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -28,6 +28,8 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BrowseFrameLayout;
import android.support.v17.leanback.widget.InvisibleRowPresenter;
import android.support.v17.leanback.widget.ListRow;
@@ -86,6 +88,56 @@
private static final String IS_PAGE_ROW = "isPageRow";
private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+ /**
+ * State to hide headers fragment.
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionStartState();
+ }
+ };
+
+ /**
+ * Event for Header fragment view is created, we could perform
+ * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+ */
+ final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+ /**
+ * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+ * {@link #onEntranceTransitionPrepare()}.
+ */
+ final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+ /**
+ * Event that data for the screen is ready, this is additional requirement to launch entrance
+ * transition.
+ */
+ final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ // when headers fragment view is created we could setEntranceTransitionStartState()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+ EVT_HEADER_VIEW_CREATED);
+
+ // add additional requirement for onEntranceTransitionPrepare()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ // add additional requirement to launch entrance transition.
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
+ EVT_SCREEN_DATA_READY);
+ }
+
final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
int mLastEntryCount;
int mIndexOfHeadersBackStack;
@@ -252,20 +304,21 @@
*/
private final class FragmentHostImpl implements FragmentHost {
boolean mShowTitleView = true;
- boolean mDataReady = false;
FragmentHostImpl() {
}
@Override
public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
- performPendingStates();
+ mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ if (!mIsPageRow) {
+ // If it's not a PageRow: it's a ListRow, so we already have data ready.
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
}
@Override
public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
- mDataReady = true;
-
// If fragment host is not the currently active fragment (in BrowseFragment), then
// ignore the request.
if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
@@ -277,7 +330,7 @@
return;
}
- performPendingStates();
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
}
@Override
@@ -1224,17 +1277,6 @@
}
}
- @Override
- boolean isReadyForPrepareEntranceTransition() {
- return mMainFragment != null && mMainFragment.getView() != null;
- }
-
- @Override
- boolean isReadyForStartEntranceTransition() {
- return mMainFragment != null && mMainFragment.getView() != null
- && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady);
- }
-
void createHeadersTransition() {
mHeadersTransition = TransitionHelper.loadTransition(FragmentUtil.getContext(this),
mShowingHeaders
@@ -1455,7 +1497,6 @@
swapToMainFragment();
expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
setupMainFragment();
- performPendingStates();
}
}
@@ -1565,9 +1606,7 @@
showHeaders(mShowingHeaders);
}
- if (isEntranceTransitionEnabled()) {
- setEntranceTransitionStartState();
- }
+ mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
}
private void onExpandTransitionStart(boolean expand, final Runnable callback) {
@@ -1683,8 +1722,6 @@
@Override
protected void onEntranceTransitionPrepare() {
mHeadersFragment.onTransitionPrepare();
- // setEntranceTransitionStartState() might be called when mMainFragment is null,
- // make sure it is called.
mMainFragmentAdapter.setEntranceTransitionState(false);
mMainFragmentAdapter.onTransitionPrepare();
}
@@ -1718,7 +1755,9 @@
void setEntranceTransitionStartState() {
setHeadersOnScreen(false);
setSearchOrbViewOnScreen(false);
- mMainFragmentAdapter.setEntranceTransitionState(false);
+ // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+ // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+ // one when setEntranceTransitionStartState() is called.
}
void setEntranceTransitionEndState() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
index 9215d09..967bf56 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseSupportFragment.java
@@ -31,6 +31,8 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BrowseFrameLayout;
import android.support.v17.leanback.widget.InvisibleRowPresenter;
import android.support.v17.leanback.widget.ListRow;
@@ -89,6 +91,56 @@
private static final String IS_PAGE_ROW = "isPageRow";
private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition";
+ /**
+ * State to hide headers fragment.
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionStartState();
+ }
+ };
+
+ /**
+ * Event for Header fragment view is created, we could perform
+ * {@link #setEntranceTransitionStartState()} to hide headers fragment initially.
+ */
+ final Event EVT_HEADER_VIEW_CREATED = new Event("headerFragmentViewCreated");
+
+ /**
+ * Event for {@link #getMainFragment()} view is created, it's additional requirement to execute
+ * {@link #onEntranceTransitionPrepare()}.
+ */
+ final Event EVT_MAIN_FRAGMENT_VIEW_CREATED = new Event("mainFragmentViewCreated");
+
+ /**
+ * Event that data for the screen is ready, this is additional requirement to launch entrance
+ * transition.
+ */
+ final Event EVT_SCREEN_DATA_READY = new Event("screenDataReady");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ // when headers fragment view is created we could setEntranceTransitionStartState()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_SET_ENTRANCE_START_STATE,
+ EVT_HEADER_VIEW_CREATED);
+
+ // add additional requirement for onEntranceTransitionPrepare()
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_ENTRANCE_ON_PREPARED_ON_CREATEVIEW,
+ EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ // add additional requirement to launch entrance transition.
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED, STATE_ENTRANCE_PERFORM,
+ EVT_SCREEN_DATA_READY);
+ }
+
final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
int mLastEntryCount;
int mIndexOfHeadersBackStack;
@@ -255,20 +307,21 @@
*/
private final class FragmentHostImpl implements FragmentHost {
boolean mShowTitleView = true;
- boolean mDataReady = false;
FragmentHostImpl() {
}
@Override
public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) {
- performPendingStates();
+ mStateMachine.fireEvent(EVT_MAIN_FRAGMENT_VIEW_CREATED);
+ if (!mIsPageRow) {
+ // If it's not a PageRow: it's a ListRow, so we already have data ready.
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
+ }
}
@Override
public void notifyDataReady(MainFragmentAdapter fragmentAdapter) {
- mDataReady = true;
-
// If fragment host is not the currently active fragment (in BrowseSupportFragment), then
// ignore the request.
if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) {
@@ -280,7 +333,7 @@
return;
}
- performPendingStates();
+ mStateMachine.fireEvent(EVT_SCREEN_DATA_READY);
}
@Override
@@ -1227,17 +1280,6 @@
}
}
- @Override
- boolean isReadyForPrepareEntranceTransition() {
- return mMainFragment != null && mMainFragment.getView() != null;
- }
-
- @Override
- boolean isReadyForStartEntranceTransition() {
- return mMainFragment != null && mMainFragment.getView() != null
- && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady);
- }
-
void createHeadersTransition() {
mHeadersTransition = TransitionHelper.loadTransition(getContext(),
mShowingHeaders
@@ -1458,7 +1500,6 @@
swapToMainFragment();
expandMainFragment(!(mCanShowHeaders && mShowingHeaders));
setupMainFragment();
- performPendingStates();
}
}
@@ -1568,9 +1609,7 @@
showHeaders(mShowingHeaders);
}
- if (isEntranceTransitionEnabled()) {
- setEntranceTransitionStartState();
- }
+ mStateMachine.fireEvent(EVT_HEADER_VIEW_CREATED);
}
private void onExpandTransitionStart(boolean expand, final Runnable callback) {
@@ -1686,8 +1725,6 @@
@Override
protected void onEntranceTransitionPrepare() {
mHeadersSupportFragment.onTransitionPrepare();
- // setEntranceTransitionStartState() might be called when mMainFragment is null,
- // make sure it is called.
mMainFragmentAdapter.setEntranceTransitionState(false);
mMainFragmentAdapter.onTransitionPrepare();
}
@@ -1721,7 +1758,9 @@
void setEntranceTransitionStartState() {
setHeadersOnScreen(false);
setSearchOrbViewOnScreen(false);
- mMainFragmentAdapter.setEntranceTransitionState(false);
+ // NOTE that mMainFragmentAdapter.setEntranceTransitionState(false) will be called
+ // in onEntranceTransitionPrepare() because mMainFragmentAdapter is still the dummy
+ // one when setEntranceTransitionStartState() is called.
}
void setEntranceTransitionEndState() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
index c372888..5bae3d0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsBackgroundVideoHelper.java
@@ -77,6 +77,7 @@
this.mDetailsParallax = detailsParallax;
this.mBackgroundDrawable = backgroundDrawable;
mBackgroundDrawableVisible = true;
+ mBackgroundDrawable.setAlpha(255);
startParallax();
}
@@ -163,9 +164,23 @@
}, CROSSFADE_DELAY);
}
- private void crossFadeBackgroundToVideo(final boolean crossFadeToVideo) {
+ void crossFadeBackgroundToVideo(boolean crossFadeToVideo) {
+ crossFadeBackgroundToVideo(crossFadeToVideo, false);
+ }
+
+ void crossFadeBackgroundToVideo(boolean crossFadeToVideo, boolean immediate) {
final boolean newVisible = !crossFadeToVideo;
if (mBackgroundDrawableVisible == newVisible) {
+ if (immediate) {
+ if (mBackgroundAnimator != null) {
+ mBackgroundAnimator.cancel();
+ mBackgroundAnimator = null;
+ }
+ if (mBackgroundDrawable != null) {
+ mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
+ return;
+ }
+ }
return;
}
mBackgroundDrawableVisible = newVisible;
@@ -180,6 +195,10 @@
if (mBackgroundDrawable == null) {
return;
}
+ if (immediate) {
+ mBackgroundDrawable.setAlpha(crossFadeToVideo ? 0 : 255);
+ return;
+ }
mBackgroundAnimator = ValueAnimator.ofFloat(startAlpha, endAlpha);
mBackgroundAnimator.setDuration(BACKGROUND_CROSS_FADE_DURATION);
mBackgroundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 7e54146..57a85a0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -24,6 +24,8 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -41,6 +43,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.Window;
import java.lang.ref.WeakReference;
@@ -87,22 +90,190 @@
static final String TAG = "DetailsFragment";
static boolean DEBUG = false;
+ final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ mRowsFragment.setEntranceTransitionState(false);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
+
+ void switchToVideoBeforeVideoFragmentCreated() {
+ // if the video fragment is not ready: immediately fade out covering drawable,
+ // hide title and mark mPendingFocusOnVideo and set focus on it later.
+ mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+ showTitle(false);
+ mPendingFocusOnVideo = true;
+ slideOutGridView();
+ }
+
+ final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
+ false, false) {
+ @Override
+ public void run() {
+ switchToVideoBeforeVideoFragmentCreated();
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
+ false, false) {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout != null) {
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ // clear the activity enter/sharedElement transition, return transitions are kept.
+ // keep the return transitions and clear enter transition
+ if (getActivity() != null) {
+ Window window = getActivity().getWindow();
+ Object returnTransition = TransitionHelper.getReturnTransition(window);
+ Object sharedReturnTransition = TransitionHelper
+ .getSharedElementReturnTransition(window);
+ TransitionHelper.setEnterTransition(window, null);
+ TransitionHelper.setSharedElementEnterTransition(window, null);
+ TransitionHelper.setReturnTransition(window, returnTransition);
+ TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
+ }
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
+ true, false);
+
+ final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
+ TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout == null) {
+ new WaitEnterTransitionTimeout(DetailsFragment.this);
+ }
+ }
+ };
+
/**
- * Flag for "possibly" having enter transition not finished yet.
- * @see #mStartAndTransitionFlag
+ * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+ * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
*/
- static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
- /**
- * Flag for having entrance transition not finished yet.
- * @see #mStartAndTransitionFlag
- */
- static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
- /**
- * Flag that onStart() has been called and about to call onSafeStart() when
- * pending transitions are finished.
- * @see #mStartAndTransitionFlag
- */
- static final int PF_PENDING_START = 0x1 << 2;
+ static class WaitEnterTransitionTimeout implements Runnable {
+ static final long WAIT_ENTERTRANSITION_START = 200;
+
+ final WeakReference<DetailsFragment> mRef;
+
+ WaitEnterTransitionTimeout(DetailsFragment f) {
+ mRef = new WeakReference(f);
+ f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+ }
+
+ @Override
+ public void run() {
+ DetailsFragment f = mRef.get();
+ if (f != null) {
+ f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
+ }
+ }
+ }
+
+ final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
+ @Override
+ public void run() {
+ onSafeStart();
+ }
+ };
+
+ final Event EVT_ONSTART = new Event("onStart");
+
+ final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
+
+ final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
+
+ final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
+
+ final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ mStateMachine.addState(STATE_ON_SAFE_START);
+ mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ /**
+ * Part 1: Processing enter transitions after fragment.onCreate
+ */
+ mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
+ // if transition is not supported, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ // if transition is not set on Activity, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_NO_ENTER_TRANSITION);
+ // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
+ // complete.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
+ // once after onCreateView, we cannot skip the enter transition, add a listener and wait
+ // it to finish
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
+ EVT_ON_CREATEVIEW);
+ // when enter transition finishes, go to complete, however this might never happen if
+ // the activity is not giving transition options in startActivity, there is no API to query
+ // if this activity is started in a enter transition mode. So we rely on a timer below:
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
+ // we are expecting app to start delayed enter transition shortly after details row is
+ // loaded, so create a timer and wait for enter transition start.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
+ // if enter transition not started in the timer, skip to DONE, this can be also true when
+ // startActivity is not giving transition option.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_ENTER_TRANSIITON_DONE);
+
+ /**
+ * Part 2: modification to the entrance transition defined in BaseFragment
+ */
+ // Must finish enter transition before perform entrance transition.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
+ // Calling switch to video would hide immediately and skip entrance transition
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
+ // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
+ // still need to do the switchToVideo.
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+
+ // for once the view is created in onStart and prepareEntranceTransition was called, we
+ // could setEntranceStartState:
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
+
+ /**
+ * Part 3: onSafeStart()
+ */
+ // for onSafeStart: the condition is onStart called, entrance transition complete
+ mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
+ }
private class SetSelectionRunnable implements Runnable {
int mPosition;
@@ -120,33 +291,6 @@
}
}
- /**
- * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
- * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
- * @see #mStartAndTransitionFlag
- */
- static class WaitEnterTransitionTimeout implements Runnable {
- static final long WAIT_ENTERTRANSITION_START = 200;
-
- final WeakReference<DetailsFragment> mRef;
-
- WaitEnterTransitionTimeout(DetailsFragment f) {
- mRef = new WeakReference(f);
- f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
- }
-
- @Override
- public void run() {
- DetailsFragment f = mRef.get();
- if (f != null) {
- f.clearPendingEnterTransition();
- }
- }
- }
-
- /**
- * @see #mStartAndTransitionFlag
- */
TransitionListener mEnterTransitionListener = new TransitionListener() {
@Override
public void onTransitionStart(Object transition) {
@@ -159,12 +303,12 @@
@Override
public void onTransitionCancel(Object transition) {
- clearPendingEnterTransition();
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
}
@Override
public void onTransitionEnd(Object transition) {
- clearPendingEnterTransition();
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
}
};
@@ -187,23 +331,10 @@
BaseOnItemViewClickedListener mOnItemViewClickedListener;
DetailsFragmentBackgroundController mDetailsBackgroundController;
+ // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
+ // true, we will focus to VideoFragment immediately after video fragment's view is created.
+ boolean mPendingFocusOnVideo = false;
- /**
- * Flags for enter transition, entrance transition and onStart. When onStart() is called
- * and both enter transiton and entrance transition are finished, we could call onSafeStart().
- * 1. in onCreate:
- * if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
- * if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
- * user will run enterTransition or not.
- * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
- * start. If enter transition onTransitionStart is not invoked with a period, we can assume
- * there is no enter transition running, then WaitEnterTransitionTimeout will clear
- * PF_ENTER_TRANSITION_PENDING.
- * 3. When enterTransition runs (either postponed or not), we will stop the
- * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
- * PF_ENTER_TRANSITION_PENDING.
- */
- int mStartAndTransitionFlag = 0;
WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
Object mSceneAfterEntranceTransition;
@@ -287,14 +418,15 @@
Activity activity = getActivity();
if (activity != null) {
Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
- if (transition != null) {
- mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
- TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ if (transition == null) {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
}
transition = TransitionHelper.getReturnTransition(activity.getWindow());
if (transition != null) {
TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
}
+ } else {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
}
}
@@ -449,6 +581,22 @@
}
}
+ void switchToVideo() {
+ if (mVideoFragment != null && mVideoFragment.getView() != null) {
+ mVideoFragment.getView().requestFocus();
+ } else {
+ mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
+ }
+ }
+
+ void switchToRows() {
+ mPendingFocusOnVideo = false;
+ VerticalGridView verticalGridView = getVerticalGridView();
+ if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
+ verticalGridView.requestFocus();
+ }
+ }
+
/**
* This method asks DetailsFragmentBackgroundController to add a fragment for rendering video.
* In case the fragment is already there, it will return the existing one. The method must be
@@ -465,6 +613,18 @@
ft2.add(android.support.v17.leanback.R.id.video_surface_container,
fragment = mDetailsBackgroundController.onCreateVideoFragment());
ft2.commit();
+ if (mPendingFocusOnVideo) {
+ // wait next cycle for Fragment view created so we can focus on it.
+ // This is a bit hack eventually we will do commitNow() which get view immediately.
+ getView().post(new Runnable() {
+ public void run() {
+ if (getView() != null) {
+ switchToVideo();
+ }
+ mPendingFocusOnVideo = false;
+ }
+ });
+ }
}
mVideoFragment = fragment;
return mVideoFragment;
@@ -473,7 +633,7 @@
void onRowSelected(int selectedPosition, int selectedSubPosition) {
ObjectAdapter adapter = getAdapter();
if (( mRowsFragment != null && mRowsFragment.getView() != null
- && mRowsFragment.getView().hasFocus())
+ && mRowsFragment.getView().hasFocus() && !mPendingFocusOnVideo)
&& (adapter == null || adapter.size() == 0
|| (getVerticalGridView().getSelectedPosition() == 0
&& getVerticalGridView().getSelectedSubPosition() == 0))) {
@@ -484,10 +644,8 @@
if (adapter != null && adapter.size() > selectedPosition) {
final VerticalGridView gridView = getVerticalGridView();
final int count = gridView.getChildCount();
- if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
- if (mWaitEnterTransitionTimeout == null) {
- mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
- }
+ if (count > 0) {
+ mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
}
for (int i = 0; i < count; i++) {
ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
@@ -501,25 +659,6 @@
}
}
- void clearPendingEnterTransition() {
- if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
- mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
- dispatchOnStartAndTransitionFinished();
- }
- }
-
- void dispatchOnStartAndTransitionFinished() {
- /**
- * if onStart() was called and there is no pending enter transition or entrance transition.
- */
- if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
- && (mStartAndTransitionFlag
- & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
- mStartAndTransitionFlag &= ~PF_PENDING_START;
- onSafeStart();
- }
- }
-
/**
* Called when onStart and enter transition (postponed/none postponed) and entrance transition
* are all finished.
@@ -614,17 +753,14 @@
public void onStart() {
super.onStart();
- mStartAndTransitionFlag |= PF_PENDING_START;
- dispatchOnStartAndTransitionFinished();
-
setupChildFragmentLayout();
- if (isEntranceTransitionEnabled()) {
- mRowsFragment.setEntranceTransitionState(false);
- }
+ mStateMachine.fireEvent(EVT_ONSTART);
if (mDetailsParallax != null) {
mDetailsParallax.setRecyclerView(mRowsFragment.getVerticalGridView());
}
- if (!getView().hasFocus()) {
+ if (mPendingFocusOnVideo) {
+ slideOutGridView();
+ } else if (!getView().hasFocus()) {
mRowsFragment.getVerticalGridView().requestFocus();
}
}
@@ -642,14 +778,11 @@
@Override
protected void onEntranceTransitionEnd() {
- mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
- dispatchOnStartAndTransitionFinished();
mRowsFragment.onTransitionEnd();
}
@Override
protected void onEntranceTransitionPrepare() {
- mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
mRowsFragment.onTransitionPrepare();
}
@@ -711,8 +844,10 @@
public void onRequestChildFocus(View child, View focused) {
if (child != mRootView.getFocusedChild()) {
if (child.getId() == R.id.details_fragment_root) {
- slideInGridView();
- showTitle(true);
+ if (!mPendingFocusOnVideo) {
+ slideInGridView();
+ showTitle(true);
+ }
} else if (child.getId() == R.id.video_surface_container) {
slideOutGridView();
showTitle(false);
@@ -758,8 +893,10 @@
if (mVideoFragment != null && mVideoFragment.getView() != null
&& mVideoFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
- getVerticalGridView().requestFocus();
- return true;
+ if (getVerticalGridView().getChildCount() > 0) {
+ getVerticalGridView().requestFocus();
+ return true;
+ }
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
index 625a196..f6f389c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragmentBackgroundController.java
@@ -204,6 +204,10 @@
bottomDrawable,
coverDrawableParallaxTarget);
mFragment.setBackgroundDrawable(mParallaxDrawable);
+ // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
+ // before PlaybackGlue is ready.
+ mVideoHelper = new DetailsBackgroundVideoHelper(null,
+ mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
}
/**
@@ -227,12 +231,7 @@
mPlaybackGlue.setHost(null);
}
mPlaybackGlue = playbackGlue;
- if (mVideoHelper == null && mPlaybackGlue != null) {
- mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
- mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
- } else if (mVideoHelper != null) {
- mVideoHelper.setPlaybackGlue(mPlaybackGlue);
- }
+ mVideoHelper.setPlaybackGlue(mPlaybackGlue);
if (mCanUseHost && mPlaybackGlue != null) {
mPlaybackGlue.setHost(onCreateGlueHost());
}
@@ -259,23 +258,36 @@
return mPlaybackGlue != null;
}
+ void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
+ mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+ }
+
/**
* Switch to video fragment, note that this method is not affected by result of
- * {@link #canNavigateToVideoFragment()}.
+ * {@link #canNavigateToVideoFragment()}. If the method is called in DetailsFragment.onCreate()
+ * it will make video fragment to be initially focused once it is created.
+ * <p>
+ * Calling switchToVideo() in DetailsFragment.onCreate() will clear the activity enter
+ * transition and shared element transition.
+ * </p>
+ * <p>
+ * If switchToVideo() is called after {@link DetailsFragment#prepareEntranceTransition()} and
+ * before {@link DetailsFragment#onEntranceTransitionEnd()}, it will be ignored.
+ * </p>
+ * <p>
+ * If {@link DetailsFragment#prepareEntranceTransition()} is called after switchToVideo(), an
+ * IllegalStateException will be thrown.
+ * </p>
*/
public final void switchToVideo() {
- if (mFragment.mVideoFragment != null && mFragment.mVideoFragment.getView() != null) {
- mFragment.mVideoFragment.getView().requestFocus();
- }
+ mFragment.switchToVideo();
}
/**
* Switch to rows fragment.
*/
public final void switchToRows() {
- if (mFragment.mRowsFragment != null && mFragment.mRowsFragment.getView() != null) {
- mFragment.mRowsFragment.getView().requestFocus();
- }
+ mFragment.switchToRows();
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
index 511a90b..1a74ce1 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragment.java
@@ -27,6 +27,8 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
import android.support.v17.leanback.transition.TransitionListener;
+import android.support.v17.leanback.util.StateMachine.Event;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BaseOnItemViewClickedListener;
import android.support.v17.leanback.widget.BaseOnItemViewSelectedListener;
import android.support.v17.leanback.widget.BrowseFrameLayout;
@@ -44,6 +46,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.Window;
import java.lang.ref.WeakReference;
@@ -90,22 +93,190 @@
static final String TAG = "DetailsSupportFragment";
static boolean DEBUG = false;
+ final State STATE_SET_ENTRANCE_START_STATE = new State("STATE_SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ mRowsSupportFragment.setEntranceTransitionState(false);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_INIT = new State("STATE_ENTER_TRANSIITON_INIT");
+
+ void switchToVideoBeforeVideoSupportFragmentCreated() {
+ // if the video fragment is not ready: immediately fade out covering drawable,
+ // hide title and mark mPendingFocusOnVideo and set focus on it later.
+ mDetailsBackgroundController.crossFadeBackgroundToVideo(true, true);
+ showTitle(false);
+ mPendingFocusOnVideo = true;
+ slideOutGridView();
+ }
+
+ final State STATE_SWITCH_TO_VIDEO_IN_ON_CREATE = new State("STATE_SWITCH_TO_VIDEO_IN_ON_CREATE",
+ false, false) {
+ @Override
+ public void run() {
+ switchToVideoBeforeVideoSupportFragmentCreated();
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_CANCEL = new State("STATE_ENTER_TRANSITION_CANCEL",
+ false, false) {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout != null) {
+ mWaitEnterTransitionTimeout.mRef.clear();
+ }
+ // clear the activity enter/sharedElement transition, return transitions are kept.
+ // keep the return transitions and clear enter transition
+ if (getActivity() != null) {
+ Window window = getActivity().getWindow();
+ Object returnTransition = TransitionHelper.getReturnTransition(window);
+ Object sharedReturnTransition = TransitionHelper
+ .getSharedElementReturnTransition(window);
+ TransitionHelper.setEnterTransition(window, null);
+ TransitionHelper.setSharedElementEnterTransition(window, null);
+ TransitionHelper.setReturnTransition(window, returnTransition);
+ TransitionHelper.setSharedElementReturnTransition(window, sharedReturnTransition);
+ }
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_COMPLETE = new State("STATE_ENTER_TRANSIITON_COMPLETE",
+ true, false);
+
+ final State STATE_ENTER_TRANSITION_ADDLISTENER = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ Object transition = TransitionHelper.getEnterTransition(getActivity().getWindow());
+ TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ }
+ };
+
+ final State STATE_ENTER_TRANSITION_PENDING = new State("STATE_ENTER_TRANSITION_PENDING") {
+ @Override
+ public void run() {
+ if (mWaitEnterTransitionTimeout == null) {
+ new WaitEnterTransitionTimeout(DetailsSupportFragment.this);
+ }
+ }
+ };
+
/**
- * Flag for "possibly" having enter transition not finished yet.
- * @see #mStartAndTransitionFlag
+ * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
+ * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
*/
- static final int PF_ENTER_TRANSITION_PENDING = 0x1 << 0;
- /**
- * Flag for having entrance transition not finished yet.
- * @see #mStartAndTransitionFlag
- */
- static final int PF_ENTRANCE_TRANSITION_PENDING = 0x1 << 1;
- /**
- * Flag that onStart() has been called and about to call onSafeStart() when
- * pending transitions are finished.
- * @see #mStartAndTransitionFlag
- */
- static final int PF_PENDING_START = 0x1 << 2;
+ static class WaitEnterTransitionTimeout implements Runnable {
+ static final long WAIT_ENTERTRANSITION_START = 200;
+
+ final WeakReference<DetailsSupportFragment> mRef;
+
+ WaitEnterTransitionTimeout(DetailsSupportFragment f) {
+ mRef = new WeakReference(f);
+ f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
+ }
+
+ @Override
+ public void run() {
+ DetailsSupportFragment f = mRef.get();
+ if (f != null) {
+ f.mStateMachine.fireEvent(f.EVT_ENTER_TRANSIITON_DONE);
+ }
+ }
+ }
+
+ final State STATE_ON_SAFE_START = new State("STATE_ON_SAFE_START") {
+ @Override
+ public void run() {
+ onSafeStart();
+ }
+ };
+
+ final Event EVT_ONSTART = new Event("onStart");
+
+ final Event EVT_NO_ENTER_TRANSITION = new Event("EVT_NO_ENTER_TRANSITION");
+
+ final Event EVT_DETAILS_ROW_LOADED = new Event("onFirstRowLoaded");
+
+ final Event EVT_ENTER_TRANSIITON_DONE = new Event("onEnterTransitionDone");
+
+ final Event EVT_SWITCH_TO_VIDEO = new Event("switchToVideo");
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ mStateMachine.addState(STATE_ON_SAFE_START);
+ mStateMachine.addState(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_INIT);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_ADDLISTENER);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_CANCEL);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_PENDING);
+ mStateMachine.addState(STATE_ENTER_TRANSITION_COMPLETE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ /**
+ * Part 1: Processing enter transitions after fragment.onCreate
+ */
+ mStateMachine.addTransition(STATE_START, STATE_ENTER_TRANSITION_INIT, EVT_ON_CREATE);
+ // if transition is not supported, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ COND_TRANSITION_NOT_SUPPORTED);
+ // if transition is not set on Activity, skip to complete
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_NO_ENTER_TRANSITION);
+ // if switchToVideo is called before EVT_ON_CREATEVIEW, clear enter transition and skip to
+ // complete.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_CANCEL,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_CANCEL, STATE_ENTER_TRANSITION_COMPLETE);
+ // once after onCreateView, we cannot skip the enter transition, add a listener and wait
+ // it to finish
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_INIT, STATE_ENTER_TRANSITION_ADDLISTENER,
+ EVT_ON_CREATEVIEW);
+ // when enter transition finishes, go to complete, however this might never happen if
+ // the activity is not giving transition options in startActivity, there is no API to query
+ // if this activity is started in a enter transition mode. So we rely on a timer below:
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_COMPLETE, EVT_ENTER_TRANSIITON_DONE);
+ // we are expecting app to start delayed enter transition shortly after details row is
+ // loaded, so create a timer and wait for enter transition start.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_ADDLISTENER,
+ STATE_ENTER_TRANSITION_PENDING, EVT_DETAILS_ROW_LOADED);
+ // if enter transition not started in the timer, skip to DONE, this can be also true when
+ // startActivity is not giving transition option.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_PENDING, STATE_ENTER_TRANSITION_COMPLETE,
+ EVT_ENTER_TRANSIITON_DONE);
+
+ /**
+ * Part 2: modification to the entrance transition defined in BaseSupportFragment
+ */
+ // Must finish enter transition before perform entrance transition.
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ENTRANCE_PERFORM);
+ // Calling switch to video would hide immediately and skip entrance transition
+ mStateMachine.addTransition(STATE_ENTRANCE_INIT, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+ mStateMachine.addTransition(STATE_SWITCH_TO_VIDEO_IN_ON_CREATE, STATE_ENTRANCE_COMPLETE);
+ // if the entrance transition is skipped to complete by COND_TRANSITION_NOT_SUPPORTED, we
+ // still need to do the switchToVideo.
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_SWITCH_TO_VIDEO_IN_ON_CREATE,
+ EVT_SWITCH_TO_VIDEO);
+
+ // for once the view is created in onStart and prepareEntranceTransition was called, we
+ // could setEntranceStartState:
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ONSTART);
+
+ /**
+ * Part 3: onSafeStart()
+ */
+ // for onSafeStart: the condition is onStart called, entrance transition complete
+ mStateMachine.addTransition(STATE_START, STATE_ON_SAFE_START, EVT_ONSTART);
+ mStateMachine.addTransition(STATE_ENTRANCE_COMPLETE, STATE_ON_SAFE_START);
+ mStateMachine.addTransition(STATE_ENTER_TRANSITION_COMPLETE, STATE_ON_SAFE_START);
+ }
private class SetSelectionRunnable implements Runnable {
int mPosition;
@@ -123,33 +294,6 @@
}
}
- /**
- * Start this task when first DetailsOverviewRow is created, if there is no entrance transition
- * started, it will clear PF_ENTRANCE_TRANSITION_PENDING.
- * @see #mStartAndTransitionFlag
- */
- static class WaitEnterTransitionTimeout implements Runnable {
- static final long WAIT_ENTERTRANSITION_START = 200;
-
- final WeakReference<DetailsSupportFragment> mRef;
-
- WaitEnterTransitionTimeout(DetailsSupportFragment f) {
- mRef = new WeakReference(f);
- f.getView().postDelayed(this, WAIT_ENTERTRANSITION_START);
- }
-
- @Override
- public void run() {
- DetailsSupportFragment f = mRef.get();
- if (f != null) {
- f.clearPendingEnterTransition();
- }
- }
- }
-
- /**
- * @see #mStartAndTransitionFlag
- */
TransitionListener mEnterTransitionListener = new TransitionListener() {
@Override
public void onTransitionStart(Object transition) {
@@ -162,12 +306,12 @@
@Override
public void onTransitionCancel(Object transition) {
- clearPendingEnterTransition();
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
}
@Override
public void onTransitionEnd(Object transition) {
- clearPendingEnterTransition();
+ mStateMachine.fireEvent(EVT_ENTER_TRANSIITON_DONE);
}
};
@@ -190,23 +334,10 @@
BaseOnItemViewClickedListener mOnItemViewClickedListener;
DetailsSupportFragmentBackgroundController mDetailsBackgroundController;
+ // A temporarily flag when switchToVideo() is called in onCreate(), if mPendingFocusOnVideo is
+ // true, we will focus to VideoSupportFragment immediately after video fragment's view is created.
+ boolean mPendingFocusOnVideo = false;
- /**
- * Flags for enter transition, entrance transition and onStart. When onStart() is called
- * and both enter transiton and entrance transition are finished, we could call onSafeStart().
- * 1. in onCreate:
- * if user call prepareEntranceTransition, set PF_ENTRANCE_TRANSITION_PENDING
- * if there is enterTransition, set PF_ENTER_TRANSITION_PENDING, but we dont know if
- * user will run enterTransition or not.
- * 2. when user add row, start WaitEnterTransitionTimeout to wait possible enter transition
- * start. If enter transition onTransitionStart is not invoked with a period, we can assume
- * there is no enter transition running, then WaitEnterTransitionTimeout will clear
- * PF_ENTER_TRANSITION_PENDING.
- * 3. When enterTransition runs (either postponed or not), we will stop the
- * WaitEnterTransitionTimeout, and let onTransitionEnd/onTransitionCancel to clear
- * PF_ENTER_TRANSITION_PENDING.
- */
- int mStartAndTransitionFlag = 0;
WaitEnterTransitionTimeout mWaitEnterTransitionTimeout;
Object mSceneAfterEntranceTransition;
@@ -290,14 +421,15 @@
FragmentActivity activity = getActivity();
if (activity != null) {
Object transition = TransitionHelper.getEnterTransition(activity.getWindow());
- if (transition != null) {
- mStartAndTransitionFlag |= PF_ENTER_TRANSITION_PENDING;
- TransitionHelper.addTransitionListener(transition, mEnterTransitionListener);
+ if (transition == null) {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
}
transition = TransitionHelper.getReturnTransition(activity.getWindow());
if (transition != null) {
TransitionHelper.addTransitionListener(transition, mReturnTransitionListener);
}
+ } else {
+ mStateMachine.fireEvent(EVT_NO_ENTER_TRANSITION);
}
}
@@ -452,6 +584,22 @@
}
}
+ void switchToVideo() {
+ if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null) {
+ mVideoSupportFragment.getView().requestFocus();
+ } else {
+ mStateMachine.fireEvent(EVT_SWITCH_TO_VIDEO);
+ }
+ }
+
+ void switchToRows() {
+ mPendingFocusOnVideo = false;
+ VerticalGridView verticalGridView = getVerticalGridView();
+ if (verticalGridView != null && verticalGridView.getChildCount() > 0) {
+ verticalGridView.requestFocus();
+ }
+ }
+
/**
* This method asks DetailsSupportFragmentBackgroundController to add a fragment for rendering video.
* In case the fragment is already there, it will return the existing one. The method must be
@@ -468,6 +616,18 @@
ft2.add(android.support.v17.leanback.R.id.video_surface_container,
fragment = mDetailsBackgroundController.onCreateVideoSupportFragment());
ft2.commit();
+ if (mPendingFocusOnVideo) {
+ // wait next cycle for Fragment view created so we can focus on it.
+ // This is a bit hack eventually we will do commitNow() which get view immediately.
+ getView().post(new Runnable() {
+ public void run() {
+ if (getView() != null) {
+ switchToVideo();
+ }
+ mPendingFocusOnVideo = false;
+ }
+ });
+ }
}
mVideoSupportFragment = fragment;
return mVideoSupportFragment;
@@ -476,7 +636,7 @@
void onRowSelected(int selectedPosition, int selectedSubPosition) {
ObjectAdapter adapter = getAdapter();
if (( mRowsSupportFragment != null && mRowsSupportFragment.getView() != null
- && mRowsSupportFragment.getView().hasFocus())
+ && mRowsSupportFragment.getView().hasFocus() && !mPendingFocusOnVideo)
&& (adapter == null || adapter.size() == 0
|| (getVerticalGridView().getSelectedPosition() == 0
&& getVerticalGridView().getSelectedSubPosition() == 0))) {
@@ -487,10 +647,8 @@
if (adapter != null && adapter.size() > selectedPosition) {
final VerticalGridView gridView = getVerticalGridView();
final int count = gridView.getChildCount();
- if (count > 0 && (mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
- if (mWaitEnterTransitionTimeout == null) {
- mWaitEnterTransitionTimeout = new WaitEnterTransitionTimeout(this);
- }
+ if (count > 0) {
+ mStateMachine.fireEvent(EVT_DETAILS_ROW_LOADED);
}
for (int i = 0; i < count; i++) {
ItemBridgeAdapter.ViewHolder bridgeViewHolder = (ItemBridgeAdapter.ViewHolder)
@@ -504,25 +662,6 @@
}
}
- void clearPendingEnterTransition() {
- if ((mStartAndTransitionFlag & PF_ENTER_TRANSITION_PENDING) != 0) {
- mStartAndTransitionFlag &= ~PF_ENTER_TRANSITION_PENDING;
- dispatchOnStartAndTransitionFinished();
- }
- }
-
- void dispatchOnStartAndTransitionFinished() {
- /**
- * if onStart() was called and there is no pending enter transition or entrance transition.
- */
- if ((mStartAndTransitionFlag & PF_PENDING_START) != 0
- && (mStartAndTransitionFlag
- & (PF_ENTER_TRANSITION_PENDING | PF_ENTRANCE_TRANSITION_PENDING)) == 0) {
- mStartAndTransitionFlag &= ~PF_PENDING_START;
- onSafeStart();
- }
- }
-
/**
* Called when onStart and enter transition (postponed/none postponed) and entrance transition
* are all finished.
@@ -617,17 +756,14 @@
public void onStart() {
super.onStart();
- mStartAndTransitionFlag |= PF_PENDING_START;
- dispatchOnStartAndTransitionFinished();
-
setupChildFragmentLayout();
- if (isEntranceTransitionEnabled()) {
- mRowsSupportFragment.setEntranceTransitionState(false);
- }
+ mStateMachine.fireEvent(EVT_ONSTART);
if (mDetailsParallax != null) {
mDetailsParallax.setRecyclerView(mRowsSupportFragment.getVerticalGridView());
}
- if (!getView().hasFocus()) {
+ if (mPendingFocusOnVideo) {
+ slideOutGridView();
+ } else if (!getView().hasFocus()) {
mRowsSupportFragment.getVerticalGridView().requestFocus();
}
}
@@ -645,14 +781,11 @@
@Override
protected void onEntranceTransitionEnd() {
- mStartAndTransitionFlag &= ~PF_ENTRANCE_TRANSITION_PENDING;
- dispatchOnStartAndTransitionFinished();
mRowsSupportFragment.onTransitionEnd();
}
@Override
protected void onEntranceTransitionPrepare() {
- mStartAndTransitionFlag |= PF_ENTRANCE_TRANSITION_PENDING;
mRowsSupportFragment.onTransitionPrepare();
}
@@ -714,8 +847,10 @@
public void onRequestChildFocus(View child, View focused) {
if (child != mRootView.getFocusedChild()) {
if (child.getId() == R.id.details_fragment_root) {
- slideInGridView();
- showTitle(true);
+ if (!mPendingFocusOnVideo) {
+ slideInGridView();
+ showTitle(true);
+ }
} else if (child.getId() == R.id.video_surface_container) {
slideOutGridView();
showTitle(false);
@@ -761,8 +896,10 @@
if (mVideoSupportFragment != null && mVideoSupportFragment.getView() != null
&& mVideoSupportFragment.getView().hasFocus()) {
if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
- getVerticalGridView().requestFocus();
- return true;
+ if (getVerticalGridView().getChildCount() > 0) {
+ getVerticalGridView().requestFocus();
+ return true;
+ }
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
index 6149625..763b84f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsSupportFragmentBackgroundController.java
@@ -207,6 +207,10 @@
bottomDrawable,
coverDrawableParallaxTarget);
mFragment.setBackgroundDrawable(mParallaxDrawable);
+ // create a VideoHelper with null PlaybackGlue for changing CoverDrawable visibility
+ // before PlaybackGlue is ready.
+ mVideoHelper = new DetailsBackgroundVideoHelper(null,
+ mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
}
/**
@@ -230,12 +234,7 @@
mPlaybackGlue.setHost(null);
}
mPlaybackGlue = playbackGlue;
- if (mVideoHelper == null && mPlaybackGlue != null) {
- mVideoHelper = new DetailsBackgroundVideoHelper(mPlaybackGlue,
- mFragment.getParallax(), mParallaxDrawable.getCoverDrawable());
- } else if (mVideoHelper != null) {
- mVideoHelper.setPlaybackGlue(mPlaybackGlue);
- }
+ mVideoHelper.setPlaybackGlue(mPlaybackGlue);
if (mCanUseHost && mPlaybackGlue != null) {
mPlaybackGlue.setHost(onCreateGlueHost());
}
@@ -262,23 +261,36 @@
return mPlaybackGlue != null;
}
+ void crossFadeBackgroundToVideo(boolean fadeToBackground, boolean immediate) {
+ mVideoHelper.crossFadeBackgroundToVideo(fadeToBackground, immediate);
+ }
+
/**
* Switch to video fragment, note that this method is not affected by result of
- * {@link #canNavigateToVideoSupportFragment()}.
+ * {@link #canNavigateToVideoSupportFragment()}. If the method is called in DetailsSupportFragment.onCreate()
+ * it will make video fragment to be initially focused once it is created.
+ * <p>
+ * Calling switchToVideo() in DetailsSupportFragment.onCreate() will clear the activity enter
+ * transition and shared element transition.
+ * </p>
+ * <p>
+ * If switchToVideo() is called after {@link DetailsSupportFragment#prepareEntranceTransition()} and
+ * before {@link DetailsSupportFragment#onEntranceTransitionEnd()}, it will be ignored.
+ * </p>
+ * <p>
+ * If {@link DetailsSupportFragment#prepareEntranceTransition()} is called after switchToVideo(), an
+ * IllegalStateException will be thrown.
+ * </p>
*/
public final void switchToVideo() {
- if (mFragment.mVideoSupportFragment != null && mFragment.mVideoSupportFragment.getView() != null) {
- mFragment.mVideoSupportFragment.getView().requestFocus();
- }
+ mFragment.switchToVideo();
}
/**
* Switch to rows fragment.
*/
public final void switchToRows() {
- if (mFragment.mRowsSupportFragment != null && mFragment.mRowsSupportFragment.getView() != null) {
- mFragment.mRowsSupportFragment.getView().requestFocus();
- }
+ mFragment.switchToRows();
}
/**
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
index d3a45a0..fe0e26f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -349,6 +349,10 @@
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onCreate(vh);
}
+ RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+ RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+ rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
@Override
@@ -362,8 +366,6 @@
setRowViewExpanded(vh, mExpand);
RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
- rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onAttachedToWindow(vh);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
index 5582644..9f55aa2 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
@@ -352,6 +352,10 @@
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onCreate(vh);
}
+ RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
+ RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
+ rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
+ rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
@Override
@@ -365,8 +369,6 @@
setRowViewExpanded(vh, mExpand);
RowPresenter rowPresenter = (RowPresenter) vh.getPresenter();
RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder());
- rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
- rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener);
rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition);
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onAttachedToWindow(vh);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
index cfa27df..9e80dfc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -16,6 +16,7 @@
import android.os.Bundle;
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BrowseFrameLayout;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnChildLaidOutListener;
@@ -49,6 +50,29 @@
private int mSelectedPosition = -1;
/**
+ * State to setEntranceTransitionState(false)
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionState(false);
+ }
+ };
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
+ }
+
+ /**
* Sets the grid presenter.
*/
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
@@ -160,13 +184,8 @@
ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
installTitleView(inflater, gridFrame, savedInstanceState);
getProgressBarManager().setRootView(root);
- return root;
- }
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock);
+ ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
gridDock.addView(mGridViewHolder.view);
mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
@@ -179,6 +198,7 @@
});
updateAdapter();
+ return root;
}
private void setupFocusSearchListener() {
@@ -191,9 +211,6 @@
public void onStart() {
super.onStart();
setupFocusSearchListener();
- if (isEntranceTransitionEnabled()) {
- setEntranceTransitionState(false);
- }
}
@Override
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
index 55e079d..6327790 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridSupportFragment.java
@@ -19,6 +19,7 @@
import android.os.Bundle;
import android.support.v17.leanback.R;
import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine.State;
import android.support.v17.leanback.widget.BrowseFrameLayout;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnChildLaidOutListener;
@@ -52,6 +53,29 @@
private int mSelectedPosition = -1;
/**
+ * State to setEntranceTransitionState(false)
+ */
+ final State STATE_SET_ENTRANCE_START_STATE = new State("SET_ENTRANCE_START_STATE") {
+ @Override
+ public void run() {
+ setEntranceTransitionState(false);
+ }
+ };
+
+ @Override
+ void createStateMachineStates() {
+ super.createStateMachineStates();
+ mStateMachine.addState(STATE_SET_ENTRANCE_START_STATE);
+ }
+
+ @Override
+ void createStateMachineTransitions() {
+ super.createStateMachineTransitions();
+ mStateMachine.addTransition(STATE_ENTRANCE_ON_PREPARED,
+ STATE_SET_ENTRANCE_START_STATE, EVT_ON_CREATEVIEW);
+ }
+
+ /**
* Sets the grid presenter.
*/
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
@@ -163,13 +187,8 @@
ViewGroup gridFrame = (ViewGroup) root.findViewById(R.id.grid_frame);
installTitleView(inflater, gridFrame, savedInstanceState);
getProgressBarManager().setRootView(root);
- return root;
- }
- @Override
- public void onViewCreated(View view, Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock);
+ ViewGroup gridDock = (ViewGroup) root.findViewById(R.id.browse_grid_dock);
mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
gridDock.addView(mGridViewHolder.view);
mGridViewHolder.getGridView().setOnChildLaidOutListener(mChildLaidOutListener);
@@ -182,6 +201,7 @@
});
updateAdapter();
+ return root;
}
private void setupFocusSearchListener() {
@@ -194,9 +214,6 @@
public void onStart() {
super.onStart();
setupFocusSearchListener();
- if (isEntranceTransitionEnabled()) {
- setEntranceTransitionState(false);
- }
}
@Override
diff --git a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
index 1da1bda..48aaebe 100644
--- a/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/v17/leanback/src/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -27,6 +27,7 @@
import android.support.v17.leanback.widget.OnActionClickedListener;
import android.support.v17.leanback.widget.PlaybackControlsRow;
import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
import android.support.v17.leanback.widget.PresenterSelector;
import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
@@ -54,8 +55,10 @@
* inform the glue what speed levels are supported for fast forward/rewind.
* </p>
*
- * <p>You may override {@link #onCreateControlsRowAndPresenter()} which will set a controls
- * row and return a row presenter you can use to present the row.
+ * <p>You may override {@link #onCreateControlsRowAndPresenter()} which will create a
+ * {@link PlaybackControlsRow} and a {@link PlaybackControlsRowPresenter}. You may call
+ * {@link #setControlsRow(PlaybackControlsRow)} and
+ * {@link #setPlaybackRowPresenter(PlaybackRowPresenter)} to customize your own row and presenter.
* </p>
*
* <p>The helper sets a {@link SparseArrayObjectAdapter}
@@ -176,7 +179,7 @@
private final int[] mFastForwardSpeeds;
private final int[] mRewindSpeeds;
private PlaybackControlsRow mControlsRow;
- private PlaybackControlsRowPresenter mControlsRowPresenter;
+ private PlaybackRowPresenter mControlsRowPresenter;
private PlaybackControlsRow.PlayPauseAction mPlayPauseAction;
private PlaybackControlsRow.SkipNextAction mSkipNextAction;
private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
@@ -237,10 +240,10 @@
super.onAttachedToHost(host);
host.setOnKeyInterceptListener(this);
host.setOnActionClickedListener(this);
- if (getControlsRow() == null || getControlsRowPresenter() == null) {
+ if (getControlsRow() == null || getPlaybackRowPresenter() == null) {
onCreateControlsRowAndPresenter();
}
- host.setPlaybackRowPresenter(getControlsRowPresenter());
+ host.setPlaybackRowPresenter(getPlaybackRowPresenter());
host.setPlaybackRow(getControlsRow());
}
@@ -265,37 +268,40 @@
* {@link PlaybackControlsRowPresenter}. Subclass may override.
*/
protected void onCreateControlsRowAndPresenter() {
- PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
- setControlsRow(controlsRow);
+ if (getControlsRow() == null) {
+ PlaybackControlsRow controlsRow = new PlaybackControlsRow(this);
+ setControlsRow(controlsRow);
+ }
+ if (getPlaybackRowPresenter() == null) {
+ final AbstractDetailsDescriptionPresenter detailsPresenter =
+ new AbstractDetailsDescriptionPresenter() {
+ @Override
+ protected void onBindDescription(ViewHolder
+ viewHolder, Object object) {
+ PlaybackControlGlue glue = (PlaybackControlGlue) object;
+ if (glue.hasValidMedia()) {
+ viewHolder.getTitle().setText(glue.getMediaTitle());
+ viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
+ } else {
+ viewHolder.getTitle().setText("");
+ viewHolder.getSubtitle().setText("");
+ }
+ }
+ };
- final AbstractDetailsDescriptionPresenter detailsPresenter =
- new AbstractDetailsDescriptionPresenter() {
- @Override
- protected void onBindDescription(ViewHolder
- viewHolder, Object object) {
- PlaybackControlGlue glue = (PlaybackControlGlue) object;
- if (glue.hasValidMedia()) {
- viewHolder.getTitle().setText(glue.getMediaTitle());
- viewHolder.getSubtitle().setText(glue.getMediaSubtitle());
- } else {
- viewHolder.getTitle().setText("");
- viewHolder.getSubtitle().setText("");
+ setPlaybackRowPresenter(new PlaybackControlsRowPresenter(detailsPresenter) {
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
+ super.onBindRowViewHolder(vh, item);
+ vh.setOnKeyListener(PlaybackControlGlue.this);
}
- }
- };
-
- setControlsRowPresenter(new PlaybackControlsRowPresenter(detailsPresenter) {
- @Override
- protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
- super.onBindRowViewHolder(vh, item);
- vh.setOnKeyListener(PlaybackControlGlue.this);
- }
- @Override
- protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
- super.onUnbindRowViewHolder(vh);
- vh.setOnKeyListener(null);
- }
- });
+ @Override
+ protected void onUnbindRowViewHolder(RowPresenter.ViewHolder vh) {
+ super.onUnbindRowViewHolder(vh);
+ vh.setOnKeyListener(null);
+ }
+ });
+ }
}
/**
@@ -358,7 +364,10 @@
/**
* Sets the controls row Presenter to be managed by the glue layer.
+ * @deprecated PlaybackControlGlue supports any PlaybackRowPresenter, use
+ * {@link #setPlaybackRowPresenter(PlaybackRowPresenter)}.
*/
+ @Deprecated
public void setControlsRowPresenter(PlaybackControlsRowPresenter presenter) {
mControlsRowPresenter = presenter;
}
@@ -372,8 +381,28 @@
/**
* Returns the playback controls row Presenter managed by the glue layer.
+ * @deprecated PlaybackControlGlue supports any PlaybackRowPresenter, use
+ * {@link #getPlaybackRowPresenter()}.
*/
+ @Deprecated
public PlaybackControlsRowPresenter getControlsRowPresenter() {
+ return mControlsRowPresenter instanceof PlaybackControlsRowPresenter
+ ? (PlaybackControlsRowPresenter) mControlsRowPresenter : null;
+ }
+
+ /**
+ * Sets the controls row Presenter to be passed to {@link PlaybackGlueHost} in
+ * {@link #onAttachedToHost(PlaybackGlueHost)}.
+ */
+ public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+ mControlsRowPresenter = presenter;
+ }
+
+ /**
+ * Returns the playback row Presenter to be passed to {@link PlaybackGlueHost} in
+ * {@link #onAttachedToHost(PlaybackGlueHost)}.
+ */
+ public PlaybackRowPresenter getPlaybackRowPresenter() {
return mControlsRowPresenter;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
index bef3d8d..9f92066 100644
--- a/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/transition/TransitionHelper.java
@@ -62,100 +62,108 @@
/**
* Interface implemented by classes that support Transition animations.
*/
- static interface TransitionHelperVersionImpl {
+ interface TransitionHelperVersionImpl {
- public void setEnterTransition(android.app.Fragment fragment, Object transition);
+ void setEnterTransition(android.app.Fragment fragment, Object transition);
- public void setExitTransition(android.app.Fragment fragment, Object transition);
+ void setExitTransition(android.app.Fragment fragment, Object transition);
- public void setSharedElementEnterTransition(android.app.Fragment fragment,
+ void setSharedElementEnterTransition(android.app.Fragment fragment,
Object transition);
- public void addSharedElement(android.app.FragmentTransaction ft,
+ void addSharedElement(android.app.FragmentTransaction ft,
View view, String transitionName);
- public Object getSharedElementEnterTransition(Window window);
+ Object getSharedElementEnterTransition(Window window);
- public Object getSharedElementReturnTransition(Window window);
+ void setSharedElementEnterTransition(Window window, Object transition);
- public Object getSharedElementExitTransition(Window window);
+ Object getSharedElementReturnTransition(Window window);
- public Object getSharedElementReenterTransition(Window window);
+ void setSharedElementReturnTransition(Window window, Object transition);
- public Object getEnterTransition(Window window);
+ Object getSharedElementExitTransition(Window window);
- public Object getReturnTransition(Window window);
+ Object getSharedElementReenterTransition(Window window);
- public Object getExitTransition(Window window);
+ Object getEnterTransition(Window window);
- public Object getReenterTransition(Window window);
+ void setEnterTransition(Window window, Object transition);
- public Object createScene(ViewGroup sceneRoot, Runnable r);
+ Object getReturnTransition(Window window);
- public Object createAutoTransition();
+ void setReturnTransition(Window window, Object transition);
- public Object createSlide(int slideEdge);
+ Object getExitTransition(Window window);
- public Object createScale();
+ Object getReenterTransition(Window window);
- public Object createFadeTransition(int fadingMode);
+ Object createScene(ViewGroup sceneRoot, Runnable r);
- public Object createChangeTransform();
+ Object createAutoTransition();
- public Object createChangeBounds(boolean reparent);
+ Object createSlide(int slideEdge);
- public Object createFadeAndShortSlide(int edge);
+ Object createScale();
- public Object createFadeAndShortSlide(int edge, float distance);
+ Object createFadeTransition(int fadingMode);
- public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay);
+ Object createChangeTransform();
- public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay);
+ Object createChangeBounds(boolean reparent);
- public void setChangeBoundsStartDelay(Object changeBounds, String className,
+ Object createFadeAndShortSlide(int edge);
+
+ Object createFadeAndShortSlide(int edge, float distance);
+
+ void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay);
+
+ void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay);
+
+ void setChangeBoundsStartDelay(Object changeBounds, String className,
int startDelay);
- public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay);
+ void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay);
- public Object createTransitionSet(boolean sequential);
+ Object createTransitionSet(boolean sequential);
- public void addTransition(Object transitionSet, Object transition);
+ void addTransition(Object transitionSet, Object transition);
- public void addTransitionListener(Object transition, TransitionListener listener);
+ void addTransitionListener(Object transition, TransitionListener listener);
- public void removeTransitionListener(Object transition, TransitionListener listener);
+ void removeTransitionListener(Object transition, TransitionListener listener);
- public void runTransition(Object scene, Object transition);
+ void runTransition(Object scene, Object transition);
- public void exclude(Object transition, int targetId, boolean exclude);
+ void exclude(Object transition, int targetId, boolean exclude);
- public void exclude(Object transition, View targetView, boolean exclude);
+ void exclude(Object transition, View targetView, boolean exclude);
- public void excludeChildren(Object transition, int targetId, boolean exclude);
+ void excludeChildren(Object transition, int targetId, boolean exclude);
- public void excludeChildren(Object transition, View target, boolean exclude);
+ void excludeChildren(Object transition, View target, boolean exclude);
- public void include(Object transition, int targetId);
+ void include(Object transition, int targetId);
- public void include(Object transition, View targetView);
+ void include(Object transition, View targetView);
- public void setStartDelay(Object transition, long startDelay);
+ void setStartDelay(Object transition, long startDelay);
- public void setDuration(Object transition, long duration);
+ void setDuration(Object transition, long duration);
- public void setInterpolator(Object transition, Object timeInterpolator);
+ void setInterpolator(Object transition, Object timeInterpolator);
- public void addTarget(Object transition, View view);
+ void addTarget(Object transition, View view);
- public Object createDefaultInterpolator(Context context);
+ Object createDefaultInterpolator(Context context);
- public Object loadTransition(Context context, int resId);
+ Object loadTransition(Context context, int resId);
- public void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject);
+ void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject);
- public void setTransitionGroup(ViewGroup viewGroup, boolean transitionGroup);
+ void setTransitionGroup(ViewGroup viewGroup, boolean transitionGroup);
- public void setEpicenterCallback(Object transitionObject,
+ void setEpicenterCallback(Object transitionObject,
TransitionEpicenterCallback callback);
}
@@ -195,11 +203,19 @@
}
@Override
+ public void setSharedElementEnterTransition(Window window, Object object) {
+ }
+
+ @Override
public Object getSharedElementReturnTransition(Window window) {
return null;
}
@Override
+ public void setSharedElementReturnTransition(Window window, Object transition) {
+ }
+
+ @Override
public Object getSharedElementExitTransition(Window window) {
return null;
}
@@ -215,11 +231,19 @@
}
@Override
+ public void setEnterTransition(Window window, Object transition) {
+ }
+
+ @Override
public Object getReturnTransition(Window window) {
return null;
}
@Override
+ public void setReturnTransition(Window window, Object transition) {
+ }
+
+ @Override
public Object getExitTransition(Window window) {
return null;
}
@@ -572,11 +596,21 @@
}
@Override
+ public void setSharedElementEnterTransition(Window window, Object object) {
+ TransitionHelperApi21.setSharedElementEnterTransition(window, object);
+ }
+
+ @Override
public Object getSharedElementReturnTransition(Window window) {
return TransitionHelperApi21.getSharedElementReturnTransition(window);
}
@Override
+ public void setSharedElementReturnTransition(Window window, Object transition) {
+ TransitionHelperApi21.setSharedElementReturnTransition(window, transition);
+ }
+
+ @Override
public Object getSharedElementExitTransition(Window window) {
return TransitionHelperApi21.getSharedElementExitTransition(window);
}
@@ -607,11 +641,21 @@
}
@Override
+ public void setEnterTransition(Window window, Object transition) {
+ TransitionHelperApi21.setEnterTransition(window, transition);
+ }
+
+ @Override
public Object getReturnTransition(Window window) {
return TransitionHelperApi21.getReturnTransition(window);
}
@Override
+ public void setReturnTransition(Window window, Object transition) {
+ TransitionHelperApi21.setReturnTransition(window, transition);
+ }
+
+ @Override
public Object getExitTransition(Window window) {
return TransitionHelperApi21.getExitTransition(window);
}
@@ -662,10 +706,18 @@
return sImpl.getSharedElementEnterTransition(window);
}
+ public static void setSharedElementEnterTransition(Window window, Object transition) {
+ sImpl.setSharedElementEnterTransition(window, transition);
+ }
+
public static Object getSharedElementReturnTransition(Window window) {
return sImpl.getSharedElementReturnTransition(window);
}
+ public static void setSharedElementReturnTransition(Window window, Object transition) {
+ sImpl.setSharedElementReturnTransition(window, transition);
+ }
+
public static Object getSharedElementExitTransition(Window window) {
return sImpl.getSharedElementExitTransition(window);
}
@@ -678,10 +730,18 @@
return sImpl.getEnterTransition(window);
}
+ public static void setEnterTransition(Window window, Object transition) {
+ sImpl.setEnterTransition(window, transition);
+ }
+
public static Object getReturnTransition(Window window) {
return sImpl.getReturnTransition(window);
}
+ public static void setReturnTransition(Window window, Object transition) {
+ sImpl.setReturnTransition(window, transition);
+ }
+
public static Object getExitTransition(Window window) {
return sImpl.getExitTransition(window);
}
diff --git a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java b/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
index b9d2f2d..dfc228c 100644
--- a/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
+++ b/v17/leanback/src/android/support/v17/leanback/util/StateMachine.java
@@ -16,42 +16,169 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.support.annotation.RestrictTo;
+import android.util.Log;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
/**
- * Linear or DAG of {@link State}s. StateMachine is by default a linear model, until
- * {@link #addState(State, State)} is called. Each State has three status:
- * STATUS_ZERO, STATUS_INVOKED, STATUS_EXECUTED. We allow client to run a State, which will
- * put State in STATUS_INVOKED. A State will be executed when prior States are executed and
- * Precondition for this State is true, then the State will be marked as STATUS_EXECUTED.
- *
+ * State: each State has incoming Transitions and outgoing Transitions.
+ * When {@link State#mBranchStart} is true, all the outgoing Transitions may be triggered, when
+ * {@link State#mBranchStart} is false, only first outgoing Transition will be triggered.
+ * When {@link State#mBranchEnd} is true, all the incoming Transitions must be triggered for the
+ * State to run. When {@link State#mBranchEnd} is false, only need one incoming Transition triggered
+ * for the State to run.
+ * Transition: three types:
+ * 1. Event based transition, transition will be triggered when {@link #fireEvent(Event)} is called.
+ * 2. Auto transition, transition will be triggered when {@link Transition#mFromState} is executed.
+ * 3. Condiitonal Auto transition, transition will be triggered when {@link Transition#mFromState}
+ * is executed and {@link Transition#mCondition} passes.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public final class StateMachine {
+ static boolean DEBUG = false;
+ static final String TAG = "StateMachine";
+
/**
* No request on the State
*/
public static final int STATUS_ZERO = 0;
+
/**
- * Somebody wants to run the state but not yet executed because either the condition is
- * false or lower States are not executed.
+ * Has been executed
*/
public static final int STATUS_INVOKED = 1;
- /**
- * Somebody wants to run the State and the State was executed.
- */
- public static final int STATUS_EXECUTED = 2;
+ /**
+ * Used in Transition
+ */
+ public static class Event {
+ final String mName;
+
+ public Event(String name) {
+ mName = name;
+ }
+ }
+
+ /**
+ * Used in transition
+ */
+ public static class Condition {
+ final String mName;
+
+ public Condition(String name) {
+ mName = name;
+ }
+
+ /**
+ * @return True if can proceed and mark the transition INVOKED
+ */
+ public boolean canProceed() {
+ return true;
+ }
+ }
+
+ static class Transition {
+ final State mFromState;
+ final State mToState;
+ final Event mEvent;
+ final Condition mCondition;
+ int mState = STATUS_ZERO;
+
+ Transition(State fromState, State toState, Event event) {
+ if (event == null) {
+ throw new IllegalArgumentException();
+ }
+ mFromState = fromState;
+ mToState = toState;
+ mEvent = event;
+ mCondition = null;
+ }
+
+ Transition(State fromState, State toState) {
+ mFromState = fromState;
+ mToState = toState;
+ mEvent = null;
+ mCondition = null;
+ }
+
+ Transition(State fromState, State toState, Condition condition) {
+ if (condition == null) {
+ throw new IllegalArgumentException();
+ }
+ mFromState = fromState;
+ mToState = toState;
+ mEvent = null;
+ mCondition = condition;
+ }
+
+ @Override
+ public String toString() {
+ String signalName;
+ if (mEvent != null) {
+ signalName = mEvent.mName;
+ } else if (mCondition != null) {
+ signalName = mCondition.mName;
+ } else {
+ signalName = "auto";
+ }
+ return "[" + mFromState.mName + " -> " + mToState.mName + " <"
+ + signalName + ">]";
+ }
+ }
+
+ /**
+ * @see StateMachine
+ */
public static class State {
- private int mStatus;
- ArrayList<State> mPriorStates;
+ final String mName;
+ final boolean mBranchStart;
+ final boolean mBranchEnd;
+ int mStatus = STATUS_ZERO;
+ int mInvokedOutTransitions = 0;
+ ArrayList<Transition> mIncomings;
+ ArrayList<Transition> mOutgoings;
+
+ @Override
+ public String toString() {
+ return "[" + mName + " " + mStatus + "]";
+ }
+
+ /**
+ * Create a State which is not branch start and a branch end.
+ */
+ public State(String name) {
+ this(name, false, true);
+ }
+
+ /**
+ * Create a State
+ * @param branchStart True if can run all out going transitions or false execute the first
+ * out going transition.
+ * @param branchEnd True if wait all incoming transitions executed or false
+ * only need one of the transition executed.
+ */
+ public State(String name, boolean branchStart, boolean branchEnd) {
+ mName = name;
+ mBranchStart = branchStart;
+ mBranchEnd = branchEnd;
+ }
+
+ void addIncoming(Transition t) {
+ if (mIncomings == null) {
+ mIncomings = new ArrayList();
+ }
+ mIncomings.add(t);
+ }
+
+ void addOutgoing(Transition t) {
+ if (mOutgoings == null) {
+ mOutgoings = new ArrayList();
+ }
+ mOutgoings.add(t);
+ }
/**
* Run State, Subclass may override.
@@ -59,51 +186,66 @@
public void run() {
}
- /**
- * Returns true if State can run, false otherwise. Subclass may override.
- * @return True if State can run, false otherwise. Subclass may override.
- */
- public boolean canRun() {
- return true;
+ final boolean checkPreCondition() {
+ if (mIncomings == null) {
+ return true;
+ }
+ if (mBranchEnd) {
+ for (Transition t: mIncomings) {
+ if (t.mState != STATUS_INVOKED) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ for (Transition t: mIncomings) {
+ if (t.mState == STATUS_INVOKED) {
+ return true;
+ }
+ }
+ return false;
+ }
}
/**
* @return True if the State has been executed.
*/
final boolean runIfNeeded() {
- if (mStatus!= STATUS_EXECUTED) {
- if (mStatus == STATUS_INVOKED && canRun()) {
+ if (mStatus != STATUS_INVOKED) {
+ if (checkPreCondition()) {
+ if (DEBUG) {
+ Log.d(TAG, "execute " + this);
+ }
+ mStatus = STATUS_INVOKED;
run();
- mStatus = STATUS_EXECUTED;
- } else {
- return false;
+ signalAutoTransitionsAfterRun();
+ return true;
}
}
- return true;
+ return false;
}
- void addPriorState(State state) {
- if (mPriorStates == null) {
- mPriorStates = new ArrayList<State>();
+ final void signalAutoTransitionsAfterRun() {
+ if (mOutgoings != null) {
+ for (Transition t: mOutgoings) {
+ if (t.mEvent == null) {
+ if (t.mCondition == null || t.mCondition.canProceed()) {
+ if (DEBUG) {
+ Log.d(TAG, "signal " + t);
+ }
+ mInvokedOutTransitions++;
+ t.mState = STATUS_INVOKED;
+ if (!mBranchStart) {
+ break;
+ }
+ }
+ }
+ }
}
- if (!mPriorStates.contains(state)) {
- mPriorStates.add(state);
- }
- }
-
- final void markInvoked() {
- if (mStatus == STATUS_ZERO) {
- mStatus = STATUS_INVOKED;
- }
- }
-
- final void updateStatus(int status) {
- mStatus = status;
}
/**
- * Get status, return one of {@link #STATUS_ZERO}, {@link #STATUS_INVOKED},
- * {@link #STATUS_EXECUTED}.
+ * Get status, return one of {@link #STATUS_ZERO}, {@link #STATUS_INVOKED}.
* @return Status of the State.
*/
public final int getStatus() {
@@ -111,109 +253,127 @@
}
}
- private boolean mSorted = true;
- private final ArrayList<State> mSortedList = new ArrayList<State>();
+ final ArrayList<State> mStates = new ArrayList<State>();
+ final ArrayList<State> mFinishedStates = new ArrayList();
+ final ArrayList<State> mUnfinishedStates = new ArrayList();
+
+ public StateMachine() {
+ }
/**
* Add a State to StateMachine, ignore if it is already added.
* @param state The state to add.
*/
public void addState(State state) {
- if (!mSortedList.contains(state)) {
- state.updateStatus(STATUS_ZERO);
- mSortedList.add(state);
+ if (!mStates.contains(state)) {
+ mStates.add(state);
}
}
/**
- * Add two States to StateMachine and create an edge between this two.
- * StateMachine is by default a linear model, until {@link #addState(State, State)} is called.
- * sort() is required to sort the Direct acyclic graph.
+ * Add event-triggered transition between two states.
+ * @param fromState The from state.
+ * @param toState The to state.
+ * @param event The event that needed to perform the transition.
+ */
+ public void addTransition(State fromState, State toState, Event event) {
+ Transition transition = new Transition(fromState, toState, event);
+ toState.addIncoming(transition);
+ fromState.addOutgoing(transition);
+ }
+
+ /**
+ * Add a conditional auto transition between two states.
+ * @param fromState The from state.
+ * @param toState The to state.
+ */
+ public void addTransition(State fromState, State toState, Condition condition) {
+ Transition transition = new Transition(fromState, toState, condition);
+ toState.addIncoming(transition);
+ fromState.addOutgoing(transition);
+ }
+
+ /**
+ * Add an auto transition between two states.
* @param fromState The from state to add.
* @param toState The to state to add.
*/
- public void addState(State fromState, State toState) {
- addState(fromState);
- addState(toState);
- toState.addPriorState(fromState);
- mSorted = false;
- }
-
- void verifySorted() {
- if (!mSorted) {
- throw new RuntimeException("Graph not sorted");
- }
- }
-
- public void runState(State state) {
- verifySorted();
- state.markInvoked();
- runPendingStates();
- }
-
- public void runPendingStates() {
- verifySorted();
- for (int i = 0, size = mSortedList.size(); i < size; i++) {
- if (!mSortedList.get(i).runIfNeeded()) {
- break;
- }
- }
- }
-
- public void resetStatus() {
- for (int i = 0, size = mSortedList.size(); i < size; i++) {
- mSortedList.get(i).updateStatus(STATUS_ZERO);
- }
+ public void addTransition(State fromState, State toState) {
+ Transition transition = new Transition(fromState, toState);
+ toState.addIncoming(transition);
+ fromState.addOutgoing(transition);
}
/**
- * StateMachine is by default a linear model, until {@link #addState(State, State)} is called.
- * sort() is required to sort the Direct acyclic graph.
+ * Start the state machine.
*/
- public void sort() {
- if (mSorted) {
- return;
+ public void start() {
+ if (DEBUG) {
+ Log.d(TAG, "start");
}
- // L: Empty list that will contain the sorted States
- ArrayList<State> L = new ArrayList<State>();
- // S: Set of all nodes with no incoming edges
- ArrayList<State> S = new ArrayList<State>();
- HashMap<State, ArrayList<State>> edges = new HashMap<State, ArrayList<State>>();
- for (int i = mSortedList.size() - 1; i >= 0 ; i--) {
- State state = mSortedList.get(i);
- if (state.mPriorStates != null && state.mPriorStates.size() > 0) {
- edges.put(state, new ArrayList<State>(state.mPriorStates));
- } else {
- S.add(state);
- }
- }
+ mUnfinishedStates.addAll(mStates);
+ runUnfinishedStates();
+ }
- while (!S.isEmpty()) {
- // remove a State without incoming Node from S, add to L
- State state = S.remove(S.size() - 1);
- L.add(state);
- // for each toState that having an incoming edge from "state":
- for (Iterator<Map.Entry<State, ArrayList<State>>> iterator =
- edges.entrySet().iterator(); iterator.hasNext();) {
- Map.Entry<State, ArrayList<State>> entry = iterator.next();
- ArrayList<State> fromStates = entry.getValue();
- // remove edge from graph
- if (fromStates.remove(state)) {
- if (fromStates.size() == 0) {
- State toState = entry.getKey();
- // insert the toState to S if it has no more incoming edges
- S.add(toState);
- iterator.remove();
+ void runUnfinishedStates() {
+ boolean changed;
+ do {
+ changed = false;
+ for (int i = mUnfinishedStates.size() - 1; i >= 0; i--) {
+ State state = mUnfinishedStates.get(i);
+ if (state.runIfNeeded()) {
+ mUnfinishedStates.remove(i);
+ mFinishedStates.add(state);
+ changed = true;
+ }
+ }
+ } while (changed);
+ }
+
+ /**
+ * Find outgoing Transitions of invoked State whose Event matches, mark the Transition invoked.
+ */
+ public void fireEvent(Event event) {
+ for (int i = 0; i < mFinishedStates.size(); i++) {
+ State state = mFinishedStates.get(i);
+ if (state.mOutgoings != null) {
+ if (!state.mBranchStart && state.mInvokedOutTransitions > 0) {
+ continue;
+ }
+ for (Transition t : state.mOutgoings) {
+ if (t.mState != STATUS_INVOKED && t.mEvent == event) {
+ if (DEBUG) {
+ Log.d(TAG, "signal " + t);
+ }
+ t.mState = STATUS_INVOKED;
+ state.mInvokedOutTransitions++;
+ if (!state.mBranchStart) {
+ break;
+ }
}
}
}
}
- if (edges.size() > 0) {
- throw new RuntimeException("Cycle in Graph");
- }
+ runUnfinishedStates();
+ }
- mSortedList.clear();
- mSortedList.addAll(L);
- mSorted = true;
+ /**
+ * Reset status to orignal status
+ */
+ public void reset() {
+ if (DEBUG) {
+ Log.d(TAG, "reset");
+ }
+ mUnfinishedStates.clear();
+ mFinishedStates.clear();
+ for (State state: mStates) {
+ state.mStatus = STATUS_ZERO;
+ state.mInvokedOutTransitions = 0;
+ if (state.mOutgoings != null) {
+ for (Transition t: state.mOutgoings) {
+ t.mState = STATUS_ZERO;
+ }
+ }
+ }
}
}
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 effbc6e..2c9f069 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -1747,7 +1747,12 @@
void slideIn() {
if (mIsSlidingChildViews) {
mIsSlidingChildViews = false;
- scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+ if (mFocusPosition >= 0) {
+ scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
+ } else {
+ mLayoutEatenInSliding = false;
+ requestLayout();
+ }
if (mLayoutEatenInSliding) {
mLayoutEatenInSliding = false;
if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
@@ -1767,6 +1772,41 @@
}
}
+ int getSlideOutDistance() {
+ int distance;
+ if (mOrientation == VERTICAL) {
+ distance = -getHeight();
+ if (getChildCount() > 0) {
+ int top = getChildAt(0).getTop();
+ if (top < 0) {
+ // scroll more if first child is above top edge
+ distance = distance + top;
+ }
+ }
+ } else {
+ if (mReverseFlowPrimary) {
+ distance = getWidth();
+ if (getChildCount() > 0) {
+ int start = getChildAt(0).getRight();
+ if (start > distance) {
+ // scroll more if first child is outside right edge
+ distance = start;
+ }
+ }
+ } else {
+ distance = -getWidth();
+ if (getChildCount() > 0) {
+ int start = getChildAt(0).getLeft();
+ if (start < 0) {
+ // scroll more if first child is out side left edge
+ distance = distance + start;
+ }
+ }
+ }
+ }
+ return distance;
+ }
+
/**
* Temporarily slide out child and block layout and scroll requests.
*/
@@ -1779,31 +1819,11 @@
return;
}
if (mOrientation == VERTICAL) {
- int distance = -getHeight();
- int top = getChildAt(0).getTop();
- if (top < 0) {
- // scroll more if first child is above top edge
- distance = distance + top;
- }
- mBaseGridView.smoothScrollBy(0, distance, new AccelerateDecelerateInterpolator());
+ mBaseGridView.smoothScrollBy(0, getSlideOutDistance(),
+ new AccelerateDecelerateInterpolator());
} else {
- int distance;
- if (mReverseFlowPrimary) {
- distance = getWidth();
- int start = getChildAt(0).getRight();
- if (start > distance) {
- // scroll more if first child is outside right edge
- distance = start;
- }
- } else {
- distance = -getWidth();
- int start = getChildAt(0).getLeft();
- if (start < 0) {
- // scroll more if first child is out side left edge
- distance = distance + start;
- }
- }
- mBaseGridView.smoothScrollBy(distance, 0, new AccelerateDecelerateInterpolator());
+ mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0,
+ new AccelerateDecelerateInterpolator());
}
}
@@ -1969,8 +1989,12 @@
}
if (mIsSlidingChildViews) {
- mLayoutEatenInSliding = true;
- return;
+ // if there is already children, delay the layout process until slideIn(), if it's
+ // first time layout children: scroll them offscreen at end of onLayoutChildren()
+ if (getChildCount() > 0) {
+ mLayoutEatenInSliding = true;
+ return;
+ }
}
if (!mLayoutEnabled) {
discardLayoutInfo();
@@ -2019,10 +2043,6 @@
if (mInFastRelayout = layoutInit()) {
fastRelayout();
- // appends items till focus position.
- if (mFocusPosition != NO_POSITION) {
- scrollToFocusViewInLayout(hadFocus, scrollToFocus);
- }
} else {
mInLayoutSearchFocus = hadFocus;
if (mFocusPosition != NO_POSITION) {
@@ -2030,23 +2050,24 @@
while (appendOneColumnVisibleItems()
&& findViewByPosition(mFocusPosition) == null) ;
}
- // multiple rounds: scrollToView of first round may drag first/last child into
- // "visible window" and we update scrollMin/scrollMax then run second scrollToView
- int oldFirstVisible;
- int oldLastVisible;
- do {
- updateScrollMin();
- updateScrollMax();
- oldFirstVisible = mGrid.getFirstVisibleIndex();
- oldLastVisible = mGrid.getLastVisibleIndex();
- scrollToFocusViewInLayout(hadFocus, true);
- appendVisibleItems();
- prependVisibleItems();
- removeInvisibleViewsAtFront();
- removeInvisibleViewsAtEnd();
- } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
- || mGrid.getLastVisibleIndex() != oldLastVisible);
}
+ // multiple rounds: scrollToView of first round may drag first/last child into
+ // "visible window" and we update scrollMin/scrollMax then run second scrollToView
+ // we must do this for fastRelayout() for the append item case
+ int oldFirstVisible;
+ int oldLastVisible;
+ do {
+ updateScrollMin();
+ updateScrollMax();
+ oldFirstVisible = mGrid.getFirstVisibleIndex();
+ oldLastVisible = mGrid.getLastVisibleIndex();
+ scrollToFocusViewInLayout(hadFocus, scrollToFocus);
+ appendVisibleItems();
+ prependVisibleItems();
+ removeInvisibleViewsAtFront();
+ removeInvisibleViewsAtEnd();
+ } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
+ || mGrid.getLastVisibleIndex() != oldLastVisible);
if (scrollToFocus) {
scrollDirectionPrimary(-delta);
@@ -2081,6 +2102,9 @@
}
dispatchChildSelectedAndPositioned();
+ if (mIsSlidingChildViews) {
+ scrollDirectionPrimary(getSlideOutDistance());
+ }
mInLayout = false;
leaveContext();
if (DEBUG) Log.v(getTag(), "layoutChildren end");
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
index c8db4bd..b92e518 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsFragmentTest.java
@@ -15,9 +15,9 @@
*/
package android.support.v17.leanback.app;
-import static junit.framework.TestCase.assertFalse;
-
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.animation.PropertyValuesHolder;
@@ -29,6 +29,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
@@ -37,6 +38,8 @@
import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
import android.support.v17.leanback.media.MediaPlayerGlue;
import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
import android.support.v17.leanback.widget.DetailsParallax;
import android.support.v17.leanback.widget.DetailsParallaxDrawable;
import android.support.v17.leanback.widget.ParallaxTarget;
@@ -595,6 +598,13 @@
);
// after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null;
+ }
+ });
sendKeys(KeyEvent.KEYCODE_DPAD_UP);
assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
@@ -771,8 +781,6 @@
assertTrue(recyclerViewHeight > 0);
assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertEquals(255, detailsFragment.mDetailsBackgroundController.mParallaxDrawable
- .getAlpha());
Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
assertEquals(0, coverDrawable.getBounds().top);
assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
@@ -780,4 +788,320 @@
assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
}
+
+ public static class DetailsFragmentSwitchToVideoInOnCreate extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentSwitchToVideoInOnCreate() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreate() {
+ launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+ //SystemClock.sleep(5000);
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mVideoFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+ }
+ });
+
+ // switchToRows does nothing if there is no row
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoFragment.getView().hasFocus());
+
+ // create item, it should be layout outside screen
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world",
+ "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+ );
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildCount() > 0
+ && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ >= detailsFragment.getVerticalGridView().getHeight();
+ }
+ });
+
+ // pressing BACK will return to details row
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+ }
+ });
+ assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+ }
+
+ @Test
+ public void switchToVideoBackToQuit() {
+ launchAndWaitActivity(DetailsFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsFragment().getView().hasFocus());
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoFragment != null
+ && detailsFragment.mVideoFragment.getView() != null
+ && detailsFragment.mVideoFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackFragment) detailsFragment.mVideoFragment).mBgAlpha == 0;
+ }
+ });
+
+ // before any details row is presented, pressing BACK will quit the activity
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(mActivity));
+ }
+
+ public static class DetailsFragmentSwitchToVideoAndPrepareEntranceTransition
+ extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentSwitchToVideoAndPrepareEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+ launchAndWaitActivity(DetailsFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+ (DetailsFragmentSwitchToVideoAndPrepareEntranceTransition)
+ mActivity.getTestFragment();
+
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+ }
+
+ public static class DetailsFragmentEntranceTransition
+ extends DetailsTestFragment {
+
+ final DetailsFragmentBackgroundController mDetailsBackground =
+ new DetailsFragmentBackgroundController(this);
+
+ public DetailsFragmentEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void entranceTransitionBlocksSwitchToVideo() {
+ launchAndWaitActivity(DetailsFragmentEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsFragmentEntranceTransition detailsFragment =
+ (DetailsFragmentEntranceTransition)
+ mActivity.getTestFragment();
+
+ if (Build.VERSION.SDK_INT < 21) {
+ // when enter transition is not supported, mCanUseHost is immmediately true
+ assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ } else {
+ // calling switchToVideo() between prepareEntranceTransition and entrance transition
+ // finishes will be ignored.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ }
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ // once Entrance transition is finished, mCanUseHost will be true
+ // and we can switchToVideo and fade out the background.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
index 03277bd..2b4a2d9 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/DetailsSupportFragmentTest.java
@@ -18,9 +18,9 @@
*/
package android.support.v17.leanback.app;
-import static junit.framework.TestCase.assertFalse;
-
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.animation.PropertyValuesHolder;
@@ -32,6 +32,7 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
@@ -40,6 +41,8 @@
import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
import android.support.v17.leanback.media.MediaPlayerGlue;
import android.support.v17.leanback.testutils.PollingCheck;
+import android.support.v17.leanback.transition.TransitionHelper;
+import android.support.v17.leanback.util.StateMachine;
import android.support.v17.leanback.widget.DetailsParallax;
import android.support.v17.leanback.widget.DetailsParallaxDrawable;
import android.support.v17.leanback.widget.ParallaxTarget;
@@ -598,6 +601,13 @@
);
// after setup Video Playback the DPAD up will navigate to Video Fragment.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null;
+ }
+ });
sendKeys(KeyEvent.KEYCODE_DPAD_UP);
assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
@@ -774,8 +784,6 @@
assertTrue(recyclerViewHeight > 0);
assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
- assertEquals(255, detailsFragment.mDetailsBackgroundController.mParallaxDrawable
- .getAlpha());
Drawable coverDrawable = detailsFragment.mDetailsBackgroundController.getCoverDrawable();
assertEquals(0, coverDrawable.getBounds().top);
assertEquals(recyclerViewHeight, coverDrawable.getBounds().bottom);
@@ -783,4 +791,320 @@
assertEquals(recyclerViewHeight, bottomDrawable.getBounds().top);
assertEquals(recyclerViewHeight, bottomDrawable.getBounds().bottom);
}
+
+ public static class DetailsSupportFragmentSwitchToVideoInOnCreate extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentSwitchToVideoInOnCreate() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreate() {
+ launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+ //SystemClock.sleep(5000);
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoSupportFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+ }
+ });
+
+ // switchToRows does nothing if there is no row
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToRows();
+ }
+ }
+ );
+ assertTrue(detailsFragment.mVideoSupportFragment.getView().hasFocus());
+
+ // create item, it should be layout outside screen
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world",
+ "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ }
+ }
+ );
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildCount() > 0
+ && detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ >= detailsFragment.getVerticalGridView().getHeight();
+ }
+ });
+
+ // pressing BACK will return to details row
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.getVerticalGridView().getChildAt(0).getTop()
+ < (detailsFragment.getVerticalGridView().getHeight() * 0.7f);
+ }
+ });
+ assertTrue(detailsFragment.getVerticalGridView().getChildAt(0).hasFocus());
+ }
+
+ @Test
+ public void switchToVideoBackToQuit() {
+ launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoInOnCreate.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoInOnCreate detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoInOnCreate) mActivity.getTestFragment();
+
+ // the pending enter transition flag should be automatically cleared
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTER_TRANSITION_COMPLETE.getStatus());
+ assertNull(TransitionHelper.getEnterTransition(mActivity.getWindow()));
+ assertEquals(0, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ assertTrue(detailsFragment.getRowsSupportFragment().getView().hasFocus());
+ assertFalse(detailsFragment.isShowingTitle());
+
+ SystemClock.sleep(1000);
+ assertNull(detailsFragment.mVideoSupportFragment);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ @Override
+ public void run() {
+ final MediaPlayerGlue glue = new MediaPlayerGlue(mActivity);
+ detailsFragment.mDetailsBackgroundController.setupVideoPlayback(glue);
+ glue.setMode(MediaPlayerGlue.REPEAT_ALL);
+ glue.setArtist("A Googleer");
+ glue.setTitle("Diving with Sharks");
+ glue.setMediaSource(Uri.parse(
+ "android.resource://android.support.v17.leanback.test/raw/video"));
+ }
+ }
+ );
+ // once the video fragment is created it would be immediately assigned focus
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mVideoSupportFragment != null
+ && detailsFragment.mVideoSupportFragment.getView() != null
+ && detailsFragment.mVideoSupportFragment.getView().hasFocus();
+ }
+ });
+ // wait auto hide play controls done:
+ PollingCheck.waitFor(8000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return ((PlaybackSupportFragment) detailsFragment.mVideoSupportFragment).mBgAlpha == 0;
+ }
+ });
+
+ // before any details row is presented, pressing BACK will quit the activity
+ sendKeys(KeyEvent.KEYCODE_BACK);
+ PollingCheck.waitFor(4000, new PollingCheck.ActivityDestroy(mActivity));
+ }
+
+ public static class DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition
+ extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ mDetailsBackground.switchToVideo();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void switchToVideoInOnCreateAndPrepareEntranceTransition() {
+ launchAndWaitActivity(DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition detailsFragment =
+ (DetailsSupportFragmentSwitchToVideoAndPrepareEntranceTransition)
+ mActivity.getTestFragment();
+
+ assertEquals(StateMachine.STATUS_INVOKED,
+ detailsFragment.STATE_ENTRANCE_COMPLETE.getStatus());
+ }
+
+ public static class DetailsSupportFragmentEntranceTransition
+ extends DetailsTestSupportFragment {
+
+ final DetailsSupportFragmentBackgroundController mDetailsBackground =
+ new DetailsSupportFragmentBackgroundController(this);
+
+ public DetailsSupportFragmentEntranceTransition() {
+ mTimeToLoadOverviewRow = mTimeToLoadRelatedRow = 100;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mDetailsBackground.enableParallax();
+ prepareEntranceTransition();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ Bitmap bitmap = BitmapFactory.decodeResource(getActivity().getResources(),
+ android.support.v17.leanback.test.R.drawable.spiderman);
+ mDetailsBackground.setCoverBitmap(bitmap);
+ }
+
+ @Override
+ public void onStop() {
+ mDetailsBackground.setCoverBitmap(null);
+ super.onStop();
+ }
+ }
+
+ @Test
+ public void entranceTransitionBlocksSwitchToVideo() {
+ launchAndWaitActivity(DetailsSupportFragmentEntranceTransition.class,
+ new Options().uiVisibility(
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN), 0);
+ final DetailsSupportFragmentEntranceTransition detailsFragment =
+ (DetailsSupportFragmentEntranceTransition)
+ mActivity.getTestFragment();
+
+ if (Build.VERSION.SDK_INT < 21) {
+ // when enter transition is not supported, mCanUseHost is immmediately true
+ assertTrue(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ } else {
+ // calling switchToVideo() between prepareEntranceTransition and entrance transition
+ // finishes will be ignored.
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ assertFalse(detailsFragment.mDetailsBackgroundController.mCanUseHost);
+ }
+ assertEquals(255, getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.setItem(new PhotoItem("Hello world", "Fake content goes here",
+ android.support.v17.leanback.test.R.drawable.spiderman));
+ detailsFragment.startEntranceTransition();
+ }
+ });
+ // once Entrance transition is finished, mCanUseHost will be true
+ // and we can switchToVideo and fade out the background.
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return detailsFragment.mDetailsBackgroundController.mCanUseHost;
+ }
+ });
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ detailsFragment.mDetailsBackgroundController.switchToVideo();
+ }
+ });
+ PollingCheck.waitFor(4000, new PollingCheck.PollingCheckCondition() {
+ @Override
+ public boolean canProceed() {
+ return 0 == getCoverDrawableAlpha(detailsFragment.mDetailsBackgroundController);
+ }
+ });
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
index 193203e..5f2fc81 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsFragmentTest.java
@@ -17,6 +17,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
@@ -29,8 +30,13 @@
import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.VerticalGridView;
import android.view.KeyEvent;
import android.view.View;
@@ -232,4 +238,57 @@
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
+
+ public static class F_ListRowWithOnClick extends RowsFragment {
+ Presenter.ViewHolder mLastClickedItemViewHolder;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setOnItemViewClickedListener(new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ mLastClickedItemViewHolder = itemViewHolder;
+ }
+ });
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void prefetchChildItemsBeforeAttach() throws Throwable {
+ launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+ F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) mActivity.getTestFragment();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+ final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ gridView.setSelectedPositionSmooth(lastRowPos);
+ }
+ }
+ );
+ waitForScrollIdle(gridView);
+ ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+ RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+ final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+ prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+ fragment.mLastClickedItemViewHolder = null;
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+ }
+ }
+ );
+ assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
index 70ddbac..8ebb047 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/RowsSupportFragmentTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
@@ -32,8 +33,13 @@
import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.HeaderItem;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
import android.support.v17.leanback.widget.ListRow;
import android.support.v17.leanback.widget.ListRowPresenter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.VerticalGridView;
import android.view.KeyEvent;
import android.view.View;
@@ -235,4 +241,57 @@
assertNotNull(gridView.findViewHolderForAdapterPosition(7));
}
+
+ public static class F_ListRowWithOnClick extends RowsSupportFragment {
+ Presenter.ViewHolder mLastClickedItemViewHolder;
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setOnItemViewClickedListener(new OnItemViewClickedListener() {
+ @Override
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ mLastClickedItemViewHolder = itemViewHolder;
+ }
+ });
+ ListRowPresenter lrp = new ListRowPresenter();
+ ArrayObjectAdapter adapter = new ArrayObjectAdapter(lrp);
+ setAdapter(adapter);
+ loadData(adapter, 10, 1);
+ }
+ }
+
+ @Test
+ public void prefetchChildItemsBeforeAttach() throws Throwable {
+ launchAndWaitActivity(F_ListRowWithOnClick.class, 1000);
+
+ F_ListRowWithOnClick fragment = (F_ListRowWithOnClick) mActivity.getTestFragment();
+ final VerticalGridView gridView = fragment.getVerticalGridView();
+ View lastRow = gridView.getChildAt(gridView.getChildCount() - 1);
+ final int lastRowPos = gridView.getChildAdapterPosition(lastRow);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ gridView.setSelectedPositionSmooth(lastRowPos);
+ }
+ }
+ );
+ waitForScrollIdle(gridView);
+ ItemBridgeAdapter.ViewHolder prefetchedBridgeVh = (ItemBridgeAdapter.ViewHolder)
+ gridView.findViewHolderForAdapterPosition(lastRowPos + 1);
+ RowPresenter prefetchedRowPresenter = (RowPresenter) prefetchedBridgeVh.getPresenter();
+ final ListRowPresenter.ViewHolder prefetchedListRowVh = (ListRowPresenter.ViewHolder)
+ prefetchedRowPresenter.getRowViewHolder(prefetchedBridgeVh.getViewHolder());
+
+ fragment.mLastClickedItemViewHolder = null;
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ new Runnable() {
+ public void run() {
+ prefetchedListRowVh.getItemViewHolder(0).view.performClick();
+ }
+ }
+ );
+ assertSame(prefetchedListRowVh.getItemViewHolder(0), fragment.mLastClickedItemViewHolder);
+ }
+
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
index d17811b..6047a1e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestActivity.java
@@ -21,6 +21,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.test.R;
+import android.util.Log;
public class SingleFragmentTestActivity extends Activity {
@@ -32,10 +33,12 @@
public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+ private static final String TAG = "TestActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate " + this);
Intent intent = getIntent();
final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
index 6ecd050..ad4881e 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleFragmentTestBase.java
@@ -21,6 +21,7 @@
import android.support.test.rule.ActivityTestRule;
import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v7.widget.RecyclerView;
+import android.util.Log;
import org.junit.After;
import org.junit.Rule;
@@ -29,6 +30,7 @@
public class SingleFragmentTestBase {
private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+ private static final String TAG = "SingleFragmentTestBase";
@Rule
public TestName mUnitTestName = new TestName();
@@ -43,6 +45,7 @@
public void afterTest() throws Throwable {
final SingleFragmentTestActivity activity = mActivity;
if (activity != null) {
+ Log.d(TAG, "wait finish " + activity + " for " + this);
mActivity = null;
activityTestRule.runOnUiThread(new Runnable() {
public void run() {
@@ -105,6 +108,7 @@
options.collect(intent);
}
mActivity = activityTestRule.launchActivity(intent);
+ Log.d(TAG, "launched " + mActivity + " for " + this, new Exception());
SystemClock.sleep(waitTimeMs);
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
index 911a32e..0fc3183 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestActivity.java
@@ -24,6 +24,7 @@
import android.content.Intent;
import android.os.Bundle;
import android.support.v17.leanback.test.R;
+import android.util.Log;
public class SingleSupportFragmentTestActivity extends FragmentActivity {
@@ -35,10 +36,12 @@
public static final String EXTRA_ACTIVITY_LAYOUT = "activityLayout";
public static final String EXTRA_UI_VISIBILITY = "uiVisibility";
+ private static final String TAG = "TestActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Log.d(TAG, "onCreate " + this);
Intent intent = getIntent();
final int uiOptions = intent.getIntExtra(EXTRA_UI_VISIBILITY, 0);
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
index dc3d97f..82d69d0 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/app/SingleSupportFragmentTestBase.java
@@ -24,6 +24,7 @@
import android.support.test.rule.ActivityTestRule;
import android.support.v17.leanback.testutils.PollingCheck;
import android.support.v7.widget.RecyclerView;
+import android.util.Log;
import org.junit.After;
import org.junit.Rule;
@@ -32,6 +33,7 @@
public class SingleSupportFragmentTestBase {
private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
+ private static final String TAG = "SingleSupportFragmentTestBase";
@Rule
public TestName mUnitTestName = new TestName();
@@ -46,6 +48,7 @@
public void afterTest() throws Throwable {
final SingleSupportFragmentTestActivity activity = mActivity;
if (activity != null) {
+ Log.d(TAG, "wait finish " + activity + " for " + this);
mActivity = null;
activityTestRule.runOnUiThread(new Runnable() {
public void run() {
@@ -108,6 +111,7 @@
options.collect(intent);
}
mActivity = activityTestRule.launchActivity(intent);
+ Log.d(TAG, "launched " + mActivity + " for " + this, new Exception());
SystemClock.sleep(waitTimeMs);
}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
new file mode 100644
index 0000000..a041c53
--- /dev/null
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackControlGlueTest.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v17.leanback.media;
+
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.times;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.test.InstrumentationRegistry;
+import android.support.v17.leanback.widget.PlaybackControlsRow;
+import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class PlaybackControlGlueTest {
+
+ public static class PlaybackControlGlueImpl extends PlaybackControlGlue {
+
+ public PlaybackControlGlueImpl(Context context) {
+ super(context, new int[] {PLAYBACK_SPEED_FAST_L0, PLAYBACK_SPEED_FAST_L1});
+ }
+
+ @Override
+ public boolean hasValidMedia() {
+ return true;
+ }
+
+ @Override
+ public boolean isMediaPlaying() {
+ return false;
+ }
+
+ @Override
+ public CharSequence getMediaTitle() {
+ return null;
+ }
+
+ @Override
+ public CharSequence getMediaSubtitle() {
+ return null;
+ }
+
+ @Override
+ public int getMediaDuration() {
+ return 0;
+ }
+
+ @Override
+ public Drawable getMediaArt() {
+ return null;
+ }
+
+ @Override
+ public long getSupportedActions() {
+ return 0;
+ }
+
+ @Override
+ public int getCurrentSpeedId() {
+ return 0;
+ }
+
+ @Override
+ public int getCurrentPosition() {
+ return 0;
+ }
+ }
+
+ Context mContext;
+ PlaybackControlGlue mGlue;
+
+ @Test
+ public void usingDefaultRowAndPresenter() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+ }
+ });
+ PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ mGlue.setHost(host);
+ Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+ assertSame(mGlue, host.mGlue);
+ assertSame(host, mGlue.getHost());
+ assertTrue(host.mPlaybackRowPresenter instanceof PlaybackControlsRowPresenter);
+ assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+ }
+ @Test
+ public void customRowPresenter() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+ }
+ });
+ PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+ @Override
+ protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+ return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+ }
+ };
+ mGlue.setPlaybackRowPresenter(presenter);
+ PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ mGlue.setHost(host);
+ Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+ assertSame(mGlue, host.mGlue);
+ assertSame(host, mGlue.getHost());
+ assertSame(host.mPlaybackRowPresenter, presenter);
+ assertTrue(host.mRow instanceof PlaybackControlsRow);
+
+ }
+
+ @Test
+ public void customControlsRow() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+ }
+ });
+ PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+ mGlue.setControlsRow(row);
+ PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ mGlue.setHost(host);
+ Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+ assertSame(mGlue, host.mGlue);
+ assertSame(host, mGlue.getHost());
+ assertTrue(host.mPlaybackRowPresenter instanceof PlaybackControlsRowPresenter);
+ assertSame(host.mRow, row);
+
+ }
+
+ @Test
+ public void customRowAndPresenter() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+ @Override
+ public void run() {
+ mGlue = Mockito.spy(new PlaybackControlGlueImpl(mContext));
+ }
+ });
+ PlaybackControlsRow row = new PlaybackControlsRow(mContext);
+ mGlue.setControlsRow(row);
+ PlaybackRowPresenter presenter = new PlaybackRowPresenter() {
+ @Override
+ protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+ return new RowPresenter.ViewHolder(new LinearLayout(parent.getContext()));
+ }
+ };
+ mGlue.setPlaybackRowPresenter(presenter);
+ PlaybackGlueHostImpl host = new PlaybackGlueHostImpl();
+
+ mGlue.setHost(host);
+ Mockito.verify(mGlue, times(1)).onAttachedToHost(host);
+ assertSame(mGlue, host.mGlue);
+ assertSame(host, mGlue.getHost());
+ assertSame(host.mPlaybackRowPresenter, presenter);
+ assertSame(host.mRow, row);
+
+ }
+}
diff --git a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
index 199ab3e..2c9aa43 100644
--- a/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
+++ b/v17/leanback/tests/java/android/support/v17/leanback/media/PlaybackGlueHostImpl.java
@@ -15,12 +15,17 @@
*/
package android.support.v17.leanback.media;
+import android.support.v17.leanback.widget.PlaybackRowPresenter;
+import android.support.v17.leanback.widget.Row;
+
/**
* Fake PlaybackGlueHost used by test.
*/
public class PlaybackGlueHostImpl extends PlaybackGlueHost {
HostCallback mHostCallback;
+ Row mRow;
+ PlaybackRowPresenter mPlaybackRowPresenter;
@Override
public void setHostCallback(HostCallback callback) {
@@ -56,4 +61,14 @@
mHostCallback.onHostDestroy();
}
}
+
+ @Override
+ public void setPlaybackRow(Row row) {
+ mRow = row;
+ }
+
+ @Override
+ public void setPlaybackRowPresenter(PlaybackRowPresenter presenter) {
+ mPlaybackRowPresenter = presenter;
+ }
}
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 8bbe406..0195ef7 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
@@ -785,6 +785,73 @@
}
@Test
+ public void testAddLastItemHorizontal() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setSelectedPositionSmooth(49);
+ }
+ }
+ );
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{150});
+ }
+ });
+
+ // assert new added item aligned to right edge
+ assertEquals(mGridView.getWidth() - mGridView.getPaddingRight(),
+ mGridView.getLayoutManager().findViewByPosition(50).getRight());
+ }
+
+ @Test
+ public void testAddMultipleLastItemsHorizontal() throws Throwable {
+
+ Intent intent = new Intent();
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.horizontal_linear);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 50);
+ initActivity(intent);
+ mOrientation = BaseGridView.HORIZONTAL;
+ mNumRows = 1;
+
+ mActivityTestRule.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ mGridView.setWindowAlignment(BaseGridView.WINDOW_ALIGN_BOTH_EDGE);
+ mGridView.setWindowAlignmentOffsetPercent(50);
+ mGridView.setSelectedPositionSmooth(49);
+ }
+ }
+ );
+ waitForScrollIdle(mVerifyLayout);
+ performAndWaitForAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mActivity.addItems(50, new int[]{150, 150, 150, 150, 150, 150, 150, 150, 150,
+ 150, 150, 150, 150, 150});
+ }
+ });
+
+ // The focused item will be at center of window
+ View view = mGridView.getLayoutManager().findViewByPosition(49);
+ assertEquals(mGridView.getWidth() / 2, (view.getLeft() + view.getRight()) / 2);
+ }
+
+ @Test
public void testItemAddRemoveHorizontal() throws Throwable {
Intent intent = new Intent();
diff --git a/v7/appcompat/res-public/values/public_attrs.xml b/v7/appcompat/res-public/values/public_attrs.xml
index 2e16fe7..a7829eb 100644
--- a/v7/appcompat/res-public/values/public_attrs.xml
+++ b/v7/appcompat/res-public/values/public_attrs.xml
@@ -195,6 +195,8 @@
<public type="attr" name="tickMark"/>
<public type="attr" name="tickMarkTint"/>
<public type="attr" name="tickMarkTintMode"/>
+ <public type="attr" name="tint"/>
+ <public type="attr" name="tintMode"/>
<public type="attr" name="title"/>
<public type="attr" name="titleMargin"/>
<public type="attr" name="titleMarginBottom"/>
diff --git a/v7/appcompat/res/values/attrs.xml b/v7/appcompat/res/values/attrs.xml
index 9fc767a..d5ec5a1 100644
--- a/v7/appcompat/res/values/attrs.xml
+++ b/v7/appcompat/res/values/attrs.xml
@@ -1063,6 +1063,27 @@
<!-- Sets a drawable as the content of this ImageView. Allows the use of vector drawable
when running on older versions of the platform. -->
<attr name="srcCompat" format="reference" />
+
+ <!-- Tint to apply to the image source. -->
+ <attr name="tint" format="color" />
+
+ <!-- Blending mode used to apply the image source tint. -->
+ <attr name="tintMode">
+ <!-- The tint is drawn on top of the drawable.
+ [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] -->
+ <enum name="src_over" value="3" />
+ <!-- The tint is masked by the alpha channel of the drawable. The drawable’s
+ color channels are thrown out. [Sa * Da, Sc * Da] -->
+ <enum name="src_in" value="5" />
+ <!-- The tint is drawn above the drawable, but with the drawable’s alpha
+ channel masking the result. [Da, Sc * Da + (1 - Sa) * Dc] -->
+ <enum name="src_atop" value="9" />
+ <!-- Multiplies the color and alpha channels of the drawable with those of
+ the tint. [Sa * Da, Sc * Dc] -->
+ <enum name="multiply" value="14" />
+ <!-- [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] -->
+ <enum name="screen" value="15" />
+ </attr>
</declare-styleable>
<declare-styleable name="AppCompatSeekBar">
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
index edbd1a7..96a481e 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatAutoCompleteTextView.java
@@ -37,7 +37,7 @@
* <ul>
* <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
* {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
@@ -53,8 +53,8 @@
android.R.attr.popupBackground
};
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatTextHelper mTextHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatTextHelper mTextHelper;
public AppCompatAutoCompleteTextView(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java b/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
index a4551db..cb0ef68 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatBackgroundHelper.java
@@ -148,18 +148,18 @@
private boolean shouldApplyFrameworkTintUsingColorFilter() {
final int sdk = Build.VERSION.SDK_INT;
- if (sdk < 21) {
- // API 19 and below doesn't have framework tint
- return false;
+ if (sdk > 21) {
+ // On API 22+, if we're using an internal compat background tint, we're also
+ // responsible for applying any custom tint set via the framework impl
+ return mInternalBackgroundTint != null;
} else if (sdk == 21) {
// GradientDrawable doesn't implement setTintList on API 21, and since there is
// no nice way to unwrap DrawableContainers we have to blanket apply this
// on API 21
return true;
} else {
- // On API 22+, if we're using an internal compat background tint, we're also
- // responsible for applying any custom tint set via the framework impl
- return mInternalBackgroundTint != null;
+ // API 19 and below doesn't have framework tint
+ return false;
}
}
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
index f7fa23f..c2075b9 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatButton.java
@@ -40,7 +40,7 @@
* <ul>
* <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
* {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
index 6942cc5..5809d25 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckBox.java
@@ -35,7 +35,7 @@
* A {@link CheckBox} which supports compatible features on older version of the platform,
* including:
* <ul>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.widget.CompoundButtonCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
* {@link R.attr#buttonTintMode}.</li>
@@ -46,7 +46,7 @@
*/
public class AppCompatCheckBox extends CheckBox implements TintableCompoundButton {
- private AppCompatCompoundButtonHelper mCompoundButtonHelper;
+ private final AppCompatCompoundButtonHelper mCompoundButtonHelper;
public AppCompatCheckBox(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
index fadf328..1725c24 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatCheckedTextView.java
@@ -34,7 +34,7 @@
android.R.attr.checkMark
};
- private AppCompatTextHelper mTextHelper;
+ private final AppCompatTextHelper mTextHelper;
public AppCompatCheckedTextView(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java b/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
index 9ea02c6..d93f399 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatEditText.java
@@ -36,7 +36,7 @@
* <ul>
* <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
* {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
@@ -47,8 +47,8 @@
*/
public class AppCompatEditText extends EditText implements TintableBackgroundView {
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatTextHelper mTextHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatTextHelper mTextHelper;
public AppCompatEditText(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
index f8280c9..d0bcf00 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageButton.java
@@ -20,33 +20,44 @@
import android.content.Context;
import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.view.TintableBackgroundView;
+import android.support.v4.widget.ImageViewCompat;
+import android.support.v4.widget.TintableImageSourceView;
import android.support.v7.appcompat.R;
import android.util.AttributeSet;
import android.widget.ImageButton;
+import android.widget.ImageView;
/**
* A {@link ImageButton} which supports compatible features on older version of the platform,
* including:
* <ul>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
+ * <li>Allows dynamic tint of its image via the image tint methods in
+ * {@link ImageViewCompat}.</li>
+ * <li>Allows setting of the image tint using {@link R.attr#tint} and
+ * {@link R.attr#tintMode}.</li>
* </ul>
*
* <p>This will automatically be used when you use {@link android.widget.ImageButton} in your
* layouts. You should only need to manually use this class when writing custom views.</p>
*/
-public class AppCompatImageButton extends ImageButton implements TintableBackgroundView {
+public class AppCompatImageButton extends ImageButton implements TintableBackgroundView,
+ TintableImageSourceView {
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatImageHelper mImageHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatImageHelper mImageHelper;
public AppCompatImageButton(Context context) {
this(context, null);
@@ -73,6 +84,38 @@
}
@Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ super.setImageDrawable(drawable);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bm) {
+ super.setImageBitmap(bm);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageIcon(@Nullable Icon icon) {
+ super.setImageIcon(icon);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageURI(@Nullable Uri uri) {
+ super.setImageURI(uri);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
if (mBackgroundTintHelper != null) {
@@ -143,6 +186,61 @@
return mBackgroundTintHelper != null
? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
}
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#setImageTintList(ImageView, ColorStateList)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setSupportImageTintList(@Nullable ColorStateList tint) {
+ if (mImageHelper != null) {
+ mImageHelper.setSupportImageTintList(tint);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#getImageTintList(ImageView)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ @Nullable
+ public ColorStateList getSupportImageTintList() {
+ return mImageHelper != null
+ ? mImageHelper.getSupportImageTintList() : null;
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#setImageTintMode(ImageView, PorterDuff.Mode)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mImageHelper != null) {
+ mImageHelper.setSupportImageTintMode(tintMode);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#getImageTintMode(ImageView)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ @Nullable
+ public PorterDuff.Mode getSupportImageTintMode() {
+ return mImageHelper != null
+ ? mImageHelper.getSupportImageTintMode() : null;
+ }
@Override
protected void drawableStateChanged() {
@@ -150,6 +248,9 @@
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySupportBackgroundTint();
}
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
}
public boolean hasOverlappingRendering() {
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
index fe733f7..12b8520 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageHelper.java
@@ -18,9 +18,13 @@
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
+import android.support.v4.widget.ImageViewCompat;
import android.support.v7.appcompat.R;
import android.support.v7.content.res.AppCompatResources;
import android.util.AttributeSet;
@@ -31,22 +35,22 @@
*/
@RestrictTo(LIBRARY_GROUP)
public class AppCompatImageHelper {
-
private final ImageView mView;
+ private TintInfo mInternalImageTint;
+ private TintInfo mImageTint;
+ private TintInfo mTmpInfo;
+
public AppCompatImageHelper(ImageView view) {
mView = view;
}
public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {
- TintTypedArray a = null;
+ TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
+ R.styleable.AppCompatImageView, defStyleAttr, 0);
try {
Drawable drawable = mView.getDrawable();
-
if (drawable == null) {
- a = TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,
- R.styleable.AppCompatImageView, defStyleAttr, 0);
-
// If the view doesn't already have a drawable (from android:src), try loading
// it from srcCompat
final int id = a.getResourceId(R.styleable.AppCompatImageView_srcCompat, -1);
@@ -61,10 +65,18 @@
if (drawable != null) {
DrawableUtils.fixDrawable(drawable);
}
- } finally {
- if (a != null) {
- a.recycle();
+
+ if (a.hasValue(R.styleable.AppCompatImageView_tint)) {
+ ImageViewCompat.setImageTintList(mView,
+ a.getColorStateList(R.styleable.AppCompatImageView_tint));
}
+ if (a.hasValue(R.styleable.AppCompatImageView_tintMode)) {
+ ImageViewCompat.setImageTintMode(mView,
+ DrawableUtils.parseTintMode(
+ a.getInt(R.styleable.AppCompatImageView_tintMode, -1), null));
+ }
+ } finally {
+ a.recycle();
}
}
@@ -78,6 +90,8 @@
} else {
mView.setImageDrawable(null);
}
+
+ applySupportImageTint();
}
boolean hasOverlappingRendering() {
@@ -90,4 +104,116 @@
}
return true;
}
+
+ void setSupportImageTintList(ColorStateList tint) {
+ if (mImageTint == null) {
+ mImageTint = new TintInfo();
+ }
+ mImageTint.mTintList = tint;
+ mImageTint.mHasTintList = true;
+ applySupportImageTint();
+ }
+
+ ColorStateList getSupportImageTintList() {
+ return mImageTint != null ? mImageTint.mTintList : null;
+ }
+
+ void setSupportImageTintMode(PorterDuff.Mode tintMode) {
+ if (mImageTint == null) {
+ mImageTint = new TintInfo();
+ }
+ mImageTint.mTintMode = tintMode;
+ mImageTint.mHasTintMode = true;
+
+ applySupportImageTint();
+ }
+
+ PorterDuff.Mode getSupportImageTintMode() {
+ return mImageTint != null ? mImageTint.mTintMode : null;
+ }
+
+ void applySupportImageTint() {
+ final Drawable imageViewDrawable = mView.getDrawable();
+ if (imageViewDrawable != null) {
+ DrawableUtils.fixDrawable(imageViewDrawable);
+ }
+
+ if (imageViewDrawable != null) {
+ if (shouldApplyFrameworkTintUsingColorFilter()
+ && applyFrameworkTintUsingColorFilter(imageViewDrawable)) {
+ // This needs to be called before the internal tints below so it takes
+ // effect on any widgets using the compat tint on API 21
+ return;
+ }
+
+ if (mImageTint != null) {
+ AppCompatDrawableManager.tintDrawable(imageViewDrawable, mImageTint,
+ mView.getDrawableState());
+ } else if (mInternalImageTint != null) {
+ AppCompatDrawableManager.tintDrawable(imageViewDrawable, mInternalImageTint,
+ mView.getDrawableState());
+ }
+ }
+ }
+
+ void setInternalImageTint(ColorStateList tint) {
+ if (tint != null) {
+ if (mInternalImageTint == null) {
+ mInternalImageTint = new TintInfo();
+ }
+ mInternalImageTint.mTintList = tint;
+ mInternalImageTint.mHasTintList = true;
+ } else {
+ mInternalImageTint = null;
+ }
+ applySupportImageTint();
+ }
+
+ private boolean shouldApplyFrameworkTintUsingColorFilter() {
+ final int sdk = Build.VERSION.SDK_INT;
+ if (sdk > 21) {
+ // On API 22+, if we're using an internal compat image source tint, we're also
+ // responsible for applying any custom tint set via the framework impl
+ return mInternalImageTint != null;
+ } else if (sdk == 21) {
+ // GradientDrawable doesn't implement setTintList on API 21, and since there is
+ // no nice way to unwrap DrawableContainers we have to blanket apply this
+ // on API 21
+ return true;
+ } else {
+ // API 19 and below doesn't have framework tint
+ return false;
+ }
+ }
+
+ /**
+ * Applies the framework image source tint to a view, but using the compat method (ColorFilter)
+ *
+ * @return true if a tint was applied
+ */
+ private boolean applyFrameworkTintUsingColorFilter(@NonNull Drawable imageSource) {
+ if (mTmpInfo == null) {
+ mTmpInfo = new TintInfo();
+ }
+ final TintInfo info = mTmpInfo;
+ info.clear();
+
+ final ColorStateList tintList = ImageViewCompat.getImageTintList(mView);
+ if (tintList != null) {
+ info.mHasTintList = true;
+ info.mTintList = tintList;
+ }
+ final PorterDuff.Mode mode = ImageViewCompat.getImageTintMode(mView);
+ if (mode != null) {
+ info.mHasTintMode = true;
+ info.mTintMode = mode;
+ }
+
+ if (info.mHasTintList || info.mHasTintMode) {
+ AppCompatDrawableManager.tintDrawable(imageSource, info, mView.getDrawableState());
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
index b749d6c..532a18b 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatImageView.java
@@ -20,12 +20,17 @@
import android.content.Context;
import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.view.TintableBackgroundView;
+import android.support.v4.widget.ImageViewCompat;
+import android.support.v4.widget.TintableImageSourceView;
import android.support.v7.appcompat.R;
import android.util.AttributeSet;
import android.widget.ImageView;
@@ -34,19 +39,24 @@
* A {@link ImageView} which supports compatible features on older version of the platform,
* including:
* <ul>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
+ * <li>Allows dynamic tint of its image via the image tint methods in
+ * {@link ImageViewCompat}.</li>
+ * <li>Allows setting of the image tint using {@link R.attr#tint} and
+ * {@link R.attr#tintMode}.</li>
* </ul>
*
* <p>This will automatically be used when you use {@link android.widget.ImageView} in your
* layouts. You should only need to manually use this class when writing custom views.</p>
*/
-public class AppCompatImageView extends ImageView implements TintableBackgroundView {
+public class AppCompatImageView extends ImageView implements TintableBackgroundView,
+ TintableImageSourceView {
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatImageHelper mImageHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatImageHelper mImageHelper;
public AppCompatImageView(Context context) {
this(context, null);
@@ -77,8 +87,42 @@
*/
@Override
public void setImageResource(@DrawableRes int resId) {
- // Intercept this call and instead retrieve the Drawable via the image helper
- mImageHelper.setImageResource(resId);
+ if (mImageHelper != null) {
+ // Intercept this call and instead retrieve the Drawable via the image helper
+ mImageHelper.setImageResource(resId);
+ }
+ }
+
+ @Override
+ public void setImageDrawable(@Nullable Drawable drawable) {
+ super.setImageDrawable(drawable);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bm) {
+ super.setImageBitmap(bm);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageIcon(@Nullable Icon icon) {
+ super.setImageIcon(icon);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
+ }
+
+ @Override
+ public void setImageURI(@Nullable Uri uri) {
+ super.setImageURI(uri);
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
}
@Override
@@ -153,12 +197,71 @@
? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
}
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#setImageTintList(ImageView, ColorStateList)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setSupportImageTintList(@Nullable ColorStateList tint) {
+ if (mImageHelper != null) {
+ mImageHelper.setSupportImageTintList(tint);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#getImageTintList(ImageView)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ @Nullable
+ public ColorStateList getSupportImageTintList() {
+ return mImageHelper != null
+ ? mImageHelper.getSupportImageTintList() : null;
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#setImageTintMode(ImageView, PorterDuff.Mode)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ public void setSupportImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+ if (mImageHelper != null) {
+ mImageHelper.setSupportImageTintMode(tintMode);
+ }
+ }
+
+ /**
+ * This should be accessed via
+ * {@link android.support.v4.widget.ImageViewCompat#getImageTintMode(ImageView)}
+ *
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ @Override
+ @Nullable
+ public PorterDuff.Mode getSupportImageTintMode() {
+ return mImageHelper != null
+ ? mImageHelper.getSupportImageTintMode() : null;
+ }
+
@Override
protected void drawableStateChanged() {
super.drawableStateChanged();
if (mBackgroundTintHelper != null) {
mBackgroundTintHelper.applySupportBackgroundTint();
}
+ if (mImageHelper != null) {
+ mImageHelper.applySupportImageTint();
+ }
}
public boolean hasOverlappingRendering() {
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
index 381169d..8060d7d 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatMultiAutoCompleteTextView.java
@@ -37,7 +37,7 @@
* <ul>
* <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
* {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
@@ -53,8 +53,8 @@
android.R.attr.popupBackground
};
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatTextHelper mTextHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatTextHelper mTextHelper;
public AppCompatMultiAutoCompleteTextView(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java b/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
index 768d610..8aa22f3 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatRadioButton.java
@@ -35,7 +35,7 @@
* A {@link RadioButton} which supports compatible features on older version of the platform,
* including:
* <ul>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.widget.CompoundButtonCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
* {@link R.attr#buttonTintMode}.</li>
@@ -46,7 +46,7 @@
*/
public class AppCompatRadioButton extends RadioButton implements TintableCompoundButton {
- private AppCompatCompoundButtonHelper mCompoundButtonHelper;
+ private final AppCompatCompoundButtonHelper mCompoundButtonHelper;
public AppCompatRadioButton(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java b/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
index afaa02a..762e97a 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatRatingBar.java
@@ -31,7 +31,7 @@
*/
public class AppCompatRatingBar extends RatingBar {
- private AppCompatProgressBarHelper mAppCompatProgressBarHelper;
+ private final AppCompatProgressBarHelper mAppCompatProgressBarHelper;
public AppCompatRatingBar(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java b/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
index bac1cb8..8b476b8 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatSeekBar.java
@@ -32,7 +32,7 @@
*/
public class AppCompatSeekBar extends SeekBar {
- private AppCompatSeekBarHelper mAppCompatSeekBarHelper;
+ private final AppCompatSeekBarHelper mAppCompatSeekBarHelper;
public AppCompatSeekBar(Context context) {
this(context, null);
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java b/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
index 9f27f78..1be8ed8 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatSpinner.java
@@ -55,11 +55,11 @@
* A {@link Spinner} which supports compatible features on older versions of the platform,
* including:
* <ul>
- * <li>Dynamic tinting of the background via the background tint methods in
- * {@link android.support.v4.view.ViewCompat}.</li>
- * <li>Configuring the background tint using {@link R.attr#backgroundTint} and
- * {@link R.attr#backgroundTintMode}.</li>
- * <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
+ * <li>Allows dynamic tint of its background via the background tint methods in
+ * {@link android.support.v4.widget.CompoundButtonCompat}.</li>
+ * <li>Allows setting of the background tint using {@link R.attr#buttonTint} and
+ * {@link R.attr#buttonTintMode}.</li>
+ * <li>Setting the popup theme using {@link R.attr#popupTheme}.</li>
* </ul>
*
* <p>This will automatically be used when you use {@link Spinner} in your layouts.
@@ -77,10 +77,10 @@
private static final int MODE_DROPDOWN = 1;
private static final int MODE_THEME = -1;
- private AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
/** Context used to inflate the popup window or dialog. */
- private Context mPopupContext;
+ private final Context mPopupContext;
/** Forwarding listener used to implement drag-to-open. */
private ForwardingListener mForwardingListener;
@@ -88,13 +88,13 @@
/** Temporary holder for setAdapter() calls from the super constructor. */
private SpinnerAdapter mTempAdapter;
- private boolean mPopupSet;
+ private final boolean mPopupSet;
- DropdownPopup mPopup;
+ private DropdownPopup mPopup;
- int mDropDownWidth;
+ private int mDropDownWidth;
- final Rect mTempRect = new Rect();
+ private final Rect mTempRect = new Rect();
/**
* Construct a new spinner with the given context's theme.
diff --git a/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java b/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
index 66fd929..96dfd5a 100644
--- a/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
+++ b/v7/appcompat/src/android/support/v7/widget/AppCompatTextView.java
@@ -36,7 +36,7 @@
* <ul>
* <li>Supports {@link R.attr#textAllCaps} style attribute which works back to
* {@link android.os.Build.VERSION_CODES#GINGERBREAD Gingerbread}.</li>
- * <li>Allows dynamic tint of it background via the background tint methods in
+ * <li>Allows dynamic tint of its background via the background tint methods in
* {@link android.support.v4.view.ViewCompat}.</li>
* <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
* {@link R.attr#backgroundTintMode}.</li>
@@ -47,8 +47,8 @@
*/
public class AppCompatTextView extends TextView implements TintableBackgroundView {
- private AppCompatBackgroundHelper mBackgroundTintHelper;
- private AppCompatTextHelper mTextHelper;
+ private final AppCompatBackgroundHelper mBackgroundTintHelper;
+ private final AppCompatTextHelper mTextHelper;
public AppCompatTextView(Context context) {
this(context, null);
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index 7385a5f..14413a2 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -75,6 +75,11 @@
android:theme="@style/Theme.TextColors" />
<activity
+ android:name="android.support.v7.widget.AppCompatImageButtonActivity"
+ android:label="@string/app_compat_image_button_activity"
+ android:theme="@style/Theme.AppCompat.Light" />
+
+ <activity
android:name="android.support.v7.widget.AppCompatImageViewActivity"
android:label="@string/app_compat_image_view_activity"
android:theme="@style/Theme.AppCompat.Light" />
diff --git a/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml b/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml
new file mode 100755
index 0000000..4be0ee0
--- /dev/null
+++ b/v7/appcompat/tests/res/layout/appcompat_imagebutton_activity.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_tinted_no_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@null"
+ android:src="@drawable/test_drawable_blue"
+ app:backgroundTint="@color/color_state_list_lilac"
+ app:backgroundTintMode="src_in"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_tinted_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/test_drawable"
+ android:src="@drawable/test_drawable_blue"
+ app:backgroundTint="@color/color_state_list_lilac"
+ app:backgroundTintMode="src_in"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_untinted_no_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@null"
+ android:src="@drawable/test_drawable_blue"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_untinted_background"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@drawable/test_background_green"
+ android:src="@drawable/test_drawable_blue"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_tinted_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/test_drawable_blue"
+ app:tint="@color/color_state_list_lilac"
+ app:tintMode="src_in"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_tinted_no_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:tint="@color/color_state_list_lilac"
+ app:tintMode="src_in"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_untinted_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/test_drawable_blue"/>
+
+ <android.support.v7.widget.AppCompatImageButton
+ android:id="@+id/view_untinted_no_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml b/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
index c2aa5ed..b8af1b2 100644
--- a/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
+++ b/v7/appcompat/tests/res/layout/appcompat_imageview_activity.xml
@@ -56,11 +56,44 @@
android:background="@drawable/test_background_green" />
<android.support.v7.widget.AppCompatImageView
- android:id="@+id/view_android_src_srccompat"
- android:layout_width="30dp"
- android:layout_height="30dp"
- android:src="@drawable/test_drawable_blue"
- app:srcCompat="@drawable/test_drawable_red" />
+ android:id="@+id/view_android_src_srccompat"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:src="@drawable/test_drawable_blue"
+ app:srcCompat="@drawable/test_drawable_red" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/view_tinted_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/test_drawable_blue"
+ app:tint="@color/color_state_list_lilac"
+ app:tintMode="src_in" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/view_tinted_no_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:tint="@color/color_state_list_lilac"
+ app:tintMode="src_in" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/view_untinted_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:src="@drawable/test_drawable_blue" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/view_untinted_no_source"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <android.support.v7.widget.AppCompatImageView
+ android:id="@+id/view_tinted_android_src_srccompat"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:src="@drawable/test_drawable_blue"
+ app:srcCompat="@drawable/test_drawable_red" />
</LinearLayout>
diff --git a/v7/appcompat/tests/res/values/strings.xml b/v7/appcompat/tests/res/values/strings.xml
index 812bd61..926418a 100644
--- a/v7/appcompat/tests/res/values/strings.xml
+++ b/v7/appcompat/tests/res/values/strings.xml
@@ -53,6 +53,7 @@
<string name="app_compat_text_view_activity">AppCompat text view</string>
<string name="sample_text1">Sample text 1</string>
<string name="sample_text2">Sample text 2</string>
+ <string name="app_compat_image_button_activity">AppCompat image button</string>
<string name="app_compat_image_view_activity">AppCompat image view</string>
<string name="app_compat_button_activity">AppCompat button</string>
<string-array name="planets_array">
diff --git a/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java b/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
index de36207..357de5c 100644
--- a/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
+++ b/v7/appcompat/tests/src/android/support/v7/testutils/AppCompatTintableViewActions.java
@@ -16,26 +16,28 @@
package android.support.v7.testutils;
+import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+
+import static org.hamcrest.core.AllOf.allOf;
+import static org.hamcrest.core.AnyOf.anyOf;
+
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
-import android.support.v4.view.TintableBackgroundView;
import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.AppCompatTextView;
+import android.support.v4.widget.ImageViewCompat;
import android.view.View;
-import org.hamcrest.Matcher;
+import android.widget.ImageView;
-import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
-import static org.hamcrest.core.AllOf.allOf;
+import org.hamcrest.Matcher;
public class AppCompatTintableViewActions {
/**
- * Sets the passed color state list as the background layer on a {@link View}.
+ * Sets the passed color state list as the background tint on a {@link View}.
*/
public static ViewAction setBackgroundTintList(final ColorStateList tint) {
return new ViewAction() {
@@ -87,6 +89,58 @@
}
/**
+ * Sets the passed color state list as the image source tint on a {@link View}.
+ */
+ public static ViewAction setImageSourceTintList(final ColorStateList tint) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return anyOf(isAssignableFrom(ImageView.class));
+ }
+
+ @Override
+ public String getDescription() {
+ return "set image source tint list";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ ImageViewCompat.setImageTintList((ImageView) view, tint);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
+ * Sets the passed mode as the image source tint mode on a <code>View</code>.
+ */
+ public static ViewAction setImageSourceTintMode(final PorterDuff.Mode mode) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return anyOf(isAssignableFrom(ImageView.class));
+ }
+
+ @Override
+ public String getDescription() {
+ return "set image source tint mode";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ ImageViewCompat.setImageTintMode((ImageView) view, mode);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
+
+ /**
* Sets background drawable on a <code>View</code> that implements the
* <code>TintableBackgroundView</code> interface.
*/
@@ -140,4 +194,32 @@
};
}
+ /**
+ * Sets image resource on a <code>View</code> that implements the
+ * <code>TintableBackgroundView</code> interface and also extends the
+ * <code>ImageView</code> base class.
+ */
+ public static ViewAction setImageResource(final @DrawableRes int resId) {
+ return new ViewAction() {
+ @Override
+ public Matcher<View> getConstraints() {
+ return allOf(TestUtilsMatchers.isTintableBackgroundView(),
+ isAssignableFrom(ImageView.class));
+ }
+
+ @Override
+ public String getDescription() {
+ return "set image resource";
+ }
+
+ @Override
+ public void perform(UiController uiController, View view) {
+ uiController.loopMainThreadUntilIdle();
+
+ ((ImageView) view).setImageResource(resId);
+
+ uiController.loopMainThreadUntilIdle();
+ }
+ };
+ }
}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java
new file mode 100755
index 0000000..c8398ed
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseImageViewTest.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.v7.testutils.TestUtilsActions.setEnabled;
+
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.AppCompatTintableViewActions;
+import android.support.v7.testutils.BaseTestActivity;
+import android.support.v7.testutils.TestUtils;
+import android.widget.ImageView;
+
+import org.junit.Test;
+
+/**
+ * In addition to all tinting-related tests done by the base class, this class provides
+ * testing for tinting image resources on appcompat-v7 view classes that extend directly
+ * or indirectly the core {@link ImageView} class.
+ */
+public abstract class AppCompatBaseImageViewTest<T extends ImageView>
+ extends AppCompatBaseViewTest<BaseTestActivity, T> {
+ public AppCompatBaseImageViewTest(Class clazz) {
+ super(clazz);
+ }
+
+ private void verifyImageSourceIsColoredAs(String description, @NonNull ImageView imageView,
+ @ColorInt int color, int allowedComponentVariance) {
+ Drawable imageSource = imageView.getDrawable();
+ TestUtils.assertAllPixelsOfColor(description,
+ imageSource, imageSource.getIntrinsicWidth(), imageSource.getIntrinsicHeight(),
+ true, color, allowedComponentVariance, false);
+ }
+
+ /**
+ * This method tests that image tinting is applied to tintable image view
+ * in enabled and disabled state across a number of <code>ColorStateList</code>s set as
+ * image source tint lists on the same image source.
+ */
+ @Test
+ @SmallTest
+ public void testImageTintingAcrossStateChange() {
+ final @IdRes int viewId = R.id.view_tinted_source;
+ final Resources res = getActivity().getResources();
+ final T view = (T) mContainer.findViewById(viewId);
+
+ @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+ @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+ @ColorInt int sandDefault = ResourcesCompat.getColor(res, R.color.sand_default, null);
+ @ColorInt int sandDisabled = ResourcesCompat.getColor(res, R.color.sand_disabled, null);
+ @ColorInt int oceanDefault = ResourcesCompat.getColor(res, R.color.ocean_default, null);
+ @ColorInt int oceanDisabled = ResourcesCompat.getColor(res, R.color.ocean_disabled, null);
+
+ // Test the default state for tinting set up in the layout XML file.
+ verifyImageSourceIsColoredAs("Default lilac tinting in enabled state", view,
+ lilacDefault, 0);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("Default lilac tinting in disabled state", view,
+ lilacDisabled, 0);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state", view,
+ lilacDefault, 0);
+
+ // Load a new color state list, set it on the view and check that the image has
+ // switched to the matching entry in newly set color state list.
+ final ColorStateList sandColor = ResourcesCompat.getColorStateList(
+ res, R.color.color_state_list_sand, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintList(sandColor));
+ verifyImageSourceIsColoredAs("New sand tinting in enabled state", view,
+ sandDefault, 0);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("New sand tinting in disabled state", view,
+ sandDisabled, 0);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("New sand tinting in re-enabled state", view,
+ sandDefault, 0);
+
+ // Load another color state list, set it on the view and check that the image has
+ // switched to the matching entry in newly set color state list.
+ final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
+ res, R.color.color_state_list_ocean, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintList(oceanColor));
+ verifyImageSourceIsColoredAs("New ocean tinting in enabled state", view,
+ oceanDefault, 0);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("New ocean tinting in disabled state", view,
+ oceanDisabled, 0);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("New ocean tinting in re-enabled state", view,
+ oceanDefault, 0);
+ }
+
+ /**
+ * This method tests that image tinting is applied to tintable image view
+ * in enabled and disabled state across the same image source respects the currently set
+ * image source tinting mode.
+ */
+ @Test
+ @SmallTest
+ public void testImageTintingAcrossModeChange() {
+ final @IdRes int viewId = R.id.view_untinted_source;
+ final Resources res = getActivity().getResources();
+ final T view = (T) mContainer.findViewById(viewId);
+
+ @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+ res, R.color.emerald_translucent_default, null);
+ @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+ res, R.color.emerald_translucent_disabled, null);
+ // This is the fill color of R.drawable.test_drawable_blue set on our view
+ // that we'll be testing in this method
+ @ColorInt int sourceColor = ResourcesCompat.getColor(
+ res, R.color.test_blue, null);
+
+ // Test the default state for tinting set up in the layout XML file.
+ verifyImageSourceIsColoredAs("Default no tinting in enabled state", view,
+ sourceColor, 0);
+
+ // From this point on in this method we're allowing a margin of error in checking the
+ // color of the image source. This is due to both translucent colors being used
+ // in the color state list and off-by-one discrepancies of SRC_OVER when it's compositing
+ // translucent color on top of solid fill color. This is where the allowed variance
+ // value of 2 comes from - one for compositing and one for color translucency.
+ final int allowedComponentVariance = 2;
+
+ // Set src_in tint mode on our view
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_IN));
+
+ // Load a new color state list, set it on the view and check that the image has
+ // switched to the matching entry in newly set color state list.
+ final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
+ res, R.color.color_state_list_emerald_translucent, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintList(emeraldColor));
+ verifyImageSourceIsColoredAs("New emerald tinting in enabled state under src_in", view,
+ emeraldDefault, allowedComponentVariance);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("New emerald tinting in disabled state under src_in", view,
+ emeraldDisabled, allowedComponentVariance);
+
+ // Set src_over tint mode on our view. As the currently set tint list is using
+ // translucent colors, we expect the actual image source of the view to be different under
+ // this new mode (unlike src_in and src_over that behave identically when the destination is
+ // a fully filled rectangle and the source is an opaque color).
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_OVER));
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("New emerald tinting in enabled state under src_over", view,
+ ColorUtils.compositeColors(emeraldDefault, sourceColor),
+ allowedComponentVariance);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the newly set color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("New emerald tinting in disabled state under src_over",
+ view, ColorUtils.compositeColors(emeraldDisabled, sourceColor),
+ allowedComponentVariance);
+ }
+
+ /**
+ * This method tests that opaque tinting applied to tintable image source
+ * is applied correctly after changing the image source itself.
+ */
+ @Test
+ @SmallTest
+ public void testImageOpaqueTintingAcrossImageChange() {
+ final @IdRes int viewId = R.id.view_tinted_no_source;
+ final Resources res = getActivity().getResources();
+ final T view = (T) mContainer.findViewById(viewId);
+
+ @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+ @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+
+ // Set image source on our view
+ onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+ R.drawable.test_drawable_green));
+
+ // Test the default state for tinting set up in the layout XML file.
+ verifyImageSourceIsColoredAs("Default lilac tinting in enabled state on green source",
+ view, lilacDefault, 0);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("Default lilac tinting in disabled state on green source",
+ view, lilacDisabled, 0);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state on green source",
+ view, lilacDefault, 0);
+
+ // Set a different image source on our view based on resource ID
+ onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+ R.drawable.test_drawable_red));
+
+ // Test the default state for tinting set up in the layout XML file.
+ verifyImageSourceIsColoredAs("Default lilac tinting in enabled state on red source",
+ view, lilacDefault, 0);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("Default lilac tinting in disabled state on red source",
+ view, lilacDisabled, 0);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("Default lilac tinting in re-enabled state on red source",
+ view, lilacDefault, 0);
+ }
+
+ /**
+ * This method tests that translucent tinting applied to tintable image source
+ * is applied correctly after changing the image source itself.
+ */
+ @Test
+ @SmallTest
+ public void testImageTranslucentTintingAcrossImageChange() {
+ final @IdRes int viewId = R.id.view_untinted_no_source;
+ final Resources res = getActivity().getResources();
+ final T view = (T) mContainer.findViewById(viewId);
+
+ @ColorInt int emeraldDefault = ResourcesCompat.getColor(
+ res, R.color.emerald_translucent_default, null);
+ @ColorInt int emeraldDisabled = ResourcesCompat.getColor(
+ res, R.color.emerald_translucent_disabled, null);
+ // This is the fill color of R.drawable.test_drawable_green that will be set on our view
+ // that we'll be testing in this method
+ @ColorInt int colorGreen = ResourcesCompat.getColor(
+ res, R.color.test_green, null);
+ // This is the fill color of R.drawable.test_drawable_red that will be set on our view
+ // that we'll be testing in this method
+ @ColorInt int colorRed = ResourcesCompat.getColor(
+ res, R.color.test_red, null);
+
+ // Set src_over tint mode on our view. As the currently set tint list is using
+ // translucent colors, we expect the actual image source of the view to be different under
+ // this new mode (unlike src_in and src_over that behave identically when the destination is
+ // a fully filled rectangle and the source is an opaque color).
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_OVER));
+ // Load and set a translucent color state list as the image source tint list
+ final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
+ res, R.color.color_state_list_emerald_translucent, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintList(emeraldColor));
+
+ // Set image source on our view
+ onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+ R.drawable.test_drawable_green));
+
+ // From this point on in this method we're allowing a margin of error in checking the
+ // color of the image source. This is due to both translucent colors being used
+ // in the color state list and off-by-one discrepancies of SRC_OVER when it's compositing
+ // translucent color on top of solid fill color. This is where the allowed variance
+ // value of 2 comes from - one for compositing and one for color translucency.
+ final int allowedComponentVariance = 2;
+
+ // Test the default state for tinting set up with the just loaded tint list.
+ verifyImageSourceIsColoredAs("Emerald tinting in enabled state on green source",
+ view, ColorUtils.compositeColors(emeraldDefault, colorGreen),
+ allowedComponentVariance);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("Emerald tinting in disabled state on green source",
+ view, ColorUtils.compositeColors(emeraldDisabled, colorGreen),
+ allowedComponentVariance);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in the default color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("Emerald tinting in re-enabled state on green source",
+ view, ColorUtils.compositeColors(emeraldDefault, colorGreen),
+ allowedComponentVariance);
+
+ // Set a different image source on our view based on resource ID
+ onView(withId(viewId)).perform(AppCompatTintableViewActions.setImageResource(
+ R.drawable.test_drawable_red));
+
+ // Test the default state for tinting the new image with the same color state list
+ verifyImageSourceIsColoredAs("Emerald tinting in enabled state on red source",
+ view, ColorUtils.compositeColors(emeraldDefault, colorRed),
+ allowedComponentVariance);
+
+ // Disable the view and check that the image has switched to the matching entry
+ // in our current color state list.
+ onView(withId(viewId)).perform(setEnabled(false));
+ verifyImageSourceIsColoredAs("Emerald tinting in disabled state on red source",
+ view, ColorUtils.compositeColors(emeraldDisabled, colorRed),
+ allowedComponentVariance);
+
+ // Enable the view and check that the image has switched to the matching entry
+ // in our current color state list.
+ onView(withId(viewId)).perform(setEnabled(true));
+ verifyImageSourceIsColoredAs("Emerald tinting in re-enabled state on red source",
+ view, ColorUtils.compositeColors(emeraldDefault, colorRed),
+ allowedComponentVariance);
+ }
+
+ /**
+ * This method tests that background tinting applied on a tintable image view does not
+ * affect the tinting of the image source.
+ */
+ @Test
+ @SmallTest
+ public void testImageTintingAcrossBackgroundTintingChange() {
+ final @IdRes int viewId = R.id.view_untinted_source;
+ final Resources res = getActivity().getResources();
+ final T view = (T) mContainer.findViewById(viewId);
+
+ @ColorInt int lilacDefault = ResourcesCompat.getColor(res, R.color.lilac_default, null);
+ @ColorInt int lilacDisabled = ResourcesCompat.getColor(res, R.color.lilac_disabled, null);
+ // This is the fill color of R.drawable.test_drawable_blue set on our view
+ // that we'll be testing in this method
+ @ColorInt int sourceColor = ResourcesCompat.getColor(
+ res, R.color.test_blue, null);
+ @ColorInt int newSourceColor = ResourcesCompat.getColor(
+ res, R.color.test_red, null);
+
+ // Test the default state for tinting set up in the layout XML file.
+ verifyImageSourceIsColoredAs("Default no tinting in enabled state", view,
+ sourceColor, 0);
+
+ // Change background tinting of our image
+ final ColorStateList lilacColor = ResourcesCompat.getColorStateList(
+ mResources, R.color.color_state_list_lilac, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setBackgroundResource(
+ R.drawable.test_background_green));
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setBackgroundTintMode(PorterDuff.Mode.SRC_IN));
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setBackgroundTintList(lilacColor));
+
+ // Verify that the image still has the original color (untinted)
+ verifyImageSourceIsColoredAs("No image tinting after change in background tinting", view,
+ sourceColor, 0);
+
+ // Now set a different image source
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageResource(R.drawable.test_drawable_red));
+ // And verify that the image has the new color (untinted)
+ verifyImageSourceIsColoredAs("No image tinting after change of image source", view,
+ newSourceColor, 0);
+
+ // Change the background tinting again
+ final ColorStateList sandColor = ResourcesCompat.getColorStateList(
+ mResources, R.color.color_state_list_sand, null);
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setBackgroundTintList(sandColor));
+ // And verify that the image still has the same new color (untinted)
+ verifyImageSourceIsColoredAs("No image tinting after change in background tinting", view,
+ newSourceColor, 0);
+
+ // Now set up image tinting on our view. We're using a color state list with fully
+ // opaque colors, and we expect the matching entry in that list to be applied on the
+ // image source (ignoring the background tinting)
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintMode(PorterDuff.Mode.SRC_IN));
+ onView(withId(viewId)).perform(
+ AppCompatTintableViewActions.setImageSourceTintList(lilacColor));
+ verifyImageSourceIsColoredAs("New lilac image tinting", view,
+ lilacDefault, 0);
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
index bacb0e3..6e4e6c0 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatBaseViewTest.java
@@ -35,6 +35,7 @@
import android.support.annotation.ColorInt;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.app.BaseInstrumentationTestCase;
@@ -42,7 +43,6 @@
import android.support.v7.testutils.AppCompatTintableViewActions;
import android.support.v7.testutils.BaseTestActivity;
import android.support.v7.testutils.TestUtils;
-import android.test.suitebuilder.annotation.SmallTest;
import android.view.View;
import android.view.ViewGroup;
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java
new file mode 100644
index 0000000..1a64047
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonActivity.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.support.v7.appcompat.test.R;
+import android.support.v7.testutils.BaseTestActivity;
+
+public class AppCompatImageButtonActivity extends BaseTestActivity {
+ @Override
+ protected int getContentViewLayoutResId() {
+ return R.layout.appcompat_imagebutton_activity;
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java
new file mode 100644
index 0000000..67fca4a
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageButtonTest.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+/**
+ * In addition to all tinting-related tests done by the base class, this class provides
+ * tests specific to {@link AppCompatImageButton} class.
+ */
+public class AppCompatImageButtonTest extends AppCompatBaseImageViewTest<AppCompatImageButton> {
+ public AppCompatImageButtonTest() {
+ super(AppCompatImageButtonActivity.class);
+ }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
index d6801da..88d105c 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/AppCompatImageViewTest.java
@@ -31,8 +31,7 @@
* tests specific to {@link AppCompatImageView} class.
*/
@SmallTest
-public class AppCompatImageViewTest
- extends AppCompatBaseViewTest<AppCompatImageViewActivity, AppCompatImageView> {
+public class AppCompatImageViewTest extends AppCompatBaseImageViewTest<AppCompatImageView> {
public AppCompatImageViewTest() {
super(AppCompatImageViewActivity.class);
}
@@ -40,7 +39,7 @@
@Test
public void testImageViewBothSrcCompatAndroidSrcSet() {
final int expectedColor = mContainer.getResources().getColor(R.color.test_blue);
- ViewInteraction imageViewInteration = onView(withId(R.id.view_android_src_srccompat));
- imageViewInteration.check(matches(TestUtilsMatchers.drawable(expectedColor)));
+ ViewInteraction imageViewInteraction = onView(withId(R.id.view_android_src_srccompat));
+ imageViewInteraction.check(matches(TestUtilsMatchers.drawable(expectedColor)));
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index fe16de8..0d85b3b 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -2708,7 +2708,8 @@
record.updatePlaybackInfo();
}
if (mMediaSession != null) {
- if (mSelectedRoute == getDefaultRoute()) {
+ if (mSelectedRoute == getDefaultRoute()
+ || mSelectedRoute == getBluetoothRoute()) {
// Local route
mMediaSession.clearVolumeHandling();
} else {
diff --git a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
index 9392ee2..e72d48d 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GapWorker.java
@@ -283,7 +283,7 @@
position, false, deadlineNs);
if (holder != null) {
- if (holder.isBound()) {
+ if (holder.isBound() && !holder.isInvalid()) {
// Only give the view a chance to go into the cache if binding succeeded
// Note that we must use public method, since item may need cleanup
recycler.recycleView(holder.itemView);
@@ -335,7 +335,10 @@
long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
task.position, taskDeadlineNs);
- if (holder != null && holder.mNestedRecyclerView != null) {
+ if (holder != null
+ && holder.mNestedRecyclerView != null
+ && holder.isBound()
+ && !holder.isInvalid()) {
prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
}
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
index 9e262db..c74e20d 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSnapHelper.java
@@ -170,11 +170,7 @@
}
int distance =
Math.abs(distances[0]) > Math.abs(distances[1]) ? distances[0] : distances[1];
- if (distance > 0) {
- return (int) Math.floor(distance / distancePerChild);
- } else {
- return (int) Math.ceil(distance / distancePerChild);
- }
+ return (int) Math.round(distance / distancePerChild);
}
/**
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 86a4127..5e0bc9e 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -4119,31 +4119,16 @@
/**
* Call this method to signal that *all* adapter content has changed (generally, because of
* swapAdapter, or notifyDataSetChanged), and that once layout occurs, all attached items should
- * be discarded or animated. Note that this work is deferred because RecyclerView requires a
- * layout to resolve non-incremental changes to the data set.
+ * be discarded or animated.
*
- * Attached items are labeled as position unknown, and may no longer be cached.
+ * Attached items are labeled as invalid, and all cached items are discarded.
*
* It is still possible for items to be prefetched while mDataSetHasChangedAfterLayout == true,
- * so calling this method *must* be associated with marking the cache invalid, so that the
- * only valid items that remain in the cache, once layout occurs, are prefetched items.
+ * so this method must always discard all cached views so that the only valid items that remain
+ * in the cache, once layout occurs, are valid prefetched items.
*/
void setDataSetChangedAfterLayout() {
- if (mDataSetHasChangedAfterLayout) {
- return;
- }
mDataSetHasChangedAfterLayout = true;
- final int childCount = mChildHelper.getUnfilteredChildCount();
- for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
- if (holder != null && !holder.shouldIgnore()) {
- holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
- }
- }
- mRecycler.setAdapterPositionsAsUnknown();
-
- // immediately mark all views as invalid, so prefetched views can be
- // differentiated from views bound to previous data set - both in children, and cache
markKnownViewsInvalid();
}
@@ -6178,16 +6163,6 @@
}
}
- void setAdapterPositionsAsUnknown() {
- final int cachedCount = mCachedViews.size();
- for (int i = 0; i < cachedCount; i++) {
- final ViewHolder holder = mCachedViews.get(i);
- if (holder != null) {
- holder.addFlags(ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN);
- }
- }
- }
-
void markKnownViewsInvalid() {
if (mAdapter != null && mAdapter.hasStableIds()) {
final int cachedCount = mCachedViews.size();
@@ -11561,7 +11536,6 @@
void prepareForNestedPrefetch(Adapter adapter) {
mLayoutStep = STEP_START;
mItemCount = adapter.getItemCount();
- mStructureChanged = false;
mInPreLayout = false;
mTrackOldChangeHolders = false;
mIsMeasuring = false;
@@ -11749,7 +11723,7 @@
* @param velocityX the fling velocity on the X axis
* @param velocityY the fling velocity on the Y axis
*
- * @return true if the fling washandled, false otherwise.
+ * @return true if the fling was handled, false otherwise.
*/
public abstract boolean onFling(int velocityX, int velocityY);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
index 2da67af..ae70900 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -31,7 +31,9 @@
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
import android.support.test.filters.LargeTest;
+import android.support.test.filters.SdkSuppress;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v4.view.accessibility.AccessibilityRecordCompat;
@@ -402,6 +404,12 @@
}
}
+ // Run this test on Jelly Bean and newer because clearFocus on API 15 will call
+ // requestFocus in ViewRootImpl when clearChildFocus is called. Whereas, in API 16 and above,
+ // this call is delayed until after onFocusChange callback is called. Thus on API 16+, there's a
+ // transient state of no child having focus during which onFocusChange is executed. This
+ // transient state does not exist on API 15-.
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.JELLY_BEAN)
@Test
public void unfocusableScrollingWhenFocusCleared() throws Throwable {
// The maximum number of child views that can be visible at any time.
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 1b85884..f682593 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewCacheTest.java
@@ -20,6 +20,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
@@ -811,6 +812,58 @@
}
@Test
+ public void nestedPrefetchNotClearInnerStructureChangeFlag() {
+ LinearLayoutManager llm = new LinearLayoutManager(getContext());
+ assertEquals(2, llm.getInitialPrefetchItemCount());
+
+ mRecyclerView.setLayoutManager(llm);
+ mRecyclerView.setAdapter(new OuterAdapter());
+
+ layout(200, 200);
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+
+ // prefetch 2 (default)
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+ RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
+ assertNotNull(holder);
+ assertNotNull(holder.mNestedRecyclerView);
+ RecyclerView innerView = holder.mNestedRecyclerView.get();
+ RecyclerView.Adapter innerAdapter = innerView.getAdapter();
+ CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
+ // mStructureChanged is initially true before first layout pass.
+ assertTrue(innerView.mState.mStructureChanged);
+ assertTrue(innerView.hasPendingAdapterUpdates());
+
+ // layout position 2 and clear mStructureChanged
+ mRecyclerView.scrollToPosition(2);
+ layout(200, 200);
+ mRecyclerView.scrollToPosition(0);
+ layout(200, 200);
+ assertFalse(innerView.mState.mStructureChanged);
+ assertFalse(innerView.hasPendingAdapterUpdates());
+
+ // notify change on the cached innerView.
+ innerAdapter.notifyDataSetChanged();
+ assertTrue(innerView.mState.mStructureChanged);
+ assertTrue(innerView.hasPendingAdapterUpdates());
+
+ // prefetch again
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+ ((LinearLayoutManager) innerView.getLayoutManager())
+ .setInitialPrefetchItemCount(2);
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+ CacheUtils.verifyCacheContainsPrefetchedPositions(innerView, 0, 1);
+
+ // The re-prefetch is not necessary get the same inner view but we will get same Adapter
+ holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 2);
+ innerView = holder.mNestedRecyclerView.get();
+ assertSame(innerAdapter, innerView.getAdapter());
+ // prefetch shouldn't clear the mStructureChanged flag
+ assertTrue(innerView.mState.mStructureChanged);
+ assertTrue(innerView.hasPendingAdapterUpdates());
+ }
+
+ @Test
public void nestedPrefetchReverseInner() {
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mRecyclerView.setAdapter(new OuterAdapter(/* reverseInner = */ true));
@@ -1066,8 +1119,10 @@
@Override
public void onViewRecycled(ViewHolder holder) {
- mSavedStates.set(holder.getAdapterPosition(),
- holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
+ if (holder.getAdapterPosition() >= 0) {
+ mSavedStates.set(holder.getAdapterPosition(),
+ holder.mRecyclerView.getLayoutManager().onSaveInstanceState());
+ }
}
@Override
@@ -1122,4 +1177,55 @@
mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
CacheUtils.verifyCacheContainsPrefetchedPositions(holder.mNestedRecyclerView.get(), 0, 1);
}
+
+
+ @Test
+ public void nestedPrefetchDiscardStalePrefetch() {
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ OuterNotifyAdapter outerAdapter = new OuterNotifyAdapter();
+ mRecyclerView.setAdapter(outerAdapter);
+
+ // zero cache, so item we prefetch can't already be ready
+ mRecyclerView.setItemViewCacheSize(0);
+
+ // layout as 2x2, starting on row index 2, with empty cache
+ layout(200, 200);
+ mRecyclerView.scrollBy(0, 200);
+
+ // no views cached, or previously used (so we can trust number in mItemsBound)
+ mRecycler.mRecyclerPool.clear();
+ assertEquals(0, mRecycler.mRecyclerPool.getRecycledViewCount(0));
+ assertEquals(0, mRecycler.mCachedViews.size());
+
+ // prefetch the outer item and its inner children
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+
+ // 4 is prefetched with 2 inner children, first two binds
+ CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
+ RecyclerView.ViewHolder holder = CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4);
+ assertNotNull(holder);
+ assertNotNull(holder.mNestedRecyclerView);
+ RecyclerView innerRecyclerView = holder.mNestedRecyclerView.get();
+ assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
+ assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
+ assertEquals(2, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
+
+ // notify data set changed, so any previously prefetched items invalid, and re-prefetch
+ innerRecyclerView.getAdapter().notifyDataSetChanged();
+ mRecyclerView.mPrefetchRegistry.setPrefetchVector(0, 1);
+ mRecyclerView.mGapWorker.prefetch(RecyclerView.FOREVER_NS);
+
+ // 4 is prefetched again...
+ CacheUtils.verifyCacheContainsPrefetchedPositions(mRecyclerView, 4);
+
+ // reusing the same instance with 2 inner children...
+ assertSame(holder, CacheUtils.peekAtCachedViewForPosition(mRecyclerView, 4));
+ assertSame(innerRecyclerView, holder.mNestedRecyclerView.get());
+ assertEquals(0, innerRecyclerView.mChildHelper.getUnfilteredChildCount());
+ assertEquals(2, innerRecyclerView.mRecycler.mCachedViews.size());
+
+ // ... but there should be two new binds
+ assertEquals(4, ((InnerAdapter) innerRecyclerView.getAdapter()).mItemsBound);
+ }
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 75d59c8..6fd9a86 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -345,7 +345,9 @@
public View onFocusSearchFailed(View focused, int direction,
RecyclerView.Recycler recycler,
RecyclerView.State state) {
- assertEquals(View.FOCUS_FORWARD, direction);
+ int expectedDir = Build.VERSION.SDK_INT <= 15 ? View.FOCUS_DOWN :
+ View.FOCUS_FORWARD;
+ assertEquals(expectedDir, direction);
assertEquals(1, getChildCount());
View child0 = getChildAt(0);
View view = recycler.getViewForPosition(1);