Merge "Create v4 PathInterpolatorCompat" into lmp-mr1-ub-dev
diff --git a/design/base/android/support/design/widget/AnimationUtils.java b/design/base/android/support/design/widget/AnimationUtils.java
new file mode 100644
index 0000000..ad01cd5
--- /dev/null
+++ b/design/base/android/support/design/widget/AnimationUtils.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+class AnimationUtils {
+
+ static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
+
+ /**
+ * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
+ */
+ static float lerp(float startValue, float endValue, float fraction) {
+ return startValue + (fraction * (endValue - startValue));
+ }
+
+}
diff --git a/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java b/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
index 175e8b9..a725dd8 100644
--- a/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
+++ b/design/eclair-mr1/android/support/design/widget/FloatingActionButtonEclairMr1.java
@@ -24,16 +24,12 @@
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.view.View;
import android.view.animation.Animation;
-import android.view.animation.Interpolator;
import android.view.animation.Transformation;
class FloatingActionButtonEclairMr1 extends FloatingActionButtonImpl {
- private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
-
private Drawable mShapeDrawable;
private Drawable mRippleDrawable;
@@ -149,7 +145,7 @@
}
private Animation setupAnimation(Animation animation) {
- animation.setInterpolator(INTERPOLATOR);
+ animation.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animation.setDuration(mAnimationDuration);
return animation;
}
diff --git a/design/res/anim/snackbar_in.xml b/design/res/anim/snackbar_in.xml
new file mode 100644
index 0000000..a40524c
--- /dev/null
+++ b/design/res/anim/snackbar_in.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromYDelta="100%"
+ android:toYDelta="0"/>
diff --git a/design/res/anim/snackbar_out.xml b/design/res/anim/snackbar_out.xml
new file mode 100644
index 0000000..eb55cc0
--- /dev/null
+++ b/design/res/anim/snackbar_out.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromYDelta="0"
+ android:toYDelta="100%"/>
\ No newline at end of file
diff --git a/design/res/drawable/snackbar_background.xml b/design/res/drawable/snackbar_background.xml
new file mode 100644
index 0000000..739b516
--- /dev/null
+++ b/design/res/drawable/snackbar_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/snackbar_background_corner_radius"/>
+ <solid android:color="@color/snackbar_background_color"/>
+</shape>
\ No newline at end of file
diff --git a/design/res/layout-sw600dp/layout_snackbar.xml b/design/res/layout-sw600dp/layout_snackbar.xml
new file mode 100644
index 0000000..6605408
--- /dev/null
+++ b/design/res/layout-sw600dp/layout_snackbar.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="android.support.design.widget.Snackbar$SnackbarLayout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center_vertical"
+ style="@style/Widget.Design.Snackbar" />
\ No newline at end of file
diff --git a/design/res/layout/layout_snackbar.xml b/design/res/layout/layout_snackbar.xml
new file mode 100644
index 0000000..604aafc
--- /dev/null
+++ b/design/res/layout/layout_snackbar.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="android.support.design.widget.Snackbar$SnackbarLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ style="@style/Widget.Design.Snackbar" />
\ No newline at end of file
diff --git a/design/res/layout/layout_snackbar_include.xml b/design/res/layout/layout_snackbar_include.xml
new file mode 100644
index 0000000..0cf2002
--- /dev/null
+++ b/design/res/layout/layout_snackbar_include.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <TextView
+ android:id="@+id/snackbar_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="@dimen/snackbar_padding_vertical"
+ android:paddingBottom="@dimen/snackbar_padding_vertical"
+ android:paddingLeft="@dimen/snackbar_padding_horizontal"
+ android:paddingRight="@dimen/snackbar_padding_horizontal"
+ android:textAppearance="@style/TextAppearance.Design.Snackbar.Message"
+ android:maxLines="@integer/snackbar_text_max_lines"
+ android:layout_gravity="center_vertical|left|start"
+ android:ellipsize="end"/>
+
+ <TextView
+ android:id="@+id/snackbar_action"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/snackbar_extra_spacing_horizontal"
+ android:layout_marginStart="@dimen/snackbar_extra_spacing_horizontal"
+ android:layout_gravity="center_vertical|right|end"
+ android:background="?attr/selectableItemBackground"
+ android:paddingTop="@dimen/snackbar_padding_vertical"
+ android:paddingBottom="@dimen/snackbar_padding_vertical"
+ android:paddingLeft="@dimen/snackbar_padding_horizontal"
+ android:paddingRight="@dimen/snackbar_padding_horizontal"
+ android:visibility="gone"
+ android:textAppearance="@style/TextAppearance.Design.Snackbar.Action"/>
+
+</merge>
\ No newline at end of file
diff --git a/design/res/values-sw600dp/config.xml b/design/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..baac13b
--- /dev/null
+++ b/design/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<resources>
+
+ <integer name="snackbar_text_max_lines">1</integer>
+
+</resources>
\ No newline at end of file
diff --git a/design/res/values-sw600dp/dimens.xml b/design/res/values-sw600dp/dimens.xml
index 2d966e6..37c3ff5 100644
--- a/design/res/values-sw600dp/dimens.xml
+++ b/design/res/values-sw600dp/dimens.xml
@@ -19,4 +19,11 @@
<dimen name="tab_min_width">160dp</dimen>
+ <dimen name="snackbar_min_width">320dp</dimen>
+ <dimen name="snackbar_max_width">576dp</dimen>
+ <dimen name="snackbar_padding_vertical_2lines">@dimen/snackbar_padding_vertical</dimen>
+ <dimen name="snackbar_extra_spacing_horizontal">24dp</dimen>
+ <dimen name="snackbar_background_corner_radius">2dp</dimen>
+ <dimen name="snackbar_action_inline_max_width">0dp</dimen>
+
</resources>
\ No newline at end of file
diff --git a/design/res/values/attrs.xml b/design/res/values/attrs.xml
index 7a5582e..bf81b54 100644
--- a/design/res/values/attrs.xml
+++ b/design/res/values/attrs.xml
@@ -141,5 +141,21 @@
<flag name="end" value="0x00800005" />
</attr>
</declare-styleable>
+
+ <declare-styleable name="TextInputLayout">
+ <attr name="hintTextAppearance" format="reference" />
+ <!-- The hint to display in the floating label -->
+ <attr name="android:hint" />
+ <!-- Whether the layout is laid out as if an error will be displayed -->
+ <attr name="errorEnabled" format="boolean" />
+ <!-- TextAppearance of any error message displayed -->
+ <attr name="errorTextAppearance" format="reference" />
+ </declare-styleable>
+
+ <declare-styleable name="SnackbarLayout">
+ <attr name="android:maxWidth" />
+ <attr name="maxActionInlineWidth" format="dimension" />
+ </declare-styleable>
+
</resources>
diff --git a/design/res/values/colors.xml b/design/res/values/colors.xml
index fdd5127..f46e0ce 100644
--- a/design/res/values/colors.xml
+++ b/design/res/values/colors.xml
@@ -24,4 +24,8 @@
<!-- Shadow color for the furthest pixels of a shadow -->
<color name="shadow_end_color">@android:color/transparent</color>
+ <color name="error_color">#FFDD2C00</color>
+
+ <color name="snackbar_background_color">#323232</color>
+
</resources>
\ No newline at end of file
diff --git a/design/res/values/config.xml b/design/res/values/config.xml
new file mode 100644
index 0000000..2ff276a
--- /dev/null
+++ b/design/res/values/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+-->
+
+<resources>
+
+ <integer name="snackbar_text_max_lines">2</integer>
+
+</resources>
\ No newline at end of file
diff --git a/design/res/values/dimens.xml b/design/res/values/dimens.xml
index bb96119..23d9c0d 100644
--- a/design/res/values/dimens.xml
+++ b/design/res/values/dimens.xml
@@ -32,4 +32,21 @@
<dimen name="tab_min_width">72dp</dimen>
<dimen name="tab_max_width">264dp</dimen>
+ <dimen name="snackbar_min_width">-1px</dimen>
+ <dimen name="snackbar_max_width">-1px</dimen>
+ <dimen name="snackbar_elevation">2dp</dimen>
+ <dimen name="snackbar_background_corner_radius">0dp</dimen>
+
+ <dimen name="snackbar_padding_horizontal">12dp</dimen>
+ <dimen name="snackbar_padding_vertical">14dp</dimen>
+ <dimen name="snackbar_padding_vertical_2lines">24dp</dimen>
+
+ <!-- Extra spacing between the action and message views -->
+ <dimen name="snackbar_extra_spacing_horizontal">0dp</dimen>
+ <!-- The maximum width for a Snackbar's inline action. If the view is width than this then
+ the Snackbar will change to vertical stacking -->
+ <dimen name="snackbar_action_inline_max_width">128dp</dimen>
+
+ <dimen name="snackbar_text_size">14sp</dimen>
+
</resources>
diff --git a/design/res/values/styles.xml b/design/res/values/styles.xml
index f00293b..46e2144 100644
--- a/design/res/values/styles.xml
+++ b/design/res/values/styles.xml
@@ -59,5 +59,38 @@
<item name="textAllCaps">true</item>
</style>
+ <style name="Widget.Design.TextInputLayout" parent="android:Widget">
+ <item name="hintTextAppearance">@style/TextAppearance.Design.Hint</item>
+ <item name="errorTextAppearance">@style/TextAppearance.Design.Error</item>
+ </style>
+
+ <style name="TextAppearance.Design.Hint" parent="TextAppearance.AppCompat.Caption">
+ <item name="android:textColor">?attr/colorControlActivated</item>
+ </style>
+
+ <style name="TextAppearance.Design.Error" parent="TextAppearance.AppCompat.Caption">
+ <item name="android:textColor">@color/error_color</item>
+ </style>
+
+ <style name="TextAppearance.Design.Snackbar.Message" parent="android:TextAppearance">
+ <item name="android:textSize">@dimen/snackbar_text_size</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="TextAppearance.Design.Snackbar.Action" parent="TextAppearance.AppCompat.Button">
+ <item name="android:textColor">?colorAccent</item>
+ </style>
+
+ <style name="Widget.Design.Snackbar" parent="android:Widget">
+ <item name="android:theme">@style/ThemeOverlay.AppCompat.Dark</item>
+ <item name="android:minWidth">@dimen/snackbar_min_width</item>
+ <item name="android:maxWidth">@dimen/snackbar_max_width</item>
+ <item name="android:background">@drawable/snackbar_background</item>
+ <item name="android:paddingLeft">@dimen/snackbar_padding_horizontal</item>
+ <item name="android:paddingRight">@dimen/snackbar_padding_horizontal</item>
+ <item name="android:elevation">@dimen/snackbar_elevation</item>
+ <item name="maxActionInlineWidth">@dimen/snackbar_action_inline_max_width</item>
+ </style>
+
</resources>
diff --git a/design/src/android/support/design/widget/CollapsingTextHelper.java b/design/src/android/support/design/widget/CollapsingTextHelper.java
new file mode 100644
index 0000000..2afe4b6
--- /dev/null
+++ b/design/src/android/support/design/widget/CollapsingTextHelper.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Build;
+import android.support.design.R;
+import android.support.v4.view.ViewCompat;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+final class CollapsingTextHelper {
+
+ // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
+ // by using our own texture
+ private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
+
+ private static final boolean DEBUG_DRAW = false;
+ private static final Paint DEBUG_DRAW_PAINT;
+ static {
+ DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
+ if (DEBUG_DRAW_PAINT != null) {
+ DEBUG_DRAW_PAINT.setAntiAlias(true);
+ DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
+ }
+ }
+
+ private final View mView;
+
+ private float mExpandedFraction;
+
+ private final Rect mExpandedBounds;
+ private final Rect mCollapsedBounds;
+ private int mExpandedTextVerticalGravity = Gravity.CENTER_VERTICAL;
+ private int mCollapsedTextVerticalGravity = Gravity.CENTER_VERTICAL;
+ private float mExpandedTextSize;
+ private float mCollapsedTextSize;
+ private int mExpandedTextColor;
+ private int mCollapsedTextColor;
+
+ private float mExpandedTop;
+ private float mCollapsedTop;
+
+ private CharSequence mText;
+ private CharSequence mTextToDraw;
+ private float mTextWidth;
+
+ private boolean mUseTexture;
+ private Bitmap mExpandedTitleTexture;
+ private Paint mTexturePaint;
+ private float mTextureAscent;
+ private float mTextureDescent;
+
+ private float mCurrentLeft;
+ private float mCurrentRight;
+ private float mCurrentTop;
+ private float mScale;
+
+ private final TextPaint mTextPaint;
+
+ private Interpolator mPositionInterpolator;
+ private Interpolator mTextSizeInterpolator;
+
+ public CollapsingTextHelper(View view) {
+ mView = view;
+
+ mTextPaint = new TextPaint();
+ mTextPaint.setAntiAlias(true);
+
+ mCollapsedBounds = new Rect();
+ mExpandedBounds = new Rect();
+ }
+
+ void setTextSizeInterpolator(Interpolator interpolator) {
+ mTextSizeInterpolator = interpolator;
+ recalculate();
+ }
+
+ void setPositionInterpolator(Interpolator interpolator) {
+ mPositionInterpolator = interpolator;
+ recalculate();
+ }
+
+ void setExpandedTextSize(float textSize) {
+ if (mExpandedTextSize != textSize) {
+ mExpandedTextSize = textSize;
+ recalculate();
+ }
+ }
+
+ void setCollapsedTextSize(float textSize) {
+ if (mCollapsedTextSize != textSize) {
+ mCollapsedTextSize = textSize;
+ recalculate();
+ }
+ }
+
+ void setCollapsedTextColor(int textColor) {
+ if (mCollapsedTextColor != textColor) {
+ mCollapsedTextColor = textColor;
+ recalculate();
+ }
+ }
+
+ void setExpandedTextColor(int textColor) {
+ if (mExpandedTextColor != textColor) {
+ mExpandedTextColor = textColor;
+ recalculate();
+ }
+ }
+
+ void setExpandedBounds(int left, int top, int right, int bottom) {
+ mExpandedBounds.set(left, top, right, bottom);
+ recalculate();
+ }
+
+ void setCollapsedBounds(int left, int top, int right, int bottom) {
+ mCollapsedBounds.set(left, top, right, bottom);
+ recalculate();
+ }
+
+ void setExpandedTextVerticalGravity(int gravity) {
+ gravity &= Gravity.VERTICAL_GRAVITY_MASK;
+
+ if (mExpandedTextVerticalGravity != gravity) {
+ mExpandedTextVerticalGravity = gravity;
+ recalculate();
+ }
+ }
+
+ void setCollapsedTextVerticalGravity(int gravity) {
+ gravity &= Gravity.VERTICAL_GRAVITY_MASK;
+
+ if (mCollapsedTextVerticalGravity != gravity) {
+ mCollapsedTextVerticalGravity = gravity;
+ recalculate();
+ }
+ }
+
+ void setCollapsedTextAppearance(int resId) {
+ TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
+ if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
+ mCollapsedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, 0);
+ }
+ if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
+ mCollapsedTextSize = a.getDimensionPixelSize(
+ R.styleable.TextAppearance_android_textSize, 0);
+ }
+ a.recycle();
+
+ recalculate();
+ }
+
+ void setExpandedTextAppearance(int resId) {
+ TypedArray a = mView.getContext().obtainStyledAttributes(resId, R.styleable.TextAppearance);
+ if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {
+ mExpandedTextColor = a.getColor(R.styleable.TextAppearance_android_textColor, 0);
+ }
+ if (a.hasValue(R.styleable.TextAppearance_android_textSize)) {
+ mExpandedTextSize = a.getDimensionPixelSize(
+ R.styleable.TextAppearance_android_textSize, 0);
+ }
+ a.recycle();
+
+ recalculate();
+ }
+
+ /**
+ * Set the value indicating the current scroll value. This decides how much of the
+ * background will be displayed, as well as the title metrics/positioning.
+ *
+ * A value of {@code 0.0} indicates that the layout is fully expanded.
+ * A value of {@code 1.0} indicates that the layout is fully collapsed.
+ */
+ void setExpansionFraction(float fraction) {
+ if (fraction != mExpandedFraction) {
+ mExpandedFraction = fraction;
+ calculateOffsets();
+ }
+ }
+
+ float getExpansionFraction() {
+ return mExpandedFraction;
+ }
+
+ float getCollapsedTextSize() {
+ return mCollapsedTextSize;
+ }
+
+ float getExpandedTextSize() {
+ return mExpandedTextSize;
+ }
+
+ private void calculateOffsets() {
+ final float fraction = mExpandedFraction;
+
+ mCurrentLeft = interpolate(mExpandedBounds.left, mCollapsedBounds.left,
+ fraction, mPositionInterpolator);
+ mCurrentTop = interpolate(mExpandedTop, mCollapsedTop, fraction, mPositionInterpolator);
+ mCurrentRight = interpolate(mExpandedBounds.right, mCollapsedBounds.right,
+ fraction, mPositionInterpolator);
+ setInterpolatedTextSize(interpolate(mExpandedTextSize, mCollapsedTextSize,
+ fraction, mTextSizeInterpolator));
+
+ if (mCollapsedTextColor != mExpandedTextColor) {
+ // If the collapsed and expanded text colors are different, blend them based on the
+ // fraction
+ mTextPaint.setColor(blendColors(mExpandedTextColor, mCollapsedTextColor, fraction));
+ } else {
+ mTextPaint.setColor(mCollapsedTextColor);
+ }
+
+ ViewCompat.postInvalidateOnAnimation(mView);
+ }
+
+ private void calculateBaselines() {
+ // We then calculate the collapsed text size, using the same logic
+ mTextPaint.setTextSize(mCollapsedTextSize);
+ switch (mCollapsedTextVerticalGravity) {
+ case Gravity.BOTTOM:
+ mCollapsedTop = mCollapsedBounds.bottom;
+ break;
+ case Gravity.TOP:
+ mCollapsedTop = mCollapsedBounds.top - mTextPaint.ascent();
+ break;
+ case Gravity.CENTER_VERTICAL:
+ default:
+ float textHeight = mTextPaint.descent() - mTextPaint.ascent();
+ float textOffset = (textHeight / 2) - mTextPaint.descent();
+ mCollapsedTop = mCollapsedBounds.centerY() + textOffset;
+ break;
+ }
+
+ mTextPaint.setTextSize(mExpandedTextSize);
+ switch (mExpandedTextVerticalGravity) {
+ case Gravity.BOTTOM:
+ mExpandedTop = mExpandedBounds.bottom;
+ break;
+ case Gravity.TOP:
+ mExpandedTop = mExpandedBounds.top - mTextPaint.ascent();
+ break;
+ case Gravity.CENTER_VERTICAL:
+ default:
+ float textHeight = mTextPaint.descent() - mTextPaint.ascent();
+ float textOffset = (textHeight / 2) - mTextPaint.descent();
+ mExpandedTop = mExpandedBounds.centerY() + textOffset;
+ break;
+ }
+ mTextureAscent = mTextPaint.ascent();
+ mTextureDescent = mTextPaint.descent();
+
+ // The bounds have changed so we need to clear the texture
+ clearTexture();
+ }
+
+ public void draw(Canvas canvas) {
+ final int saveCount = canvas.save();
+
+ if (mTextToDraw != null) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(mView)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+
+ float x = isRtl ? mCurrentRight : mCurrentLeft;
+ float y = mCurrentTop;
+
+ final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
+
+ final float ascent;
+ final float descent;
+
+ if (drawTexture) {
+ ascent = mTextureAscent * mScale;
+ descent = mTextureDescent * mScale;
+ } else {
+ ascent = mTextPaint.ascent() * mScale;
+ descent = mTextPaint.descent() * mScale;
+ }
+
+ if (DEBUG_DRAW) {
+ // Just a debug tool, which drawn a Magneta rect in the text bounds
+ canvas.drawRect(mCurrentLeft, y + ascent, mCurrentRight, y + descent,
+ DEBUG_DRAW_PAINT);
+ }
+
+ if (drawTexture) {
+ y += ascent;
+ }
+
+ if (mScale != 1f) {
+ canvas.scale(mScale, mScale, x, y);
+ }
+
+ if (isRtl) {
+ x -= mTextWidth;
+ }
+
+ if (drawTexture) {
+ // If we should use a texture, draw it instead of text
+ canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
+ } else {
+ canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
+ }
+ }
+
+ canvas.restoreToCount(saveCount);
+ }
+
+ private void setInterpolatedTextSize(final float textSize) {
+ if (mText == null) return;
+
+ boolean textSizeChanged;
+ float availableWidth;
+
+ if (isClose(textSize, mCollapsedTextSize)) {
+ textSizeChanged = mTextPaint.getTextSize() != mCollapsedTextSize;
+ mTextPaint.setTextSize(mCollapsedTextSize);
+ mScale = 1f;
+ availableWidth = mCollapsedBounds.width();
+ } else {
+ textSizeChanged = mTextPaint.getTextSize() != mExpandedTextSize;
+ mTextPaint.setTextSize(mExpandedTextSize);
+
+ if (isClose(textSize, mExpandedTextSize)) {
+ // If we're close to the expanded text size, snap to it and use a scale of 1
+ mScale = 1f;
+ } else {
+ // Else, we'll scale down from the expanded text size
+ mScale = textSize / mExpandedTextSize;
+ }
+ availableWidth = mExpandedBounds.width();
+ }
+
+ if (mTextToDraw == null || textSizeChanged) {
+ // If we don't currently have text to draw, or the text size has changed, ellipsize...
+ final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
+ availableWidth, TextUtils.TruncateAt.END);
+ if (mTextToDraw == null || !mTextToDraw.equals(title)) {
+ mTextToDraw = title;
+ }
+ mTextWidth = mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length());
+ }
+
+ // Use our texture if the scale isn't 1.0
+ mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
+
+ if (mUseTexture) {
+ // Make sure we have an expanded texture if needed
+ ensureExpandedTexture();
+ }
+
+ ViewCompat.postInvalidateOnAnimation(mView);
+ }
+
+ private void ensureExpandedTexture() {
+ if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty()
+ || TextUtils.isEmpty(mTextToDraw)) {
+ return;
+ }
+
+ mTextPaint.setTextSize(mExpandedTextSize);
+ mTextPaint.setColor(mExpandedTextColor);
+
+ final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
+ final int h = Math.round(mTextPaint.descent() - mTextPaint.ascent());
+ mTextWidth = w;
+
+ if (w <= 0 && h <= 0) {
+ return; // If the width or height are 0, return
+ }
+
+ mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+
+ Canvas c = new Canvas(mExpandedTitleTexture);
+ c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
+
+ if (mTexturePaint == null) {
+ // Make sure we have a paint
+ mTexturePaint = new Paint();
+ mTexturePaint.setAntiAlias(true);
+ mTexturePaint.setFilterBitmap(true);
+ }
+ }
+
+ private void recalculate() {
+ if (ViewCompat.isLaidOut(mView)) {
+ // If we've already been laid out, calculate everything now otherwise we'll wait
+ // until a layout
+ calculateBaselines();
+ calculateOffsets();
+ }
+ }
+
+ /**
+ * Set the title to display
+ *
+ * @param text
+ */
+ void setText(CharSequence text) {
+ if (text == null || !text.equals(mText)) {
+ mText = text;
+ clearTexture();
+ recalculate();
+ }
+ }
+
+ CharSequence getText() {
+ return mText;
+ }
+
+ private void clearTexture() {
+ if (mExpandedTitleTexture != null) {
+ mExpandedTitleTexture.recycle();
+ mExpandedTitleTexture = null;
+ }
+ }
+
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ recalculate();
+ }
+
+ /**
+ * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
+ * defined as it's difference being < 0.001.
+ */
+ private static boolean isClose(float value, float targetValue) {
+ return Math.abs(value - targetValue) < 0.001f;
+ }
+
+ int getExpandedTextColor() {
+ return mExpandedTextColor;
+ }
+
+ int getCollapsedTextColor() {
+ return mCollapsedTextColor;
+ }
+
+ /**
+ * Blend {@code color1} and {@code color2} using the given ratio.
+ *
+ * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
+ * 1.0 will return {@code color2}.
+ */
+ private static int blendColors(int color1, int color2, float ratio) {
+ final float inverseRatio = 1f - ratio;
+ float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
+ float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
+ float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
+ float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
+ return Color.argb((int) a, (int) r, (int) g, (int) b);
+ }
+
+ private static float interpolate(float startValue, float endValue, float fraction,
+ Interpolator interpolator) {
+ if (interpolator != null) {
+ fraction = interpolator.getInterpolation(fraction);
+ }
+ return AnimationUtils.lerp(startValue, endValue, fraction);
+ }
+}
diff --git a/design/src/android/support/design/widget/CoordinatorLayout.java b/design/src/android/support/design/widget/CoordinatorLayout.java
index f943d55..d78562c 100644
--- a/design/src/android/support/design/widget/CoordinatorLayout.java
+++ b/design/src/android/support/design/widget/CoordinatorLayout.java
@@ -28,7 +28,6 @@
import android.os.SystemClock;
import android.support.design.R;
import android.support.v4.view.GravityCompat;
-import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
@@ -148,6 +147,7 @@
private final Rect mTempRect1 = new Rect();
private final Rect mTempRect2 = new Rect();
private final Rect mTempRect3 = new Rect();
+ private final int[] mTempIntPair = new int[2];
private Paint mScrimPaint;
private boolean mIsAttachedToWindow;
@@ -155,6 +155,8 @@
private int[] mKeylines;
private View mBehaviorTouchView;
+ private View mNestedScrollingDirectChild;
+ private View mNestedScrollingTarget;
private OnPreDrawListener mOnPreDrawListener;
private boolean mNeedsPreDrawListener;
@@ -191,7 +193,7 @@
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
- resetBehaviors();
+ resetTouchBehaviors();
if (mNeedsPreDrawListener) {
if (mOnPreDrawListener == null) {
mOnPreDrawListener = new OnPreDrawListener();
@@ -205,11 +207,14 @@
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
- resetBehaviors();
+ resetTouchBehaviors();
if (mNeedsPreDrawListener && mOnPreDrawListener != null) {
final ViewTreeObserver vto = getViewTreeObserver();
vto.removeOnPreDrawListener(mOnPreDrawListener);
}
+ if (mNestedScrollingTarget != null) {
+ onStopNestedScroll(mNestedScrollingTarget);
+ }
mIsAttachedToWindow = false;
}
@@ -219,7 +224,7 @@
* in response to an UP or CANCEL event, when intercept is request-disallowed
* and similar cases where an event stream in progress will be aborted.
*/
- private void resetBehaviors() {
+ private void resetTouchBehaviors() {
if (mBehaviorTouchView != null) {
final Behavior b = ((LayoutParams) mBehaviorTouchView.getLayoutParams()).getBehavior();
if (b != null) {
@@ -322,7 +327,7 @@
// Make sure we reset in case we had missed a previous important event.
if (action == MotionEvent.ACTION_DOWN) {
- resetBehaviors();
+ resetTouchBehaviors();
}
final boolean intercepted = performIntercept(ev);
@@ -332,7 +337,7 @@
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- resetBehaviors();
+ resetTouchBehaviors();
}
return intercepted;
@@ -377,7 +382,7 @@
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
- resetBehaviors();
+ resetTouchBehaviors();
}
return handled;
@@ -387,7 +392,7 @@
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
super.requestDisallowInterceptTouchEvent(disallowIntercept);
if (disallowIntercept) {
- resetBehaviors();
+ resetTouchBehaviors();
}
}
@@ -444,8 +449,12 @@
LayoutParams getResolvedLayoutParams(View child) {
final LayoutParams result = (LayoutParams) child.getLayoutParams();
if (!result.mBehaviorResolved) {
- final Class<?> childClass = child.getClass();
- final DefaultBehavior defaultBehavior = childClass.getAnnotation(DefaultBehavior.class);
+ Class<?> childClass = child.getClass();
+ DefaultBehavior defaultBehavior = null;
+ while (childClass != null &&
+ (defaultBehavior = childClass.getAnnotation(DefaultBehavior.class)) == null) {
+ childClass = childClass.getSuperclass();
+ }
if (defaultBehavior != null) {
try {
result.setBehavior(defaultBehavior.value().newInstance());
@@ -957,7 +966,7 @@
final Rect oldRect = mTempRect1;
final Rect newRect = mTempRect2;
getLastChildRect(child, oldRect);
- getChildRect(child, false, newRect);
+ getChildRect(child, true, newRect);
if (oldRect.equals(newRect)) {
continue;
}
@@ -1100,6 +1109,27 @@
return r.contains(x, y);
}
+ /**
+ * Check whether two views overlap each other. The views need to be descendants of this
+ * {@link CoordinatorLayout} in the view hierarchy.
+ *
+ * @param first first child view to test
+ * @param second second child view to test
+ * @return true if both views are visible and overlap each other
+ */
+ public boolean doViewsOverlap(View first, View second) {
+ if (first.getVisibility() == VISIBLE && second.getVisibility() == VISIBLE) {
+ final Rect firstRect = mTempRect1;
+ getChildRect(first, first.getParent() != this, firstRect);
+ final Rect secondRect = mTempRect2;
+ getChildRect(second, second.getParent() != this, secondRect);
+
+ return !(firstRect.left > secondRect.right || firstRect.top > secondRect.bottom
+ || firstRect.right < secondRect.left || firstRect.bottom < secondRect.top);
+ }
+ return false;
+ }
+
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
@@ -1126,37 +1156,151 @@
}
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
- // TODO
- return false;
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
+ nestedScrollAxes);
+ handled |= accepted;
+
+ lp.acceptNestedScroll(accepted);
+ } else {
+ lp.acceptNestedScroll(false);
+ }
+ }
+ return handled;
}
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
- // TODO
+ mNestedScrollingDirectChild = child;
+ mNestedScrollingTarget = target;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onNestedScrollAccepted(this, view, child, target, nestedScrollAxes);
+ }
+ }
}
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
- // TODO
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onStopNestedScroll(this, view, target);
+ }
+ lp.resetNestedScroll();
+ }
+
+ mNestedScrollingDirectChild = null;
+ mNestedScrollingTarget = null;
}
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed) {
- // TODO
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ viewBehavior.onNestedScroll(this, view, target, dxConsumed, dyConsumed,
+ dxUnconsumed, dyUnconsumed);
+ }
+ }
}
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
- // TODO
+ int xConsumed = 0;
+ int yConsumed = 0;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ mTempIntPair[0] = mTempIntPair[1] = 0;
+ viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
+
+ xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
+ : Math.min(xConsumed, mTempIntPair[0]);
+ yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
+ : Math.min(yConsumed, mTempIntPair[1]);
+ }
+ }
+
+ consumed[0] = xConsumed;
+ consumed[1] = yConsumed;
}
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
- // TODO
- return false;
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ handled |= viewBehavior.onNestedFling(this, view, target, velocityX, velocityY,
+ consumed);
+ }
+ }
+ return handled;
}
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
- // TODO
- return false;
+ boolean handled = false;
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View view = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (!lp.isNestedScrollAccepted()) {
+ continue;
+ }
+
+ final Behavior viewBehavior = lp.getBehavior();
+ if (viewBehavior != null) {
+ handled |= viewBehavior.onNestedPreFling(this, view, target, velocityX, velocityY);
+ }
+ }
+ return handled;
}
public int getNestedScrollAxes() {
@@ -1469,6 +1613,208 @@
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
return lp.mBehaviorTag;
}
+
+
+ /**
+ * Called when a descendant of the CoordinatorLayout attempts to initiate a nested scroll.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may respond
+ * to this event and return true to indicate that the CoordinatorLayout should act as
+ * a nested scrolling parent for this scroll. Only Behaviors that return true from
+ * this method will receive subsequent nested scroll events.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param directTargetChild the child view of the CoordinatorLayout that either is or
+ * contains the target of the nested scroll operation
+ * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
+ * @param nestedScrollAxes the axes that this nested scroll applies to. See
+ * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
+ * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
+ * @return true if the Behavior wishes to accept this nested scroll
+ *
+ * @see NestedScrollingParent#onStartNestedScroll(View, View, int)
+ */
+ public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
+ V child, View directTargetChild, View target, int nestedScrollAxes) {
+ return false;
+ }
+
+ /**
+ * Called when a nested scroll has been accepted by the CoordinatorLayout.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param directTargetChild the child view of the CoordinatorLayout that either is or
+ * contains the target of the nested scroll operation
+ * @param target the descendant view of the CoordinatorLayout initiating the nested scroll
+ * @param nestedScrollAxes the axes that this nested scroll applies to. See
+ * {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
+ * {@link ViewCompat#SCROLL_AXIS_VERTICAL}
+ *
+ * @see NestedScrollingParent#onNestedScrollAccepted(View, View, int)
+ */
+ public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, V child,
+ View directTargetChild, View target, int nestedScrollAxes) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll has ended.
+ *
+ * <p>Any Behavior associated with any direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onStopNestedScroll</code> marks the end of a single nested scroll event
+ * sequence. This is a good place to clean up any state related to the nested scroll.
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout that initiated
+ * the nested scroll
+ *
+ * @see NestedScrollingParent#onStopNestedScroll(View)
+ */
+ public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll in progress has updated and the target has scrolled or
+ * attempted to scroll.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedScroll</code> is called each time the nested scroll is updated by the
+ * nested scrolling child, with both consumed and unconsumed components of the scroll
+ * supplied in pixels. <em>Each Behavior responding to the nested scroll will receive the
+ * same values.</em>
+ * </p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param dxConsumed horizontal pixels consumed by the target's own scrolling operation
+ * @param dyConsumed vertical pixels consumed by the target's own scrolling operation
+ * @param dxUnconsumed horizontal pixels not consumed by the target's own scrolling
+ * operation, but requested by the user
+ * @param dyUnconsumed vertical pixels not consumed by the target's own scrolling operation,
+ * but requested by the user
+ *
+ * @see NestedScrollingParent#onNestedScroll(View, int, int, int, int)
+ */
+ public void onNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target,
+ int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scroll in progress is about to update, before the target has
+ * consumed any of the scrolled distance.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedPreScroll</code> is called each time the nested scroll is updated
+ * by the nested scrolling child, before the nested scrolling child has consumed the scroll
+ * distance itself. <em>Each Behavior responding to the nested scroll will receive the
+ * same values.</em> The CoordinatorLayout will report as consumed the maximum number
+ * of pixels in either direction that any Behavior responding to the nested scroll reported
+ * as consumed.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param dx the raw horizontal number of pixels that the user attempted to scroll
+ * @param dy the raw vertical number of pixels that the user attempted to scroll
+ * @param consumed out parameter. consumed[0] should be set to the distance of dx that
+ * was consumed, consumed[1] should be set to the distance of dy that
+ * was consumed
+ *
+ * @see NestedScrollingParent#onNestedPreScroll(View, int, int, int[])
+ */
+ public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
+ int dx, int dy, int[] consumed) {
+ // Do nothing
+ }
+
+ /**
+ * Called when a nested scrolling child is starting a fling or an action that would
+ * be a fling.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedFling</code> is called when the current nested scrolling child view
+ * detects the proper conditions for a fling. It reports if the child itself consumed
+ * the fling. If it did not, the child is expected to show some sort of overscroll
+ * indication. This method should return true if it consumes the fling, so that a child
+ * that did not itself take an action in response can choose not to show an overfling
+ * indication.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param velocityX horizontal velocity of the attempted fling
+ * @param velocityY vertical velocity of the attempted fling
+ * @param consumed true if the nested child view consumed the fling
+ * @return true if the Behavior consumed the fling
+ *
+ * @see NestedScrollingParent#onNestedFling(View, float, float, boolean)
+ */
+ public boolean onNestedFling(CoordinatorLayout coordinatorLayout, V child, View target,
+ float velocityX, float velocityY, boolean consumed) {
+ return false;
+ }
+
+ /**
+ * Called when a nested scrolling child is about to start a fling.
+ *
+ * <p>Any Behavior associated with the direct child of the CoordinatorLayout may elect
+ * to accept the nested scroll as part of {@link #onStartNestedScroll}. Each Behavior
+ * that returned true will receive subsequent nested scroll events for that nested scroll.
+ * </p>
+ *
+ * <p><code>onNestedPreFling</code> is called when the current nested scrolling child view
+ * detects the proper conditions for a fling, but it has not acted on it yet. A
+ * Behavior can return true to indicate that it consumed the fling. If at least one
+ * Behavior returns true, the fling should not be acted upon by the child.</p>
+ *
+ * @param coordinatorLayout the CoordinatorLayout parent of the view this Behavior is
+ * associated with
+ * @param child the child view of the CoordinatorLayout this Behavior is associated with
+ * @param target the descendant view of the CoordinatorLayout performing the nested scroll
+ * @param velocityX horizontal velocity of the attempted fling
+ * @param velocityY vertical velocity of the attempted fling
+ * @return true if the Behavior consumed the fling
+ *
+ * @see NestedScrollingParent#onNestedPreFling(View, float, float)
+ */
+ public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
+ float velocityX, float velocityY) {
+ return false;
+ }
}
/**
@@ -1511,7 +1857,8 @@
View mAnchorView;
View mAnchorDirectChild;
- boolean mDidBlockInteraction;
+ private boolean mDidBlockInteraction;
+ private boolean mDidAcceptNestedScroll;
final Rect mLastChildRect = new Rect();
@@ -1686,6 +2033,18 @@
mDidBlockInteraction = false;
}
+ void resetNestedScroll() {
+ mDidAcceptNestedScroll = false;
+ }
+
+ void acceptNestedScroll(boolean accept) {
+ mDidAcceptNestedScroll = accept;
+ }
+
+ boolean isNestedScrollAccepted() {
+ return mDidAcceptNestedScroll;
+ }
+
/**
* Check if an associated child view depends on another child view of the CoordinatorLayout.
*
diff --git a/design/src/android/support/design/widget/FloatingActionButton.java b/design/src/android/support/design/widget/FloatingActionButton.java
index 0ea5878..d51ae1d 100644
--- a/design/src/android/support/design/widget/FloatingActionButton.java
+++ b/design/src/android/support/design/widget/FloatingActionButton.java
@@ -26,12 +26,18 @@
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.design.R;
+import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
+import java.lang.ref.WeakReference;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
/**
* Floating action buttons are used for a special type of promoted action. They are distinguished
* by a circled icon floating above the UI and have special motion behaviors related to morphing,
@@ -41,6 +47,7 @@
* the mini, which should only be used to create visual continuity with other elements on the
* screen.
*/
+@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
public class FloatingActionButton extends ImageView {
// These values must match those in the attrs declaration
@@ -281,7 +288,7 @@
* our parent's edges.
*/
private void updateOffset() {
- if (mShadowPadding.width() > 0 && mShadowPadding.height() > 0) {
+ if (mShadowPadding.centerX() != 0 || mShadowPadding.centerY() != 0) {
int offsetTB = 0, offsetLR = 0;
if (isOnRightParentEdge()) {
@@ -333,4 +340,97 @@
}
return false;
}
+
+ /**
+ * Behavior designed for use with {@link FloatingActionButton} instances. It's main function
+ * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
+ * not cover them.
+ */
+ public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
+ // We only support the FAB <> Snackbar shift movement on Honeycomb and above. This is
+ // because we can use view translation properties which greatly simplifies the code.
+ private static final boolean SNACKBAR_BEHAVIOR_ENABLED = Build.VERSION.SDK_INT >= 11;
+
+ private float mTranslationY;
+ private Set<WeakReference<View>> mSnackbars;
+
+ @Override
+ public boolean layoutDependsOn(CoordinatorLayout parent,
+ FloatingActionButton child,
+ View dependency) {
+ // We're dependent on all SnackbarLayouts
+ if (SNACKBAR_BEHAVIOR_ENABLED && dependency instanceof Snackbar.SnackbarLayout) {
+ if (!containsView(dependency)) {
+ if (mSnackbars == null) mSnackbars = new HashSet<>();
+ mSnackbars.add(new WeakReference<>(dependency));
+ }
+ cleanUpSet();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
+ View snackbar) {
+ updateFabTranslation(parent, child, snackbar);
+ return false;
+ }
+
+ private void updateFabTranslation(CoordinatorLayout parent, FloatingActionButton fab,
+ View snackbar) {
+ final float translationY = getTranslationYForFab(parent, fab);
+ if (translationY != mTranslationY) {
+ // First, cancel any current animation
+ ViewCompat.animate(fab).cancel();
+
+ if (Math.abs(translationY - mTranslationY) == snackbar.getHeight()) {
+ // If we're travelling by the height of the Snackbar then we probably need to
+ // animate to the value
+ ViewCompat.animate(fab).translationY(translationY)
+ .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+ } else {
+ // Else we'll set use setTranslationY
+ ViewCompat.setTranslationY(fab, translationY);
+ }
+ mTranslationY = translationY;
+ }
+ }
+
+ private float getTranslationYForFab(CoordinatorLayout parent, FloatingActionButton fab) {
+ float minOffset = 0;
+ if (mSnackbars != null && !mSnackbars.isEmpty()) {
+ for (WeakReference<View> viewRef : mSnackbars) {
+ final View view = viewRef.get();
+ if (view != null && parent.doViewsOverlap(fab, view)) {
+ minOffset = Math.min(minOffset,
+ ViewCompat.getTranslationY(view) - view.getHeight());
+ }
+ }
+ }
+ return minOffset;
+ }
+
+ private void cleanUpSet() {
+ if (mSnackbars != null && !mSnackbars.isEmpty()) {
+ for (final Iterator<WeakReference<View>> i = mSnackbars.iterator(); i.hasNext();) {
+ WeakReference<View> ref = i.next();
+ if (ref == null || ref.get() == null) {
+ i.remove();
+ }
+ }
+ }
+ }
+
+ private boolean containsView(View dependency) {
+ if (mSnackbars != null && !mSnackbars.isEmpty()) {
+ for (WeakReference<View> viewRef : mSnackbars) {
+ if (viewRef.get() == dependency) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
}
diff --git a/design/src/android/support/design/widget/Snackbar.java b/design/src/android/support/design/widget/Snackbar.java
new file mode 100644
index 0000000..041985c
--- /dev/null
+++ b/design/src/android/support/design/widget/Snackbar.java
@@ -0,0 +1,587 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.IntDef;
+import android.support.annotation.StringRes;
+import android.support.design.R;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
+
+/**
+ * Snackbars provide lightweight feedback about an operation. They show a brief message at the
+ * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other
+ * elements on screen and only one can be displayed at a time.
+ * <p>
+ * They automatically disappear after a timeout or after user interaction elsewhere on the screen,
+ * particularly after interactions that summon a new surface or activity. Snackbars can be swiped
+ * off screen.
+ * <p>
+ * Snackbars can contain an action which is set via
+ * {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
+ */
+public class Snackbar {
+
+ /**
+ * @hide
+ */
+ @IntDef({LENGTH_SHORT, LENGTH_LONG})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Duration {}
+
+ /**
+ * Show the Snackbar for a short period of time.
+ *
+ * @see #setDuration
+ */
+ public static final int LENGTH_SHORT = -1;
+
+ /**
+ * Show the Snackbar for a long period of time.
+ *
+ * @see #setDuration
+ */
+ public static final int LENGTH_LONG = 0;
+
+ private static final int ANIMATION_DURATION = 250;
+ private static final int ANIMATION_FADE_DURATION = 180;
+
+ private static final Handler sHandler;
+ private static final int MSG_SHOW = 0;
+ private static final int MSG_DISMISS = 1;
+
+ static {
+ sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_SHOW:
+ ((Snackbar) message.obj).showView();
+ return true;
+ case MSG_DISMISS:
+ ((Snackbar) message.obj).hideView();
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ private final ViewGroup mParent;
+ private final Context mContext;
+ private final SnackbarLayout mView;
+ private int mDuration;
+
+ Snackbar(ViewGroup parent) {
+ mParent = parent;
+ mContext = parent.getContext();
+
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ mView = (SnackbarLayout) inflater.inflate(R.layout.layout_snackbar, mParent, false);
+ }
+
+ /**
+ * Make a Snackbar to display a message.
+ *
+ * @param parent The parent to add Snackbars to.
+ * @param text The text to show. Can be formatted text.
+ * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
+ * #LENGTH_LONG}
+ */
+ public static Snackbar make(ViewGroup parent, CharSequence text, @Duration int duration) {
+ Snackbar snackbar = new Snackbar(parent);
+ snackbar.setText(text);
+ snackbar.setDuration(duration);
+ return snackbar;
+ }
+
+ /**
+ * Make a Snackbar to display a message.
+ *
+ * @param parent The parent to add Snackbars to.
+ * @param resId The resource id of the string resource to use. Can be formatted text.
+ * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
+ * #LENGTH_LONG}
+ */
+ public static Snackbar make(ViewGroup parent, int resId, @Duration int duration) {
+ return make(parent, parent.getResources().getText(resId), duration);
+ }
+
+ /**
+ * Set the action to be displayed in this {@link Snackbar}.
+ *
+ * @param resId String resource to display
+ * @param listener callback to be invoked when the action is clicked
+ */
+ public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
+ return setAction(mContext.getText(resId), listener);
+ }
+
+ /**
+ * Set the action to be displayed in this {@link Snackbar}.
+ *
+ * @param text Text to display
+ * @param listener callback to be invoked when the action is clicked
+ */
+ public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
+ final TextView tv = mView.getActionView();
+
+ if (TextUtils.isEmpty(text) || listener == null) {
+ tv.setVisibility(View.GONE);
+ tv.setOnClickListener(null);
+ } else {
+ tv.setVisibility(View.VISIBLE);
+ tv.setText(text);
+ tv.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ listener.onClick(view);
+
+ // Now dismiss the Snackbar
+ dismiss();
+ }
+ });
+ }
+ return this;
+ }
+
+ /**
+ * Update the text in this {@link Snackbar}.
+ *
+ * @param message The new text for the Toast.
+ */
+ public Snackbar setText(CharSequence message) {
+ final TextView tv = mView.getMessageView();
+ tv.setText(message);
+ return this;
+ }
+
+ /**
+ * Update the text in this {@link Snackbar}.
+ *
+ * @param resId The new text for the Toast.
+ */
+ public Snackbar setText(@StringRes int resId) {
+ return setText(mContext.getText(resId));
+ }
+
+ /**
+ * Set how long to show the view for.
+ *
+ * @param duration either be one of the predefined lengths:
+ * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
+ * in milliseconds.
+ */
+ public Snackbar setDuration(@Duration int duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Return the duration.
+ *
+ * @see #setDuration
+ */
+ @Duration
+ public int getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Returns the {@link Snackbar}'s view.
+ */
+ public View getView() {
+ return mView;
+ }
+
+ /**
+ * Show the {@link Snackbar}.
+ */
+ public void show() {
+ SnackbarManager.getInstance().show(mDuration, mManagerCallback);
+ }
+
+ /**
+ * Dismiss the {@link Snackbar}.
+ */
+ public void dismiss() {
+ SnackbarManager.getInstance().dismiss(mManagerCallback);
+ }
+
+ private final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
+ @Override
+ public void show() {
+ sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, Snackbar.this));
+ }
+
+ @Override
+ public void dismiss() {
+ sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, Snackbar.this));
+ }
+ };
+
+ final void showView() {
+ if (mView.getParent() == null) {
+ final ViewGroup.LayoutParams lp = mView.getLayoutParams();
+
+ if (lp instanceof CoordinatorLayout.LayoutParams) {
+ // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
+
+ final Behavior behavior = new Behavior();
+ behavior.setStartAlphaSwipeDistance(0.1f);
+ behavior.setEndAlphaSwipeDistance(0.6f);
+ behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
+ behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
+ @Override
+ public void onDismiss(View view) {
+ dismiss();
+ }
+
+ @Override
+ public void onDragStateChanged(int state) {
+ switch (state) {
+ case SwipeDismissBehavior.STATE_DRAGGING:
+ case SwipeDismissBehavior.STATE_SETTLING:
+ // If the view is being dragged or settling, cancel the timeout
+ SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
+ break;
+ case SwipeDismissBehavior.STATE_IDLE:
+ // If the view has been released and is idle, restore the timeout
+ SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
+ break;
+ }
+ }
+ });
+ ((CoordinatorLayout.LayoutParams) lp).setBehavior(behavior);
+ }
+
+ mParent.addView(mView);
+ }
+
+ if (ViewCompat.isLaidOut(mView)) {
+ // If the view is already laid out, animate it now
+ animateViewIn();
+ } else {
+ // Otherwise, add one of our layout change listeners and animate it in when laid out
+ mView.setOnLayoutChangeListener(new SnackbarLayout.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View view, int left, int top, int right, int bottom) {
+ animateViewIn();
+ mView.setOnLayoutChangeListener(null);
+ }
+ });
+ }
+ }
+
+ private void animateViewIn() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ ViewCompat.setTranslationY(mView, mView.getHeight());
+ ViewCompat.animate(mView).translationY(0f)
+ .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
+ .setDuration(ANIMATION_DURATION)
+ .setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(View view) {
+ mView.animateChildrenIn(ANIMATION_DURATION - ANIMATION_FADE_DURATION,
+ ANIMATION_FADE_DURATION);
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ SnackbarManager.getInstance().onShown(mManagerCallback);
+ }
+ }).start();
+ } else {
+ Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_in);
+ anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
+ anim.setDuration(ANIMATION_DURATION);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ SnackbarManager.getInstance().onShown(mManagerCallback);
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mView.startAnimation(anim);
+ }
+ }
+
+ private void animateViewOut() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ ViewCompat.animate(mView).translationY(mView.getHeight())
+ .setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR)
+ .setDuration(ANIMATION_DURATION)
+ .setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(View view) {
+ mView.animateChildrenOut(0, ANIMATION_FADE_DURATION);
+ }
+
+ @Override
+ public void onAnimationEnd(View view) {
+ onViewHidden();
+ }
+ }).start();
+ } else {
+ Animation anim = AnimationUtils.loadAnimation(mView.getContext(), R.anim.snackbar_out);
+ anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
+ anim.setDuration(ANIMATION_DURATION);
+ anim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ onViewHidden();
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mView.startAnimation(anim);
+ }
+ }
+
+ final void hideView() {
+ if (mView.getVisibility() != View.VISIBLE || isBeingDragged()) {
+ onViewHidden();
+ } else {
+ animateViewOut();
+ }
+ }
+
+ private void onViewHidden() {
+ // First remove the view from the parent
+ mParent.removeView(mView);
+ // Now, tell the SnackbarManager that it has been dismissed
+ SnackbarManager.getInstance().onDismissed(mManagerCallback);
+ }
+
+ /**
+ * @return if the view is being being dragged or settled by {@link SwipeDismissBehavior}.
+ */
+ private boolean isBeingDragged() {
+ final ViewGroup.LayoutParams lp = mView.getLayoutParams();
+
+ if (lp instanceof CoordinatorLayout.LayoutParams) {
+ final CoordinatorLayout.LayoutParams cllp = (CoordinatorLayout.LayoutParams) lp;
+ final CoordinatorLayout.Behavior behavior = cllp.getBehavior();
+
+ if (behavior instanceof SwipeDismissBehavior) {
+ return ((SwipeDismissBehavior) behavior).getDragState()
+ != SwipeDismissBehavior.STATE_IDLE;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @hide
+ */
+ public static class SnackbarLayout extends LinearLayout {
+ private TextView mMessageView;
+ private TextView mActionView;
+
+ private int mMaxWidth;
+ private int mMaxInlineActionWidth;
+
+ interface OnLayoutChangeListener {
+ public void onLayoutChange(View view, int left, int top, int right, int bottom);
+ }
+
+ private OnLayoutChangeListener mOnLayoutChangeListener;
+
+ public SnackbarLayout(Context context) {
+ this(context, null);
+ }
+
+ public SnackbarLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
+ mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
+ mMaxInlineActionWidth = a.getDimensionPixelSize(
+ R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
+ a.recycle();
+
+ setClickable(true);
+
+ // Now inflate our content. We need to do this manually rather than using an <include>
+ // in the layout since older versions of the Android do not inflate includes with
+ // the correct Context.
+ LayoutInflater.from(context).inflate(R.layout.layout_snackbar_include, this);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mMessageView = (TextView) findViewById(R.id.snackbar_text);
+ mActionView = (TextView) findViewById(R.id.snackbar_action);
+ }
+
+ TextView getMessageView() {
+ return mMessageView;
+ }
+
+ TextView getActionView() {
+ return mActionView;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ final int multiLineVPadding = getResources().getDimensionPixelSize(
+ R.dimen.snackbar_padding_vertical_2lines);
+ final int singleLineVPadding = getResources().getDimensionPixelSize(
+ R.dimen.snackbar_padding_vertical);
+ final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
+
+ boolean remeasure = false;
+ if (isMultiLine && mMaxInlineActionWidth > 0
+ && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
+ if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
+ multiLineVPadding - singleLineVPadding)) {
+ remeasure = true;
+ }
+ } else {
+ final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
+ if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
+ remeasure = true;
+ }
+ }
+
+ if (remeasure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ void animateChildrenIn(int delay, int duration) {
+ ViewCompat.setAlpha(mMessageView, 0f);
+ ViewCompat.animate(mMessageView).alpha(1f).setDuration(duration)
+ .setStartDelay(delay).start();
+
+ if (mActionView.getVisibility() == VISIBLE) {
+ ViewCompat.setAlpha(mActionView, 0f);
+ ViewCompat.animate(mActionView).alpha(1f).setDuration(duration)
+ .setStartDelay(delay).start();
+ }
+ }
+
+ void animateChildrenOut(int delay, int duration) {
+ ViewCompat.setAlpha(mMessageView, 1f);
+ ViewCompat.animate(mMessageView).alpha(0f).setDuration(duration)
+ .setStartDelay(delay).start();
+
+ if (mActionView.getVisibility() == VISIBLE) {
+ ViewCompat.setAlpha(mActionView, 1f);
+ ViewCompat.animate(mActionView).alpha(0f).setDuration(duration)
+ .setStartDelay(delay).start();
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ if (changed && mOnLayoutChangeListener != null) {
+ mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
+ }
+ }
+
+ void setOnLayoutChangeListener(OnLayoutChangeListener onLayoutChangeListener) {
+ mOnLayoutChangeListener = onLayoutChangeListener;
+ }
+
+ private boolean updateViewsWithinLayout(final int orientation,
+ final int messagePadTop, final int messagePadBottom) {
+ boolean changed = false;
+ if (orientation != getOrientation()) {
+ setOrientation(orientation);
+ changed = true;
+ }
+ if (mMessageView.getPaddingTop() != messagePadTop
+ || mMessageView.getPaddingBottom() != messagePadBottom) {
+ updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
+ changed = true;
+ }
+ return changed;
+ }
+
+ private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
+ if (ViewCompat.isPaddingRelative(view)) {
+ ViewCompat.setPaddingRelative(view,
+ ViewCompat.getPaddingStart(view), topPadding,
+ ViewCompat.getPaddingEnd(view), bottomPadding);
+ } else {
+ view.setPadding(view.getPaddingLeft(), topPadding,
+ view.getPaddingRight(), bottomPadding);
+ }
+ }
+ }
+
+ final class Behavior extends SwipeDismissBehavior<SnackbarLayout> {
+ @Override
+ public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarLayout child,
+ MotionEvent event) {
+ // We want to make sure that we disable any Snackbar timeouts if the user is
+ // currently touching the Snackbar. We restore the timeout when complete
+ if (parent.isPointInChildBounds(child, (int) event.getX(), (int) event.getY())) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ SnackbarManager.getInstance().cancelTimeout(mManagerCallback);
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ SnackbarManager.getInstance().restoreTimeout(mManagerCallback);
+ break;
+ }
+ }
+
+ return super.onInterceptTouchEvent(parent, child, event);
+ }
+ }
+}
diff --git a/design/src/android/support/design/widget/SnackbarManager.java b/design/src/android/support/design/widget/SnackbarManager.java
new file mode 100644
index 0000000..3b09f17
--- /dev/null
+++ b/design/src/android/support/design/widget/SnackbarManager.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+/**
+ * Manages {@link Snackbar}s.
+ */
+class SnackbarManager {
+
+ private static final int MSG_TIMEOUT = 0;
+
+ private static final int SHORT_DURATION_MS = 1500;
+ private static final int LONG_DURATION_MS = 2750;
+
+ private static SnackbarManager sSnackbarManager;
+
+ static SnackbarManager getInstance() {
+ if (sSnackbarManager == null) {
+ sSnackbarManager = new SnackbarManager();
+ }
+ return sSnackbarManager;
+ }
+
+ private final Object mLock;
+ private final Handler mHandler;
+
+ private SnackbarRecord mCurrentSnackbar;
+ private SnackbarRecord mNextSnackbar;
+
+ private SnackbarManager() {
+ mLock = new Object();
+ mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_TIMEOUT:
+ handleTimeout((SnackbarRecord) message.obj);
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ interface Callback {
+ void show();
+ void dismiss();
+ }
+
+ public void show(int duration, Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ // Means that the callback is already in the queue. We'll just update the duration
+ mCurrentSnackbar.duration = duration;
+
+ // If this is the Snackbar currently being shown, call re-schedule it's
+ // timeout
+ mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
+ scheduleTimeoutLocked(mCurrentSnackbar);
+ return;
+ } else if (isNextSnackbar(callback)) {
+ // We'll just update the duration
+ mNextSnackbar.duration = duration;
+ } else {
+ // Else, we need to create a new record and queue it
+ mNextSnackbar = new SnackbarRecord(duration, callback);
+ }
+
+ if (mCurrentSnackbar != null) {
+ // If the new Snackbar isn't in position 0, we'll cancel the current one and wait
+ // in line
+ cancelSnackbarLocked(mCurrentSnackbar);
+ } else {
+ // Otherwise, just show it now
+ showNextSnackbarLocked();
+ }
+ }
+ }
+
+ public void dismiss(Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ cancelSnackbarLocked(mCurrentSnackbar);
+ }
+ if (isNextSnackbar(callback)) {
+ cancelSnackbarLocked(mNextSnackbar);
+ }
+ }
+ }
+
+ /**
+ * Should be called when a Snackbar is no longer displayed. This is after any exit
+ * animation has finished.
+ */
+ public void onDismissed(Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ // If the callback is from a Snackbar currently show, remove it and show a new one
+ mCurrentSnackbar = null;
+ if (mNextSnackbar != null) {
+ showNextSnackbarLocked();
+ }
+ }
+ }
+ }
+
+ /**
+ * Should be called when a Snackbar is being shown. This is after any entrance animation has
+ * finished.
+ */
+ public void onShown(Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ scheduleTimeoutLocked(mCurrentSnackbar);
+ }
+ }
+ }
+
+ public void cancelTimeout(Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
+ }
+ }
+ }
+
+ public void restoreTimeout(Callback callback) {
+ synchronized (mLock) {
+ if (isCurrentSnackbar(callback)) {
+ scheduleTimeoutLocked(mCurrentSnackbar);
+ }
+ }
+ }
+
+ private static class SnackbarRecord {
+ private Callback callback;
+ private int duration;
+
+ SnackbarRecord(int duration, Callback callback) {
+ this.callback = callback;
+ this.duration = duration;
+ }
+ }
+
+ private void showNextSnackbarLocked() {
+ if (mNextSnackbar != null) {
+ mCurrentSnackbar = mNextSnackbar;
+ mCurrentSnackbar.callback.show();
+ mNextSnackbar = null;
+ }
+ }
+
+ private void cancelSnackbarLocked(SnackbarRecord record) {
+ record.callback.dismiss();
+ }
+
+ private boolean isCurrentSnackbar(Callback callback) {
+ return mCurrentSnackbar != null && mCurrentSnackbar.callback == callback;
+ }
+
+ private boolean isNextSnackbar(Callback callback) {
+ return mNextSnackbar != null && mNextSnackbar.callback == callback;
+ }
+
+ private void scheduleTimeoutLocked(SnackbarRecord r) {
+ mHandler.removeCallbacksAndMessages(r);
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r),
+ r.duration == Snackbar.LENGTH_LONG
+ ? LONG_DURATION_MS
+ : SHORT_DURATION_MS);
+ }
+
+ private void handleTimeout(SnackbarRecord record) {
+ synchronized (mLock) {
+ if (mCurrentSnackbar == record || mNextSnackbar == record) {
+ cancelSnackbarLocked(record);
+ }
+ }
+ }
+
+}
diff --git a/design/src/android/support/design/widget/SwipeDismissBehavior.java b/design/src/android/support/design/widget/SwipeDismissBehavior.java
new file mode 100644
index 0000000..d6c1338
--- /dev/null
+++ b/design/src/android/support/design/widget/SwipeDismissBehavior.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.support.annotation.IntDef;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.ViewDragHelper;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An interaction behavior plugin for child views of {@link CoordinatorLayout} to provide support
+ * for the 'swipe-to-dismiss' gesture.
+ */
+public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
+
+ /**
+ * A view is not currently being dragged or animating as a result of a fling/snap.
+ */
+ public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
+
+ /**
+ * A view is currently being dragged. The position is currently changing as a result
+ * of user input or simulated user input.
+ */
+ public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
+
+ /**
+ * A view is currently settling into place as a result of a fling or
+ * predefined non-interactive motion.
+ */
+ public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
+
+ /** @hide */
+ @IntDef({SWIPE_DIRECTION_START_TO_END, SWIPE_DIRECTION_END_TO_START, SWIPE_DIRECTION_ANY})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface SwipeDirection {}
+
+ /**
+ * Swipe direction that only allows swiping in the direction of start-to-end. That is
+ * left-to-right in LTR, or right-to-left in RTL.
+ */
+ public static final int SWIPE_DIRECTION_START_TO_END = 0;
+
+ /**
+ * Swipe direction that only allows swiping in the direction of end-to-start. That is
+ * right-to-left in LTR or left-to-right in RTL.
+ */
+ public static final int SWIPE_DIRECTION_END_TO_START = 1;
+
+ /**
+ * Swipe direction which allows swiping in either direction.
+ */
+ public static final int SWIPE_DIRECTION_ANY = 2;
+
+ private static final float DEFAULT_DRAG_DISMISS_THRESHOLD = 0.5f;
+ private static final float DEFAULT_ALPHA_START_DISTANCE = 0f;
+ private static final float DEFAULT_ALPHA_END_DISTANCE = DEFAULT_DRAG_DISMISS_THRESHOLD;
+
+ private ViewDragHelper mViewDragHelper;
+ private OnDismissListener mListener;
+ private boolean mIgnoreEvents;
+
+ private float mSensitivity = 0f;
+ private boolean mSensitivitySet;
+
+ private int mSwipeDirection = SWIPE_DIRECTION_ANY;
+ private float mDragDismissThreshold = DEFAULT_DRAG_DISMISS_THRESHOLD;
+ private float mAlphaStartSwipeDistance = DEFAULT_ALPHA_START_DISTANCE;
+ private float mAlphaEndSwipeDistance = DEFAULT_ALPHA_END_DISTANCE;
+
+ /**
+ * Callback interface used to notify the application that the view has been dismissed.
+ */
+ public interface OnDismissListener {
+ /**
+ * Called when {@code view} has been dismissed via swiping.
+ */
+ public void onDismiss(View view);
+
+ /**
+ * Called when the drag state has changed.
+ *
+ * @param state the new state. One of
+ * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
+ */
+ public void onDragStateChanged(int state);
+ }
+
+ /**
+ * Set the listener to be used when a dismiss event occurs.
+ *
+ * @param listener the listener to use.
+ */
+ public void setListener(OnDismissListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Sets the swipe direction for this behavior.
+ *
+ * @param direction one of the {@link #SWIPE_DIRECTION_START_TO_END},
+ * {@link #SWIPE_DIRECTION_END_TO_START} or {@link #SWIPE_DIRECTION_ANY}
+ */
+ public void setSwipeDirection(@SwipeDirection int direction) {
+ mSwipeDirection = direction;
+ }
+
+ /**
+ * Set the threshold for telling if a view has been dragged enough to be dismissed.
+ *
+ * @param distance a ratio of a view's width, values are clamped to 0 >= x <= 1f;
+ */
+ public void setDragDismissDistance(float distance) {
+ mDragDismissThreshold = clamp(0f, distance, 1f);
+ }
+
+ /**
+ * The minimum swipe distance before the view's alpha is modified.
+ *
+ * @param fraction the distance as a fraction of the view's width.
+ */
+ public void setStartAlphaSwipeDistance(float fraction) {
+ mAlphaStartSwipeDistance = clamp(0f, fraction, 1f);
+ }
+
+ /**
+ * The maximum swipe distance for the view's alpha is modified.
+ *
+ * @param fraction the distance as a fraction of the view's width.
+ */
+ public void setEndAlphaSwipeDistance(float fraction) {
+ mAlphaEndSwipeDistance = clamp(0f, fraction, 1f);
+ }
+
+ /**
+ * Set the sensitivity used for detecting the start of a swipe. This only takes effect if
+ * no touch handling has occured yet.
+ *
+ * @param sensitivity Multiplier for how sensitive we should be about detecting
+ * the start of a drag. Larger values are more sensitive. 1.0f is normal.
+ */
+ public void setSensitivity(float sensitivity) {
+ mSensitivity = sensitivity;
+ mSensitivitySet = true;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ mIgnoreEvents = !parent.isPointInChildBounds(child,
+ (int) event.getX(), (int) event.getY());
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ // Reset the ignore flag
+ if (mIgnoreEvents) {
+ mIgnoreEvents = false;
+ return false;
+ }
+ break;
+ }
+
+ if (mIgnoreEvents) {
+ return false;
+ }
+
+ ensureViewDragHelper(parent);
+ return mViewDragHelper.shouldInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
+ if (mViewDragHelper != null) {
+ mViewDragHelper.processTouchEvent(event);
+ return true;
+ }
+ return false;
+ }
+
+ private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
+ private int mOriginalCapturedViewLeft;
+
+ @Override
+ public boolean tryCaptureView(View child, int pointerId) {
+ mOriginalCapturedViewLeft = child.getLeft();
+ return true;
+ }
+
+ @Override
+ public void onViewDragStateChanged(int state) {
+ if (mListener != null) {
+ mListener.onDragStateChanged(state);
+ }
+ }
+
+ @Override
+ public void onViewReleased(View child, float xvel, float yvel) {
+ final int childWidth = child.getWidth();
+ int targetLeft;
+ boolean dismiss = false;
+
+ if (shouldDismiss(child, xvel)) {
+ targetLeft = child.getLeft() < mOriginalCapturedViewLeft
+ ? mOriginalCapturedViewLeft - childWidth
+ : mOriginalCapturedViewLeft + childWidth;
+ dismiss = true;
+ } else {
+ // Else, reset back to the original left
+ targetLeft = mOriginalCapturedViewLeft;
+ }
+
+ if (mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
+ ViewCompat.postOnAnimation(child, new SettleRunnable(child, dismiss));
+ } else if (dismiss && mListener != null) {
+ mListener.onDismiss(child);
+ }
+ }
+
+ private boolean shouldDismiss(View child, float xvel) {
+ if (xvel != 0f) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(child)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+
+ if (mSwipeDirection == SWIPE_DIRECTION_ANY) {
+ // We don't care about the direction so return true
+ return true;
+ } else if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
+ // We only allow start-to-end swiping, so the fling needs to be in the
+ // correct direction
+ return isRtl ? xvel < 0f : xvel > 0f;
+ } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
+ // We only allow end-to-start swiping, so the fling needs to be in the
+ // correct direction
+ return isRtl ? xvel > 0f : xvel < 0f;
+ }
+ } else {
+ final int distance = child.getLeft() - mOriginalCapturedViewLeft;
+ final int thresholdDistance = Math.round(child.getWidth() * mDragDismissThreshold);
+ return Math.abs(distance) >= thresholdDistance;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getViewHorizontalDragRange(View child) {
+ return child.getWidth();
+ }
+
+ @Override
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ final boolean isRtl = ViewCompat.getLayoutDirection(child)
+ == ViewCompat.LAYOUT_DIRECTION_RTL;
+ int min, max;
+
+ if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
+ if (isRtl) {
+ min = mOriginalCapturedViewLeft - child.getWidth();
+ max = mOriginalCapturedViewLeft;
+ } else {
+ min = mOriginalCapturedViewLeft;
+ max = mOriginalCapturedViewLeft + child.getWidth();
+ }
+ } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
+ if (isRtl) {
+ min = mOriginalCapturedViewLeft;
+ max = mOriginalCapturedViewLeft + child.getWidth();
+ } else {
+ min = mOriginalCapturedViewLeft - child.getWidth();
+ max = mOriginalCapturedViewLeft;
+ }
+ } else {
+ min = mOriginalCapturedViewLeft - child.getWidth();
+ max = mOriginalCapturedViewLeft + child.getWidth();
+ }
+
+ return clamp(min, left, max);
+ }
+
+ @Override
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ return child.getTop();
+ }
+
+ @Override
+ public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
+ final float startAlphaDistance = child.getWidth() * mAlphaStartSwipeDistance;
+ final float endAlphaDistance = child.getWidth() * mAlphaEndSwipeDistance;
+
+ if (left <= startAlphaDistance) {
+ ViewCompat.setAlpha(child, 1f);
+ } else if (left >= endAlphaDistance) {
+ ViewCompat.setAlpha(child, 0f);
+ } else {
+ // We're between the start and end distances
+ final float distance = fraction(startAlphaDistance, endAlphaDistance, left);
+ ViewCompat.setAlpha(child, clamp(0f, 1f - distance, 1f));
+ }
+ }
+ };
+
+ private void ensureViewDragHelper(ViewGroup parent) {
+ if (mViewDragHelper == null) {
+ mViewDragHelper = mSensitivitySet
+ ? ViewDragHelper.create(parent, mSensitivity, mDragCallback)
+ : ViewDragHelper.create(parent, mDragCallback);
+ }
+ }
+
+ private class SettleRunnable implements Runnable {
+ private final View mView;
+ private final boolean mDismiss;
+
+ SettleRunnable(View view, boolean dismiss) {
+ mView = view;
+ mDismiss = dismiss;
+ }
+
+ @Override
+ public void run() {
+ if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
+ ViewCompat.postOnAnimation(mView, this);
+ } else {
+ if (mDismiss && mListener != null) {
+ mListener.onDismiss(mView);
+ }
+ }
+ }
+ }
+
+ private static float clamp(float min, float value, float max) {
+ return Math.min(Math.max(min, value), max);
+ }
+
+ private static int clamp(int min, int value, int max) {
+ return Math.min(Math.max(min, value), max);
+ }
+
+ /**
+ * Retrieve the current drag state of this behavior. This will return one of
+ * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
+ *
+ * @return The current drag state
+ */
+ public int getDragState() {
+ return mViewDragHelper != null ? mViewDragHelper.getViewDragState() : STATE_IDLE;
+ }
+
+ /**
+ * Linear interpolation between {@code startValue} and {@code endValue} by the fraction {@code
+ * fraction}.
+ */
+ static float lerp(float startValue, float endValue, float fraction) {
+ return startValue + (fraction * (endValue - startValue));
+ }
+
+ /**
+ * The fraction that {@code value} is between {@code startValue} and {@code endValue}.
+ */
+ static float fraction(float startValue, float endValue, float value) {
+ return (value - startValue) / (endValue - startValue);
+ }
+}
\ No newline at end of file
diff --git a/design/src/android/support/design/widget/TabLayout.java b/design/src/android/support/design/widget/TabLayout.java
index 62f7056..d9df727 100755
--- a/design/src/android/support/design/widget/TabLayout.java
+++ b/design/src/android/support/design/widget/TabLayout.java
@@ -30,7 +30,6 @@
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.ActionBar;
import android.support.v7.internal.widget.TintManager;
import android.support.v7.widget.AppCompatTextView;
@@ -44,7 +43,6 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Animation;
-import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
@@ -57,6 +55,8 @@
import java.util.ArrayList;
import java.util.Iterator;
+import static android.support.design.widget.AnimationUtils.lerp;
+
/**
* TabLayout provides a horizontal layout to display tabs. <p> Population of the tabs to display is
* done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can
@@ -87,7 +87,6 @@
*/
public class TabLayout extends HorizontalScrollView {
- private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private static final int MAX_TAB_TEXT_LINES = 2;
private static final int DEFAULT_HEIGHT = 48; // dps
@@ -270,8 +269,12 @@
* part of a scrolling container such as {@link ViewPager}.
* <p>
* Calling this method does not update the selected tab, it is only used for drawing purposes.
+ *
+ * @param position current scroll position
+ * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
+ * @param updateSelectedText Whether to update the text's selected state.
*/
- public void setScrollPosition(int position, float positionOffset) {
+ public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
if (isAnimationRunning(getAnimation())) {
return;
}
@@ -284,7 +287,9 @@
scrollTo(calculateScrollXForTab(position, positionOffset), 0);
// Update the 'selected state' view as we scroll
- setSelectedTabView(Math.round(position + positionOffset));
+ if (updateSelectedText) {
+ setSelectedTabView(Math.round(position + positionOffset));
+ }
}
/**
@@ -308,11 +313,21 @@
* through to all of the methods.
*/
public ViewPager.OnPageChangeListener createOnPageChangeListener() {
- return new ViewPager.SimpleOnPageChangeListener() {
+ return new ViewPager.OnPageChangeListener() {
+ private int mScrollState;
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ mScrollState = state;
+ }
+
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
- setScrollPosition(position, positionOffset);
+ // Update the scroll position, only update the text selection if we're being
+ // dragged
+ setScrollPosition(position, positionOffset,
+ mScrollState == ViewPager.SCROLL_STATE_DRAGGING);
}
@Override
@@ -676,7 +691,7 @@
if (getWindowToken() == null || !ViewCompat.isLaidOut(this)) {
// If we don't have a window token, or we haven't been laid out yet just draw the new
// position now
- setScrollPosition(newPosition, 0f);
+ setScrollPosition(newPosition, 0f, true);
return;
}
@@ -688,11 +703,12 @@
final Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
- final float value = lerp(startScrollX, targetScrollX, interpolatedTime);
+ final float value = AnimationUtils.lerp(startScrollX, targetScrollX,
+ interpolatedTime);
scrollTo((int) value, 0);
}
};
- animation.setInterpolator(INTERPOLATOR);
+ animation.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
animation.setDuration(duration);
startAnimation(animation);
}
@@ -705,8 +721,7 @@
final int tabCount = mTabStrip.getChildCount();
for (int i = 0; i < tabCount; i++) {
final View child = mTabStrip.getChildAt(i);
- final boolean isSelected = i == position;
- child.setSelected(isSelected);
+ child.setSelected(i == position);
}
}
@@ -729,7 +744,7 @@
if ((mSelectedTab == null || mSelectedTab.getPosition() == Tab.INVALID_POSITION)
&& newPosition != Tab.INVALID_POSITION) {
// If we don't currently have a tab, just draw the indicator
- setScrollPosition(newPosition, 0f);
+ setScrollPosition(newPosition, 0f, true);
} else {
animateToTab(newPosition);
}
@@ -1349,11 +1364,11 @@
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
setIndicatorPosition(
- (int) lerp(startLeft, targetLeft, interpolatedTime),
- (int) lerp(startRight, targetRight, interpolatedTime));
+ (int) AnimationUtils.lerp(startLeft, targetLeft, interpolatedTime),
+ (int) AnimationUtils.lerp(startRight, targetRight, interpolatedTime));
}
};
- anim.setInterpolator(INTERPOLATOR);
+ anim.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
anim.setDuration(duration);
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
@@ -1385,12 +1400,4 @@
}
}
- /**
- * Linear interpolation between {@code startValue} and {@code endValue} by the fraction {@code
- * fraction}.
- */
- static float lerp(float startValue, float endValue, float fraction) {
- return startValue + (fraction * (endValue - startValue));
- }
-
}
diff --git a/design/src/android/support/design/widget/TextInputLayout.java b/design/src/android/support/design/widget/TextInputLayout.java
new file mode 100644
index 0000000..4992d7f
--- /dev/null
+++ b/design/src/android/support/design/widget/TextInputLayout.java
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.design.widget;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.Handler;
+import android.os.Message;
+import android.support.design.R;
+import android.support.v4.view.AccessibilityDelegateCompat;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewPropertyAnimatorListenerAdapter;
+import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.Transformation;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Layout which wraps an {@link android.widget.EditText} to show a floating label when
+ * the hint is hidden due to the user inputting text.
+ */
+public class TextInputLayout extends LinearLayout {
+
+ private static final long ANIMATION_DURATION = 200;
+ private static final int MSG_UPDATE_LABEL = 0;
+
+ private EditText mEditText;
+ private CharSequence mHint;
+
+ private boolean mErrorEnabled;
+ private TextView mErrorView;
+ private int mErrorTextAppearance;
+
+ private ColorStateList mLabelTextColor;
+
+ private final CollapsingTextHelper mCollapsingTextHelper;
+ private final Handler mHandler;
+
+ public TextInputLayout(Context context) {
+ this(context, null);
+ }
+
+ public TextInputLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ setOrientation(VERTICAL);
+ setWillNotDraw(false);
+
+ mCollapsingTextHelper = new CollapsingTextHelper(this);
+ mHandler = new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_LABEL:
+ updateLabelVisibility(true);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
+ mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
+ mCollapsingTextHelper.setCollapsedTextVerticalGravity(Gravity.TOP);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
+ mHint = a.getText(R.styleable.TextInputLayout_android_hint);
+
+ final int hintAppearance = a.getResourceId(
+ R.styleable.TextInputLayout_hintTextAppearance, -1);
+ if (hintAppearance != -1) {
+ mCollapsingTextHelper.setCollapsedTextAppearance(hintAppearance);
+ }
+
+ mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
+ final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
+
+ // We create a ColorStateList using the specified text color, combining it with our
+ // theme's textColorHint
+ mLabelTextColor = createLabelTextColorStateList(
+ mCollapsingTextHelper.getCollapsedTextColor());
+
+ mCollapsingTextHelper.setCollapsedTextColor(mLabelTextColor.getDefaultColor());
+ mCollapsingTextHelper.setExpandedTextColor(mLabelTextColor.getDefaultColor());
+
+ a.recycle();
+
+ if (errorEnabled) {
+ setErrorEnabled(true);
+ }
+
+ if (ViewCompat.getImportantForAccessibility(this)
+ == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+ // Make sure we're important for accessibility if we haven't been explicitly not
+ ViewCompat.setImportantForAccessibility(this,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ if (child instanceof EditText) {
+ params = setEditText((EditText) child, params);
+ super.addView(child, 0, params);
+ } else {
+ // Carry on adding the View...
+ super.addView(child, index, params);
+ }
+ }
+
+ private LayoutParams setEditText(EditText editText, ViewGroup.LayoutParams lp) {
+ // If we already have an EditText, throw an exception
+ if (mEditText != null) {
+ throw new IllegalArgumentException("We already have an EditText, can only have one");
+ }
+ mEditText = editText;
+
+ // Use the EditText's text size for our expanded text
+ mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
+
+ // Add a TextWatcher so that we know when the text input has changed
+ mEditText.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_LABEL);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ });
+
+ // Add focus listener to the EditText so that we can notify the label that it is activated.
+ // Allows the use of a ColorStateList for the text color on the label
+ mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean focused) {
+ mHandler.sendEmptyMessage(MSG_UPDATE_LABEL);
+ }
+ });
+
+ // If we do not have a valid hint, try and retrieve it from the EditText
+ if (TextUtils.isEmpty(mHint)) {
+ setHint(mEditText.getHint());
+ // Clear the EditText's hint as we will display it ourselves
+ mEditText.setHint(null);
+ }
+
+ if (mErrorView != null) {
+ // Add some start/end padding to the error so that it matches the EditText
+ ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText),
+ 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
+ }
+
+ // Update the label visibility with no animation
+ updateLabelVisibility(false);
+
+ // Create a new FrameLayout.LayoutParams so that we can add enough top margin
+ // to the EditText so make room for the label
+ LayoutParams newLp = new LayoutParams(lp);
+ Paint paint = new Paint();
+ paint.setTextSize(mCollapsingTextHelper.getExpandedTextSize());
+ newLp.topMargin = (int) -paint.ascent();
+
+ return newLp;
+ }
+
+ private void updateLabelVisibility(boolean animate) {
+ boolean hasText = !TextUtils.isEmpty(mEditText.getText());
+ boolean isFocused = mEditText.isFocused();
+
+ mCollapsingTextHelper.setCollapsedTextColor(mLabelTextColor.getColorForState(
+ isFocused ? FOCUSED_STATE_SET : EMPTY_STATE_SET,
+ mLabelTextColor.getDefaultColor()));
+
+ if (hasText || isFocused) {
+ // We should be showing the label so do so if it isn't already
+ collapseHint(animate);
+ } else {
+ // We should not be showing the label so hide it
+ expandHint(animate);
+ }
+ }
+
+ /**
+ * @return the {@link android.widget.EditText} text input
+ */
+ public EditText getEditText() {
+ return mEditText;
+ }
+
+ /**
+ * Set the hint to be displayed in the floating label
+ */
+ public void setHint(CharSequence hint) {
+ mHint = hint;
+ mCollapsingTextHelper.setText(hint);
+
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
+
+ /**
+ * Whether the error functionality is enabled or not in this layout. Enabling this
+ * functionality before setting an error message via {@link #setError(CharSequence)}, will mean
+ * that this layout will not change size when an error is displayed.
+ *
+ * @attr R.attr.errorEnabled
+ */
+ public void setErrorEnabled(boolean enabled) {
+ if (mErrorEnabled != enabled) {
+ if (enabled) {
+ mErrorView = new TextView(getContext());
+ mErrorView.setTextAppearance(getContext(), mErrorTextAppearance);
+ mErrorView.setVisibility(INVISIBLE);
+ addView(mErrorView);
+
+ if (mEditText != null) {
+ // Add some start/end padding to the error so that it matches the EditText
+ ViewCompat.setPaddingRelative(mErrorView, ViewCompat.getPaddingStart(mEditText),
+ 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
+ }
+ } else {
+ removeView(mErrorView);
+ mErrorView = null;
+ }
+ mErrorEnabled = enabled;
+ }
+ }
+
+ /**
+ * Sets an error message that will be displayed below our {@link EditText}. If the
+ * {@code error} is {@code null}, the error message will be cleared.
+ * <p>
+ * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
+ * it will be automatically enabled if {@code error} is not empty.
+ *
+ * @param error Error message to display, or null to clear
+ */
+ public void setError(CharSequence error) {
+ if (!mErrorEnabled) {
+ if (TextUtils.isEmpty(error)) {
+ // If error isn't enabled, and the error is empty, just return
+ return;
+ }
+ // Else, we'll assume that they want to enable the error functionality
+ setErrorEnabled(true);
+ }
+
+ if (!TextUtils.isEmpty(error)) {
+ mErrorView.setText(error);
+ mErrorView.setVisibility(VISIBLE);
+ ViewCompat.setAlpha(mErrorView, 0f);
+ ViewCompat.animate(mErrorView)
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
+ .setListener(null)
+ .start();
+ } else {
+ if (mErrorView.getVisibility() == VISIBLE) {
+ ViewCompat.animate(mErrorView)
+ .alpha(0f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR)
+ .setListener(new ViewPropertyAnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(View view) {
+ mErrorView.setText(null);
+ mErrorView.setVisibility(INVISIBLE);
+ }
+ }).start();
+ }
+ }
+
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ mCollapsingTextHelper.draw(canvas);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ mCollapsingTextHelper.onLayout(changed, left, top, right, bottom);
+
+ if (mEditText != null) {
+ final int l = mEditText.getLeft() + mEditText.getPaddingLeft();
+ final int r = mEditText.getRight() - mEditText.getPaddingRight();
+
+ mCollapsingTextHelper.setExpandedBounds(l,
+ mEditText.getTop() + mEditText.getPaddingTop(),
+ r, mEditText.getBottom() - mEditText.getPaddingBottom());
+
+ // Set the collapsed bounds to be the the full height (minus padding) to match the
+ // EditText's editable area
+ mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
+ r, bottom - top - getPaddingBottom());
+ }
+ }
+
+ private void collapseHint(boolean animate) {
+ if (animate) {
+ animateToExpansionFraction(1f);
+ } else {
+ mCollapsingTextHelper.setExpansionFraction(1f);
+ }
+ }
+
+ private void expandHint(boolean animate) {
+ if (animate) {
+ animateToExpansionFraction(0f);
+ } else {
+ mCollapsingTextHelper.setExpansionFraction(0f);
+ }
+ }
+
+ private void animateToExpansionFraction(final float target) {
+ final float current = mCollapsingTextHelper.getExpansionFraction();
+
+ Animation anim = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mCollapsingTextHelper.setExpansionFraction(
+ AnimationUtils.lerp(current, target, interpolatedTime));
+ }
+ };
+ anim.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
+ anim.setDuration(ANIMATION_DURATION);
+ startAnimation(anim);
+ }
+
+ private ColorStateList createLabelTextColorStateList(int color) {
+ final int[][] states = new int[2][];
+ final int[] colors = new int[2];
+ int i = 0;
+
+ // Focused
+ states[i] = FOCUSED_STATE_SET;
+ colors[i] = color;
+ i++;
+
+ states[i] = EMPTY_STATE_SET;
+ colors[i] = getThemeAttrColor(android.R.attr.textColorHint);
+ i++;
+
+ return new ColorStateList(states, colors);
+ }
+
+ private int getThemeAttrColor(int attr) {
+ TypedValue tv = new TypedValue();
+ if (getContext().getTheme().resolveAttribute(attr, tv, true)) {
+ return tv.data;
+ } else {
+ return Color.MAGENTA;
+ }
+ }
+
+ private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+ event.setClassName(TextInputLayout.class.getSimpleName());
+ }
+
+ @Override
+ public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onPopulateAccessibilityEvent(host, event);
+
+ final CharSequence text = mCollapsingTextHelper.getText();
+ if (!TextUtils.isEmpty(text)) {
+ event.getText().add(text);
+ }
+ }
+
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.setClassName(TextInputLayout.class.getSimpleName());
+
+ final CharSequence text = mCollapsingTextHelper.getText();
+ if (!TextUtils.isEmpty(text)) {
+ info.setText(text);
+ }
+ if (mEditText != null) {
+ info.setLabelFor(mEditText);
+ }
+ final CharSequence error = mErrorView != null ? mErrorView.getText() : null;
+ if (!TextUtils.isEmpty(error)) {
+ info.setContentInvalid(true);
+ info.setError(error);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/v17/leanback/Android.mk b/v17/leanback/Android.mk
index 1574cb7..baf8719 100644
--- a/v17/leanback/Android.mk
+++ b/v17/leanback/Android.mk
@@ -107,6 +107,9 @@
LOCAL_MODULE_CLASS := JAVA_LIBRARIES
LOCAL_MODULE_TAGS := optional
+res.COMMON := $(call intermediates-dir-for,$(LOCAL_MODULE_CLASS),android-support-v17-leanback-res,,COMMON)
+leanback.docs.src_files := $(leanback.docs.src_files) $(call all-java-files-under, ../../../../$(res.COMMON))
+
intermediates.COMMON := $(call intermediates-dir-for,$(LOCAL_MODULE_CLASS),android-support-v17-leanback,,COMMON)
LOCAL_SRC_FILES := $(leanback.docs.src_files)
@@ -162,6 +165,7 @@
leanback.docs.src_files :=
leanback.docs.java_libraries :=
intermediates.COMMON :=
+res.COMMON :=
leanback_internal_api_file :=
leanback_stubs_stamp :=
leanback.docs.stubpackages :=
diff --git a/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_end.xml b/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_end.xml
new file mode 100644
index 0000000..df3aca2
--- /dev/null
+++ b/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_end.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedstep_slide_end_distance"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+
+</set>
\ No newline at end of file
diff --git a/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_start.xml b/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_start.xml
new file mode 100644
index 0000000..49ddc12
--- /dev/null
+++ b/v17/leanback/res/animator-v21/lb_guidedstep_slide_in_from_start.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedstep_slide_start_distance"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_end.xml b/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_end.xml
new file mode 100644
index 0000000..d481273
--- /dev/null
+++ b/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_end.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="translationX"
+ android:valueFrom="0.0"
+ android:valueTo="@dimen/lb_guidedstep_slide_end_distance"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_start.xml b/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_start.xml
new file mode 100644
index 0000000..b172e86
--- /dev/null
+++ b/v17/leanback/res/animator-v21/lb_guidedstep_slide_out_to_start.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="translationX"
+ android:valueFrom="0.0"
+ android:valueTo="@dimen/lb_guidedstep_slide_start_distance"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:interpolator="@android:interpolator/fast_out_slow_in"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator/lb_decelerator_2.xml b/v17/leanback/res/animator/lb_decelerator_2.xml
new file mode 100644
index 0000000..b1f886a
--- /dev/null
+++ b/v17/leanback/res/animator/lb_decelerator_2.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2015, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:factor="2.0"/>
diff --git a/v17/leanback/res/animator/lb_guidance_entry.xml b/v17/leanback/res/animator/lb_guidance_entry.xml
new file mode 100644
index 0000000..e10d2ef
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidance_entry.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="sequentially">
+
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_delay"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidance_entry_translationX"
+ android:valueTo="@dimen/lb_guidance_entry_translationX"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_delay"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+ </set>
+
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_duration"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidance_entry_translationX"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_duration"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+ </set>
+
+</set>
diff --git a/v17/leanback/res/animator/lb_guidedactions_entry.xml b/v17/leanback/res/animator/lb_guidedactions_entry.xml
new file mode 100644
index 0000000..ec6c655
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_entry.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="sequentially">
+
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_delay"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedactions_entry_translationX"
+ android:valueTo="@dimen/lb_guidedactions_entry_translationX"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_delay"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+ </set>
+
+ <set android:ordering="together">
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_duration"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedactions_entry_translationX"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedstep_entry_animation_duration"
+ android:interpolator="@android:interpolator/decelerate_quad"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+ </set>
+</set>
\ No newline at end of file
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_checked.xml b/v17/leanback/res/animator/lb_guidedactions_item_checked.xml
new file mode 100644
index 0000000..463b9f7
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_item_checked.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/lb_guidedactions_item_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_pressed.xml b/v17/leanback/res/animator/lb_guidedactions_item_pressed.xml
new file mode 100644
index 0000000..d00e13b
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_item_pressed.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/lb_guidedactions_item_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.2"
+ android:valueType="floatType" />
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_unchecked.xml b/v17/leanback/res/animator/lb_guidedactions_item_unchecked.xml
new file mode 100644
index 0000000..86525c8
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_item_unchecked.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/lb_guidedactions_item_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
diff --git a/v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml b/v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml
new file mode 100644
index 0000000..0cf30a4
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_item_unpressed.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/lb_guidedactions_item_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="0.2"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
diff --git a/v17/leanback/res/animator/lb_guidedactions_selector_hide.xml b/v17/leanback/res/animator/lb_guidedactions_selector_hide.xml
new file mode 100644
index 0000000..f829eb3
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_selector_hide.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/lb_guidedactions_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:interpolator="@animator/lb_decelerator_2"
+ android:valueType="floatType" />
diff --git a/v17/leanback/res/animator/lb_guidedactions_selector_show.xml b/v17/leanback/res/animator/lb_guidedactions_selector_show.xml
new file mode 100644
index 0000000..e8d69e5
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedactions_selector_show.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedactions_animation_duration"
+ android:propertyName="alpha"
+ android:valueFrom="0"
+ android:valueTo="1.0"
+ android:interpolator="@animator/lb_decelerator_2"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@integer/lb_guidedactions_animation_duration"
+ android:propertyName="scaleY"
+ android:interpolator="@animator/lb_decelerator_2"
+ android:valueType="floatType" />
+</set>
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_in_from_end.xml b/v17/leanback/res/animator/lb_guidedstep_slide_in_from_end.xml
new file mode 100644
index 0000000..1dacdbc
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedstep_slide_in_from_end.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedstep_slide_end_distance"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_in_from_start.xml b/v17/leanback/res/animator/lb_guidedstep_slide_in_from_start.xml
new file mode 100644
index 0000000..3c01324
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedstep_slide_in_from_start.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="translationX"
+ android:valueFrom="@dimen/lb_guidedstep_slide_start_distance"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="alpha"
+ android:valueFrom="0.0"
+ android:valueTo="1.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_out_to_end.xml b/v17/leanback/res/animator/lb_guidedstep_slide_out_to_end.xml
new file mode 100644
index 0000000..879a0cf
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedstep_slide_out_to_end.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="translationX"
+ android:valueFrom="0.0"
+ android:valueTo="@dimen/lb_guidedstep_slide_end_distance"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/animator/lb_guidedstep_slide_out_to_start.xml b/v17/leanback/res/animator/lb_guidedstep_slide_out_to_start.xml
new file mode 100644
index 0000000..4c9af82
--- /dev/null
+++ b/v17/leanback/res/animator/lb_guidedstep_slide_out_to_start.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together" >
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="translationX"
+ android:valueFrom="0.0"
+ android:valueTo="@dimen/lb_guidedstep_slide_start_distance"
+ android:valueType="floatType" />
+
+ <objectAnimator
+ android:duration="@android:integer/config_longAnimTime"
+ android:propertyName="alpha"
+ android:valueFrom="1.0"
+ android:valueTo="0.0"
+ android:valueType="floatType" />
+
+</set>
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_guidedactions_item_chevron.png b/v17/leanback/res/drawable-hdpi/lb_ic_guidedactions_item_chevron.png
new file mode 100644
index 0000000..f06c02d
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_guidedactions_item_chevron.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_guidedactions_item_chevron.png b/v17/leanback/res/drawable-mdpi/lb_ic_guidedactions_item_chevron.png
new file mode 100644
index 0000000..149e214
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_guidedactions_item_chevron.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png b/v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
new file mode 100644
index 0000000..6a65ccf
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_guidedactions_item_chevron.png
Binary files differ
diff --git a/v17/leanback/res/drawable/lb_guidedactions_item_checkmark.xml b/v17/leanback/res/drawable/lb_guidedactions_item_checkmark.xml
new file mode 100644
index 0000000..ec7903b
--- /dev/null
+++ b/v17/leanback/res/drawable/lb_guidedactions_item_checkmark.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval" >
+
+ <size
+ android:height="@dimen/lb_guidedactions_item_checkmark_diameter"
+ android:width="@dimen/lb_guidedactions_item_checkmark_diameter" />
+
+ <solid android:color="@color/lb_tv_white" />
+
+</shape>
diff --git a/v17/leanback/res/layout/lb_guidance.xml b/v17/leanback/res/layout/lb_guidance.xml
new file mode 100644
index 0000000..28c0220
--- /dev/null
+++ b/v17/leanback/res/layout/lb_guidance.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ style="?attr/guidanceContainerStyle" >
+
+ <ImageView
+ android:id="@+id/guidance_icon"
+ style="?attr/guidanceIconStyle"
+ tools:ignore="ContentDescription" />
+
+ <TextView
+ android:id="@+id/guidance_title"
+ style="?attr/guidanceTitleStyle" />
+
+ <TextView
+ android:id="@+id/guidance_breadcrumb"
+ style="?attr/guidanceBreadcrumbStyle" />
+
+ <TextView
+ android:id="@+id/guidance_description"
+ style="?attr/guidanceDescriptionStyle" />
+
+ </RelativeLayout>
+
+</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_guidedactions.xml b/v17/leanback/res/layout/lb_guidedactions.xml
new file mode 100644
index 0000000..43617c9
--- /dev/null
+++ b/v17/leanback/res/layout/lb_guidedactions.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Layout for the settings list fragment -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <RelativeLayout
+ style="?attr/guidedActionsContainerStyle" >
+
+ <FrameLayout
+ android:id="@+id/guidedactions_selector"
+ style="?attr/guidedActionsSelectorStyle" />
+
+ <android.support.v17.leanback.widget.VerticalGridView
+ android:id="@+id/guidedactions_list"
+ style="?attr/guidedActionsListStyle" />
+
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/v17/leanback/res/layout/lb_guidedactions_item.xml b/v17/leanback/res/layout/lb_guidedactions_item.xml
new file mode 100644
index 0000000..dfda22f
--- /dev/null
+++ b/v17/leanback/res/layout/lb_guidedactions_item.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Layout for an action item displayed in the 2 pane actions fragment. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/guidedActionItemContainerStyle" >
+
+ <ImageView
+ android:id="@+id/guidedactions_item_checkmark"
+ style="?attr/guidedActionItemCheckmarkStyle"
+ tools:ignore="ContentDescription" />
+
+ <ImageView
+ android:id="@+id/guidedactions_item_icon"
+ style="?attr/guidedActionItemIconStyle"
+ tools:ignore="ContentDescription" />
+
+ <LinearLayout
+ android:id="@+id/guidedactions_item_content"
+ style="?attr/guidedActionItemContentStyle" >
+
+ <TextView
+ android:id="@+id/guidedactions_item_title"
+ style="?attr/guidedActionItemTitleStyle" />
+
+ <TextView
+ android:id="@+id/guidedactions_item_description"
+ style="?attr/guidedActionItemDescriptionStyle" />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/guidedactions_item_chevron"
+ style="?attr/guidedActionItemChevronStyle"
+ tools:ignore="ContentDescription" />
+
+</LinearLayout>
diff --git a/v17/leanback/res/layout/lb_guidedstep_fragment.xml b/v17/leanback/res/layout/lb_guidedstep_fragment.xml
new file mode 100644
index 0000000..6e0b7ad
--- /dev/null
+++ b/v17/leanback/res/layout/lb_guidedstep_fragment.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Layout for the frame of a 2 pane actions fragment. -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/content_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <FrameLayout
+ android:id="@+id/content_fragment"
+ android:layout_width="@dimen/lb_guidedstep_guidance_section_width"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true" />
+
+ <FrameLayout
+ android:id="@+id/action_fragment"
+ android:layout_width="@dimen/lb_guidedactions_section_width_with_shadow"
+ android:layout_height="match_parent"
+ android:layout_alignParentEnd="true" />
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/v17/leanback/res/values-ldrtl/dimens.xml b/v17/leanback/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000..9f54273
--- /dev/null
+++ b/v17/leanback/res/values-ldrtl/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!-- GuidedStepFragment -->
+ <dimen name="lb_guidedstep_slide_start_distance">200dp</dimen>
+ <dimen name="lb_guidedstep_slide_end_distance">-200dp</dimen>
+ <dimen name="lb_guidance_entry_translationX">120dp</dimen>
+ <dimen name="lb_guidedactions_entry_translationX">-384dp</dimen>
+ <!-- end GuidedStepFragment -->
+
+</resources>
diff --git a/v17/leanback/res/values-v19/themes.xml b/v17/leanback/res/values-v19/themes.xml
index a466ad7..53befec 100644
--- a/v17/leanback/res/values-v19/themes.xml
+++ b/v17/leanback/res/values-v19/themes.xml
@@ -20,5 +20,8 @@
<item name="playbackProgressPrimaryColor">@color/lb_playback_progress_color_no_theme</item>
<item name="playbackControlsIconHighlightColor">@color/lb_playback_icon_highlight_no_theme</item>
<item name="defaultBrandColor">@color/lb_default_brand_color</item>
+
+ <item name="android:windowOverscan">true</item>
+ <item name="guidedStepTheme">@style/Theme.Leanback.GuidedStep</item>
</style>
</resources>
diff --git a/v17/leanback/res/values-v21/themes.xml b/v17/leanback/res/values-v21/themes.xml
index 9061674..3b48dae 100644
--- a/v17/leanback/res/values-v21/themes.xml
+++ b/v17/leanback/res/values-v21/themes.xml
@@ -21,5 +21,8 @@
<item name="playbackControlsIconHighlightColor">?android:attr/colorAccent</item>
<item name="defaultBrandColor">?android:attr/colorPrimary</item>
<item name="android:colorPrimary">@color/lb_default_brand_color</item>
+
+ <item name="android:windowOverscan">true</item>
+ <item name="guidedStepTheme">@style/Theme.Leanback.GuidedStep</item>
</style>
</resources>
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index 923ba3e..1b77694 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -278,6 +278,156 @@
<attr name="overlayDimActiveLevel" format="fraction" />
<!-- Default level of dimming for dimmed views. -->
<attr name="overlayDimDimmedLevel" format="fraction" />
+ </declare-styleable>
+
+ <declare-styleable name="LeanbackGuidedStepTheme">
+
+ <!-- Theme attribute for the overall theme used in a GuidedStepFragment. The Leanback themes
+ set the default for this, but a custom theme that does not derive from a leanback theme
+ can set this to <code>@style/Theme.Leanback.GuidedStep</code> in order to specify the
+ default GuidedStepFragment styles. -->
+ <attr name="guidedStepTheme" format="reference" />
+
+ <!-- @hide
+ Theme attribute used to inspect theme inheritance. -->
+ <attr name="guidedStepThemeFlag" format="boolean" />
+
+ <!-- Theme attribute for the animation used when a guided step element is animated in on
+ fragment stack push. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedstep_slide_in_from_end}. -->
+ <attr name="guidedStepEntryAnimation" format="reference" />
+ <!-- Theme attribute for the animation used when a guided step element is animated out on
+ fragment stack push. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedstep_slide_out_to_start}. -->
+ <attr name="guidedStepExitAnimation" format="reference" />
+ <!-- Theme attribute for the animation used when a guided step element is animated in on
+ fragment stack pop. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedstep_slide_in_from_start}. -->
+ <attr name="guidedStepReentryAnimation" format="reference" />
+ <!-- Theme attribute for the animation used when a guided step element is animated out on
+ fragment stack pop. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedstep_slide_out_to_end}. -->
+ <attr name="guidedStepReturnAnimation" format="reference" />
+
+ <!-- Theme attribute for the animation used when the guidance is animated in at activity
+ start. Default is {@link android.support.v17.leanback.R.animator#lb_guidance_entry}.
+ -->
+ <attr name="guidanceEntryAnimation" format="reference" />
+ <!-- Theme attribute for the style of the main container in a GuidanceStylist. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidanceContainerStyle}.-->
+ <attr name="guidanceContainerStyle" format="reference" />
+ <!-- Theme attribute for the style of the title in a GuidanceStylist. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidanceTitleStyle}. -->
+ <attr name="guidanceTitleStyle" format="reference" />
+ <!-- Theme attribute for the style of the description in a GuidanceStylist. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidanceDescriptionStyle}. -->
+ <attr name="guidanceDescriptionStyle" format="reference" />
+ <!-- Theme attribute for the style of the breadcrumb in a GuidanceStylist. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidanceBreadcrumbStyle}. -->
+ <attr name="guidanceBreadcrumbStyle" format="reference" />
+ <!-- Theme attribute for the style of the icon in a GuidanceStylist. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidanceIconStyle}. -->
+ <attr name="guidanceIconStyle" format="reference" />
+
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when the actions
+ list is animated in at activity start. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_entry}. -->
+ <attr name="guidedActionsEntryAnimation" format="reference" />
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when the action
+ selector is animated in at activity start. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_selector_show}. -->
+ <attr name="guidedActionsSelectorShowAnimation" format="reference" />
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when the action
+ selector is animated in at activity start. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_selector_hide}. -->
+ <attr name="guidedActionsSelectorHideAnimation" format="reference" />
+ <!-- Theme attribute for the style of the container in a GuidedActionsPresenter. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionsContainerStyle}. -->
+ <attr name="guidedActionsContainerStyle" format="reference" />
+ <!-- Theme attribute for the style of the item selector in a GuidedActionsPresenter. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionsSelectorStyle}. -->
+ <attr name="guidedActionsSelectorStyle" format="reference" />
+ <!-- Theme attribute for the style of the list in a GuidedActionsPresenter. Default is
+ {@link android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionsListStyle}.-->
+ <attr name="guidedActionsListStyle" format="reference" />
+
+ <!-- Theme attribute for the style of the container of a single action in a
+ GuidedActionsPresenter. Default is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemContainerStyle}. -->
+ <attr name="guidedActionItemContainerStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's checkmark in a GuidedActionsPresenter.
+ Default is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemCheckmarkStyle}. -->
+ <attr name="guidedActionItemCheckmarkStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's icon in a GuidedActionsPresenter. Default
+ is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemIconStyle}. -->
+ <attr name="guidedActionItemIconStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's content in a GuidedActionsPresenter.
+ Default is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemContentStyle}. -->
+ <attr name="guidedActionItemContentStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's title in a GuidedActionsPresenter. Default
+ is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemTitleStyle}. -->
+ <attr name="guidedActionItemTitleStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's description in a GuidedActionsPresenter.
+ Default is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemDescriptionStyle}. -->
+ <attr name="guidedActionItemDescriptionStyle" format="reference" />
+ <!-- Theme attribute for the style of an action's chevron decoration in a
+ GuidedActionsPresenter. Default is {@link
+ android.support.v17.leanback.R.style#Widget_Leanback_GuidedActionItemChevronStyle}. -->
+ <attr name="guidedActionItemChevronStyle" format="reference" />
+
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when an action
+ is checked. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_item_checked}. -->
+ <attr name="guidedActionCheckedAnimation" format="reference" />
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when an action
+ is unchecked. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_item_unchecked}. -->
+ <attr name="guidedActionUncheckedAnimation" format="reference" />
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when an action
+ is pressed. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_item_pressed}. -->
+ <attr name="guidedActionPressedAnimation" format="reference" />
+ <!-- Theme attribute for the animation used in a GuidedActionsPresenter when an action
+ is unpressed. Default is {@link
+ android.support.v17.leanback.R.animator#lb_guidedactions_item_unpressed}. -->
+ <attr name="guidedActionUnpressedAnimation" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the alpha value of the chevron
+ decoration when its action is enabled. Default is {@link
+ android.support.v17.leanback.R.string#lb_guidedactions_item_enabled_chevron_alpha}. -->
+ <attr name="guidedActionEnabledChevronAlpha" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the alpha value of the chevron
+ decoration when its action is disabled. Default is {@link
+ android.support.v17.leanback.R.string#lb_guidedactions_item_disabled_chevron_alpha}. -->
+ <attr name="guidedActionDisabledChevronAlpha" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the width of the text area of
+ a single action when there is an icon present. Default is {@link
+ android.support.v17.leanback.R.dimen#lb_guidedactions_item_text_width}. -->
+ <attr name="guidedActionContentWidth" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the width of the text area of
+ a single action when there is no icon present. Default is {@link
+ android.support.v17.leanback.R.dimen#lb_guidedactions_item_text_width_no_icon}. -->
+ <attr name="guidedActionContentWidthNoIcon" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the max lines of the title text
+ view when the action's isMultilineDescription is set to false. Default is {@link
+ android.support.v17.leanback.R.integer#lb_guidedactions_item_title_min_lines}. -->
+ <attr name="guidedActionTitleMinLines" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the max lines of the title text
+ view when the action's isMultilineDescription is set to true. Default is {@link
+ android.support.v17.leanback.R.integer#lb_guidedactions_item_title_max_lines}. -->
+ <attr name="guidedActionTitleMaxLines" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the max lines of the title text
+ view when the action's isMultilineDescription is set to false. Default is {@link
+ android.support.v17.leanback.R.integer#lb_guidedactions_item_description_min_lines}. -->
+ <attr name="guidedActionDescriptionMinLines" format="reference" />
+ <!-- Theme attribute used in a GuidedActionsPresenter for the vertical padding between
+ action views in the list. Default is {@link
+ android.support.v17.leanback.R.dimen#lb_guidedactions_vertical_padding}. -->
+ <attr name="guidedActionVerticalPadding" format="reference" />
</declare-styleable>
diff --git a/v17/leanback/res/values/colors.xml b/v17/leanback/res/values/colors.xml
index ba65d2f..1549af9 100644
--- a/v17/leanback/res/values/colors.xml
+++ b/v17/leanback/res/values/colors.xml
@@ -65,4 +65,14 @@
<color name="lb_playback_controls_time_text_color">#B2EEEEEE</color>
<color name="lb_search_plate_hint_text_color">#FFCCCCCC</color>
+
+ <!-- GuidedStepFragment -->
+ <color name="lb_tv_white">#FFCCCCCC</color>
+
+ <!-- refactor naming here -->
+ <color name="lb_guidedactions_background">#FF111111</color>
+ <color name="lb_guidedactions_selector_color">#26FFFFFF</color>
+ <color name="lb_guidedactions_item_unselected_text_color">#FFF1F1F1</color>
+ <!-- end refactor naming -->
+
</resources>
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index 8c4c198..c248af7 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -206,4 +206,45 @@
<dimen name="lb_rounded_rect_corner_radius">2dp</dimen>
+ <!-- GuidedStepFragment -->
+ <dimen name="lb_guidedstep_guidance_section_width">576dp</dimen>
+ <dimen name="lb_guidedstep_slide_start_distance">-200dp</dimen>
+ <dimen name="lb_guidedstep_slide_end_distance">200dp</dimen>
+
+ <dimen name="lb_guidance_entry_translationX">-120dp</dimen>
+
+ <dimen name="lb_guidedactions_entry_translationX">384dp</dimen>
+ <dimen name="lb_guidedactions_section_width">384dp</dimen>
+ <dimen name="lb_guidedactions_section_width_with_shadow">400dp</dimen>
+ <dimen name="lb_guidedactions_elevation">12dp</dimen>
+ <dimen name="lb_guidedactions_selector_min_height">8dp</dimen>
+ <dimen name="lb_guidedactions_vertical_padding">12dp</dimen>
+
+ <item name="lb_guidedactions_item_unselected_text_alpha" format="float" type="string">1.00</item>
+ <item name="lb_guidedactions_item_unselected_description_text_alpha" format="float" type="string">0.50</item>
+ <item name="lb_guidedactions_item_enabled_chevron_alpha" format="float" type="string">1.00</item>
+ <item name="lb_guidedactions_item_disabled_chevron_alpha" format="float" type="string">0.50</item>
+
+ <dimen name="lb_guidedactions_item_text_width">248dp</dimen>
+ <dimen name="lb_guidedactions_item_text_width_no_icon">284dp</dimen>
+ <dimen name="lb_guidedactions_item_min_height">64dp</dimen>
+ <dimen name="lb_guidedactions_item_start_padding">20dp</dimen>
+ <dimen name="lb_guidedactions_item_end_padding">28dp</dimen>
+ <dimen name="lb_guidedactions_item_delimiter_padding">4dp</dimen>
+ <dimen name="lb_guidedactions_item_checkmark_diameter">8dp</dimen>
+ <dimen name="lb_guidedactions_item_icon_width">32dp</dimen>
+ <dimen name="lb_guidedactions_item_icon_height">32dp</dimen>
+ <dimen name="lb_guidedactions_item_title_font_size">18sp</dimen>
+ <dimen name="lb_guidedactions_item_description_font_size">12sp</dimen>
+
+ <integer name="lb_guidedstep_entry_animation_delay">550</integer>
+ <integer name="lb_guidedstep_entry_animation_duration">250</integer>
+
+ <integer name="lb_guidedactions_item_animation_duration">100</integer>
+ <integer name="lb_guidedactions_animation_duration">150</integer>
+ <integer name="lb_guidedactions_item_title_min_lines">1</integer>
+ <integer name="lb_guidedactions_item_title_max_lines">3</integer>
+ <integer name="lb_guidedactions_item_description_min_lines">2</integer>
+ <!-- end GuidedStepFragment -->
+
</resources>
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index 71c5122..3ee2821 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -290,4 +290,175 @@
<item name="closed_captioning">@drawable/lb_ic_cc</item>
</style>
+ <!-- Style for the main container view in a GuidanceStylist's default layout. -->
+ <style name="Widget.Leanback.GuidanceContainerStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:paddingStart">48dp</item>
+ <item name="android:paddingEnd">16dp</item>
+ <item name="android:clipToPadding">false</item>
+ </style>
+
+ <!-- Style for the title view in a GuidanceStylist's default layout. -->
+ <style name="Widget.Leanback.GuidanceTitleStyle">
+ <item name="android:layout_toStartOf">@id/guidance_icon</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignWithParentIfMissing">true</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:gravity">end</item>
+ <item name="android:maxLines">2</item>
+ <item name="android:paddingBottom">4dp</item>
+ <item name="android:paddingTop">2dp</item>
+ <item name="android:textColor">#FFF1F1F1</item>
+ <item name="android:textSize">36sp</item>
+ </style>
+
+ <!-- Style for the description view in a GuidanceStylist's default layout. -->
+ <style name="Widget.Leanback.GuidanceDescriptionStyle">
+ <item name="android:layout_below">@id/guidance_title</item>
+ <item name="android:layout_toStartOf">@id/guidance_icon</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignWithParentIfMissing">true</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:fontFamily">sans-serif</item>
+ <item name="android:gravity">end</item>
+ <item name="android:maxLines">6</item>
+ <item name="android:textColor">#88F1F1F1</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:lineSpacingExtra">3dp</item>
+ </style>
+
+ <!-- Style for the breadcrumb view in a GuidanceStylist's default layout. -->
+ <style name="Widget.Leanback.GuidanceBreadcrumbStyle">
+ <item name="android:layout_above">@id/guidance_title</item>
+ <item name="android:layout_toStartOf">@id/guidance_icon</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignWithParentIfMissing">true</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textColor">#88F1F1F1</item>
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <!-- Style for the icon view in a GuidanceStylist's default layout. -->
+ <style name="Widget.Leanback.GuidanceIconStyle">
+ <item name="android:layout_width">140dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_alignParentEnd">true</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:layout_marginStart">16dp</item>
+ <item name="android:maxHeight">280dp</item>
+ <item name="android:scaleType">fitCenter</item>
+ </style>
+
+ <!-- Style for the container view in a GuidedActionsStylist's default layout. -->
+ <style name="Widget.Leanback.GuidedActionsContainerStyle">
+ <item name="android:layout_width">@dimen/lb_guidedactions_section_width</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_alignParentEnd">true</item>
+ <item name="android:background">@color/lb_guidedactions_background</item>
+ <item name="android:elevation">@dimen/lb_guidedactions_elevation</item>
+ </style>
+
+ <!-- Style for the selector view in a GuidedActionsStylist's default layout. -->
+ <style name="Widget.Leanback.GuidedActionsSelectorStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/lb_guidedactions_selector_min_height</item>
+ <item name="android:layout_centerVertical">true</item>
+ <item name="android:alpha">0</item>
+ <item name="android:background">@color/lb_guidedactions_selector_color</item>
+ </style>
+
+ <!-- Style for the vertical grid of actions in a GuidedActionsStylist's default layout. -->
+ <style name="Widget.Leanback.GuidedActionsListStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+
+ <!-- Style for an action's container in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemContainerStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:focusable">true</item>
+ <item name="android:minHeight">@dimen/lb_guidedactions_item_min_height</item>
+ <item name="android:paddingBottom">@dimen/lb_guidedactions_vertical_padding</item>
+ <item name="android:paddingStart">@dimen/lb_guidedactions_item_start_padding</item>
+ <item name="android:paddingEnd">@dimen/lb_guidedactions_item_end_padding</item>
+ <item name="android:paddingTop">@dimen/lb_guidedactions_vertical_padding</item>
+ </style>
+
+ <!-- Style for an action's checkmark in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemCheckmarkStyle">
+ <item name="android:layout_width">@dimen/lb_guidedactions_item_checkmark_diameter</item>
+ <item name="android:layout_height">@dimen/lb_guidedactions_item_checkmark_diameter</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:layout_marginEnd">@dimen/lb_guidedactions_item_delimiter_padding</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:src">@drawable/lb_guidedactions_item_checkmark</item>
+ <item name="android:visibility">invisible</item>
+ </style>
+
+ <!-- Style for an action's icon in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemIconStyle">
+ <item name="android:layout_width">@dimen/lb_guidedactions_item_icon_width</item>
+ <item name="android:layout_height">@dimen/lb_guidedactions_item_icon_height</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:layout_marginEnd">@dimen/lb_guidedactions_item_delimiter_padding</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:visibility">gone</item>
+ </style>
+
+ <!-- Style for an action's text content in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemContentStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">start|center_vertical</item>
+ <item name="android:layout_weight">1</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <!-- Style for an action's title in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemTitleStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:alpha">@string/lb_guidedactions_item_unselected_text_alpha</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:maxLines">@integer/lb_guidedactions_item_title_min_lines</item>
+ <item name="android:textColor">@color/lb_guidedactions_item_unselected_text_color</item>
+ <item name="android:textSize">@dimen/lb_guidedactions_item_title_font_size</item>
+ </style>
+
+ <!-- Style for an action's description in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemDescriptionStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:alpha">@string/lb_guidedactions_item_unselected_description_text_alpha</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:maxLines">@integer/lb_guidedactions_item_description_min_lines</item>
+ <item name="android:textColor">@color/lb_guidedactions_item_unselected_text_color</item>
+ <item name="android:textSize">@dimen/lb_guidedactions_item_description_font_size</item>
+ <item name="android:visibility">gone</item>
+ </style>
+
+ <!-- Style for an action's chevron in a GuidedActionsStylist's default item layout. -->
+ <style name="Widget.Leanback.GuidedActionItemChevronStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:layout_marginStart">@dimen/lb_guidedactions_item_delimiter_padding</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="android:src">@drawable/lb_ic_guidedactions_item_chevron</item>
+ <item name="android:visibility">gone</item>
+ </style>
+
</resources>
diff --git a/v17/leanback/res/values/themes.xml b/v17/leanback/res/values/themes.xml
index 457798b..cd331e9 100644
--- a/v17/leanback/res/values/themes.xml
+++ b/v17/leanback/res/values/themes.xml
@@ -22,12 +22,13 @@
<item name="playbackProgressPrimaryColor">@color/lb_playback_progress_color_no_theme</item>
<item name="playbackControlsIconHighlightColor">@color/lb_playback_icon_highlight_no_theme</item>
<item name="defaultBrandColor">@color/lb_default_brand_color</item>
+
+ <item name="android:windowOverscan">true</item>
+ <item name="guidedStepTheme">@style/Theme.Leanback.GuidedStep</item>
</style>
<style name="Theme.Leanback" parent="Theme.LeanbackBase">
- <item name="android:windowOverscan">true</item>
-
<item name="baseCardViewStyle">@style/Widget.Leanback.BaseCardViewStyle</item>
<item name="imageCardViewStyle">@style/Widget.Leanback.ImageCardViewStyle</item>
@@ -86,6 +87,7 @@
<item name="overlayDimMaskColor">@color/lb_view_dim_mask_color</item>
<item name="overlayDimActiveLevel">@fraction/lb_view_active_level</item>
<item name="overlayDimDimmedLevel">@fraction/lb_view_dimmed_level</item>
+
</style>
<style name="Theme.Leanback.Browse" parent="Theme.Leanback">
@@ -100,4 +102,48 @@
<item name="android:windowSharedElementReturnTransition">@transition/lb_shared_element_return_transition</item>
</style>
+ <style name="Theme.Leanback.GuidedStep" parent="Theme.LeanbackBase">
+ <item name="guidedStepThemeFlag">true</item>
+
+ <item name="guidedStepEntryAnimation">@animator/lb_guidedstep_slide_in_from_end</item>
+ <item name="guidedStepExitAnimation">@animator/lb_guidedstep_slide_out_to_start</item>
+ <item name="guidedStepReentryAnimation">@animator/lb_guidedstep_slide_in_from_start</item>
+ <item name="guidedStepReturnAnimation">@animator/lb_guidedstep_slide_out_to_end</item>
+ <item name="guidanceEntryAnimation">@animator/lb_guidance_entry</item>
+ <item name="guidedActionsEntryAnimation">@animator/lb_guidedactions_entry</item>
+
+ <item name="guidanceContainerStyle">@style/Widget.Leanback.GuidanceContainerStyle</item>
+ <item name="guidanceIconStyle">@style/Widget.Leanback.GuidanceIconStyle</item>
+ <item name="guidanceTitleStyle">@style/Widget.Leanback.GuidanceTitleStyle</item>
+ <item name="guidanceBreadcrumbStyle">@style/Widget.Leanback.GuidanceBreadcrumbStyle</item>
+ <item name="guidanceDescriptionStyle">@style/Widget.Leanback.GuidanceDescriptionStyle</item>
+
+ <item name="guidedActionsContainerStyle">@style/Widget.Leanback.GuidedActionsContainerStyle</item>
+ <item name="guidedActionsSelectorStyle">@style/Widget.Leanback.GuidedActionsSelectorStyle</item>
+ <item name="guidedActionsListStyle">@style/Widget.Leanback.GuidedActionsListStyle</item>
+ <item name="guidedActionsSelectorShowAnimation">@animator/lb_guidedactions_selector_show</item>
+ <item name="guidedActionsSelectorHideAnimation">@animator/lb_guidedactions_selector_hide</item>
+
+ <item name="guidedActionItemContainerStyle">@style/Widget.Leanback.GuidedActionItemContainerStyle</item>
+ <item name="guidedActionItemCheckmarkStyle">@style/Widget.Leanback.GuidedActionItemCheckmarkStyle</item>
+ <item name="guidedActionItemIconStyle">@style/Widget.Leanback.GuidedActionItemIconStyle</item>
+ <item name="guidedActionItemContentStyle">@style/Widget.Leanback.GuidedActionItemContentStyle</item>
+ <item name="guidedActionItemTitleStyle">@style/Widget.Leanback.GuidedActionItemTitleStyle</item>
+ <item name="guidedActionItemDescriptionStyle">@style/Widget.Leanback.GuidedActionItemDescriptionStyle</item>
+ <item name="guidedActionItemChevronStyle">@style/Widget.Leanback.GuidedActionItemChevronStyle</item>
+
+ <item name="guidedActionCheckedAnimation">@animator/lb_guidedactions_item_checked</item>
+ <item name="guidedActionUncheckedAnimation">@animator/lb_guidedactions_item_unchecked</item>
+ <item name="guidedActionPressedAnimation">@animator/lb_guidedactions_item_pressed</item>
+ <item name="guidedActionUnpressedAnimation">@animator/lb_guidedactions_item_unpressed</item>
+ <item name="guidedActionEnabledChevronAlpha">@string/lb_guidedactions_item_enabled_chevron_alpha</item>
+ <item name="guidedActionDisabledChevronAlpha">@string/lb_guidedactions_item_disabled_chevron_alpha</item>
+ <item name="guidedActionContentWidth">@dimen/lb_guidedactions_item_text_width</item>
+ <item name="guidedActionContentWidthNoIcon">@dimen/lb_guidedactions_item_text_width_no_icon</item>
+ <item name="guidedActionTitleMinLines">@integer/lb_guidedactions_item_title_min_lines</item>
+ <item name="guidedActionTitleMaxLines">@integer/lb_guidedactions_item_title_max_lines</item>
+ <item name="guidedActionDescriptionMinLines">@integer/lb_guidedactions_item_description_min_lines</item>
+ <item name="guidedActionVerticalPadding">@dimen/lb_guidedactions_vertical_padding</item>
+ </style>
+
</resources>
diff --git a/v17/leanback/src/android/support/v17/leanback/animation/UntargetableAnimatorSet.java b/v17/leanback/src/android/support/v17/leanback/animation/UntargetableAnimatorSet.java
new file mode 100644
index 0000000..9cdbc0c
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/animation/UntargetableAnimatorSet.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.animation;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.TimeInterpolator;
+
+import java.util.ArrayList;
+
+/**
+ * Custom fragment animations supplied by Fragment.onCreateAnimator have their targets set to the
+ * fragment's main view by the fragment manager. Sometimes, this isn't what you want; you may be
+ * supplying a heterogeneous collection of animations that already have targets. This class helps
+ * you return such a collection of animations from onCreateAnimator without having their targets
+ * reset.
+ *
+ * Note that one does not simply subclass AnimatorSet and override setTarget() because AnimatorSet
+ * is final.
+ * @hide
+ */
+public class UntargetableAnimatorSet extends Animator {
+
+ private final AnimatorSet mAnimatorSet;
+
+ public UntargetableAnimatorSet(AnimatorSet animatorSet) {
+ mAnimatorSet = animatorSet;
+ }
+
+ @Override
+ public void addListener(Animator.AnimatorListener listener) {
+ mAnimatorSet.addListener(listener);
+ }
+
+ @Override
+ public void cancel() {
+ mAnimatorSet.cancel();
+ }
+
+ @Override
+ public Animator clone() {
+ return mAnimatorSet.clone();
+ }
+
+ @Override
+ public void end() {
+ mAnimatorSet.end();
+ }
+
+ @Override
+ public long getDuration() {
+ return mAnimatorSet.getDuration();
+ }
+
+ @Override
+ public ArrayList<Animator.AnimatorListener> getListeners() {
+ return mAnimatorSet.getListeners();
+ }
+
+ @Override
+ public long getStartDelay() {
+ return mAnimatorSet.getStartDelay();
+ }
+
+ @Override
+ public boolean isRunning() {
+ return mAnimatorSet.isRunning();
+ }
+
+ @Override
+ public boolean isStarted() {
+ return mAnimatorSet.isStarted();
+ }
+
+ @Override
+ public void removeAllListeners() {
+ mAnimatorSet.removeAllListeners();
+ }
+
+ @Override
+ public void removeListener(Animator.AnimatorListener listener) {
+ mAnimatorSet.removeListener(listener);
+ }
+
+ @Override
+ public Animator setDuration(long duration) {
+ return mAnimatorSet.setDuration(duration);
+ }
+
+ @Override
+ public void setInterpolator(TimeInterpolator value) {
+ mAnimatorSet.setInterpolator(value);
+ }
+
+ @Override
+ public void setStartDelay(long startDelay) {
+ mAnimatorSet.setStartDelay(startDelay);
+ }
+
+ @Override
+ public void setTarget(Object target) {
+ // ignore
+ }
+
+ @Override
+ public void setupEndValues() {
+ mAnimatorSet.setupEndValues();
+ }
+
+ @Override
+ public void setupStartValues() {
+ mAnimatorSet.setupStartValues();
+ }
+
+ @Override
+ public void start() {
+ mAnimatorSet.start();
+ }
+}
+
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
index c6faa18..5965ba6 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
@@ -15,6 +15,7 @@
import java.lang.ref.WeakReference;
+import android.graphics.PixelFormat;
import android.support.v17.leanback.R;
import android.animation.Animator;
import android.animation.ValueAnimator;
@@ -161,7 +162,7 @@
@Override
public int getOpacity() {
- return android.graphics.PixelFormat.OPAQUE;
+ return android.graphics.PixelFormat.TRANSLUCENT;
}
@Override
@@ -184,30 +185,136 @@
}
private static class DrawableWrapper {
- protected int mAlpha;
- protected Drawable mDrawable;
+ private int mAlpha = FULL_ALPHA;
+ private Drawable mDrawable;
public DrawableWrapper(Drawable drawable) {
mDrawable = drawable;
- setAlpha(FULL_ALPHA);
+ updateAlpha();
+ }
+ public DrawableWrapper(DrawableWrapper wrapper, Drawable drawable) {
+ mDrawable = drawable;
+ mAlpha = wrapper.getAlpha();
+ updateAlpha();
}
public Drawable getDrawable() {
return mDrawable;
}
public void setAlpha(int alpha) {
mAlpha = alpha;
- mDrawable.setAlpha(alpha);
+ updateAlpha();
}
public int getAlpha() {
return mAlpha;
}
+ private void updateAlpha() {
+ mDrawable.setAlpha(mAlpha);
+ }
public void setColor(int color) {
((ColorDrawable) mDrawable).setColor(color);
}
}
- private LayerDrawable mLayerDrawable;
- private DrawableWrapper mLayerWrapper;
+ private static class TranslucentLayerDrawable extends LayerDrawable {
+ private DrawableWrapper[] mWrapper;
+ private Paint mPaint = new Paint();
+
+ public static TranslucentLayerDrawable create(LayerDrawable layerDrawable) {
+ int numChildren = layerDrawable.getNumberOfLayers();
+ Drawable[] drawables = new Drawable[numChildren];
+ for (int i = 0; i < numChildren; i++) {
+ drawables[i] = layerDrawable.getDrawable(i);
+ }
+ TranslucentLayerDrawable result = new TranslucentLayerDrawable(drawables);
+ for (int i = 0; i < numChildren; i++) {
+ result.setId(i, layerDrawable.getId(i));
+ }
+ return result;
+ }
+
+ public TranslucentLayerDrawable(Drawable[] drawables) {
+ super(drawables);
+ int count = drawables.length;
+ mWrapper = new DrawableWrapper[count];
+ for (int i = 0; i < count; i++) {
+ mWrapper[i] = new DrawableWrapper(drawables[i]);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mPaint.getAlpha() != alpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public Drawable mutate() {
+ Drawable drawable = super.mutate();
+ int count = getNumberOfLayers();
+ for (int i = 0; i < count; i++) {
+ if (mWrapper[i] != null) {
+ mWrapper[i] = new DrawableWrapper(mWrapper[i], getDrawable(i));
+ }
+ }
+ return drawable;
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public boolean setDrawableByLayerId(int id, Drawable drawable) {
+ return updateDrawable(id, drawable) != null;
+ }
+
+ public DrawableWrapper updateDrawable(int id, Drawable drawable) {
+ super.setDrawableByLayerId(id, drawable);
+ for (int i = 0; i < getNumberOfLayers(); i++) {
+ if (getId(i) == id) {
+ mWrapper[i] = new DrawableWrapper(drawable);
+ return mWrapper[i];
+ }
+ }
+ return null;
+ }
+
+ public void clearDrawable(int id, Context context) {
+ for (int i = 0; i < getNumberOfLayers(); i++) {
+ if (getId(i) == id) {
+ mWrapper[i] = null;
+ super.setDrawableByLayerId(id, createEmptyDrawable(context));
+ break;
+ }
+ }
+ }
+
+ public DrawableWrapper findWrapperById(int id) {
+ for (int i = 0; i < getNumberOfLayers(); i++) {
+ if (getId(i) == id) {
+ return mWrapper[i];
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mPaint.getAlpha() < FULL_ALPHA) {
+ canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(),
+ mPaint, Canvas.ALL_SAVE_FLAG);
+ }
+ super.draw(canvas);
+ if (mPaint.getAlpha() < FULL_ALPHA) {
+ canvas.restore();
+ }
+ }
+ }
+
+ private TranslucentLayerDrawable mLayerDrawable;
private DrawableWrapper mImageInWrapper;
private DrawableWrapper mImageOutWrapper;
private DrawableWrapper mColorWrapper;
@@ -225,12 +332,16 @@
}
@Override
public void onAnimationEnd(Animator animation) {
+ // mImageInWrapper should be full alpha, but mImageOutWrapper may not be 0 alpha
+ if (mImageOutWrapper != null) {
+ mImageOutWrapper.setAlpha(0);
+ mImageOutWrapper = null;
+ }
if (mChangeRunnable != null) {
long delayMs = getRunnableDelay();
if (DEBUG) Log.v(TAG, "animation ended, found change runnable delayMs " + delayMs);
mHandler.postDelayed(mChangeRunnable, delayMs);
}
- mImageOutWrapper = null;
}
@Override
public void onAnimationCancel(Animator animation) {
@@ -244,6 +355,8 @@
int fadeInAlpha = (Integer) animation.getAnimatedValue();
if (mImageInWrapper != null) {
mImageInWrapper.setAlpha(fadeInAlpha);
+ } else if (mImageOutWrapper != null) {
+ mImageOutWrapper.setAlpha(255 - fadeInAlpha);
}
}
};
@@ -329,7 +442,7 @@
drawable = mService.getThemeDrawable(mContext, mThemeDrawableResourceId);
}
if (drawable == null) {
- drawable = createEmptyDrawable();
+ drawable = createEmptyDrawable(mContext);
}
return drawable;
}
@@ -479,18 +592,15 @@
return;
}
- mLayerDrawable = (LayerDrawable) ContextCompat.getDrawable(mContext,
- R.drawable.lb_background).mutate();
+ LayerDrawable layerDrawable = (LayerDrawable)
+ ContextCompat.getDrawable(mContext, R.drawable.lb_background).mutate();
+ mLayerDrawable = TranslucentLayerDrawable.create(layerDrawable);
mBgView.setBackground(mLayerDrawable);
- mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
+ mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
+ mLayerDrawable.updateDrawable(R.id.background_theme, getThemeDrawable());
- mLayerDrawable.setDrawableByLayerId(R.id.background_theme, getThemeDrawable());
-
- mLayerWrapper = new DrawableWrapper(mLayerDrawable);
-
- mColorWrapper = new DrawableWrapper(
- mLayerDrawable.findDrawableByLayerId(R.id.background_color));
+ mColorWrapper = mLayerDrawable.findWrapperById(R.id.background_color);
updateDimWrapper();
}
@@ -499,11 +609,10 @@
if (mDimDrawable == null) {
mDimDrawable = getDefaultDimLayer();
}
- mDimWrapper = new DrawableWrapper(mDimDrawable.getConstantState().newDrawable(
- mContext.getResources()).mutate());
- if (mLayerDrawable != null) {
- mLayerDrawable.setDrawableByLayerId(R.id.background_dim, mDimWrapper.getDrawable());
- }
+ Drawable dimDrawable = mDimDrawable.getConstantState().newDrawable(
+ mContext.getResources()).mutate();
+ mDimWrapper = mLayerDrawable == null ? null : mLayerDrawable.updateDrawable(
+ R.id.background_dim, dimDrawable);
}
/**
@@ -589,12 +698,11 @@
public void release() {
if (DEBUG) Log.v(TAG, "release " + this);
if (mLayerDrawable != null) {
- mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
- mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
- mLayerDrawable.setDrawableByLayerId(R.id.background_theme, createEmptyDrawable());
+ mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
+ mLayerDrawable.clearDrawable(R.id.background_imageout, mContext);
+ mLayerDrawable.clearDrawable(R.id.background_theme, mContext);
mLayerDrawable = null;
}
- mLayerWrapper = null;
mImageInWrapper = null;
mImageOutWrapper = null;
mColorWrapper = null;
@@ -642,11 +750,11 @@
showWallpaper(mBackgroundColor == Color.TRANSPARENT);
if (mBackgroundDrawable == null) {
- mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
+ mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
} else {
if (DEBUG) Log.v(TAG, "Background drawable is available");
- mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
- mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
+ mImageInWrapper = mLayerDrawable.updateDrawable(
+ R.id.background_imagein, mBackgroundDrawable);
if (mDimWrapper != null) {
mDimWrapper.setAlpha(FULL_ALPHA);
}
@@ -693,6 +801,14 @@
}
mHandler.removeCallbacks(mChangeRunnable);
}
+
+ if (mLayerDrawable == null) {
+ if (DEBUG) Log.v(TAG, "setDrawableInternal while in released state");
+ mBackgroundDrawable = drawable;
+ updateImmediate();
+ return;
+ }
+
mChangeRunnable = new ChangeBackgroundRunnable(drawable);
if (mAnimator.isStarted()) {
@@ -761,7 +877,7 @@
}
private void applyBackgroundChanges() {
- if (!mAttached || mLayerWrapper == null) {
+ if (!mAttached) {
return;
}
@@ -775,8 +891,8 @@
if (mImageInWrapper == null && mBackgroundDrawable != null) {
if (DEBUG) Log.v(TAG, "creating new imagein drawable");
- mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
- mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
+ mImageInWrapper = mLayerDrawable.updateDrawable(
+ R.id.background_imagein, mBackgroundDrawable);
if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting");
mImageInWrapper.setAlpha(0);
dimAlpha = FULL_ALPHA;
@@ -842,7 +958,10 @@
}
private void runTask() {
- lazyInit();
+ if (mLayerDrawable == null) {
+ if (DEBUG) Log.v(TAG, "runTask while released - should not happen");
+ return;
+ }
if (sameDrawable(mDrawable, mBackgroundDrawable)) {
if (DEBUG) Log.v(TAG, "new drawable same as current");
@@ -853,14 +972,11 @@
if (mImageInWrapper != null) {
if (DEBUG) Log.v(TAG, "moving image in to image out");
- mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable());
- mImageOutWrapper.setAlpha(FULL_ALPHA);
-
// Order is important! Setting a drawable "removes" the
// previous one from the view
- mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
- mLayerDrawable.setDrawableByLayerId(R.id.background_imageout,
- mImageOutWrapper.getDrawable());
+ mLayerDrawable.clearDrawable(R.id.background_imagein, mContext);
+ mImageOutWrapper = mLayerDrawable.updateDrawable(R.id.background_imageout,
+ mImageInWrapper.getDrawable());
mImageInWrapper = null;
}
@@ -871,9 +987,9 @@
}
}
- private Drawable createEmptyDrawable() {
+ private static Drawable createEmptyDrawable(Context context) {
Bitmap bitmap = null;
- return new BitmapDrawable(mContext.getResources(), bitmap);
+ return new BitmapDrawable(context.getResources(), bitmap);
}
private void showWallpaper(boolean show) {
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedActionAdapter.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedActionAdapter.java
new file mode 100644
index 0000000..e9c6d37
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedActionAdapter.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.media.AudioManager;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
+ * Presentation (view creation and state animation) is delegated to a {@link
+ * GuidedActionsStylist}, while clients are notified of interactions via
+ * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
+ */
+class GuidedActionAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "GuidedActionAdapter";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Object listening for click events within a {@link GuidedActionAdapter}.
+ */
+ public interface ClickListener {
+
+ /**
+ * Called when the user clicks on an action.
+ */
+ public void onGuidedActionClicked(GuidedAction action);
+ }
+
+ /**
+ * Object listening for focus events within a {@link GuidedActionAdapter}.
+ */
+ public interface FocusListener {
+
+ /**
+ * Called when the user focuses on an action.
+ */
+ public void onGuidedActionFocused(GuidedAction action);
+ }
+
+ /**
+ * View holder containing a {@link GuidedAction}.
+ */
+ private static class ActionViewHolder extends ViewHolder {
+
+ private final GuidedActionsStylist.ViewHolder mStylistViewHolder;
+ private GuidedAction mAction;
+
+ /**
+ * Constructs a view holder that can be associated with a GuidedAction.
+ */
+ public ActionViewHolder(View v, GuidedActionsStylist.ViewHolder subViewHolder) {
+ super(v);
+ mStylistViewHolder = subViewHolder;
+ }
+
+ /**
+ * Retrieves the action associated with this view holder.
+ * @return The GuidedAction associated with this view holder.
+ */
+ public GuidedAction getAction() {
+ return mAction;
+ }
+
+ /**
+ * Sets the action associated with this view holder.
+ * @param action The GuidedAction associated with this view holder.
+ */
+ public void setAction(GuidedAction action) {
+ mAction = action;
+ }
+ }
+
+ private RecyclerView mRecyclerView;
+ private final ActionOnKeyListener mActionOnKeyListener;
+ private final ActionOnFocusListener mActionOnFocusListener;
+ private final List<GuidedAction> mActions;
+ private ClickListener mClickListener;
+ private GuidedActionsStylist mStylist;
+ private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (v != null && v.getWindowToken() != null && mClickListener != null) {
+ ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
+ GuidedAction action = avh.getAction();
+ if (action.isEnabled() && !action.infoOnly()) {
+ mClickListener.onGuidedActionClicked(action);
+ }
+ }
+ }
+ };
+
+ /**
+ * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
+ * focus listeners, and the given presenter.
+ * @param actions The list of guided actions this adapter will manage.
+ * @param clickListener The click listener for items in this adapter.
+ * @param focusListener The focus listener for items in this adapter.
+ * @param presenter The presenter that will manage the display of items in this adapter.
+ */
+ public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
+ FocusListener focusListener, GuidedActionsStylist presenter) {
+ super();
+ mActions = new ArrayList<GuidedAction>(actions);
+ mClickListener = clickListener;
+ mStylist = presenter;
+ mActionOnKeyListener = new ActionOnKeyListener(clickListener, mActions);
+ mActionOnFocusListener = new ActionOnFocusListener(focusListener);
+ }
+
+ /**
+ * Sets the list of actions managed by this adapter.
+ * @param actions The list of actions to be managed.
+ */
+ public void setActions(List<GuidedAction> actions) {
+ mActionOnFocusListener.unFocus();
+ mActions.clear();
+ mActions.addAll(actions);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Returns the count of actions managed by this adapter.
+ * @return The count of actions managed by this adapter.
+ */
+ public int getCount() {
+ return mActions.size();
+ }
+
+ /**
+ * Returns the GuidedAction at the given position in the managed list.
+ * @param position The position of the desired GuidedAction.
+ * @return The GuidedAction at the given position.
+ */
+ public GuidedAction getItem(int position) {
+ return mActions.get(position);
+ }
+
+ /**
+ * Sets the click listener for items managed by this adapter.
+ * @param clickListener The click listener for this adapter.
+ */
+ public void setClickListener(ClickListener clickListener) {
+ mClickListener = clickListener;
+ mActionOnKeyListener.setListener(clickListener);
+ }
+
+ /**
+ * Sets the focus listener for items managed by this adapter.
+ * @param focusListener The focus listener for this adapter.
+ */
+ public void setFocusListener(FocusListener focusListener) {
+ mActionOnFocusListener.setFocusListener(focusListener);
+ }
+
+ /**
+ * Used for serialization only.
+ * @hide
+ */
+ public List<GuidedAction> getActions() {
+ return new ArrayList<GuidedAction>(mActions);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onAttachedToRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent);
+ View v = vh.view;
+ v.setOnKeyListener(mActionOnKeyListener);
+ v.setOnClickListener(mOnClickListener);
+ v.setOnFocusChangeListener(mActionOnFocusListener);
+
+ return new ActionViewHolder(v, vh);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ if (position >= mActions.size()) {
+ return;
+ }
+ ActionViewHolder avh = (ActionViewHolder)holder;
+ GuidedAction action = mActions.get(position);
+ avh.setAction(action);
+ mStylist.onBindViewHolder(avh.mStylistViewHolder, action);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getItemCount() {
+ return mActions.size();
+ }
+
+ private class ActionOnFocusListener implements View.OnFocusChangeListener {
+
+ private FocusListener mFocusListener;
+ private View mSelectedView;
+
+ ActionOnFocusListener(FocusListener focusListener) {
+ mFocusListener = focusListener;
+ }
+
+ public void setFocusListener(FocusListener focusListener) {
+ mFocusListener = focusListener;
+ }
+
+ public void unFocus() {
+ if (mSelectedView != null) {
+ ViewHolder vh = mRecyclerView.getChildViewHolder(mSelectedView);
+ if (vh != null) {
+ ActionViewHolder avh = (ActionViewHolder)vh;
+ mStylist.onAnimateItemFocused(avh.mStylistViewHolder, false);
+ } else {
+ Log.w(TAG, "RecyclerView returned null view holder",
+ new Throwable());
+ }
+ }
+ }
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
+ mStylist.onAnimateItemFocused(avh.mStylistViewHolder, hasFocus);
+ if (hasFocus) {
+ mSelectedView = v;
+ if (mFocusListener != null) {
+ // We still call onGuidedActionFocused so that listeners can clear
+ // state if they want.
+ mFocusListener.onGuidedActionFocused(avh.getAction());
+ }
+ } else {
+ if (mSelectedView == v) {
+ mSelectedView = null;
+ }
+ }
+ }
+ }
+
+ private class ActionOnKeyListener implements View.OnKeyListener {
+
+ private final List<GuidedAction> mActions;
+ private boolean mKeyPressed = false;
+ private ClickListener mClickListener;
+
+ public ActionOnKeyListener(ClickListener listener,
+ List<GuidedAction> actions) {
+ mClickListener = listener;
+ mActions = actions;
+ }
+
+ public void setListener(ClickListener listener) {
+ mClickListener = listener;
+ }
+
+ private void playSound(View v, int soundEffect) {
+ if (v.isSoundEffectsEnabled()) {
+ Context ctx = v.getContext();
+ AudioManager manager = (AudioManager)ctx.getSystemService(Context.AUDIO_SERVICE);
+ manager.playSoundEffect(soundEffect);
+ }
+ }
+
+ /**
+ * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
+ */
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (v == null || event == null) {
+ return false;
+ }
+ boolean handled = false;
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_NUMPAD_ENTER:
+ case KeyEvent.KEYCODE_BUTTON_X:
+ case KeyEvent.KEYCODE_BUTTON_Y:
+ case KeyEvent.KEYCODE_ENTER:
+
+ ActionViewHolder avh = (ActionViewHolder)mRecyclerView.getChildViewHolder(v);
+ GuidedAction action = avh.getAction();
+
+ if (!action.isEnabled() || action.infoOnly()) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ // TODO: requires API 19
+ //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
+ }
+ return true;
+ }
+
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (!mKeyPressed) {
+ mKeyPressed = true;
+
+ playSound(v, AudioManager.FX_KEY_CLICK);
+
+ if (DEBUG) {
+ Log.d(TAG, "Enter Key down");
+ }
+
+ mStylist.onAnimateItemPressed(avh.mStylistViewHolder,
+ mKeyPressed);
+ handled = true;
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ if (mKeyPressed) {
+ mKeyPressed = false;
+
+ if (DEBUG) {
+ Log.d(TAG, "Enter Key up");
+ }
+
+ mStylist.onAnimateItemPressed(avh.mStylistViewHolder,
+ mKeyPressed);
+ handleCheckedActions(avh, action);
+ mClickListener.onGuidedActionClicked(action);
+ handled = true;
+ }
+ break;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+ return handled;
+ }
+
+ private void handleCheckedActions(ActionViewHolder avh, GuidedAction action) {
+ int actionCheckSetId = action.getCheckSetId();
+ if (actionCheckSetId != GuidedAction.NO_CHECK_SET) {
+ // Find any actions that are checked and are in the same group
+ // as the selected action. Fade their checkmarks out.
+ for (int i = 0, size = mActions.size(); i < size; i++) {
+ GuidedAction a = mActions.get(i);
+ if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
+ a.setChecked(false);
+ ViewHolder vh = mRecyclerView.findViewHolderForPosition(i);
+ if (vh != null) {
+ GuidedActionsStylist.ViewHolder subViewHolder =
+ ((ActionViewHolder)avh).mStylistViewHolder;
+ mStylist.onAnimateItemChecked(subViewHolder, false);
+ }
+ }
+ }
+
+ // If we we'ren't already checked, fade our checkmark in.
+ if (!action.isChecked()) {
+ action.setChecked(true);
+ mStylist.onAnimateItemChecked(avh.mStylistViewHolder, true);
+ }
+ }
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
new file mode 100644
index 0000000..bb40e2e
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/GuidedStepFragment.java
@@ -0,0 +1,531 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.animation.UntargetableAnimatorSet;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.GuidanceStylist;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A GuidedStepFragment is used to guide the user through a decision or series of decisions.
+ * It is composed of a guidance view on the left, a view on the right containing a list of possible
+ * actions, and an optional view in the center to allow for additional configuration.
+ * <p>
+ * <h3>Basic Usage</h3>
+ * <p>
+ * Clients of GuidedStepFragment typically create a custom subclass to attach to their Activities.
+ * This custom subclass provides the information necessary to construct the user interface and
+ * respond to user actions. At a minimum, subclasses should override:
+ * <ul>
+ * <li>{@link #onCreateGuidance}, to provide instructions to the user</li>
+ * <li>{@link #onCreateActions}, to provide a set of {@link GuidedAction}s the user can take</li>
+ * <li>{@link #onGuidedActionClicked}, to respond to those actions</li>
+ * </ul>
+ * <p>
+ * <h3>Theming and Stylists</h3>
+ * <p>
+ * GuidedStepFragment delegates its visual styling to classes called stylists. The {@link
+ * GuidanceStylist} is responsible for the left guidance view, while the {@link
+ * GuidedActionsStylist} is responsible for the right actions view. The stylists use theme
+ * attributes to derive values associated with the presentation, such as colors, animations, etc.
+ * Most simple visual aspects of GuidanceStylist and GuidedActionsStylist can be customized
+ * via theming; see their documentation for more information.
+ * <p>
+ * GuidedStepFragments must have access to an appropriate theme in order for the stylists to
+ * function properly. Specifically, the fragment must receive {@link
+ * android.support.v17.leanback.R.style#Theme_Leanback_GuidedStep}, or a theme whose parent is
+ * is set to that theme. Themes can be provided in one of three ways:
+ * <ul>
+ * <li>The simplest way is to set the theme for the host Activity to the GuidedStep theme or a
+ * theme that derives from it.</li>
+ * <li>If the Activity already has a theme and setting its parent theme is inconvenient, the
+ * existing Activity theme can have an entry added for the attribute {@link
+ * android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme}. If present,
+ * this theme will be used by GuidedStepFragment as an overlay to the Activity's theme.</li>
+ * <li>Finally, custom subclasses of GuidedStepFragment may provide a theme through the {@link
+ * #onProvideTheme} method. This can be useful if a subclass is used across multiple
+ * Activities.</li>
+ * </ul>
+ * <p>
+ * If the theme is provided in multiple ways, the onProvideTheme override has priority, followed by
+ * the Activty's theme. (Themes whose parent theme is already set to the guided step theme do not
+ * need to set the guidedStepTheme attribute; if set, it will be ignored.)
+ * <p>
+ * If themes do not provide enough customizability, the stylists themselves may be subclassed and
+ * provided to the GuidedStepFragment through the {@link #onCreateGuidanceStylist} and {@link
+ * #onCreateActionsStylist} methods. The stylists have simple hooks so that subclasses
+ * may override layout files; subclasses may also have more complex logic to determine styling.
+ * <p>
+ * <h3>Guided sequences</h3>
+ * <p>
+ * GuidedStepFragments can be grouped together to provide a guided sequence. GuidedStepFragments
+ * grouped as a sequence use custom animations provided by {@link GuidanceStylist} and
+ * {@link GuidedActionsStylist} (or subclasses) during transitions between steps. Clients
+ * should use {@link #add} to place subsequent GuidedFragments onto the fragment stack so that
+ * custom animations are properly configured. (Custom animations are triggered automatically when
+ * the fragment stack is subsequently popped by any normal mechanism.)
+ * <p>
+ * <i>Note: Currently GuidedStepFragments grouped in this way must all be defined programmatically,
+ * rather than in XML. This restriction may be removed in the future.</i>
+ * <p>
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepTheme
+ * @see GuidanceStylist
+ * @see GuidanceStylist.Guidance
+ * @see GuidedAction
+ * @see GuidedActionsStylist
+ */
+public class GuidedStepFragment extends Fragment implements GuidedActionAdapter.ClickListener,
+ GuidedActionAdapter.FocusListener {
+
+ private static final String TAG_LEAN_BACK_ACTIONS_FRAGMENT = "leanBackGuidedStepFragment";
+ private static final String EXTRA_ACTION_SELECTED_INDEX = "selectedIndex";
+ private static final String EXTRA_ACTION_ENTRY_TRANSITION_ENABLED = "entryTransitionEnabled";
+ private static final String EXTRA_ENTRY_TRANSITION_PERFORMED = "entryTransitionPerformed";
+ private static final String TAG = "GuidedStepFragment";
+ private static final boolean DEBUG = true;
+ private static final int ANIMATION_FRAGMENT_ENTER = 1;
+ private static final int ANIMATION_FRAGMENT_EXIT = 2;
+ private static final int ANIMATION_FRAGMENT_ENTER_POP = 3;
+ private static final int ANIMATION_FRAGMENT_EXIT_POP = 4;
+
+ private int mTheme;
+ private GuidanceStylist mGuidanceStylist;
+ private GuidedActionsStylist mActionsStylist;
+ private GuidedActionAdapter mAdapter;
+ private VerticalGridView mListView;
+ private List<GuidedAction> mActions = new ArrayList<GuidedAction>();
+ private int mSelectedIndex = -1;
+ private boolean mEntryTransitionPerformed;
+ private boolean mEntryTransitionEnabled = true;
+
+ public GuidedStepFragment() {
+ // We need to supply the theme before any potential call to onInflate in order
+ // for the defaulting to work properly.
+ mTheme = onProvideTheme();
+ mGuidanceStylist = onCreateGuidanceStylist();
+ mActionsStylist = onCreateActionsStylist();
+ }
+
+ /**
+ * Creates the presenter used to style the guidance panel. The default implementation returns
+ * a basic GuidanceStylist.
+ * @return The GuidanceStylist used in this fragment.
+ */
+ public GuidanceStylist onCreateGuidanceStylist() {
+ return new GuidanceStylist();
+ }
+
+ /**
+ * Creates the presenter used to style the guided actions panel. The default implementation
+ * returns a basic GuidedActionsStylist.
+ * @return The GuidedActionsStylist used in this fragment.
+ */
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist();
+ }
+
+ /**
+ * Returns the theme used for styling the fragment. The default returns -1, indicating that the
+ * host Activity's theme should be used.
+ * @return The theme resource ID of the theme to use in this fragment, or -1 to use the
+ * host Activity's theme.
+ */
+ public int onProvideTheme() {
+ return -1;
+ }
+
+ /**
+ * Returns the information required to provide guidance to the user. This hook is called during
+ * {@link #onCreateView}. May be overridden to return a custom subclass of {@link
+ * GuidanceStylist.Guidance} for use in a subclass of {@link GuidanceStylist}. The default
+ * returns a Guidance object with empty fields; subclasses should override.
+ * @param savedInstanceState The saved instance state from onCreateView.
+ * @return The Guidance object representing the information used to guide the user.
+ */
+ public @NonNull Guidance onCreateGuidance(Bundle savedInstanceState) {
+ return new Guidance("", "", "", null);
+ }
+
+ /**
+ * Fills out the set of actions available to the user. This hook is called during {@link
+ * #onCreate}. The default leaves the list of actions empty; subclasses should override.
+ * @param actions A non-null, empty list ready to be populated.
+ * @param savedInstanceState The saved instance state from onCreate.
+ */
+ public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
+ }
+
+ /**
+ * Callback invoked when an action is taken by the user. Subclasses should override in
+ * order to act on the user's decisions.
+ * @param action The chosen action.
+ */
+ @Override
+ public void onGuidedActionClicked(GuidedAction action) {
+ }
+
+ /**
+ * Callback invoked when an action is focused (made to be the current selection) by the user.
+ */
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ }
+
+ /**
+ * Adds the specified GuidedStepFragment to the fragment stack, replacing any existing
+ * GuidedStepFragments in the stack, and configuring the fragment-to-fragment custom animations.
+ * <p>
+ * Note: currently fragments added using this method must be created programmatically rather
+ * than via XML.
+ * @param fragmentManager The FragmentManager to be used in the transaction.
+ * @param fragment The GuidedStepFragment to be inserted into the fragment stack.
+ * @return The ID returned by the call FragmentTransaction.replace.
+ */
+ public static int add(FragmentManager fragmentManager, GuidedStepFragment fragment) {
+ return add(fragmentManager, fragment, android.R.id.content);
+ }
+
+ // Note, this method used to be public, but I haven't found a good way for a client
+ // to specify an id.
+ private static int add(FragmentManager fm, GuidedStepFragment f, int id) {
+ boolean inGuidedStep = getCurrentGuidedStepFragment(fm) != null;
+ FragmentTransaction ft = fm.beginTransaction();
+
+ if (inGuidedStep) {
+ ft.setCustomAnimations(ANIMATION_FRAGMENT_ENTER,
+ ANIMATION_FRAGMENT_EXIT, ANIMATION_FRAGMENT_ENTER_POP,
+ ANIMATION_FRAGMENT_EXIT_POP);
+ ft.addToBackStack(null);
+ }
+ return ft.replace(id, f, TAG_LEAN_BACK_ACTIONS_FRAGMENT).commit();
+ }
+
+ /**
+ * Returns the current GuidedStepFragment on the fragment transaction stack.
+ * @return The current GuidedStepFragment, if any, on the fragment transaction stack.
+ */
+ public static GuidedStepFragment getCurrentGuidedStepFragment(FragmentManager fm) {
+ Fragment f = fm.findFragmentByTag(TAG_LEAN_BACK_ACTIONS_FRAGMENT);
+ if (f instanceof GuidedStepFragment) {
+ return (GuidedStepFragment) f;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the GuidanceStylist that displays guidance information for the user.
+ * @return The GuidanceStylist for this fragment.
+ */
+ public GuidanceStylist getGuidanceStylist() {
+ return mGuidanceStylist;
+ }
+
+ /**
+ * Returns the GuidedActionsStylist that displays the actions the user may take.
+ * @return The GuidedActionsStylist for this fragment.
+ */
+ public GuidedActionsStylist getGuidedActionsStylist() {
+ return mActionsStylist;
+ }
+
+ /**
+ * Returns the list of GuidedActions that the user may take in this fragment.
+ * @return The list of GuidedActions for this fragment.
+ */
+ public List<GuidedAction> getActions() {
+ return mActions;
+ }
+
+ /**
+ * Sets the list of GuidedActions that the user may take in this fragment.
+ * @param actions The list of GuidedActions for this fragment.
+ */
+ public void setActions(List<GuidedAction> actions) {
+ mActions = actions;
+ if (mAdapter != null) {
+ mAdapter.setActions(mActions);
+ }
+ }
+
+ /**
+ * Returns the view corresponding to the action at the indicated position in the list of
+ * actions for this fragment.
+ * @param position The integer position of the action of interest.
+ * @return The View corresponding to the action at the indicated position, or null if that
+ * action is not currently onscreen.
+ */
+ public View getActionItemView(int position) {
+ return mListView.findViewHolderForPosition(position).itemView;
+ }
+
+ /**
+ * Scrolls the action list to the position indicated, selecting that action's view.
+ * @param position The integer position of the action of interest.
+ */
+ public void setSelectedActionPosition(int position) {
+ mListView.setSelectedPosition(position);
+ }
+
+ /**
+ * Returns the position if the currently selected GuidedAction.
+ * @return position The integer position of the currently selected action.
+ */
+ public int getSelectedActionPosition() {
+ return mListView.getSelectedPosition();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (DEBUG) Log.v(TAG, "onCreate");
+ Bundle state = (savedInstanceState != null) ? savedInstanceState : getArguments();
+ if (state != null) {
+ if (mSelectedIndex == -1) {
+ mSelectedIndex = state.getInt(EXTRA_ACTION_SELECTED_INDEX, -1);
+ }
+ mEntryTransitionEnabled = state.getBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, true);
+ mEntryTransitionPerformed = state.getBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, false);
+ }
+ mActions.clear();
+ onCreateActions(mActions, savedInstanceState);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (DEBUG) Log.v(TAG, "onCreateView");
+
+ resolveTheme();
+ inflater = getThemeInflater(inflater);
+
+ View v = inflater.inflate(R.layout.lb_guidedstep_fragment, container, false);
+ ViewGroup guidanceContainer = (ViewGroup) v.findViewById(R.id.content_fragment);
+ ViewGroup actionContainer = (ViewGroup) v.findViewById(R.id.action_fragment);
+
+ Guidance guidance = onCreateGuidance(savedInstanceState);
+ View guidanceView = mGuidanceStylist.onCreateView(inflater, guidanceContainer, guidance);
+ guidanceContainer.addView(guidanceView);
+
+ View actionsView = mActionsStylist.onCreateView(inflater, actionContainer);
+ actionContainer.addView(actionsView);
+
+ mAdapter = new GuidedActionAdapter(mActions, this, this, mActionsStylist);
+
+ mListView = mActionsStylist.getActionsGridView();
+ mListView.setAdapter(mAdapter);
+ int pos = (mSelectedIndex >= 0 && mSelectedIndex < mActions.size()) ?
+ mSelectedIndex : getFirstCheckedAction();
+ mListView.setSelectedPosition(pos);
+
+ return v;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(EXTRA_ACTION_SELECTED_INDEX,
+ (mListView != null) ? getSelectedActionPosition() : mSelectedIndex);
+ outState.putBoolean(EXTRA_ACTION_ENTRY_TRANSITION_ENABLED, mEntryTransitionEnabled);
+ outState.putBoolean(EXTRA_ENTRY_TRANSITION_PERFORMED, mEntryTransitionPerformed);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onStart() {
+ if (DEBUG) Log.v(TAG, "onStart");
+ super.onStart();
+ if (isEntryTransitionEnabled() && !mEntryTransitionPerformed) {
+ mEntryTransitionPerformed = true;
+ performEntryTransition();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
+ if (DEBUG) Log.v(TAG, "onCreateAnimator: " + transit + " " + enter + " " + nextAnim);
+ View mainView = getView();
+
+ ArrayList<Animator> animators = new ArrayList<Animator>();
+ switch (nextAnim) {
+ case ANIMATION_FRAGMENT_ENTER:
+ mGuidanceStylist.onFragmentEnter(animators);
+ mActionsStylist.onFragmentEnter(animators);
+ break;
+ case ANIMATION_FRAGMENT_EXIT:
+ mGuidanceStylist.onFragmentExit(animators);
+ mActionsStylist.onFragmentExit(animators);
+ break;
+ case ANIMATION_FRAGMENT_ENTER_POP:
+ mGuidanceStylist.onFragmentReenter(animators);
+ mActionsStylist.onFragmentReenter(animators);
+ break;
+ case ANIMATION_FRAGMENT_EXIT_POP:
+ mGuidanceStylist.onFragmentReturn(animators);
+ mActionsStylist.onFragmentReturn(animators);
+ break;
+ default:
+ return super.onCreateAnimator(transit, enter, nextAnim);
+ }
+
+ mEntryTransitionPerformed = true;
+ return createDummyAnimator(mainView, animators);
+ }
+
+ /**
+ * Returns whether entry transitions are enabled for this fragment.
+ * @return Whether entry transitions are enabled for this fragment.
+ */
+ protected boolean isEntryTransitionEnabled() {
+ return mEntryTransitionEnabled;
+ }
+
+ /**
+ * Sets whether entry transitions are enabled for this fragment.
+ * @param enabled Whether to enable entry transitions for this fragment.
+ */
+ protected void setEntryTransitionEnabled(boolean enabled) {
+ mEntryTransitionEnabled = enabled;
+ }
+
+ private boolean isGuidedStepTheme(Context context) {
+ int resId = R.attr.guidedStepThemeFlag;
+ TypedValue typedValue = new TypedValue();
+ boolean found = context.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme flag? " + found);
+ return found && typedValue.type == TypedValue.TYPE_INT_BOOLEAN && typedValue.data != 0;
+ }
+
+ private void resolveTheme() {
+ boolean hasThemeReference = true;
+ // Look up the guidedStepTheme in the currently specified theme. If it exists,
+ // replace the theme with its value.
+ Activity activity = getActivity();
+ if (mTheme == -1 && !isGuidedStepTheme(activity)) {
+ // Look up the guidedStepTheme in the activity's currently specified theme. If it
+ // exists, replace the theme with its value.
+ int resId = R.attr.guidedStepTheme;
+ TypedValue typedValue = new TypedValue();
+ boolean found = activity.getTheme().resolveAttribute(resId, typedValue, true);
+ if (DEBUG) Log.v(TAG, "Found guided step theme reference? " + found);
+ if (found) {
+ if (isGuidedStepTheme(new ContextThemeWrapper(activity, typedValue.resourceId))) {
+ mTheme = typedValue.resourceId;
+ } else {
+ found = false;
+ }
+ }
+ if (!found) {
+ Log.e(TAG, "GuidedStepFragment does not have an appropriate theme set.");
+ }
+ }
+ }
+
+ private LayoutInflater getThemeInflater(LayoutInflater inflater) {
+ if (mTheme == -1) {
+ return inflater;
+ } else {
+ Context ctw = new ContextThemeWrapper(getActivity(), mTheme);
+ return inflater.cloneInContext(ctw);
+ }
+ }
+
+ private int getFirstCheckedAction() {
+ for (int i = 0, size = mActions.size(); i < size; i++) {
+ if (mActions.get(i).isChecked()) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ private void performEntryTransition() {
+ if (DEBUG) Log.v(TAG, "performEntryTransition");
+ final View mainView = getView();
+
+ mainView.setVisibility(View.INVISIBLE);
+
+ ArrayList<Animator> animators = new ArrayList<Animator>();
+ mGuidanceStylist.onActivityEnter(animators);
+ mActionsStylist.onActivityEnter(animators);
+
+ final Animator animator = createDummyAnimator(mainView, animators);
+
+ // We need to defer the animation until the first layout has occurred, as we don't yet
+ // know the final locations of views.
+ mainView.getViewTreeObserver().addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mainView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ if (!isAdded()) {
+ // We have been detached before this could run,
+ // so just bail
+ return;
+ }
+
+ mainView.setVisibility(View.VISIBLE);
+ animator.start();
+ }
+ });
+ }
+
+ private Animator createDummyAnimator(final View v, ArrayList<Animator> animators) {
+ final AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(animators);
+ return new UntargetableAnimatorSet(animatorSet);
+ }
+
+}
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 74fa734..47ad9e4 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -183,9 +183,11 @@
final int count = listView.getChildCount();
for (int i = 0; i < count; i++) {
View view = listView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
listView.getChildViewHolder(view);
- setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
+ RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
+ RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
+ vh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
}
}
}
@@ -309,16 +311,10 @@
((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
}
- private static void setOnItemViewSelectedListener(ItemBridgeAdapter.ViewHolder vh,
- OnItemViewSelectedListener listener) {
- ((RowPresenter) vh.getPresenter()).setOnItemViewSelectedListener(listener);
- }
-
private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
new ItemBridgeAdapter.AdapterListener() {
@Override
public void onAddPresenter(Presenter presenter, int type) {
- ((RowPresenter) presenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onAddPresenter(presenter, type);
}
@@ -350,9 +346,10 @@
// but again it should use the unchanged mExpand value, so we don't need do any
// thing in onBind.
setRowViewExpanded(vh, mExpand);
- setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
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 2f8deff..029ddbd 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsSupportFragment.java
@@ -185,9 +185,11 @@
final int count = listView.getChildCount();
for (int i = 0; i < count; i++) {
View view = listView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
listView.getChildViewHolder(view);
- setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
+ RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter();
+ RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder());
+ vh.setOnItemViewSelectedListener(mOnItemViewSelectedListener);
}
}
}
@@ -311,16 +313,10 @@
((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
}
- private static void setOnItemViewSelectedListener(ItemBridgeAdapter.ViewHolder vh,
- OnItemViewSelectedListener listener) {
- ((RowPresenter) vh.getPresenter()).setOnItemViewSelectedListener(listener);
- }
-
private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
new ItemBridgeAdapter.AdapterListener() {
@Override
public void onAddPresenter(Presenter presenter, int type) {
- ((RowPresenter) presenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
if (mExternalAdapterListener != null) {
mExternalAdapterListener.onAddPresenter(presenter, type);
}
@@ -352,9 +348,10 @@
// but again it should use the unchanged mExpand value, so we don't need do any
// thing in onBind.
setRowViewExpanded(vh, mExpand);
- setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
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/widget/DetailsOverviewRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
index 9259a1d..c686a36 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
@@ -76,13 +76,14 @@
@Override
public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) {
- if (getOnItemViewClickedListener() != null || mActionClickedListener != null) {
+ if (mViewHolder.getOnItemViewClickedListener() != null ||
+ mActionClickedListener != null) {
ibvh.getPresenter().setOnClickListener(
ibvh.getViewHolder(), new View.OnClickListener() {
@Override
public void onClick(View v) {
- if (getOnItemViewClickedListener() != null) {
- getOnItemViewClickedListener().onItemClicked(
+ if (mViewHolder.getOnItemViewClickedListener() != null) {
+ mViewHolder.getOnItemViewClickedListener().onItemClicked(
ibvh.getViewHolder(), ibvh.getItem(),
mViewHolder, mViewHolder.getRow());
}
@@ -95,7 +96,8 @@
}
@Override
public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) {
- if (getOnItemViewClickedListener() != null || mActionClickedListener != null) {
+ if (mViewHolder.getOnItemViewClickedListener() != null ||
+ mActionClickedListener != null) {
ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null);
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java b/v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
new file mode 100644
index 0000000..8bd0007
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/FragmentAnimationProvider.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.animation.Animator;
+import android.support.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * FragmentAnimationProvider supplies animations for use during a fragment's onCreateAnimator
+ * callback. Animators added here will be added to an animation set and played together. This
+ * allows presenters used by a fragment to control their own fragment lifecycle animations.
+ */
+public interface FragmentAnimationProvider {
+
+ /**
+ * Animates the entry of the fragment in the case where the activity is first being presented.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onActivityEnter(@NonNull List<Animator> animators);
+
+ /**
+ * Animates the exit of the fragment in the case where the activity is about to pause.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onActivityExit(@NonNull List<Animator> animators);
+
+ /**
+ * Animates the entry of the fragment in the case where there is a previous step fragment
+ * participating in the animation. Entry occurs when the fragment is preparing to be shown
+ * as it is pushed onto the back stack.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onFragmentEnter(@NonNull List<Animator> animators);
+
+ /**
+ * Animates the exit of the fragment in the case where there is a previous step fragment
+ * participating in the animation. Exit occurs when the fragment is preparing to be removed,
+ * hidden, or detached due to pushing another fragment onto the back stack.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onFragmentExit(@NonNull List<Animator> animators);
+
+ /**
+ * Animates the re-entry of the fragment in the case where there is a previous step fragment
+ * participating in the animation. Re-entry occurs when the fragment is preparing to be shown
+ * due to popping the back stack.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onFragmentReenter(@NonNull List<Animator> animators);
+
+ /**
+ * Animates the return of the fragment in the case where there is a previous step fragment
+ * participating in the animation. Return occurs when the fragment is preparing to be removed,
+ * hidden, or detached due to popping the back stack.
+ * @param animators A list of animations to which this provider's animations should be added.
+ */
+ public abstract void onFragmentReturn(@NonNull List<Animator> animators);
+
+}
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 3858b34..b0849e9 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -943,7 +943,8 @@
// if focus position is never set before, initialize it to 0
mFocusPosition = 0;
}
- if (!mState.didStructureChange() && !mForceFullLayout && mGrid != null) {
+ if (!mState.didStructureChange() && mGrid.getFirstVisibleIndex() >= 0 &&
+ !mForceFullLayout && mGrid != null) {
updateScrollController();
updateScrollSecondAxis();
mGrid.setMargin(mMarginPrimary);
@@ -1642,15 +1643,14 @@
fastRelayout();
// appends items till focus position.
if (mFocusPosition != NO_POSITION) {
- View focusView;
- while ((focusView = findViewByPosition(mFocusPosition)) == null) {
- appendOneColumnVisibleItems();
- }
- if (scrollToFocus) {
- scrollToView(focusView, false);
- }
- if (hadFocus) {
- focusView.requestFocus();
+ View focusView = findViewByPosition(mFocusPosition);
+ if (focusView != null) {
+ if (scrollToFocus) {
+ scrollToView(focusView, false);
+ }
+ if (hadFocus) {
+ focusView.requestFocus();
+ }
}
}
} else {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
new file mode 100644
index 0000000..8d12510
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidanceStylist.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.R;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * GuidanceStylist is used within a {@link android.support.v17.leanback.app.GuidedStepFragment}
+ * to display contextual information for the decision(s) required at that step.
+ * <p>
+ * Many aspects of the base GuidanceStylist can be customized through theming; see the theme
+ * attributes below. Note that these attributes are not set on individual elements in layout
+ * XML, but instead would be set in a custom theme. See
+ * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
+ * for more information.
+ * <p>
+ * If these hooks are insufficient, this class may also be subclassed. Subclasses
+ * may wish to override the {@link #onProvideLayoutId} method to change the layout file used to
+ * display the guidance; more complex layouts may be supported by also providing a subclass of
+ * {@link GuidanceStylist.Guidance} with extra fields.
+ * <p>
+ * Note: If an alternate layout is provided, the following view IDs should be used to refer to base
+ * elements:
+ * <ul>
+ * <li>{@link android.support.v17.leanback.R.id#guidance_title}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidance_description}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidance_breadcrumb}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidance_icon}</li>
+ * </ul><p>
+ * View IDs are allowed to be missing, in which case the corresponding views will be null.
+ *
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceEntryAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepEntryAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepExitAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepReentryAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedStepReturnAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceContainerStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceTitleStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceDescriptionStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceBreadcrumbStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidanceIconStyle
+ * @see android.support.v17.leanback.app.GuidedStepFragment
+ * @see GuidanceStylist.Guidance
+ */
+public class GuidanceStylist implements FragmentAnimationProvider {
+
+ /**
+ * A data class representing contextual information for a {@link
+ * android.support.v17.leanback.app.GuidedStepFragment}. Guidance consists of a short title,
+ * a longer description, a breadcrumb to help with global navigation (often indicating where
+ * the back button will lead), and an optional icon. All this information is intended to
+ * provide users with the appropriate context to make the decision(s) required by the current
+ * step.
+ * <p>
+ * Clients may provide a subclass of this if they wish to remember auxiliary data for use in
+ * a customized GuidanceStylist.
+ */
+ public static class Guidance {
+ private final String mTitle;
+ private final String mDescription;
+ private final String mBreadcrumb;
+ private final Drawable mIconDrawable;
+
+ /**
+ * Constructs a Guidance object with the specified title, description, breadcrumb, and
+ * icon drawable.
+ * @param title The title for the current guided step.
+ * @param description The description for the current guided step.
+ * @param breadcrumb The breadcrumb for the current guided step.
+ * @param icon The icon drawable representing the current guided step.
+ */
+ public Guidance(String title, String description, String breadcrumb, Drawable icon) {
+ mBreadcrumb = breadcrumb;
+ mTitle = title;
+ mDescription = description;
+ mIconDrawable = icon;
+ }
+
+ /**
+ * Returns the title specified when this Guidance was constructed.
+ * @return The title for this Guidance.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Returns the description specified when this Guidance was constructed.
+ * @return The description for this Guidance.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Returns the breadcrumb specified when this Guidance was constructed.
+ * @return The breadcrumb for this Guidance.
+ */
+ public String getBreadcrumb() {
+ return mBreadcrumb;
+ }
+
+ /**
+ * Returns the icon drawable specified when this Guidance was constructed.
+ * @return The icon for this Guidance.
+ */
+ public Drawable getIconDrawable() {
+ return mIconDrawable;
+ }
+ }
+
+ private TextView mTitleView;
+ private TextView mDescriptionView;
+ private TextView mBreadcrumbView;
+ private ImageView mIconView;
+
+ /**
+ * Creates an appropriately configured view for the given Guidance, using the provided
+ * inflater and container.
+ * <p>
+ * <i>Note: Does not actually add the created view to the container; the caller should do
+ * this.</i>
+ * @param inflater The layout inflater to be used when constructing the view.
+ * @param container The view group to be passed in the call to
+ * <code>LayoutInflater.inflate</code>.
+ * @param guidance The guidance data for the view.
+ * @return The view to be added to the caller's view hierarchy.
+ */
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Guidance guidance) {
+ View guidanceView = inflater.inflate(onProvideLayoutId(), container, false);
+ mTitleView = (TextView) guidanceView.findViewById(R.id.guidance_title);
+ mBreadcrumbView = (TextView) guidanceView.findViewById(R.id.guidance_breadcrumb);
+ mDescriptionView = (TextView) guidanceView.findViewById(R.id.guidance_description);
+ mIconView = (ImageView) guidanceView.findViewById(R.id.guidance_icon);
+
+ // We allow any of the cached subviews to be null, so that subclasses can choose not to
+ // display a particular piece of information.
+ if (mTitleView != null) {
+ mTitleView.setText(guidance.getTitle());
+ }
+ if (mBreadcrumbView != null) {
+ mBreadcrumbView.setText(guidance.getBreadcrumb());
+ }
+ if (mDescriptionView != null) {
+ mDescriptionView.setText(guidance.getDescription());
+ }
+ if (mIconView != null) {
+ mIconView.setImageDrawable(guidance.getIconDrawable());
+ }
+ return guidanceView;
+ }
+
+ /**
+ * Provides the resource ID of the layout defining the guidance view. Subclasses may override
+ * to provide their own customized layouts. The base implementation returns
+ * {@link android.support.v17.leanback.R.layout#lb_guidance}. If overridden, the substituted
+ * layout should contain matching IDs for any views that should be managed by the base class;
+ * this can be achieved by starting with a copy of the base layout file.
+ * @return The resource ID of the layout to be inflated to define the guidance view.
+ */
+ public int onProvideLayoutId() {
+ return R.layout.lb_guidance;
+ }
+
+ /**
+ * Returns the view displaying the title of the guidance.
+ * @return The text view object for the title.
+ */
+ public TextView getTitleView() {
+ return mTitleView;
+ }
+
+ /**
+ * Returns the view displaying the description of the guidance.
+ * @return The text view object for the description.
+ */
+ public TextView getDescriptionView() {
+ return mDescriptionView;
+ }
+
+ /**
+ * Returns the view displaying the breadcrumb of the guidance.
+ * @return The text view object for the breadcrumb.
+ */
+ public TextView getBreadcrumbView() {
+ return mBreadcrumbView;
+ }
+
+ /**
+ * Returns the view displaying the icon of the guidance.
+ * @return The image view object for the icon.
+ */
+ public ImageView getIconView() {
+ return mIconView;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityEnter(@NonNull List<Animator> animators) {
+ addAnimator(animators, mTitleView, R.attr.guidanceEntryAnimation);
+ addAnimator(animators, mBreadcrumbView, R.attr.guidanceEntryAnimation);
+ addAnimator(animators, mDescriptionView, R.attr.guidanceEntryAnimation);
+ addAnimator(animators, mIconView, R.attr.guidanceEntryAnimation);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityExit(@NonNull List<Animator> animators) {}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentEnter(@NonNull List<Animator> animators) {
+ addAnimator(animators, mTitleView, R.attr.guidedStepEntryAnimation);
+ addAnimator(animators, mBreadcrumbView, R.attr.guidedStepEntryAnimation);
+ addAnimator(animators, mDescriptionView, R.attr.guidedStepEntryAnimation);
+ addAnimator(animators, mIconView, R.attr.guidedStepEntryAnimation);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentExit(@NonNull List<Animator> animators) {
+ addAnimator(animators, mTitleView, R.attr.guidedStepExitAnimation);
+ addAnimator(animators, mBreadcrumbView, R.attr.guidedStepExitAnimation);
+ addAnimator(animators, mDescriptionView, R.attr.guidedStepExitAnimation);
+ addAnimator(animators, mIconView, R.attr.guidedStepExitAnimation);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentReenter(@NonNull List<Animator> animators) {
+ addAnimator(animators, mTitleView, R.attr.guidedStepReentryAnimation);
+ addAnimator(animators, mBreadcrumbView, R.attr.guidedStepReentryAnimation);
+ addAnimator(animators, mDescriptionView, R.attr.guidedStepReentryAnimation);
+ addAnimator(animators, mIconView, R.attr.guidedStepReentryAnimation);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentReturn(@NonNull List<Animator> animators) {
+ addAnimator(animators, mTitleView, R.attr.guidedStepReturnAnimation);
+ addAnimator(animators, mBreadcrumbView, R.attr.guidedStepReturnAnimation);
+ addAnimator(animators, mDescriptionView, R.attr.guidedStepReturnAnimation);
+ addAnimator(animators, mIconView, R.attr.guidedStepReturnAnimation);
+ }
+
+ private void addAnimator(List<Animator> animators, View v, int attrId) {
+ if (v != null) {
+ Context ctx = v.getContext();
+ TypedValue typedValue = new TypedValue();
+ ctx.getTheme().resolveAttribute(attrId, typedValue, true);
+ Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
+ animator.setTarget(v);
+ animators.add(animator);
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
new file mode 100644
index 0000000..e4db2eb
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedAction.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+/**
+ * A data class which represents an action within a {@link
+ * android.support.v17.leanback.app.GuidedStepFragment}. GuidedActions contain at minimum a title
+ * and a description, and typically also an icon.
+ * <p>
+ * A GuidedAction typically represents a single action a user may take, but may also represent a
+ * possible choice out of a group of mutually exclusive choices (similar to radio buttons), or an
+ * information-only label (in which case the item cannot be clicked).
+ * <p>
+ * GuidedActions may optionally be checked. They may also indicate that they will request further
+ * user input on selection, in which case they will be displayed with a chevron indicator.
+ */
+public class GuidedAction extends Action {
+
+ private static final String TAG = "GuidedAction";
+
+ public static final int NO_DRAWABLE = 0;
+ public static final int NO_CHECK_SET = 0;
+ public static final int DEFAULT_CHECK_SET_ID = 1;
+
+ /**
+ * Builds a {@link GuidedAction} object.
+ */
+ public static class Builder {
+ private long mId;
+ private String mTitle;
+ private String mDescription;
+ private Drawable mIcon;
+ private boolean mChecked;
+ private boolean mMultilineDescription;
+ private boolean mHasNext;
+ private boolean mInfoOnly;
+ private int mCheckSetId = NO_CHECK_SET;
+ private boolean mEnabled = true;
+ private Intent mIntent;
+
+ /**
+ * Builds the GuidedAction corresponding to this Builder.
+ * @return the GuidedAction as configured through this Builder.
+ */
+ public GuidedAction build() {
+ GuidedAction action = new GuidedAction();
+ // Base Action values
+ action.setId(mId);
+ action.setLabel1(mTitle);
+ action.setLabel2(mDescription);
+ action.setIcon(mIcon);
+
+ // Subclass values
+ action.mIntent = mIntent;
+ action.mChecked = mChecked;
+ action.mCheckSetId = mCheckSetId;
+ action.mMultilineDescription = mMultilineDescription;
+ action.mHasNext = mHasNext;
+ action.mInfoOnly = mInfoOnly;
+ action.mEnabled = mEnabled;
+ return action;
+ }
+
+ /**
+ * Sets the ID associated with this action. The ID can be any value the client wishes;
+ * it is typically used to determine what to do when an action is clicked.
+ * @param id The ID to associate with this action.
+ */
+ public Builder id(long id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the title for this action. The title is typically a short string indicating the
+ * action to be taken on click, e.g. "Continue" or "Cancel".
+ * @param title The title for this action.
+ */
+ public Builder title(String title) {
+ mTitle = title;
+ return this;
+ }
+
+ /**
+ * Sets the description for this action. The description is typically a longer string
+ * providing extra information on what the action will do.
+ * @param description The description for this action.
+ */
+ public Builder description(String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Sets the intent associated with this action. Clients would typically fire this intent
+ * directly when the action is clicked.
+ * @param intent The intent associated with this action.
+ */
+ public Builder intent(Intent intent) {
+ mIntent = intent;
+ return this;
+ }
+
+ /**
+ * Sets the action's icon drawable.
+ * @param icon The drawable for the icon associated with this action.
+ */
+ public Builder icon(Drawable icon) {
+ mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Sets the action's icon drawable by retrieving it by resource ID from the specified
+ * context. This is a convenience function that simply looks up the drawable and calls
+ * {@link #icon}.
+ * @param iconResourceId The resource ID for the icon associated with this action.
+ * @param context The context whose resource ID should be retrieved.
+ */
+ public Builder iconResourceId(int iconResourceId, Context context) {
+ return icon(context.getResources().getDrawable(iconResourceId));
+ }
+
+ /**
+ * Indicates whether this action is initially checked.
+ * @param checked Whether this action is checked.
+ */
+ public Builder checked(boolean checked) {
+ mChecked = checked;
+ return this;
+ }
+
+ /**
+ * Indicates whether this action is part of a single-select group similar to radio buttons.
+ * When one item in a check set is checked, all others with the same check set ID will be
+ * unchecked automatically.
+ * @param checkSetId The check set ID, or {@link #NO_CHECK_SET) to indicate no check set.
+ */
+ public Builder checkSetId(int checkSetId) {
+ mCheckSetId = checkSetId;
+ return this;
+ }
+
+ /**
+ * Indicates whether the title and description are long, and should be displayed
+ * appropriately.
+ * @param multilineDescription Whether this action has a multiline description.
+ */
+ public Builder multilineDescription(boolean multilineDescription) {
+ mMultilineDescription = multilineDescription;
+ return this;
+ }
+
+ /**
+ * Indicates whether this action has a next state and should display a chevron.
+ * @param hasNext Whether this action has a next state.
+ */
+ public Builder hasNext(boolean hasNext) {
+ mHasNext = hasNext;
+ return this;
+ }
+
+ /**
+ * Indicates whether this action is for information purposes only and cannot be clicked.
+ * @param infoOnly Whether this action has a next state.
+ */
+ public Builder infoOnly(boolean infoOnly) {
+ mInfoOnly = infoOnly;
+ return this;
+ }
+
+ /**
+ * Indicates whether this action is enabled. If not enabled, an action cannot be clicked.
+ * @param enabled Whether the action is enabled.
+ */
+ public Builder enabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+ }
+
+ private boolean mChecked;
+ private boolean mMultilineDescription;
+ private boolean mHasNext;
+ private boolean mInfoOnly;
+ private int mCheckSetId;
+ private boolean mEnabled;
+
+ private Intent mIntent;
+
+ private GuidedAction() {
+ super(0);
+ }
+
+ /**
+ * Returns the title of this action.
+ * @return The title set when this action was built.
+ */
+ public CharSequence getTitle() {
+ return getLabel1();
+ }
+
+ /**
+ * Returns the description of this action.
+ * @return The description set when this action was built.
+ */
+ public CharSequence getDescription() {
+ return getLabel2();
+ }
+
+ /**
+ * Returns the intent associated with this action.
+ * @return The intent set when this action was built.
+ */
+ public Intent getIntent() {
+ return mIntent;
+ }
+
+ /**
+ * Returns whether this action is checked.
+ * @return true if the action is currently checked, false otherwise.
+ */
+ public boolean isChecked() {
+ return mChecked;
+ }
+
+ /**
+ * Sets whether this action is checked.
+ * @param checked Whether this action should be checked.
+ */
+ public void setChecked(boolean checked) {
+ mChecked = checked;
+ }
+
+ /**
+ * Returns the check set id this action is a part of. All actions in the
+ * same list with the same check set id are considered linked. When one
+ * of the actions within that set is selected, that action becomes
+ * checked, while all the other actions become unchecked.
+ *
+ * @return an integer representing the check set this action is a part of, or
+ * {@link #NO_CHECK_SET} if this action isn't a part of a check set.
+ */
+ public int getCheckSetId() {
+ return mCheckSetId;
+ }
+
+ /**
+ * Returns whether this action is has a multiline description.
+ * @return true if the action was constructed as having a multiline description, false
+ * otherwise.
+ */
+ public boolean hasMultilineDescription() {
+ return mMultilineDescription;
+ }
+
+ /**
+ * Returns whether this action is enabled.
+ * @return true if the action is currently enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Sets whether this action is enabled.
+ * @param enabled Whether this action should be enabled.
+ */
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ /**
+ * Returns whether this action will request further user input when selected, such as showing
+ * another GuidedStepFragment or launching a new activity. Configured during construction.
+ * @return true if the action will request further user input when selected, false otherwise.
+ */
+ public boolean hasNext() {
+ return mHasNext;
+ }
+
+ /**
+ * Returns whether the action will only display information and is thus not clickable. If both
+ * this and {@link #hasNext()} are true, infoOnly takes precedence. The default is false. For
+ * example, this might represent e.g. the amount of storage a document uses, or the cost of an
+ * app.
+ * @return true if will only display information, false otherwise.
+ */
+ public boolean infoOnly() {
+ return mInfoOnly;
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
new file mode 100644
index 0000000..3562234
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GuidedActionsStylist.java
@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.animation.DecelerateInterpolator;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * GuidedActionsStylist is used within a GuidedStepFragment to supply the right-side panel
+ * where users can take actions. It consists of a container for the list of actions, and a
+ * stationary selector view that indicates visually the location of focus.
+ * <p>
+ * Many aspects of the base GuidedActionsStylist can be customized through theming; see the
+ * theme attributes below. Note that these attributes are not set on individual elements in layout
+ * XML, but instead would be set in a custom theme. See
+ * <a href="http://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>
+ * for more information.
+ * <p>
+ * If these hooks are insufficient, this class may also be subclassed. Subclasses may wish to
+ * override the {@link #onProvideLayoutId} method to change the layout used to display the
+ * list container and selector, or the {@link #onProvideItemLayoutId} method to change the layout
+ * used to display each action.
+ * <p>
+ * Note: If an alternate list layout is provided, the following view IDs must be supplied:
+ * <ul>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_selector}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_list}</li>
+ * </ul><p>
+ * These view IDs must be present in order for the stylist to function. The list ID must correspond
+ * to a {@link VerticalGridView} or subclass.
+ * <p>
+ * If an alternate item layout is provided, the following view IDs should be used to refer to base
+ * elements:
+ * <ul>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_content}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_title}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_description}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_icon}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_checkmark}</li>
+ * <li>{@link android.support.v17.leanback.R.id#guidedactions_item_chevron}</li>
+ * </ul><p>
+ * These view IDs are allowed to be missing, in which case the corresponding views in {@link
+ * GuidedActionsStylist.ViewHolder} will be null.
+ *
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsEntryAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorShowAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorHideAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsContainerStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsSelectorStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionsListStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContainerStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemCheckmarkStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemIconStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemContentStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemTitleStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemDescriptionStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionItemChevronStyle
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionCheckedAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUncheckedAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionPressedAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionUnpressedAnimation
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionEnabledChevronAlpha
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDisabledChevronAlpha
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidth
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionContentWidthNoIcon
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMinLines
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionTitleMaxLines
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionDescriptionMinLines
+ * @attr ref android.support.v17.leanback.R.styleable#LeanbackGuidedStepTheme_guidedActionVerticalPadding
+ * @see android.support.v17.leanback.app.GuidedStepFragment
+ * @see GuidedAction
+ */
+public class GuidedActionsStylist implements FragmentAnimationProvider {
+
+ /**
+ * ViewHolder caches information about the action item layouts' subviews. Subclasses of {@link
+ * GuidedActionsStylist} may also wish to subclass this in order to add fields.
+ * @see GuidedAction
+ */
+ public static class ViewHolder {
+
+ public final View view;
+
+ private View mContentView;
+ private TextView mTitleView;
+ private TextView mDescriptionView;
+ private ImageView mIconView;
+ private ImageView mCheckmarkView;
+ private ImageView mChevronView;
+
+ /**
+ * Constructs an ViewHolder and caches the relevant subviews.
+ */
+ public ViewHolder(View v) {
+ view = v;
+
+ mContentView = v.findViewById(R.id.guidedactions_item_content);
+ mTitleView = (TextView) v.findViewById(R.id.guidedactions_item_title);
+ mDescriptionView = (TextView) v.findViewById(R.id.guidedactions_item_description);
+ mIconView = (ImageView) v.findViewById(R.id.guidedactions_item_icon);
+ mCheckmarkView = (ImageView) v.findViewById(R.id.guidedactions_item_checkmark);
+ mChevronView = (ImageView) v.findViewById(R.id.guidedactions_item_chevron);
+ }
+
+ /**
+ * Returns the content view within this view holder's view, where title and description are
+ * shown.
+ */
+ public View getContentView() {
+ return mContentView;
+ }
+
+ /**
+ * Returns the title view within this view holder's view.
+ */
+ public TextView getTitleView() {
+ return mTitleView;
+ }
+
+ /**
+ * Returns the description view within this view holder's view.
+ */
+ public TextView getDescriptionView() {
+ return mDescriptionView;
+ }
+
+ /**
+ * Returns the icon view within this view holder's view.
+ */
+ public ImageView getIconView() {
+ return mIconView;
+ }
+
+ /**
+ * Returns the checkmark view within this view holder's view.
+ */
+ public ImageView getCheckmarkView() {
+ return mCheckmarkView;
+ }
+
+ /**
+ * Returns the chevron view within this view holder's view.
+ */
+ public ImageView getChevronView() {
+ return mChevronView;
+ }
+
+ }
+
+ private static String TAG = "GuidedActionsStylist";
+
+ protected View mMainView;
+ protected VerticalGridView mActionsGridView;
+ protected View mSelectorView;
+
+ // Cached values from resources
+ private float mEnabledChevronAlpha;
+ private float mDisabledChevronAlpha;
+ private int mContentWidth;
+ private int mContentWidthNoIcon;
+ private int mTitleMinLines;
+ private int mTitleMaxLines;
+ private int mDescriptionMinLines;
+ private int mVerticalPadding;
+ private int mDisplayHeight;
+
+ /**
+ * Creates a view appropriate for displaying a list of GuidedActions, using the provided
+ * inflater and container.
+ * <p>
+ * <i>Note: Does not actually add the created view to the container; the caller should do
+ * this.</i>
+ * @param inflater The layout inflater to be used when constructing the view.
+ * @param container The view group to be passed in the call to
+ * <code>LayoutInflater.inflate</code>.
+ * @return The view to be added to the caller's view hierarchy.
+ */
+ public View onCreateView(LayoutInflater inflater, ViewGroup container) {
+ mMainView = inflater.inflate(onProvideLayoutId(), container, false);
+ mSelectorView = mMainView.findViewById(R.id.guidedactions_selector);
+ if (mMainView instanceof VerticalGridView) {
+ mActionsGridView = (VerticalGridView) mMainView;
+ } else {
+ mActionsGridView = (VerticalGridView) mMainView.findViewById(R.id.guidedactions_list);
+ if (mActionsGridView == null) {
+ throw new IllegalStateException("No ListView exists.");
+ }
+ mActionsGridView.setWindowAlignmentOffset(0);
+ mActionsGridView.setWindowAlignmentOffsetPercent(50f);
+ mActionsGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ if (mSelectorView != null) {
+ mActionsGridView.setOnScrollListener(new
+ SelectorAnimator(mSelectorView, mActionsGridView));
+ }
+ }
+
+ mActionsGridView.requestFocusFromTouch();
+
+ if (mSelectorView != null) {
+ // ALlow focus to move to other views
+ mActionsGridView.getViewTreeObserver().addOnGlobalFocusChangeListener(
+ new ViewTreeObserver.OnGlobalFocusChangeListener() {
+ private boolean mChildFocused;
+
+ @Override
+ public void onGlobalFocusChanged(View oldFocus, View newFocus) {
+ View focusedChild = mActionsGridView.getFocusedChild();
+ if (focusedChild == null) {
+ mSelectorView.setVisibility(View.INVISIBLE);
+ mChildFocused = false;
+ } else if (!mChildFocused) {
+ mChildFocused = true;
+ mSelectorView.setVisibility(View.VISIBLE);
+ updateSelectorView(focusedChild);
+ }
+ }
+ });
+ }
+
+ // Cache widths, chevron alpha values, max and min text lines, etc
+ Context ctx = mMainView.getContext();
+ TypedValue val = new TypedValue();
+ mEnabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionEnabledChevronAlpha);
+ mDisabledChevronAlpha = getFloat(ctx, val, R.attr.guidedActionDisabledChevronAlpha);
+ mContentWidth = getDimension(ctx, val, R.attr.guidedActionContentWidth);
+ mContentWidthNoIcon = getDimension(ctx, val, R.attr.guidedActionContentWidthNoIcon);
+ mTitleMinLines = getInteger(ctx, val, R.attr.guidedActionTitleMinLines);
+ mTitleMaxLines = getInteger(ctx, val, R.attr.guidedActionTitleMaxLines);
+ mDescriptionMinLines = getInteger(ctx, val, R.attr.guidedActionDescriptionMinLines);
+ mVerticalPadding = getDimension(ctx, val, R.attr.guidedActionVerticalPadding);
+ mDisplayHeight = ((WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE))
+ .getDefaultDisplay().getHeight();
+
+ return mMainView;
+ }
+
+ /**
+ * Returns the VerticalGridView that displays the list of GuidedActions.
+ * @return The VerticalGridView for this presenter.
+ */
+ public VerticalGridView getActionsGridView() {
+ return mActionsGridView;
+ }
+
+ /**
+ * Provides the resource ID of the layout defining the host view for the list of guided actions.
+ * Subclasses may override to provide their own customized layouts. The base implementation
+ * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions}. If overridden, the
+ * substituted layout should contain matching IDs for any views that should be managed by the
+ * base class; this can be achieved by starting with a copy of the base layout file.
+ * @return The resource ID of the layout to be inflated to define the host view for the list
+ * of GuidedActions.
+ */
+ public int onProvideLayoutId() {
+ return R.layout.lb_guidedactions;
+ }
+
+ /**
+ * Provides the resource ID of the layout defining the view for an individual guided actions.
+ * Subclasses may override to provide their own customized layouts. The base implementation
+ * returns {@link android.support.v17.leanback.R.layout#lb_guidedactions_item}. If overridden,
+ * the substituted layout should contain matching IDs for any views that should be managed by
+ * the base class; this can be achieved by starting with a copy of the base layout file.
+ * @return The resource ID of the layout to be inflated to define the view to display an
+ * individual GuidedAction.
+ */
+ public int onProvideItemLayoutId() {
+ return R.layout.lb_guidedactions_item;
+ }
+
+ /**
+ * Constructs a {@link ViewHolder} capable of representing {@link GuidedAction}s. Subclasses
+ * may choose to return a subclass of ViewHolder.
+ * <p>
+ * <i>Note: Should not actually add the created view to the parent; the caller will do
+ * this.</i>
+ * @param parent The view group to be used as the parent of the new view.
+ * @return The view to be added to the caller's view hierarchy.
+ */
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+ View v = inflater.inflate(onProvideItemLayoutId(), parent, false);
+ return new ViewHolder(v);
+ }
+
+ /**
+ * Binds a {@link ViewHolder} to a particular {@link GuidedAction}.
+ * @param vh The view holder to be associated with the given action.
+ * @param action The guided action to be displayed by the view holder's view.
+ * @return The view to be added to the caller's view hierarchy.
+ */
+ public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
+
+ if (vh.mTitleView != null) {
+ vh.mTitleView.setText(action.getTitle());
+ }
+ if (vh.mDescriptionView != null) {
+ vh.mDescriptionView.setText(action.getDescription());
+ vh.mDescriptionView.setVisibility(TextUtils.isEmpty(action.getDescription()) ?
+ View.GONE : View.VISIBLE);
+ }
+ // Clients might want the check mark view to be gone entirely, in which case, ignore it.
+ if (vh.mCheckmarkView != null && vh.mCheckmarkView.getVisibility() != View.GONE) {
+ vh.mCheckmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ if (vh.mContentView != null) {
+ ViewGroup.LayoutParams contentLp = vh.mContentView.getLayoutParams();
+ if (setIcon(vh.mIconView, action)) {
+ contentLp.width = mContentWidth;
+ } else {
+ contentLp.width = mContentWidthNoIcon;
+ }
+ vh.mContentView.setLayoutParams(contentLp);
+ }
+
+ if (vh.mChevronView != null) {
+ vh.mChevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
+ vh.mChevronView.setAlpha(action.isEnabled() ? mEnabledChevronAlpha :
+ mDisabledChevronAlpha);
+ }
+
+ if (action.hasMultilineDescription()) {
+ if (vh.mTitleView != null) {
+ vh.mTitleView.setMaxLines(mTitleMaxLines);
+ if (vh.mDescriptionView != null) {
+ vh.mDescriptionView.setMaxHeight(getDescriptionMaxHeight(vh.view.getContext(),
+ vh.mTitleView));
+ }
+ }
+ } else {
+ if (vh.mTitleView != null) {
+ vh.mTitleView.setMaxLines(mTitleMinLines);
+ }
+ if (vh.mDescriptionView != null) {
+ vh.mDescriptionView.setMaxLines(mDescriptionMinLines);
+ }
+ }
+ }
+
+ /**
+ * Animates the view holder's view (or subviews thereof) when the action has had its focus
+ * state changed.
+ * @param vh The view holder associated with the relevant action.
+ * @param focused True if the action has become focused, false if it has lost focus.
+ */
+ public void onAnimateItemFocused(ViewHolder vh, boolean focused) {
+ // No animations for this, currently, because the animation is done on
+ // mSelectorView
+ }
+
+ /**
+ * Animates the view holder's view (or subviews thereof) when the action has had its press
+ * state changed.
+ * @param vh The view holder associated with the relevant action.
+ * @param pressed True if the action has been pressed, false if it has been unpressed.
+ */
+ public void onAnimateItemPressed(ViewHolder vh, boolean pressed) {
+ int attr = pressed ? R.attr.guidedActionPressedAnimation :
+ R.attr.guidedActionUnpressedAnimation;
+ createAnimator(vh.view, attr).start();
+ }
+
+ /**
+ * Animates the view holder's view (or subviews thereof) when the action has had its check
+ * state changed.
+ * @param vh The view holder associated with the relevant action.
+ * @param checked True if the action has become checked, false if it has become unchecked.
+ */
+ public void onAnimateItemChecked(ViewHolder vh, boolean checked) {
+ final View checkView = vh.mCheckmarkView;
+ if (checkView != null) {
+ if (checked) {
+ checkView.setVisibility(View.VISIBLE);
+ createAnimator(checkView, R.attr.guidedActionCheckedAnimation).start();
+ } else {
+ Animator animator = createAnimator(checkView, R.attr.guidedActionCheckedAnimation);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ checkView.setVisibility(View.INVISIBLE);
+ }
+ });
+ animator.start();
+ }
+ }
+ }
+
+ /*
+ * ==========================================
+ * FragmentAnimationProvider overrides
+ * ==========================================
+ */
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityEnter(@NonNull List<Animator> animators) {
+ animators.add(createAnimator(mMainView, R.attr.guidedActionsEntryAnimation));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onActivityExit(@NonNull List<Animator> animators) {}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentEnter(@NonNull List<Animator> animators) {
+ animators.add(createAnimator(mActionsGridView, R.attr.guidedStepEntryAnimation));
+ animators.add(createAnimator(mSelectorView, R.attr.guidedStepEntryAnimation));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentExit(@NonNull List<Animator> animators) {
+ animators.add(createAnimator(mActionsGridView, R.attr.guidedStepExitAnimation));
+ animators.add(createAnimator(mSelectorView, R.attr.guidedStepExitAnimation));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentReenter(@NonNull List<Animator> animators) {
+ animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReentryAnimation));
+ animators.add(createAnimator(mSelectorView, R.attr.guidedStepReentryAnimation));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onFragmentReturn(@NonNull List<Animator> animators) {
+ animators.add(createAnimator(mActionsGridView, R.attr.guidedStepReturnAnimation));
+ animators.add(createAnimator(mSelectorView, R.attr.guidedStepReturnAnimation));
+ }
+
+ /*
+ * ==========================================
+ * Private methods
+ * ==========================================
+ */
+
+ private void updateSelectorView(View focusedChild) {
+ // Display the selector view.
+ int height = focusedChild.getHeight();
+ LayoutParams lp = mSelectorView.getLayoutParams();
+ lp.height = height;
+ mSelectorView.setLayoutParams(lp);
+ mSelectorView.setAlpha(1f);
+ }
+
+ private float getFloat(Context ctx, TypedValue typedValue, int attrId) {
+ ctx.getTheme().resolveAttribute(attrId, typedValue, true);
+ // Android resources don't have a native float type, so we have to use strings.
+ return Float.valueOf(ctx.getResources().getString(typedValue.resourceId));
+ }
+
+ private int getInteger(Context ctx, TypedValue typedValue, int attrId) {
+ ctx.getTheme().resolveAttribute(attrId, typedValue, true);
+ return ctx.getResources().getInteger(typedValue.resourceId);
+ }
+
+ private int getDimension(Context ctx, TypedValue typedValue, int attrId) {
+ ctx.getTheme().resolveAttribute(attrId, typedValue, true);
+ return ctx.getResources().getDimensionPixelSize(typedValue.resourceId);
+ }
+
+ private static Animator createAnimator(View v, int attrId) {
+ Context ctx = v.getContext();
+ TypedValue typedValue = new TypedValue();
+ ctx.getTheme().resolveAttribute(attrId, typedValue, true);
+ Animator animator = AnimatorInflater.loadAnimator(ctx, typedValue.resourceId);
+ animator.setTarget(v);
+ return animator;
+ }
+
+ private boolean setIcon(final ImageView iconView, GuidedAction action) {
+ Drawable icon = null;
+ if (iconView != null) {
+ Context context = iconView.getContext();
+ icon = action.getIcon();
+ if (icon != null) {
+ iconView.setImageDrawable(icon);
+ iconView.setVisibility(View.VISIBLE);
+ } else {
+ iconView.setVisibility(View.GONE);
+ }
+ }
+ return icon != null;
+ }
+
+ /**
+ * @return the max height in pixels the description can be such that the
+ * action nicely takes up the entire screen.
+ */
+ private int getDescriptionMaxHeight(Context context, TextView title) {
+ // The 2 multiplier on the title height calculation is a
+ // conservative estimate for font padding which can not be
+ // calculated at this stage since the view hasn't been rendered yet.
+ return (int)(mDisplayHeight - 2*mVerticalPadding - 2*mTitleMaxLines*title.getLineHeight());
+ }
+
+ /**
+ * SelectorAnimator
+ * Controls animation for selected item backgrounds
+ * TODO: Move into focus animation override?
+ */
+ private static class SelectorAnimator extends RecyclerView.OnScrollListener {
+
+ private final View mSelectorView;
+ private final ViewGroup mParentView;
+ private volatile boolean mFadedOut = true;
+
+ SelectorAnimator(View selectorView, ViewGroup parentView) {
+ mSelectorView = selectorView;
+ mParentView = parentView;
+ }
+
+ // We want to fade in the selector if we've stopped scrolling on it. If
+ // we're scrolling, we want to ensure to dim the selector if we haven't
+ // already. We dim the last highlighted view so that while a user is
+ // scrolling, nothing is highlighted.
+ @Override
+ public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+ Animator animator = null;
+ boolean fadingOut = false;
+ if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+ // The selector starts with a height of 0. In order to scale up from
+ // 0, we first need the set the height to 1 and scale from there.
+ View focusedChild = mParentView.getFocusedChild();
+ if (focusedChild != null) {
+ int selectorHeight = mSelectorView.getHeight();
+ float scaleY = (float) focusedChild.getHeight() / selectorHeight;
+ AnimatorSet animators = (AnimatorSet)createAnimator(mSelectorView,
+ R.attr.guidedActionsSelectorShowAnimation);
+ if (mFadedOut) {
+ // selector is completely faded out, so we can just scale before fading in.
+ mSelectorView.setScaleY(scaleY);
+ animator = animators.getChildAnimations().get(0);
+ } else {
+ // selector is not faded out, so we must animate the scale as we fade in.
+ ((ObjectAnimator)animators.getChildAnimations().get(1))
+ .setFloatValues(scaleY);
+ animator = animators;
+ }
+ }
+ } else {
+ animator = createAnimator(mSelectorView, R.attr.guidedActionsSelectorHideAnimation);
+ fadingOut = true;
+ }
+ if (animator != null) {
+ animator.addListener(new Listener(fadingOut));
+ animator.start();
+ }
+ }
+
+ /**
+ * Sets {@link BaseScrollAdapterFragment#mFadedOut}
+ * {@link BaseScrollAdapterFragment#mFadedOut} is true, iff
+ * {@link BaseScrollAdapterFragment#mSelectorView} has an alpha of 0
+ * (faded out). If false the view either has an alpha of 1 (visible) or
+ * is in the process of animating.
+ */
+ private class Listener implements Animator.AnimatorListener {
+ private boolean mFadingOut;
+ private boolean mCanceled;
+
+ public Listener(boolean fadingOut) {
+ mFadingOut = fadingOut;
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (!mFadingOut) {
+ mFadedOut = false;
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCanceled && mFadingOut) {
+ mFadedOut = true;
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
index feb9e55..c324844 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
@@ -94,14 +94,14 @@
@Override
public void onBind(final ItemBridgeAdapter.ViewHolder viewHolder) {
// Only when having an OnItemClickListner, we will attach the OnClickListener.
- if (getOnItemViewClickedListener() != null) {
+ if (mRowViewHolder.getOnItemViewClickedListener() != null) {
viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
mRowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView);
- if (getOnItemViewClickedListener() != null) {
- getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder,
+ if (mRowViewHolder.getOnItemViewClickedListener() != null) {
+ mRowViewHolder.getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder,
ibh.mItem, mRowViewHolder, (ListRow) mRowViewHolder.mRow);
}
}
@@ -111,7 +111,7 @@
@Override
public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) {
- if (getOnItemViewClickedListener() != null) {
+ if (mRowViewHolder.getOnItemViewClickedListener() != null) {
viewHolder.mHolder.view.setOnClickListener(null);
}
}
@@ -343,21 +343,21 @@
rowViewHolder.mGridView.getChildViewHolder(view);
if (mHoverCardPresenterSelector != null) {
- rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
- ibh.mItem);
+ rowViewHolder.mHoverCardViewSwitcher.select(
+ rowViewHolder.mGridView, view, ibh.mItem);
}
- if (getOnItemViewSelectedListener() != null) {
- getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem,
- rowViewHolder, rowViewHolder.mRow);
+ if (rowViewHolder.getOnItemViewSelectedListener() != null) {
+ rowViewHolder.getOnItemViewSelectedListener().onItemSelected(
+ ibh.mHolder, ibh.mItem, rowViewHolder, rowViewHolder.mRow);
}
}
} else {
if (mHoverCardPresenterSelector != null) {
rowViewHolder.mHoverCardViewSwitcher.unselect();
}
- if (getOnItemViewSelectedListener() != null) {
- getOnItemViewSelectedListener().onItemSelected(null, null,
- rowViewHolder, rowViewHolder.mRow);
+ if (rowViewHolder.getOnItemViewSelectedListener() != null) {
+ rowViewHolder.getOnItemViewSelectedListener().onItemSelected(
+ null, null, rowViewHolder, rowViewHolder.mRow);
}
}
}
@@ -431,8 +431,8 @@
}
if (selected) {
- if (getOnItemViewSelectedListener() != null) {
- getOnItemViewSelectedListener().onItemSelected(
+ if (holder.getOnItemViewSelectedListener() != null) {
+ holder.getOnItemViewSelectedListener().onItemSelected(
itemViewHolder.getViewHolder(), itemViewHolder.mItem, vh, vh.getRow());
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
index fc7b38c..9cfe3eb 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PlaybackControlsRowPresenter.java
@@ -174,8 +174,8 @@
public void onControlClicked(Presenter.ViewHolder itemViewHolder, Object item,
ControlBarPresenter.BoundData data) {
ViewHolder vh = ((BoundData) data).mRowViewHolder;
- if (getOnItemViewClickedListener() != null) {
- getOnItemViewClickedListener().onItemClicked(itemViewHolder, item,
+ if (vh.getOnItemViewClickedListener() != null) {
+ vh.getOnItemViewClickedListener().onItemClicked(itemViewHolder, item,
vh, vh.getRow());
}
if (mOnActionClickedListener != null && item instanceof Action) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
index cf5e36b..30e1b33 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
@@ -150,6 +150,8 @@
float mSelectLevel = 0f; // initially unselected
protected final ColorOverlayDimmer mColorDimmer;
private View.OnKeyListener mOnKeyListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
/**
* Constructor for ViewHolder.
@@ -241,11 +243,42 @@
public View.OnKeyListener getOnKeyListener() {
return mOnKeyListener;
}
+
+ /**
+ * Sets the listener for item or row selection. RowPresenter fires row selection
+ * event with null item. A subclass of RowPresenter e.g. {@link ListRowPresenter} may
+ * fire a selection event with selected item.
+ */
+ public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Returns the listener for item or row selection.
+ */
+ public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mOnItemViewSelectedListener;
+ }
+
+ /**
+ * Sets the listener for item click event. RowPresenter does nothing but subclass of
+ * RowPresenter may fire item click event if it has the concept of item.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ */
+ public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ }
+
+ /**
+ * Returns the listener for item click event.
+ */
+ public final OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
}
private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter();
- private OnItemViewSelectedListener mOnItemViewSelectedListener;
- private OnItemViewClickedListener mOnItemViewClickedListener;
boolean mSelectEffectEnabled = true;
int mSyncActivatePolicy = SYNC_ACTIVATED_TO_EXPANDED;
@@ -419,8 +452,8 @@
*/
protected void dispatchItemSelectedListener(ViewHolder vh, boolean selected) {
if (selected) {
- if (mOnItemViewSelectedListener != null) {
- mOnItemViewSelectedListener.onItemSelected(null, null, vh, vh.getRow());
+ if (vh.mOnItemViewSelectedListener != null) {
+ vh.mOnItemViewSelectedListener.onItemSelected(null, null, vh, vh.getRow());
}
}
}
@@ -573,40 +606,6 @@
}
/**
- * Set listener for item or row selection. RowPresenter fires row selection
- * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can
- * fire a selection event with selected item.
- */
- public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
- mOnItemViewSelectedListener = listener;
- }
-
- /**
- * Get listener for item or row selection.
- */
- public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
- return mOnItemViewSelectedListener;
- }
-
- /**
- * Set listener for item click event. RowPresenter does nothing but subclass of
- * RowPresenter may fire item click event if it does have a concept of item.
- * OnItemViewClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
- */
- public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
- mOnItemViewClickedListener = listener;
- }
-
- /**
- * Set listener for item click event.
- */
- public final OnItemViewClickedListener getOnItemViewClickedListener() {
- return mOnItemViewClickedListener;
- }
-
- /**
* Freeze/Unfreeze the row, typically used when transition starts/ends.
* This method is called by fragment, app should not call it directly.
*/
diff --git a/v17/tests/src/android/support/v17/leanback/widget/GridWidgetTest.java b/v17/tests/src/android/support/v17/leanback/widget/GridWidgetTest.java
index f19de7a..178d59a 100644
--- a/v17/tests/src/android/support/v17/leanback/widget/GridWidgetTest.java
+++ b/v17/tests/src/android/support/v17/leanback/widget/GridWidgetTest.java
@@ -36,6 +36,7 @@
public class GridWidgetTest extends ActivityInstrumentationTestCase2<GridActivity> {
private static final boolean HUMAN_DELAY = false;
+ private static final long WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS = 60000;
protected GridActivity mActivity;
protected Instrumentation mInstrumentation;
@@ -100,8 +101,12 @@
*/
protected void waitForScrollIdle(Runnable verify) throws Throwable {
Thread.sleep(100);
+ int total = 0;
while (mGridView.getLayoutManager().isSmoothScrolling() ||
mGridView.getScrollState() != BaseGridView.SCROLL_STATE_IDLE) {
+ if ((total += 100) >= WAIT_FOR_SCROLL_IDLE_TIMEOUT_MS) {
+ throw new RuntimeException("waitForScrollIdle Timeout");
+ }
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
@@ -1046,4 +1051,42 @@
}
}
+
+ public void testScrollToNoneExisting() throws Throwable {
+ mInstrumentation = getInstrumentation();
+ Intent intent = new Intent(mInstrumentation.getContext(), GridActivity.class);
+ intent.putExtra(GridActivity.EXTRA_LAYOUT_RESOURCE_ID,
+ R.layout.vertical_grid);
+ intent.putExtra(GridActivity.EXTRA_NUM_ITEMS, 100);
+ intent.putExtra(GridActivity.EXTRA_STAGGERED, false);
+ mOrientation = BaseGridView.VERTICAL;
+ mNumRows = 3;
+ initActivity(intent);
+
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.setSelectedPositionSmooth(99);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.setSelectedPositionSmooth(50);
+ }
+ });
+ Thread.sleep(100);
+ runTestOnUiThread(new Runnable() {
+ public void run() {
+ mGridView.requestLayout();
+ mGridView.setSelectedPositionSmooth(0);
+ }
+ });
+ waitForScrollIdle(mVerifyLayout);
+ humanDelay(500);
+
+ }
+
}
diff --git a/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java b/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java
index 0ae3a5c..ce6abf3 100644
--- a/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java
+++ b/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java
@@ -16,6 +16,7 @@
package android.support.v4.view.accessibility;
+import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
@@ -46,6 +47,22 @@
columnSpan, heading, selected);
}
+ public static CharSequence getError(Object info) {
+ return ((AccessibilityNodeInfo) info).getError();
+ }
+
+ public static void setError(Object info, CharSequence error) {
+ ((AccessibilityNodeInfo) info).setError(error);
+ }
+
+ public static void setLabelFor(Object info, View labeled) {
+ ((AccessibilityNodeInfo) info).setLabelFor(labeled);
+ }
+
+ public static void setLabelFor(Object info, View root, int virtualDescendantId) {
+ ((AccessibilityNodeInfo) info).setLabelFor(root, virtualDescendantId);
+ }
+
static class CollectionItemInfo {
public static boolean isSelected(Object info) {
return ((AccessibilityNodeInfo.CollectionItemInfo) info).isSelected();
diff --git a/v4/java/android/support/v4/app/Fragment.java b/v4/java/android/support/v4/app/Fragment.java
index f869e0a..eb6ab0d 100644
--- a/v4/java/android/support/v4/app/Fragment.java
+++ b/v4/java/android/support/v4/app/Fragment.java
@@ -31,6 +31,7 @@
import android.support.annotation.StringRes;
import android.support.v4.util.SimpleArrayMap;
import android.support.v4.util.DebugUtils;
+import android.support.v4.view.LayoutInflaterCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -932,7 +933,7 @@
public LayoutInflater getLayoutInflater(Bundle savedInstanceState) {
LayoutInflater result = mActivity.getLayoutInflater().cloneInContext(mActivity);
getChildFragmentManager(); // Init if needed; use raw implementation below.
- result.setFactory(mChildFragmentManager.getLayoutInflaterFactory());
+ LayoutInflaterCompat.setFactory(result, mChildFragmentManager.getLayoutInflaterFactory());
return result;
}
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index 9bcb8b6..566ffed 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -297,7 +297,7 @@
return super.onCreateView(name, context, attrs);
}
- final View v = mFragments.onCreateView(name, context, attrs);
+ final View v = mFragments.onCreateView(null, name, context, attrs);
if (v == null) {
return super.onCreateView(name, context, attrs);
}
diff --git a/v4/java/android/support/v4/app/FragmentManager.java b/v4/java/android/support/v4/app/FragmentManager.java
index f15bb79..6d41d45 100644
--- a/v4/java/android/support/v4/app/FragmentManager.java
+++ b/v4/java/android/support/v4/app/FragmentManager.java
@@ -30,11 +30,11 @@
import android.support.annotation.StringRes;
import android.support.v4.util.DebugUtils;
import android.support.v4.util.LogWriter;
+import android.support.v4.view.LayoutInflaterFactory;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
-import android.view.LayoutInflater;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
@@ -410,7 +410,7 @@
/**
* Container for fragments associated with an activity.
*/
-final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory {
+final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory {
static boolean DEBUG = false;
static final String TAG = "FragmentManager";
@@ -2118,7 +2118,7 @@
}
@Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
+ public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (!"fragment".equals(name)) {
return null;
}
@@ -2138,7 +2138,6 @@
return null;
}
- View parent = null; // NOTE: no way to get parent pre-Honeycomb.
int containerId = parent != null ? parent.getId() : 0;
if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {
throw new IllegalArgumentException(attrs.getPositionDescription()
@@ -2210,7 +2209,7 @@
return fragment.mView;
}
- LayoutInflater.Factory getLayoutInflaterFactory() {
+ LayoutInflaterFactory getLayoutInflaterFactory() {
return this;
}
diff --git a/v4/java/android/support/v4/graphics/ColorUtils.java b/v4/java/android/support/v4/graphics/ColorUtils.java
index 91c61af..f2d56da 100644
--- a/v4/java/android/support/v4/graphics/ColorUtils.java
+++ b/v4/java/android/support/v4/graphics/ColorUtils.java
@@ -32,18 +32,27 @@
* Composite two potentially translucent colors over each other and returns the result.
*/
public static int compositeColors(int foreground, int background) {
- final float alpha1 = Color.alpha(foreground) / 255f;
- final float alpha2 = Color.alpha(background) / 255f;
+ int bgAlpha = Color.alpha(background);
+ int fgAlpha = Color.alpha(foreground);
+ int a = compositeAlpha(fgAlpha, bgAlpha);
- float a = (alpha1 + alpha2) * (1f - alpha1);
- float r = (Color.red(foreground) * alpha1)
- + (Color.red(background) * alpha2 * (1f - alpha1));
- float g = (Color.green(foreground) * alpha1)
- + (Color.green(background) * alpha2 * (1f - alpha1));
- float b = (Color.blue(foreground) * alpha1)
- + (Color.blue(background) * alpha2 * (1f - alpha1));
+ int r = compositeComponent(Color.red(foreground), fgAlpha,
+ Color.red(background), bgAlpha, a);
+ int g = compositeComponent(Color.green(foreground), fgAlpha,
+ Color.green(background), bgAlpha, a);
+ int b = compositeComponent(Color.blue(foreground), fgAlpha,
+ Color.blue(background), bgAlpha, a);
- return Color.argb((int) a, (int) r, (int) g, (int) b);
+ return Color.argb(a, r, g, b);
+ }
+
+ private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
+ return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
+ }
+
+ private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
+ if (a == 0) return 0;
+ return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
}
/**
diff --git a/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
index 055598f..1b239a7 100644
--- a/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -279,6 +279,12 @@
public AccessibilityNodeInfoCompat getTraversalAfter(Object info);
public void setTraversalAfter(Object info, View view);
public void setTraversalAfter(Object info, View root, int virtualDescendantId);
+ public void setContentInvalid(Object info, boolean contentInvalid);
+ public boolean isContentInvalid(Object info);
+ public void setError(Object info, CharSequence error);
+ public CharSequence getError(Object info);
+ public void setLabelFor(Object info, View labeled);
+ public void setLabelFor(Object info, View root, int virtualDescendantId);
}
static class AccessibilityNodeInfoStubImpl implements AccessibilityNodeInfoImpl {
@@ -732,6 +738,32 @@
@Override
public void setTraversalAfter(Object info, View root, int virtualDescendantId) {
}
+
+ @Override
+ public void setContentInvalid(Object info, boolean contentInvalid) {
+ }
+
+ @Override
+ public boolean isContentInvalid(Object info) {
+ return false;
+ }
+
+ @Override
+ public void setError(Object info, CharSequence error) {
+ }
+
+ @Override
+ public CharSequence getError(Object info) {
+ return null;
+ }
+
+ @Override
+ public void setLabelFor(Object info, View labeled) {
+ }
+
+ @Override
+ public void setLabelFor(Object info, View root, int virtualDescendantId) {
+ }
}
static class AccessibilityNodeInfoIcsImpl extends AccessibilityNodeInfoStubImpl {
@@ -1140,6 +1172,16 @@
public void setCollectionItemInfo(Object info, Object collectionItemInfo) {
AccessibilityNodeInfoCompatKitKat.setCollectionItemInfo(info, collectionItemInfo);
}
+
+ @Override
+ public void setContentInvalid(Object info, boolean contentInvalid) {
+ AccessibilityNodeInfoCompatKitKat.setContentInvalid(info, contentInvalid);
+ }
+
+ @Override
+ public boolean isContentInvalid(Object info) {
+ return AccessibilityNodeInfoCompatKitKat.isContentInvalid(info);
+ }
}
static class AccessibilityNodeInfoApi21Impl extends AccessibilityNodeInfoKitKatImpl {
@@ -1186,6 +1228,26 @@
public boolean isCollectionItemSelected(Object info) {
return AccessibilityNodeInfoCompatApi21.CollectionItemInfo.isSelected(info);
}
+
+ @Override
+ public CharSequence getError(Object info) {
+ return AccessibilityNodeInfoCompatApi21.getError(info);
+ }
+
+ @Override
+ public void setError(Object info, CharSequence error) {
+ AccessibilityNodeInfoCompatApi21.setError(info, error);
+ }
+
+ @Override
+ public void setLabelFor(Object info, View labeled) {
+ AccessibilityNodeInfoCompatApi21.setLabelFor(info, labeled);
+ }
+
+ @Override
+ public void setLabelFor(Object info, View root, int virtualDescendantId) {
+ AccessibilityNodeInfoCompatApi21.setLabelFor(info, root, virtualDescendantId);
+ }
}
static class AccessibilityNodeInfoApi22Impl extends AccessibilityNodeInfoApi21Impl {
@@ -2531,6 +2593,83 @@
}
}
+ /**
+ * Sets if the content of this node is invalid. For example,
+ * a date is not well-formed.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param contentInvalid If the node content is invalid.
+ */
+ public void setContentInvalid(boolean contentInvalid) {
+ IMPL.setContentInvalid(mInfo, contentInvalid);
+ }
+
+ /**
+ * Gets if the content of this node is invalid. For example,
+ * a date is not well-formed.
+ *
+ * @return If the node content is invalid.
+ */
+ public boolean isContentInvalid() {
+ return IMPL.isContentInvalid(mInfo);
+ }
+
+ /**
+ * Sets the error text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param error The error text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setError(CharSequence error) {
+ IMPL.setError(mInfo, error);
+ }
+
+ /**
+ * Gets the error text of this node.
+ *
+ * @return The error text.
+ */
+ public CharSequence getError() {
+ return IMPL.getError(mInfo);
+ }
+
+ /**
+ * Sets the view for which the view represented by this info serves as a
+ * label for accessibility purposes.
+ *
+ * @param labeled The view for which this info serves as a label.
+ */
+ public void setLabelFor(View labeled) {
+ IMPL.setLabelFor(mInfo, labeled);
+ }
+
+ /**
+ * Sets the view for which the view represented by this info serves as a
+ * label for accessibility purposes. If <code>virtualDescendantId</code>
+ * is {@link View#NO_ID} the root is set as the labeled.
+ * <p>
+ * A virtual descendant is an imaginary View that is reported as a part of the view
+ * hierarchy for accessibility purposes. This enables custom views that draw complex
+ * content to report themselves as a tree of virtual views, thus conveying their
+ * logical structure.
+ * </p>
+ *
+ * @param root The root whose virtual descendant serves as a label.
+ * @param virtualDescendantId The id of the virtual descendant.
+ */
+ public void setLabelFor(View root, int virtualDescendantId) {
+ IMPL.setLabelFor(mInfo, root, virtualDescendantId);
+ }
@Override
public int hashCode() {
diff --git a/v4/java/android/support/v4/widget/ViewDragHelper.java b/v4/java/android/support/v4/widget/ViewDragHelper.java
index c6bebd3..3f4e571 100644
--- a/v4/java/android/support/v4/widget/ViewDragHelper.java
+++ b/v4/java/android/support/v4/widget/ViewDragHelper.java
@@ -1003,6 +1003,8 @@
}
case MotionEvent.ACTION_MOVE: {
+ if (mInitialMotionX == null || mInitialMotionY == null) break;
+
// First to cross a touch slop over a draggable view wins. Also report edge drags.
final int pointerCount = MotionEventCompat.getPointerCount(ev);
for (int i = 0; i < pointerCount; i++) {
diff --git a/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java b/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
index 16af9bd..32fb86f 100644
--- a/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
+++ b/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
@@ -63,6 +63,14 @@
columnSpan, heading);
}
+ public static void setContentInvalid(Object info, boolean contentInvalid) {
+ ((AccessibilityNodeInfo) info).setContentInvalid(contentInvalid);
+ }
+
+ public static boolean isContentInvalid(Object info) {
+ return ((AccessibilityNodeInfo) info).isContentInvalid();
+ }
+
static class CollectionInfo {
static int getColumnCount(Object info) {
return ((AccessibilityNodeInfo.CollectionInfo) info).getColumnCount();
diff --git a/v7/appcompat/src/android/support/v7/internal/widget/TintManager.java b/v7/appcompat/src/android/support/v7/internal/widget/TintManager.java
index e041005..777f5a0 100644
--- a/v7/appcompat/src/android/support/v7/internal/widget/TintManager.java
+++ b/v7/appcompat/src/android/support/v7/internal/widget/TintManager.java
@@ -24,6 +24,7 @@
import android.graphics.drawable.LayerDrawable;
import android.os.Build;
import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.ColorUtils;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.util.LruCache;
import android.support.v7.appcompat.R;
@@ -440,22 +441,25 @@
final int[] colors = new int[4];
int i = 0;
+ final int colorButtonNormal = getThemeAttrColor(context, R.attr.colorButtonNormal);
+ final int colorControlHighlight = getThemeAttrColor(context, R.attr.colorControlHighlight);
+
// Disabled state
states[i] = ThemeUtils.DISABLED_STATE_SET;
colors[i] = getDisabledThemeAttrColor(context, R.attr.colorButtonNormal);
i++;
states[i] = ThemeUtils.PRESSED_STATE_SET;
- colors[i] = getThemeAttrColor(context, R.attr.colorControlHighlight);
+ colors[i] = ColorUtils.compositeColors(colorControlHighlight, colorButtonNormal);
i++;
states[i] = ThemeUtils.FOCUSED_STATE_SET;
- colors[i] = getThemeAttrColor(context, R.attr.colorControlHighlight);
+ colors[i] = ColorUtils.compositeColors(colorControlHighlight, colorButtonNormal);
i++;
// Default enabled state
states[i] = ThemeUtils.EMPTY_STATE_SET;
- colors[i] = getThemeAttrColor(context, R.attr.colorButtonNormal);
+ colors[i] = colorButtonNormal;
i++;
return new ColorStateList(states, colors);
@@ -515,6 +519,12 @@
} else {
background.clearColorFilter();
}
+
+ if (Build.VERSION.SDK_INT <= 10) {
+ // On Gingerbread, GradientDrawable does not invalidate itself when it's ColorFilter
+ // has changed, so we need to force an invalidation
+ view.invalidate();
+ }
}
private static void setPorterDuffColorFilter(Drawable d, int color, PorterDuff.Mode mode) {
diff --git a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
index ad5c7f8..f64dee4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/GridLayoutManager.java
@@ -42,7 +42,10 @@
*/
static final int MAIN_DIR_SPEC =
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
-
+ /**
+ * Span size have been changed but we've not done a new layout calculation.
+ */
+ boolean mPendingSpanCountChange = false;
int mSpanCount = DEFAULT_SPAN_COUNT;
/**
* The size of each span
@@ -153,6 +156,9 @@
validateChildOrder();
}
clearPreLayoutSpanMappingCache();
+ if (!state.isPreLayout()) {
+ mPendingSpanCountChange = false;
+ }
}
private void clearPreLayoutSpanMappingCache() {
@@ -553,6 +559,7 @@
if (spanCount == mSpanCount) {
return;
}
+ mPendingSpanCountChange = true;
if (spanCount < 1) {
throw new IllegalArgumentException("Span count should be at least 1. Provided "
+ spanCount);
@@ -732,7 +739,7 @@
@Override
public boolean supportsPredictiveItemAnimations() {
- return mPendingSavedState == null;
+ return mPendingSavedState == null && !mPendingSpanCountChange;
}
/**
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index 058a565..1702a32 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -895,7 +895,8 @@
return;
}
if (DEBUG) {
- Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState, new Exception());
+ Log.d(TAG, "setting scroll state to " + state + " from " + mScrollState,
+ new Exception());
}
mScrollState = state;
if (state != SCROLL_STATE_SETTLING) {
@@ -1570,7 +1571,7 @@
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
- if (!mLayout.onAddFocusables(this, views, direction, focusableMode)) {
+ if (mLayout == null || !mLayout.onAddFocusables(this, views, direction, focusableMode)) {
super.addFocusables(views, direction, focusableMode);
}
}
@@ -3233,6 +3234,24 @@
}
}
+ /**
+ * Returns whether there are pending adapter updates which are not yet applied to the layout.
+ * <p>
+ * If this method returns <code>true</code>, it means that what user is currently seeing may not
+ * reflect them adapter contents (depending on what has changed).
+ * You may use this information to defer or cancel some operations.
+ * <p>
+ * This method returns true if RecyclerView has not yet calculated the first layout after it is
+ * attached to the Window or the Adapter has been replaced.
+ *
+ * @return True if there are some adapter updates which are not yet reflected to layout or false
+ * if layout is up to date.
+ */
+ public boolean hasPendingAdapterUpdates() {
+ return !mFirstLayoutComplete || mDataSetHasChangedAfterLayout
+ || mAdapterHelper.hasPendingUpdates();
+ }
+
private class ViewFlinger implements Runnable {
private int mLastFlingX;
private int mLastFlingY;
@@ -6402,16 +6421,21 @@
final int offScreenBottom = Math.max(0, childBottom - parentBottom);
// Favor the "start" layout direction over the end when bringing one side or the other
- // of a large rect into view.
+ // of a large rect into view. If we decide to bring in end because start is already
+ // visible, limit the scroll such that start won't go out of bounds.
final int dx;
- if (ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL) {
- dx = offScreenRight != 0 ? offScreenRight : offScreenLeft;
+ if (getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ dx = offScreenRight != 0 ? offScreenRight
+ : Math.max(offScreenLeft, childRight - parentRight);
} else {
- dx = offScreenLeft != 0 ? offScreenLeft : offScreenRight;
+ dx = offScreenLeft != 0 ? offScreenLeft
+ : Math.min(childLeft - parentLeft, offScreenRight);
}
- // Favor bringing the top into view over the bottom
- final int dy = offScreenTop != 0 ? offScreenTop : offScreenBottom;
+ // Favor bringing the top into view over the bottom. If top is already visible and
+ // we should scroll to make bottom visible, make sure top does not go out of bounds.
+ final int dy = offScreenTop != 0 ? offScreenTop
+ : Math.min(childTop - parentTop, offScreenBottom);
if (dx != 0 || dy != 0) {
if (immediate) {
@@ -6782,7 +6806,6 @@
*/
public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
AccessibilityNodeInfoCompat info) {
- info.setClassName(RecyclerView.class.getName());
if (ViewCompat.canScrollVertically(mRecyclerView, -1) ||
ViewCompat.canScrollHorizontally(mRecyclerView, -1)) {
info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java
index ed7dfd6..3fe9abc 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerViewAccessibilityDelegate.java
@@ -35,12 +35,16 @@
mRecyclerView = recyclerView;
}
+ private boolean shouldIgnore() {
+ return mRecyclerView.hasPendingAdapterUpdates();
+ }
+
@Override
public boolean performAccessibilityAction(View host, int action, Bundle args) {
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
- if (mRecyclerView.getLayoutManager() != null) {
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().performAccessibilityAction(action, args);
}
@@ -51,7 +55,7 @@
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
info.setClassName(RecyclerView.class.getName());
- if (mRecyclerView.getLayoutManager() != null) {
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(info);
}
}
@@ -60,7 +64,7 @@
public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(host, event);
event.setClassName(RecyclerView.class.getName());
- if (host instanceof RecyclerView) {
+ if (host instanceof RecyclerView && !shouldIgnore()) {
RecyclerView rv = (RecyclerView) host;
if (rv.getLayoutManager() != null) {
rv.getLayoutManager().onInitializeAccessibilityEvent(event);
@@ -76,7 +80,7 @@
@Override
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(host, info);
- if (mRecyclerView.getLayoutManager() != null) {
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
mRecyclerView.getLayoutManager().
onInitializeAccessibilityNodeInfoForItem(host, info);
}
@@ -87,7 +91,7 @@
if (super.performAccessibilityAction(host, action, args)) {
return true;
}
- if (mRecyclerView.getLayoutManager() != null) {
+ if (!shouldIgnore() && mRecyclerView.getLayoutManager() != null) {
return mRecyclerView.getLayoutManager().
performAccessibilityActionForItem(host, action, args);
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index f58bce2..0d7a685 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -33,6 +33,8 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
abstract public class BaseRecyclerViewInstrumentationTest extends
ActivityInstrumentationTestCase2<TestActivity> {
@@ -338,7 +340,26 @@
return super.toString() + " item:" + mBoundItem;
}
}
+ class DumbLayoutManager extends TestLayoutManager {
+ ReentrantLock mLayoutLock = new ReentrantLock();
+ public void blockLayout() {
+ mLayoutLock.lock();
+ }
+ public void unblockLayout() {
+ mLayoutLock.unlock();
+ }
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ mLayoutLock.lock();
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, 0, state.getItemCount());
+ if (layoutLatch != null) {
+ layoutLatch.countDown();
+ }
+ mLayoutLock.unlock();
+ }
+ }
class TestLayoutManager extends RecyclerView.LayoutManager {
CountDownLatch layoutLatch;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
index be3557b..fa31494 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerTest.java
@@ -528,6 +528,26 @@
}
}
+ public void testSpanSizeChange() throws Throwable {
+ final RecyclerView rv = setupBasic(new Config(3, 100));
+ waitForFirstLayout(rv);
+ assertTrue(mGlm.supportsPredictiveItemAnimations());
+ mGlm.expectLayout(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGlm.setSpanCount(5);
+ assertFalse(mGlm.supportsPredictiveItemAnimations());
+ }
+ });
+ checkForMainThreadException();
+ mGlm.waitForLayout(2);
+ mGlm.expectLayout(2);
+ mAdapter.deleteAndNotify(3, 2);
+ mGlm.waitForLayout(2);
+ assertTrue(mGlm.supportsPredictiveItemAnimations());
+ }
+
public void testCacheSpanIndices() throws Throwable {
final RecyclerView rv = setupBasic(new Config(3, 100));
mGlm.mSpanSizeLookup.setSpanIndexCacheEnabled(true);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityTest.java
index f08b3c0..42ad90a 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAccessibilityTest.java
@@ -26,6 +26,11 @@
import java.util.concurrent.atomic.AtomicBoolean;
public class RecyclerViewAccessibilityTest extends BaseRecyclerViewInstrumentationTest {
+
+ public RecyclerViewAccessibilityTest() {
+ super(false);
+ }
+
public void testOnInitializeAccessibilityNodeInfo() throws Throwable {
for (boolean vBefore : new boolean[]{true, false}) {
for (boolean vAfter : new boolean[]{true, false}) {
@@ -39,6 +44,7 @@
}
}
}
+
public void onInitializeAccessibilityNodeInfoTest(final boolean verticalScrollBefore,
final boolean horizontalScrollBefore, final boolean verticalScrollAfter,
final boolean horizontalScrollAfter) throws Throwable {
@@ -200,15 +206,58 @@
assertEquals(verticalScrollAfter, vScrolledFwd.get());
}
- void performAccessibilityAction(final AccessibilityDelegateCompat delegate,
- final RecyclerView recyclerView, final int action) throws Throwable {
+ public void testIgnoreAccessibilityIfAdapterHasChanged() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity()) {
+ //@Override
+ public boolean canScrollHorizontally(int direction) {
+ return true;
+ }
+
+ //@Override
+ public boolean canScrollVertically(int direction) {
+ return true;
+ }
+ };
+ final DumbLayoutManager layoutManager = new DumbLayoutManager();
+ final TestAdapter adapter = new TestAdapter(10);
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(layoutManager);
+ layoutManager.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ layoutManager.waitForLayout(1);
+
+ final RecyclerViewAccessibilityDelegate delegateCompat = recyclerView
+ .getCompatAccessibilityDelegate();
+ final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
runTestOnUiThread(new Runnable() {
@Override
public void run() {
- delegate.performAccessibilityAction(recyclerView, action, null);
+ delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info);
+ }
+ });
+ assertTrue("test sanity", info.isScrollable());
+ final AccessibilityNodeInfoCompat info2 = AccessibilityNodeInfoCompat.obtain();
+ layoutManager.blockLayout();
+ layoutManager.expectLayouts(1);
+ adapter.deleteAndNotify(1, 1);
+ // we can run this here since we blocked layout.
+ delegateCompat.onInitializeAccessibilityNodeInfo(recyclerView, info2);
+ layoutManager.unblockLayout();
+ assertFalse("info should not be filled if data is out of date", info2.isScrollable());
+ layoutManager.waitForLayout(1);
+ }
+
+ boolean performAccessibilityAction(final AccessibilityDelegateCompat delegate,
+ final RecyclerView recyclerView, final int action) throws Throwable {
+ final boolean[] result = new boolean[1];
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ result[0] = delegate.performAccessibilityAction(recyclerView, action, null);
}
});
getInstrumentation().waitForIdleSync();
Thread.sleep(250);
+ return result[0];
}
}
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 178225d..9384450 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -17,9 +17,9 @@
package android.support.v7.widget;
+import android.graphics.Color;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.os.Debug;
import android.os.SystemClock;
import android.support.v4.view.ViewCompat;
import android.test.TouchUtils;
@@ -30,6 +30,7 @@
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
import android.widget.TextView;
import java.util.ArrayList;
@@ -304,6 +305,89 @@
return true;
}
+ private void assertPendingUpdatesAndLayout(TestLayoutManager testLayoutManager,
+ final Runnable runnable) throws Throwable {
+ testLayoutManager.expectLayouts(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ runnable.run();
+ assertTrue(mRecyclerView.hasPendingAdapterUpdates());
+ }
+ });
+ testLayoutManager.waitForLayout(1);
+ assertFalse(mRecyclerView.hasPendingAdapterUpdates());
+ }
+
+ private void setupBasic(RecyclerView recyclerView, TestLayoutManager tlm,
+ TestAdapter adapter, boolean waitForFirstLayout) throws Throwable {
+ recyclerView.setLayoutManager(tlm);
+ recyclerView.setAdapter(adapter);
+ if (waitForFirstLayout) {
+ tlm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ tlm.waitForLayout(1);
+ } else {
+ setRecyclerView(recyclerView);
+ }
+ }
+
+ public void testHasPendingUpdatesBeforeFirstLayout() throws Throwable {
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ TestLayoutManager layoutManager = new DumbLayoutManager();
+ TestAdapter testAdapter = new TestAdapter(10);
+ setupBasic(recyclerView, layoutManager, testAdapter, false);
+ assertTrue(mRecyclerView.hasPendingAdapterUpdates());
+ }
+
+ public void testNoPendingUpdatesAfterLayout() throws Throwable {
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ TestLayoutManager layoutManager = new DumbLayoutManager();
+ TestAdapter testAdapter = new TestAdapter(10);
+ setupBasic(recyclerView, layoutManager, testAdapter, true);
+ assertFalse(mRecyclerView.hasPendingAdapterUpdates());
+ }
+
+ public void testHasPendingUpdatesWhenAdapterIsChanged() throws Throwable {
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ TestLayoutManager layoutManager = new DumbLayoutManager();
+ final TestAdapter testAdapter = new TestAdapter(10);
+ setupBasic(recyclerView, layoutManager, testAdapter, false);
+ assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyItemRemoved(1);
+ }
+ });
+ assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyItemInserted(2);
+ }
+ });
+
+ assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyItemMoved(2, 3);
+ }
+ });
+
+ assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyItemChanged(2);
+ }
+ });
+
+ assertPendingUpdatesAndLayout(layoutManager, new Runnable() {
+ @Override
+ public void run() {
+ testAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+
public void testTransientStateRecycleViaAdapter() throws Throwable {
transientStateRecycleTest(true, false);
}
@@ -2390,6 +2474,141 @@
checkForMainThreadException();
}
+ public void testFocusBigViewOnTop() throws Throwable {
+ focusTooBigViewTest(Gravity.TOP);
+ }
+
+ public void testFocusBigViewOnLeft() throws Throwable {
+ focusTooBigViewTest(Gravity.LEFT);
+ }
+
+ public void testFocusBigViewOnRight() throws Throwable {
+ focusTooBigViewTest(Gravity.RIGHT);
+ }
+
+ public void testFocusBigViewOnBottom() throws Throwable {
+ focusTooBigViewTest(Gravity.BOTTOM);
+ }
+
+ public void testFocusBigViewOnLeftRTL() throws Throwable {
+ focusTooBigViewTest(Gravity.LEFT, true);
+ assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
+ mRecyclerView.getLayoutManager().getLayoutDirection());
+ }
+
+ public void testFocusBigViewOnRightRTL() throws Throwable {
+ focusTooBigViewTest(Gravity.RIGHT, true);
+ assertEquals("test sanity", ViewCompat.LAYOUT_DIRECTION_RTL,
+ mRecyclerView.getLayoutManager().getLayoutDirection());
+ }
+
+ public void focusTooBigViewTest(final int gravity) throws Throwable {
+ focusTooBigViewTest(gravity, false);
+ }
+ public void focusTooBigViewTest(final int gravity, final boolean rtl) throws Throwable {
+ RecyclerView rv = new RecyclerView(getActivity());
+ if (rtl) {
+ ViewCompat.setLayoutDirection(rv, ViewCompat.LAYOUT_DIRECTION_RTL);
+ }
+ final AtomicInteger vScrollDist = new AtomicInteger(0);
+ final AtomicInteger hScrollDist = new AtomicInteger(0);
+ final AtomicInteger vDesiredDist = new AtomicInteger(0);
+ final AtomicInteger hDesiredDist = new AtomicInteger(0);
+ TestLayoutManager tlm = new TestLayoutManager() {
+
+ @Override
+ public int getLayoutDirection() {
+ return rtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR;
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ final View view = recycler.getViewForPosition(0);
+ addView(view);
+ int left = 0, top = 0;
+ view.setBackgroundColor(Color.rgb(0, 0, 255));
+ switch (gravity) {
+ case Gravity.LEFT:
+ case Gravity.RIGHT:
+ view.measure(
+ View.MeasureSpec.makeMeasureSpec((int) (getWidth() * 1.5),
+ View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec((int) (getHeight() * .9),
+ View.MeasureSpec.AT_MOST));
+ left = gravity == Gravity.LEFT ? getWidth() - view.getMeasuredWidth() - 80
+ : 90;
+ top = 0;
+ if (ViewCompat.LAYOUT_DIRECTION_RTL == getLayoutDirection()) {
+ hDesiredDist.set((left + view.getMeasuredWidth()) - getWidth());
+ } else {
+ hDesiredDist.set(left);
+ }
+ break;
+ case Gravity.TOP:
+ case Gravity.BOTTOM:
+ view.measure(
+ View.MeasureSpec.makeMeasureSpec((int) (getWidth() * .9),
+ View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec((int) (getHeight() * 1.5),
+ View.MeasureSpec.EXACTLY));
+ top = gravity == Gravity.TOP ? getHeight() - view.getMeasuredHeight() -
+ 80 : 90;
+ left = 0;
+ vDesiredDist.set(top);
+ break;
+ }
+
+ view.layout(left, top, left + view.getMeasuredWidth(),
+ top + view.getMeasuredHeight());
+ layoutLatch.countDown();
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return true;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return super.canScrollHorizontally();
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ vScrollDist.addAndGet(dy);
+ getChildAt(0).offsetTopAndBottom(-dy);
+ return dy;
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ hScrollDist.addAndGet(dx);
+ getChildAt(0).offsetLeftAndRight(-dx);
+ return dx;
+ }
+ };
+ TestAdapter adapter = new TestAdapter(10);
+ rv.setAdapter(adapter);
+ rv.setLayoutManager(tlm);
+ tlm.expectLayouts(1);
+ setRecyclerView(rv);
+ tlm.waitForLayout(2);
+ View view = rv.getChildAt(0);
+ requestFocus(view);
+ Thread.sleep(1000);
+ assertEquals(vDesiredDist.get(), vScrollDist.get());
+ assertEquals(hDesiredDist.get(), hScrollDist.get());
+ assertEquals(mRecyclerView.getPaddingTop(), view.getTop());
+ if (rtl) {
+ assertEquals(mRecyclerView.getWidth() - mRecyclerView.getPaddingRight(), view.getRight());
+ } else {
+ assertEquals(mRecyclerView.getPaddingLeft(), view.getLeft());
+ }
+ }
+
public void testFocusRectOnScreenWithDecorOffsets() throws Throwable {
focusRectOnScreenTest(true);
}