diff options
author | Arthur Hsu <arthurhsu@google.com> | 2016-05-05 11:06:16 -0700 |
---|---|---|
committer | Arthur Hsu <arthurhsu@google.com> | 2016-05-05 11:06:16 -0700 |
commit | d572fa97fb6bae8e3089241645e98dd38b75f345 (patch) | |
tree | 04ec463a978c02655712b8d1c0a945e08e1e2127 | |
parent | 365509ca70a12218fae9df7030152a0c0dd9e5df (diff) | |
parent | 7ce5036f2c1a3486fde5c849378a74ba926ba968 (diff) | |
download | setupwizard-d572fa97fb6bae8e3089241645e98dd38b75f345.tar.gz |
Merge remote-tracking branch 'goog/nyc-andromeda-dev'; commit '7ce5036f2c1a3486fde5c849378a74ba926ba968' into merge
77 files changed, 3621 insertions, 322 deletions
diff --git a/library/eclair-mr1/res/layout/suw_items_switch.xml b/library/eclair-mr1/res/layout/suw_items_switch.xml new file mode 100644 index 0000000..14c7650 --- /dev/null +++ b/library/eclair-mr1/res/layout/suw_items_switch.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + style="@style/SuwItemContainer.Verbose" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:descendantFocusability="blocksDescendants" + android:orientation="horizontal"> + + <FrameLayout + android:id="@+id/suw_items_icon_container" + android:layout_width="@dimen/suw_items_icon_container_width" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:gravity="start"> + + <ImageView + android:id="@+id/suw_items_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + tools:ignore="ContentDescription" /> + + </FrameLayout> + + <LinearLayout + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginBottom="@dimen/suw_items_verbose_padding_bottom_extra" + android:layout_weight="1" + android:orientation="vertical"> + + <TextView + android:id="@+id/suw_items_title" + style="@style/SuwItemTitle.Verbose" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start" + android:textAlignment="viewStart" + tools:ignore="UnusedAttribute" /> + + <TextView + android:id="@+id/suw_items_summary" + style="@style/SuwItemSummary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start" + android:textAlignment="viewStart" + android:visibility="gone" + tools:ignore="UnusedAttribute" /> + + </LinearLayout> + + <android.support.v7.widget.SwitchCompat + android:id="@+id/suw_items_switch" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" /> + +</LinearLayout> diff --git a/library/eclair-mr1/res/values/attrs.xml b/library/eclair-mr1/res/values/attrs.xml new file mode 100644 index 0000000..34cf3da --- /dev/null +++ b/library/eclair-mr1/res/values/attrs.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + + <declare-styleable name="SuwSwitchItem"> + <attr name="android:checked" /> + </declare-styleable> + +</resources> diff --git a/library/eclair-mr1/res/values/styles.xml b/library/eclair-mr1/res/values/styles.xml index 7e467ac..0b2dcda 100644 --- a/library/eclair-mr1/res/values/styles.xml +++ b/library/eclair-mr1/res/values/styles.xml @@ -26,6 +26,7 @@ <item name="android:listPreferredItemHeight">@dimen/suw_items_preferred_height</item> <item name="android:navigationBarColor" tools:ignore="NewApi">@android:color/black</item> <item name="android:statusBarColor" tools:ignore="NewApi">@android:color/black</item> + <item name="android:textAppearanceListItemSmall" tools:ignore="NewApi">?attr/textAppearanceListItemSmall</item> <item name="android:textColorLink">@color/suw_link_color_dark</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowDisablePreview">true</item> @@ -38,7 +39,7 @@ <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item> <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item> <item name="suwNavBarTheme">@style/SuwNavBarThemeDark</item> - <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item> + <item name="textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item> </style> <style name="SuwThemeMaterial.Light" parent="Theme.AppCompat.Light.NoActionBar"> @@ -48,6 +49,7 @@ <item name="android:listPreferredItemHeight">@dimen/suw_items_preferred_height</item> <item name="android:navigationBarColor" tools:ignore="NewApi">@android:color/black</item> <item name="android:statusBarColor" tools:ignore="NewApi">@android:color/black</item> + <item name="android:textAppearanceListItemSmall" tools:ignore="NewApi">?attr/textAppearanceListItemSmall</item> <item name="android:textColorLink">@color/suw_link_color_light</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowDisablePreview">true</item> @@ -60,7 +62,7 @@ <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item> <item name="suwMarginSides">@dimen/suw_layout_margin_sides</item> <item name="suwNavBarTheme">@style/SuwNavBarThemeLight</item> - <item name="textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Body1</item> + <item name="textAppearanceListItemSmall">@style/TextAppearance.SuwItemSummary</item> </style> <!-- Placeholder for GLIF dark theme, colors are not updated yet --> @@ -71,13 +73,14 @@ <item name="android:listPreferredItemHeight">@dimen/suw_items_preferred_height</item> <item name="android:navigationBarColor" tools:ignore="NewApi">@android:color/black</item> <item name="android:statusBarColor" tools:ignore="NewApi">@android:color/transparent</item> + <item name="android:textAppearanceListItemSmall" tools:ignore="NewApi">?attr/textAppearanceListItemSmall</item> <item name="android:textColorLink">@color/suw_link_color_light</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowDisablePreview">true</item> <item name="android:windowSoftInputMode">adjustResize</item> - <item name="colorAccent">@color/suw_color_accent_light</item> - <item name="colorPrimary">@color/suw_color_accent_light</item> + <item name="colorAccent">@color/suw_color_accent_glif_dark</item> + <item name="colorPrimary">@color/suw_color_accent_glif_dark</item> <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item> <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item> <item name="suwListItemIconColor">@color/suw_list_item_icon_color_dark</item> @@ -95,13 +98,14 @@ <item name="android:listPreferredItemPaddingStart" tools:ignore="NewApi">?attr/suwMarginSides</item> <item name="android:navigationBarColor" tools:ignore="NewApi">@android:color/black</item> <item name="android:statusBarColor" tools:ignore="NewApi">@android:color/transparent</item> + <item name="android:textAppearanceListItemSmall" tools:ignore="NewApi">?attr/textAppearanceListItemSmall</item> <item name="android:textColorLink">@color/suw_link_color_light</item> <item name="android:windowAnimationStyle">@style/Animation.SuwWindowAnimation</item> <item name="android:windowDisablePreview">true</item> <item name="android:windowSoftInputMode">adjustResize</item> - <item name="colorAccent">@color/suw_color_accent_light</item> - <item name="colorPrimary">@color/suw_color_accent_light</item> + <item name="colorAccent">@color/suw_color_accent_glif_light</item> + <item name="colorPrimary">@color/suw_color_accent_glif_light</item> <item name="listPreferredItemPaddingLeft">?attr/suwMarginSides</item> <item name="listPreferredItemPaddingRight">?attr/suwMarginSides</item> <item name="suwListItemIconColor">@color/suw_list_item_icon_color_light</item> @@ -137,6 +141,16 @@ <item name="android:textAppearance">?attr/textAppearanceListItemSmall</item> </style> + <!-- Button styles --> + + <style name="SuwButtonItem" /> + + <style name="SuwButtonItem.Colored"> + <item name="android:buttonStyle">@style/Widget.AppCompat.Button</item> + <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> + <item name="colorButtonNormal">?attr/colorAccent</item> + </style> + <!-- Card layout (for tablets) --> <style name="TextAppearance.SuwCardTitle" parent="@style/TextAppearance.AppCompat.Display1"> diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java new file mode 100644 index 0000000..aaeaf34 --- /dev/null +++ b/library/eclair-mr1/src/com/android/setupwizardlib/items/SwitchItem.java @@ -0,0 +1,100 @@ +/* + * 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 com.android.setupwizardlib.items; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.v7.widget.SwitchCompat; +import android.util.AttributeSet; +import android.view.View; +import android.widget.CompoundButton; + +import com.android.setupwizardlib.R; + +/** + * An Item with a switch, which the user can + */ +public class SwitchItem extends Item implements CompoundButton.OnCheckedChangeListener { + + public interface OnCheckedChangeListener { + void onCheckedChange(SwitchItem item, boolean isChecked); + } + + private boolean mChecked = false; + private OnCheckedChangeListener mListener; + + public SwitchItem() { + super(); + } + + public SwitchItem(Context context, AttributeSet attrs) { + super(context, attrs); + final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwSwitchItem); + mChecked = a.getBoolean(R.styleable.SuwSwitchItem_android_checked, false); + a.recycle(); + } + + public void setChecked(boolean checked) { + if (mChecked != checked) { + mChecked = checked; + notifyChanged(); + if (mListener != null) { + mListener.onCheckedChange(this, checked); + } + } + } + + public boolean isChecked() { + return mChecked; + } + + @Override + protected int getDefaultLayoutResource() { + return R.layout.suw_items_switch; + } + + /** + * Toggle the checked state of the switch, without invalidating the entire item. + * + * @param view The root view of this item, typically from the argument of onItemClick. + */ + public void toggle(View view) { + mChecked = !mChecked; + final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch); + switchView.setChecked(mChecked); + } + + @Override + public void onBindView(View view) { + super.onBindView(view); + final SwitchCompat switchView = (SwitchCompat) view.findViewById(R.id.suw_items_switch); + switchView.setChecked(mChecked); + switchView.setOnCheckedChangeListener(this); + switchView.setEnabled(isEnabled()); + } + + public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { + mListener = listener; + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (mListener != null) { + mListener.onCheckedChange(this, isChecked); + } + } +} diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java b/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java new file mode 100644 index 0000000..2c53ee7 --- /dev/null +++ b/library/eclair-mr1/src/com/android/setupwizardlib/util/LinkAccessibilityHelper.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.util; + +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import java.util.List; + +/** + * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and + * clicked by accessibility services. + * + * <p />Sample usage: + * <pre> + * LinkAccessibilityHelper mAccessibilityHelper; + * + * private void init() { + * mAccessibilityHelper = new LinkAccessibilityHelper(myTextView); + * ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper); + * } + * + * {@literal @}Override + * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) { + * if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { + * return true; + * } + * return super.dispatchHoverEvent(event); + * } + * </pre> + * + * @see com.android.setupwizardlib.view.RichTextView + * @see android.support.v4.widget.ExploreByTouchHelper + */ +public class LinkAccessibilityHelper extends ExploreByTouchHelper { + + private static final String TAG = "LinkAccessibilityHelper"; + + private final TextView mView; + private final Rect mTempRect = new Rect(); + + public LinkAccessibilityHelper(TextView view) { + super(view); + mView = view; + } + + @Override + protected int getVirtualViewAt(float x, float y) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + final int offset = getOffsetForPosition(mView, x, y); + ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (linkSpans.length == 1) { + ClickableSpan linkSpan = linkSpans[0]; + return spannedText.getSpanStart(linkSpan); + } + } + return INVALID_ID; + } + + @Override + protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { + final CharSequence text = mView.getText(); + if (text instanceof Spanned) { + final Spanned spannedText = (Spanned) text; + ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(), + ClickableSpan.class); + for (ClickableSpan span : linkSpans) { + virtualViewIds.add(spannedText.getSpanStart(span)); + } + } + } + + @Override + protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + event.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + event.setContentDescription(mView.getText()); + } + } + + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat info) { + final ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + info.setContentDescription(getTextForSpan(span)); + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + info.setContentDescription(mView.getText()); + } + info.setFocusable(true); + info.setClickable(true); + getBoundsForSpan(span, mTempRect); + if (!mTempRect.isEmpty()) { + info.setBoundsInParent(getBoundsForSpan(span, mTempRect)); + } else { + Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId); + mTempRect.set(0, 0, 1, 1); + info.setBoundsInParent(mTempRect); + } + info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); + } + + @Override + protected boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) { + ClickableSpan span = getSpanForOffset(virtualViewId); + if (span != null) { + span.onClick(mView); + return true; + } else { + Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId); + } + } + return false; + } + + private ClickableSpan getSpanForOffset(int offset) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class); + if (spans.length == 1) { + return spans[0]; + } + } + return null; + } + + private CharSequence getTextForSpan(ClickableSpan span) { + CharSequence text = mView.getText(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + return spannedText.subSequence(spannedText.getSpanStart(span), + spannedText.getSpanEnd(span)); + } + return text; + } + + // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the + // section on the first line. + private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) { + CharSequence text = mView.getText(); + outRect.setEmpty(); + if (text instanceof Spanned) { + Spanned spannedText = (Spanned) text; + final int spanStart = spannedText.getSpanStart(span); + final int spanEnd = spannedText.getSpanEnd(span); + final Layout layout = mView.getLayout(); + final float xStart = layout.getPrimaryHorizontal(spanStart); + final float xEnd = layout.getPrimaryHorizontal(spanEnd); + final int lineStart = layout.getLineForOffset(spanStart); + final int lineEnd = layout.getLineForOffset(spanEnd); + layout.getLineBounds(lineStart, outRect); + outRect.left = (int) xStart; + if (lineEnd == lineStart) { + outRect.right = (int) xEnd; + } // otherwise just leave it at the end of the start line + + // Offset for padding + outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop()); + } + return outRect; + } + + // Compat implementation of TextView#getOffsetForPosition(). + + private static int getOffsetForPosition(TextView view, float x, float y) { + if (view.getLayout() == null) return -1; + final int line = getLineAtCoordinate(view, y); + return getOffsetAtCoordinate(view, line, x); + } + + private static float convertToLocalHorizontalCoordinate(TextView view, float x) { + x -= view.getTotalPaddingLeft(); + // Clamp the position to inside of the view. + x = Math.max(0.0f, x); + x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x); + x += view.getScrollX(); + return x; + } + + private static int getLineAtCoordinate(TextView view, float y) { + y -= view.getTotalPaddingTop(); + // Clamp the position to inside of the view. + y = Math.max(0.0f, y); + y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y); + y += view.getScrollY(); + return view.getLayout().getLineForVertical((int) y); + } + + private static int getOffsetAtCoordinate(TextView view, int line, float x) { + x = convertToLocalHorizontalCoordinate(view, x); + return view.getLayout().getOffsetForHorizontal(line, x); + } +} diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java b/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java index 1ffc034..6e555a1 100644 --- a/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java +++ b/library/eclair-mr1/src/com/android/setupwizardlib/view/NavigationBarButton.java @@ -44,7 +44,7 @@ public class NavigationBarButton extends Button { Drawable[] drawables = getCompoundDrawablesRelative(); for (int i = 0; i < drawables.length; i++) { if (drawables[i] != null) { - drawables[i] = TintedDrawable.wrap(drawables[i].mutate()); + drawables[i] = TintedDrawable.wrap(drawables[i]); } } setCompoundDrawablesRelativeWithIntrinsicBounds(drawables[0], drawables[1], @@ -54,10 +54,10 @@ public class NavigationBarButton extends Button { @Override public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) { - if (left != null) left = TintedDrawable.wrap(left.mutate()); - if (top != null) top = TintedDrawable.wrap(top.mutate()); - if (right != null) right = TintedDrawable.wrap(right.mutate()); - if (bottom != null) bottom = TintedDrawable.wrap(bottom.mutate()); + if (left != null) left = TintedDrawable.wrap(left); + if (top != null) top = TintedDrawable.wrap(top); + if (right != null) right = TintedDrawable.wrap(right); + if (bottom != null) bottom = TintedDrawable.wrap(bottom); super.setCompoundDrawables(left, top, right, bottom); tintDrawables(); } @@ -65,10 +65,10 @@ public class NavigationBarButton extends Button { @Override public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, Drawable bottom) { - if (start != null) start = TintedDrawable.wrap(start.mutate()); - if (top != null) top = TintedDrawable.wrap(top.mutate()); - if (end != null) end = TintedDrawable.wrap(end.mutate()); - if (bottom != null) bottom = TintedDrawable.wrap(bottom.mutate()); + if (start != null) start = TintedDrawable.wrap(start); + if (top != null) top = TintedDrawable.wrap(top); + if (end != null) end = TintedDrawable.wrap(end); + if (bottom != null) bottom = TintedDrawable.wrap(bottom); super.setCompoundDrawablesRelative(start, top, end, bottom); tintDrawables(); } @@ -114,7 +114,7 @@ public class NavigationBarButton extends Button { if (drawable instanceof TintedDrawable) { return (TintedDrawable) drawable; } - return new TintedDrawable(drawable); + return new TintedDrawable(drawable.mutate()); } private ColorStateList mTintList = null; diff --git a/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java b/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java new file mode 100644 index 0000000..d79d149 --- /dev/null +++ b/library/eclair-mr1/src/com/android/setupwizardlib/view/RichTextView.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.view; + +import android.content.Context; +import android.support.v4.view.ViewCompat; +import android.text.Annotation; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.TextAppearanceSpan; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.widget.TextView; + +import com.android.setupwizardlib.span.LinkSpan; +import com.android.setupwizardlib.span.SpanHelper; +import com.android.setupwizardlib.util.LinkAccessibilityHelper; + +/** + * An extension of TextView that automatically replaces the annotation tags as specified in + * {@link SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} + */ +public class RichTextView extends TextView { + + /* static section */ + + private static final String TAG = "RichTextView"; + + private static final String ANNOTATION_LINK = "link"; + private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance"; + + /** + * Replace <annotation> tags in strings to become their respective types. Currently 2 + * types are supported: + * <ol> + * <li><annotation link="foobar"> will create a + * {@link com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key + * "foobar"</li> + * <li><annotation textAppearance="TextAppearance.FooBar"> will create a + * {@link android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar</li> + * </ol> + */ + public static CharSequence getRichText(Context context, CharSequence text) { + if (text instanceof Spanned) { + final SpannableString spannable = new SpannableString(text); + final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class); + for (Annotation span : spans) { + final String key = span.getKey(); + if (ANNOTATION_TEXT_APPEARANCE.equals(key)) { + String textAppearance = span.getValue(); + final int style = context.getResources() + .getIdentifier(textAppearance, "style", context.getPackageName()); + if (style == 0) { + Log.w(TAG, "Cannot find resource: " + style); + } + final TextAppearanceSpan textAppearanceSpan = + new TextAppearanceSpan(context, style); + SpanHelper.replaceSpan(spannable, span, textAppearanceSpan); + } else if (ANNOTATION_LINK.equals(key)) { + LinkSpan link = new LinkSpan(span.getValue()); + SpanHelper.replaceSpan(spannable, span, link); + } + } + return spannable; + } + return text; + } + + /* non-static section */ + + private LinkAccessibilityHelper mAccessibilityHelper; + + public RichTextView(Context context) { + super(context); + init(); + } + + public RichTextView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + mAccessibilityHelper = new LinkAccessibilityHelper(this); + ViewCompat.setAccessibilityDelegate(this, mAccessibilityHelper); + setMovementMethod(LinkMovementMethod.getInstance()); + } + + @Override + public void setText(CharSequence text, BufferType type) { + text = getRichText(getContext(), text); + super.setText(text, type); + } + + @Override + protected boolean dispatchHoverEvent(MotionEvent event) { + if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) { + return true; + } + return super.dispatchHoverEvent(event); + } +} diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java new file mode 100644 index 0000000..8d42fa3 --- /dev/null +++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/LinkAccessibilityHelperTest.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.widget.ExploreByTouchHelper; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.SpannableStringBuilder; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.accessibility.AccessibilityEvent; +import android.widget.TextView; + +import com.android.setupwizardlib.span.LinkSpan; +import com.android.setupwizardlib.util.LinkAccessibilityHelper; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class LinkAccessibilityHelperTest extends AndroidTestCase { + + private TextView mTextView; + private TestLinkAccessibilityHelper mHelper; + private LinkSpan mSpan; + + private DisplayMetrics mDisplayMetrics; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mSpan = new LinkSpan("foobar"); + SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world"); + ssb.setSpan(mSpan, 1, 2, 0 /* flags */); + + mTextView = new TextView(getContext()); + mTextView.setText(ssb); + mTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15); + mHelper = new TestLinkAccessibilityHelper(mTextView); + + mTextView.measure(dp2Px(500), dp2Px(500)); + mTextView.layout(dp2Px(0), dp2Px(0), dp2Px(500), dp2Px(500)); + } + + @SmallTest + public void testGetVirtualViewAt() { + final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(15), dp2Px(10)); + assertEquals("Virtual view ID should be 1", 1, virtualViewId); + } + + @SmallTest + public void testGetVirtualViewAtHost() { + final int virtualViewId = mHelper.getVirtualViewAt(dp2Px(100), dp2Px(100)); + assertEquals("Virtual view ID should be INVALID_ID", + ExploreByTouchHelper.INVALID_ID, virtualViewId); + } + + @SmallTest + public void testGetVisibleVirtualViews() { + List<Integer> virtualViewIds = new ArrayList<>(); + mHelper.getVisibleVirtualViews(virtualViewIds); + + assertEquals("VisibleVirtualViews should be [1]", + Collections.singletonList(1), virtualViewIds); + } + + @SmallTest + public void testOnPopulateEventForVirtualView() { + AccessibilityEvent event = AccessibilityEvent.obtain(); + mHelper.onPopulateEventForVirtualView(1, event); + + // LinkSpan is set on substring(1, 2) of "Hello world" --> "e" + assertEquals("LinkSpan description should be \"e\"", + "e", event.getContentDescription().toString()); + + event.recycle(); + } + + @SmallTest + public void testOnPopulateEventForVirtualViewHost() { + AccessibilityEvent event = AccessibilityEvent.obtain(); + mHelper.onPopulateEventForVirtualView(ExploreByTouchHelper.INVALID_ID, event); + + assertEquals("Host view description should be \"Hello world\"", "Hello world", + event.getContentDescription().toString()); + + event.recycle(); + } + + @SmallTest + public void testOnPopulateNodeForVirtualView() { + AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(); + mHelper.onPopulateNodeForVirtualView(1, info); + + assertEquals("LinkSpan description should be \"e\"", + "e", info.getContentDescription().toString()); + assertTrue("LinkSpan should be focusable", info.isFocusable()); + assertTrue("LinkSpan should be clickable", info.isClickable()); + Rect bounds = new Rect(); + info.getBoundsInParent(bounds); + assertEquals("LinkSpan bounds should be (10.5dp, 0dp, 18.5dp, 20.5dp)", + new Rect(dp2Px(10.5f), dp2Px(0f), dp2Px(18.5f), dp2Px(20.5f)), bounds); + + info.recycle(); + } + + private int dp2Px(float dp) { + if (mDisplayMetrics == null) { + mDisplayMetrics = getContext().getResources().getDisplayMetrics(); + } + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDisplayMetrics); + } + + private static class TestLinkAccessibilityHelper extends LinkAccessibilityHelper { + + public TestLinkAccessibilityHelper(TextView view) { + super(view); + } + + @Override + public int getVirtualViewAt(float x, float y) { + return super.getVirtualViewAt(x, y); + } + + @Override + public void getVisibleVirtualViews(List<Integer> virtualViewIds) { + super.getVisibleVirtualViews(virtualViewIds); + } + + @Override + public void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { + super.onPopulateEventForVirtualView(virtualViewId, event); + } + + @Override + public void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat info) { + super.onPopulateNodeForVirtualView(virtualViewId, info); + } + + @Override + public boolean onPerformActionForVirtualView(int virtualViewId, int action, + Bundle arguments) { + return super.onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } +} diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java new file mode 100644 index 0000000..c591580 --- /dev/null +++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/RichTextViewTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.Annotation; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.TextAppearanceSpan; + +import com.android.setupwizardlib.span.LinkSpan; +import com.android.setupwizardlib.view.RichTextView; + +import java.util.Arrays; + +public class RichTextViewTest extends AndroidTestCase { + + @SmallTest + public void testLinkAnnotation() { + Annotation link = new Annotation("link", "foobar"); + SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world"); + ssb.setSpan(link, 1, 2, 0 /* flags */); + + RichTextView textView = new RichTextView(getContext()); + textView.setText(ssb); + + final CharSequence text = textView.getText(); + assertTrue("Text should be spanned", text instanceof Spanned); + + Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class); + assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length); + + spans = ((Spanned) text).getSpans(0, text.length(), LinkSpan.class); + assertEquals("There should be one span " + Arrays.toString(spans), 1, spans.length); + assertTrue("The span should be a LinkSpan", spans[0] instanceof LinkSpan); + assertEquals("The LinkSpan should have id \"foobar\"", + "foobar", ((LinkSpan) spans[0]).getId()); + } + + @SmallTest + public void testTextStyle() { + Annotation link = new Annotation("textAppearance", "foobar"); + SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world"); + ssb.setSpan(link, 1, 2, 0 /* flags */); + + RichTextView textView = new RichTextView(getContext()); + textView.setText(ssb); + + final CharSequence text = textView.getText(); + assertTrue("Text should be spanned", text instanceof Spanned); + + Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class); + assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length); + + spans = ((Spanned) text).getSpans(0, text.length(), TextAppearanceSpan.class); + assertEquals("There should be one span " + Arrays.toString(spans), 1, spans.length); + assertTrue("The span should be a TextAppearanceSpan", + spans[0] instanceof TextAppearanceSpan); + } +} diff --git a/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java new file mode 100644 index 0000000..e7c93ba --- /dev/null +++ b/library/eclair-mr1/test/src/com/android/setupwizardlib/test/SwitchItemTest.java @@ -0,0 +1,171 @@ +/* + * 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 com.android.setupwizardlib.test; + +import android.support.v7.widget.SwitchCompat; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.items.SwitchItem; + +public class SwitchItemTest extends AndroidTestCase { + + private SwitchCompat mSwitch; + + @SmallTest + public void testChecked() { + SwitchItem item = new SwitchItem(); + item.setTitle("TestTitle"); + item.setSummary("TestSummary"); + View view = createLayout(); + + item.setChecked(true); + + item.onBindView(view); + + assertTrue("Switch should be checked", mSwitch.isChecked()); + } + + @SmallTest + public void testNotChecked() { + SwitchItem item = new SwitchItem(); + item.setTitle("TestTitle"); + item.setSummary("TestSummary"); + View view = createLayout(); + + item.setChecked(false); + + item.onBindView(view); + + assertFalse("Switch should be unchecked", mSwitch.isChecked()); + } + + @SmallTest + public void testListener() { + SwitchItem item = new SwitchItem(); + item.setTitle("TestTitle"); + item.setSummary("TestSummary"); + View view = createLayout(); + + item.setChecked(true); + + final TestOnCheckedChangeListener listener = new TestOnCheckedChangeListener(); + item.setOnCheckedChangeListener(listener); + + item.onBindView(view); + + assertTrue("Switch should be checked", mSwitch.isChecked()); + mSwitch.setChecked(false); + + assertTrue("Listener should be called", listener.called); + assertFalse("Listener should not be checked", listener.checked); + + mSwitch.setChecked(true); + + assertTrue("Listener should be called", listener.called); + assertTrue("Listener should be checked", listener.checked); + } + + @SmallTest + public void testListenerSetChecked() { + // Check that calling setChecked on the item will also call the listener. + + SwitchItem item = new SwitchItem(); + item.setTitle("TestTitle"); + item.setSummary("TestSummary"); + View view = createLayout(); + + item.setChecked(true); + + final TestOnCheckedChangeListener listener = new TestOnCheckedChangeListener(); + item.setOnCheckedChangeListener(listener); + + item.onBindView(view); + + assertTrue("Switch should be checked", mSwitch.isChecked()); + item.setChecked(false); + + assertTrue("Listener should be called", listener.called); + assertFalse("Listener should not be checked", listener.checked); + + item.setChecked(true); + + assertTrue("Listener should be called", listener.called); + assertTrue("Listener should be checked", listener.checked); + } + + @SmallTest + public void testToggle() { + SwitchItem item = new SwitchItem(); + item.setTitle("TestTitle"); + item.setSummary("TestSummary"); + View view = createLayout(); + + item.setChecked(true); + item.onBindView(view); + + assertTrue("Switch should be checked", mSwitch.isChecked()); + + item.toggle(view); + + assertFalse("Switch should be unchecked", mSwitch.isChecked()); + } + + private ViewGroup createLayout() { + ViewGroup root = new FrameLayout(mContext); + + TextView titleView = new TextView(mContext); + titleView.setId(R.id.suw_items_title); + root.addView(titleView); + + TextView summaryView = new TextView(mContext); + summaryView.setId(R.id.suw_items_summary); + root.addView(summaryView); + + FrameLayout iconContainer = new FrameLayout(mContext); + iconContainer.setId(R.id.suw_items_icon_container); + root.addView(iconContainer); + + ImageView iconView = new ImageView(mContext); + iconView.setId(R.id.suw_items_icon); + iconContainer.addView(iconView); + + mSwitch = new SwitchCompat(mContext); + mSwitch.setId(R.id.suw_items_switch); + root.addView(mSwitch); + + return root; + } + + private static class TestOnCheckedChangeListener implements SwitchItem.OnCheckedChangeListener { + + public boolean called = false; + public boolean checked = false; + + @Override + public void onCheckedChange(SwitchItem item, boolean isChecked) { + called = true; + checked = isChecked; + } + } +} diff --git a/library/full-support/res/layout/suw_glif_preference_recycler_view.xml b/library/full-support/res/layout/suw_glif_preference_recycler_view.xml new file mode 100644 index 0000000..af00160 --- /dev/null +++ b/library/full-support/res/layout/suw_glif_preference_recycler_view.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.setupwizardlib.view.HeaderRecyclerView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/suw_recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipChildren="false" + android:scrollbars="vertical" + app:suwHeader="@layout/suw_glif_header" /> diff --git a/library/full-support/res/layout/suw_glif_preference_template_header.xml b/library/full-support/res/layout/suw_glif_preference_template_header.xml new file mode 100644 index 0000000..6377616 --- /dev/null +++ b/library/full-support/res/layout/suw_glif_preference_template_header.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:id="@+id/suw_layout_content" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <com.android.setupwizardlib.view.NavigationBar + android:id="@+id/suw_layout_navigation_bar" + style="@style/SuwNavBarTheme" + android:layout_width="match_parent" + android:layout_height="@dimen/suw_navbar_height" /> + +</LinearLayout> diff --git a/library/full-support/res/layout/suw_preference_recycler_view_header.xml b/library/full-support/res/layout/suw_preference_recycler_view_header.xml new file mode 100644 index 0000000..20e1d19 --- /dev/null +++ b/library/full-support/res/layout/suw_preference_recycler_view_header.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<com.android.setupwizardlib.view.StickyHeaderRecyclerView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/suw_recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipChildren="false" + android:scrollbars="vertical" + app:suwHeader="@layout/suw_list_header" /> diff --git a/library/full-support/res/layout/suw_preference_recycler_view_normal.xml b/library/full-support/res/layout/suw_preference_recycler_view_normal.xml new file mode 100644 index 0000000..0979d91 --- /dev/null +++ b/library/full-support/res/layout/suw_preference_recycler_view_normal.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<android.support.v7.widget.RecyclerView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/suw_recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbars="vertical" /> diff --git a/library/full-support/res/layout/suw_preference_template_header.xml b/library/full-support/res/layout/suw_preference_template_header.xml new file mode 100644 index 0000000..6377616 --- /dev/null +++ b/library/full-support/res/layout/suw_preference_template_header.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright (C) 2016 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:id="@+id/suw_layout_content" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <com.android.setupwizardlib.view.NavigationBar + android:id="@+id/suw_layout_navigation_bar" + style="@style/SuwNavBarTheme" + android:layout_width="match_parent" + android:layout_height="@dimen/suw_navbar_height" /> + +</LinearLayout> diff --git a/library/full-support/res/layout/suw_recycler_template_header.xml b/library/full-support/res/layout/suw_recycler_template_header.xml index 1d453f7..d2c9622 100644 --- a/library/full-support/res/layout/suw_recycler_template_header.xml +++ b/library/full-support/res/layout/suw_recycler_template_header.xml @@ -26,6 +26,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" + android:clipChildren="false" android:scrollbars="vertical" app:suwHeader="@layout/suw_list_header" /> diff --git a/library/full-support/res/values-land/layouts.xml b/library/full-support/res/values-land/layouts.xml index 5bd3ea8..3aacec9 100644 --- a/library/full-support/res/values-land/layouts.xml +++ b/library/full-support/res/values-land/layouts.xml @@ -17,6 +17,8 @@ <resources> + <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item> + <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_header_collapsed</item> <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_header_collapsed</item> <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_header_collapsed</item> diff --git a/library/full-support/res/values-sw600dp-land/layouts.xml b/library/full-support/res/values-sw600dp-land/layouts.xml index 3ac3597..0feed90 100644 --- a/library/full-support/res/values-sw600dp-land/layouts.xml +++ b/library/full-support/res/values-sw600dp-land/layouts.xml @@ -17,6 +17,8 @@ <resources> + <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item> + <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_card_wide</item> <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_card_wide</item> <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_card_wide</item> diff --git a/library/full-support/res/values-sw600dp/layouts.xml b/library/full-support/res/values-sw600dp/layouts.xml index 98b1a79..bfd4863 100644 --- a/library/full-support/res/values-sw600dp/layouts.xml +++ b/library/full-support/res/values-sw600dp/layouts.xml @@ -17,9 +17,12 @@ <resources> + <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_normal</item> + <item name="suw_preference_template" type="layout">@layout/suw_no_scroll_template_card</item> <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_card</item> <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_card</item> + <item name="suw_glif_preference_template" type="layout">@layout/suw_glif_blank_template_card</item> <item name="suw_glif_recycler_template" type="layout">@layout/suw_glif_recycler_template_card</item> </resources> diff --git a/library/full-support/res/values/attrs.xml b/library/full-support/res/values/attrs.xml index 059da5f..86b2a56 100644 --- a/library/full-support/res/values/attrs.xml +++ b/library/full-support/res/values/attrs.xml @@ -21,12 +21,20 @@ <declare-styleable name="SuwSetupWizardRecyclerItemsLayout"> <attr name="android:entries" /> + <attr name="suwDividerInset" /> <attr name="suwHasStableIds" /> </declare-styleable> <declare-styleable name="SuwGlifRecyclerLayout"> <attr name="android:entries" /> - <attr name="suwHasStableIds" /> <attr name="suwDividerInset" /> + <attr name="suwHasStableIds" /> </declare-styleable> + + <declare-styleable name="SuwRecyclerItemAdapter"> + <attr name="android:colorBackground" /> + <attr name="android:selectableItemBackground" /> + <attr name="selectableItemBackground" /> + </declare-styleable> + </resources> diff --git a/library/full-support/res/values/layouts.xml b/library/full-support/res/values/layouts.xml index 291d8d7..957d044 100644 --- a/library/full-support/res/values/layouts.xml +++ b/library/full-support/res/values/layouts.xml @@ -17,9 +17,12 @@ <resources> + <item name="suw_preference_recycler_view" type="layout">@layout/suw_preference_recycler_view_header</item> + <item name="suw_preference_template" type="layout">@layout/suw_preference_template_header</item> <item name="suw_recycler_template" type="layout">@layout/suw_recycler_template_header</item> <item name="suw_recycler_template_short" type="layout">@layout/suw_recycler_template_header_collapsed</item> + <item name="suw_glif_preference_template" type="layout">@layout/suw_glif_blank_template_compact</item> <item name="suw_glif_recycler_template" type="layout">@layout/suw_glif_recycler_template_compact</item> </resources> diff --git a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java index ad2037d..8c937a1 100644 --- a/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java +++ b/library/full-support/src/com/android/setupwizardlib/DividerItemDecoration.java @@ -21,10 +21,14 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.view.View; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + /** * An {@link android.support.v7.widget.RecyclerView.ItemDecoration} for RecyclerView to draw * dividers between items. This ItemDecoration will draw the drawable specified by @@ -53,10 +57,14 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { boolean isDividerAllowedBelow(); } - private static final int[] ATTRS = new int[]{ - android.R.attr.listDivider, - android.R.attr.dividerHeight - }; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + DIVIDER_CONDITION_EITHER, + DIVIDER_CONDITION_BOTH}) + public @interface DividerCondition {} + + public static final int DIVIDER_CONDITION_EITHER = 0; + public static final int DIVIDER_CONDITION_BOTH = 1; /** * Creates a default instance of {@link DividerItemDecoration}, using @@ -64,14 +72,20 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { * divider height. */ public static DividerItemDecoration getDefault(Context context) { - final TypedArray a = context.obtainStyledAttributes(ATTRS); - final Drawable divider = a.getDrawable(0); - final int dividerHeight = a.getDimensionPixelSize(1, 0); + final TypedArray a = context.obtainStyledAttributes(R.styleable.SuwDividerItemDecoration); + final Drawable divider = a.getDrawable( + R.styleable.SuwDividerItemDecoration_android_listDivider); + final int dividerHeight = a.getDimensionPixelSize( + R.styleable.SuwDividerItemDecoration_android_dividerHeight, 0); + @DividerCondition final int dividerCondition = a.getInt( + R.styleable.SuwDividerItemDecoration_suwDividerCondition, + DIVIDER_CONDITION_EITHER); a.recycle(); final DividerItemDecoration decoration = new DividerItemDecoration(); decoration.setDivider(divider); decoration.setDividerHeight(dividerHeight); + decoration.setDividerCondition(dividerCondition); return decoration; } @@ -80,6 +94,8 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { private Drawable mDivider; private int mDividerHeight; private int mDividerIntrinsicHeight; + @DividerCondition + private int mDividerCondition; @Override public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { @@ -109,21 +125,29 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { private boolean shouldDrawDividerBelow(View view, RecyclerView parent) { final RecyclerView.ViewHolder holder = parent.getChildViewHolder(view); - if ((holder instanceof DividedViewHolder) - && !((DividedViewHolder) holder).isDividerAllowedBelow()) { - // Don't draw if the current view holder doesn't allow drawing below - return false; - } - final int index = parent.indexOfChild(view); - final int lastItemIndex = parent.getChildCount() - 1; - if (index == lastItemIndex) { - return false; + final int index = holder.getLayoutPosition(); + final int lastItemIndex = parent.getAdapter().getItemCount() - 1; + if ((holder instanceof DividedViewHolder)) { + if (((DividedViewHolder) holder).isDividerAllowedBelow()) { + if (mDividerCondition == DIVIDER_CONDITION_EITHER) { + // Draw the divider without consulting the next item if we only + // need permission for either above or below. + return true; + } + } else if (mDividerCondition == DIVIDER_CONDITION_BOTH || index == lastItemIndex) { + // Don't draw if the current view holder doesn't allow drawing below + // and the current theme requires permission for both the item below and above. + // Also, if this is the last item, there is no item below to ask permission + // for whether to draw a divider above, so don't draw it. + return false; + } } + // Require permission from index below to draw the divider. if (index < lastItemIndex) { - final View nextView = parent.getChildAt(index + 1); - final RecyclerView.ViewHolder nextHolder = parent.getChildViewHolder(nextView); - if ((nextHolder instanceof DividedViewHolder) && - !((DividedViewHolder) nextHolder).isDividerAllowedAbove()) { + final RecyclerView.ViewHolder nextHolder = + parent.findViewHolderForLayoutPosition(index + 1); + if ((nextHolder instanceof DividedViewHolder) + && !((DividedViewHolder) nextHolder).isDividerAllowedAbove()) { // Don't draw if the next view holder doesn't allow drawing above return false; } @@ -163,4 +187,23 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration { public int getDividerHeight() { return mDividerHeight; } + + /** + * Sets whether the divider needs permission from both the item view holder below + * and above from where the divider would draw itself or just needs permission from + * one or the other before drawing itself. + */ + public void setDividerCondition(@DividerCondition int dividerCondition) { + mDividerCondition = dividerCondition; + } + + /** + * Gets whether the divider needs permission from both the item view holder below + * and above from where the divider would draw itself or just needs permission from + * one or the other before drawing itself. + */ + @DividerCondition + public int getDividerCondition() { + return mDividerCondition; + } } diff --git a/library/full-support/src/com/android/setupwizardlib/GlifPreferenceLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifPreferenceLayout.java new file mode 100644 index 0000000..d334d7d --- /dev/null +++ b/library/full-support/src/com/android/setupwizardlib/GlifPreferenceLayout.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A layout to be used with {@code PreferenceFragment} in v14 support library. This can be specified + * as the {@code android:layout} in the {@code app:preferenceFragmentStyle} in + * {@code app:preferenceTheme}. + * + * <p />Example: + * <pre>{@code + * <style android:name="MyActivityTheme"> + * <item android:name="preferenceTheme">@style/MyPreferenceTheme</item> + * </style> + * + * <style android:name="MyPreferenceTheme"> + * <item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle</item> + * </style> + * + * <style android:name="MyPreferenceFragmentStyle"> + * <item android:name="android:layout">@layout/my_preference_layout</item> + * </style> + * }</pre> + * + * where {@code my_preference_layout} is a layout that contains + * {@link com.android.setupwizardlib.GlifPreferenceLayout}. + * + * <p />Example: + * <pre>{@code + * <com.android.setupwizardlib.GlifPreferenceLayout + * xmlns:android="http://schemas.android.com/apk/res/android" + * android:id="@id/list_container" + * android:layout_width="match_parent" + * android:layout_height="match_parent" /> + * }</pre> + * + * <p />Fragments using this layout <em>must</em> delegate {@code onCreateRecyclerView} to the + * implementation in this class: + * {@link #onCreateRecyclerView(android.view.LayoutInflater, android.view.ViewGroup, + * android.os.Bundle)} + */ +public class GlifPreferenceLayout extends GlifRecyclerLayout { + + private RecyclerView mRecyclerView; + + public GlifPreferenceLayout(Context context) { + super(context); + } + + public GlifPreferenceLayout(Context context, int template, int containerId) { + super(context, template, containerId); + } + + public GlifPreferenceLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public GlifPreferenceLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + @Override + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + containerId = R.id.suw_layout_content; + } + return super.findContainer(containerId); + } + + /** + * This method must be called in {@code PreferenceFragment#onCreateRecyclerView}. + */ + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, + Bundle savedInstanceState) { + return mRecyclerView; + } + + @Override + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_glif_preference_template; + } + return super.onInflateTemplate(inflater, template); + } + + @Override + protected void onTemplateInflated() { + // Inflate the recycler view here, so attributes on the decoration views can be applied + // immediately. + final LayoutInflater inflater = LayoutInflater.from(getContext()); + mRecyclerView = (RecyclerView) inflater.inflate(R.layout.suw_glif_preference_recycler_view, + this, false); + initRecyclerView(mRecyclerView); + } +} diff --git a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java index fd8a974..509532f 100644 --- a/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java +++ b/library/full-support/src/com/android/setupwizardlib/GlifRecyclerLayout.java @@ -28,8 +28,6 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import com.android.setupwizardlib.items.ItemGroup; import com.android.setupwizardlib.items.ItemInflater; @@ -44,8 +42,8 @@ import com.android.setupwizardlib.view.HeaderRecyclerView; public class GlifRecyclerLayout extends GlifLayout { private RecyclerView mRecyclerView; - private TextView mHeaderTextView; - private ImageView mIconView; + private View mHeader; + private DividerItemDecoration mDividerDecoration; private Drawable mDefaultDivider; private Drawable mDivider; @@ -90,7 +88,7 @@ public class GlifRecyclerLayout extends GlifLayout { a.getDimensionPixelSize(R.styleable.SuwGlifRecyclerLayout_suwDividerInset, 0); if (dividerInset == 0) { dividerInset = getResources() - .getDimensionPixelSize(R.dimen.suw_items_icon_divider_inset); + .getDimensionPixelSize(R.dimen.suw_items_glif_icon_divider_inset); } setDividerInset(dividerInset); a.recycle(); @@ -123,26 +121,28 @@ public class GlifRecyclerLayout extends GlifLayout { @Override protected void onTemplateInflated() { - final Context context = getContext(); - mRecyclerView = (RecyclerView) findViewById(R.id.suw_recycler_view); - mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); + initRecyclerView((RecyclerView) findViewById(R.id.suw_recycler_view)); + } + + protected void initRecyclerView(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); if (mRecyclerView instanceof HeaderRecyclerView) { - final View header = ((HeaderRecyclerView) mRecyclerView).getHeader(); - mHeaderTextView = (TextView) header.findViewById(R.id.suw_layout_title); - mIconView = (ImageView) header.findViewById(R.id.suw_layout_icon); + mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader(); } - mDividerDecoration = DividerItemDecoration.getDefault(context); + mDividerDecoration = DividerItemDecoration.getDefault(getContext()); mRecyclerView.addItemDecoration(mDividerDecoration); } @Override - protected TextView getHeaderTextView() { - return mHeaderTextView; - } - - @Override - protected ImageView getIconView() { - return mIconView; + protected View findManagedViewById(int id) { + if (mHeader != null) { + final View view = mHeader.findViewById(id); + if (view != null) { + return view; + } + } + return super.findViewById(id); } public RecyclerView getRecyclerView() { @@ -166,8 +166,8 @@ public class GlifRecyclerLayout extends GlifLayout { * theme and inset it {@code inset} pixels to the right (or left in RTL layouts). * * @param inset The number of pixels to inset on the "start" side of the list divider. Typically - * this will be either {@code @dimen/suw_items_icon_divider_inset} or - * {@code @dimen/suw_items_text_divider_inset}. + * this will be either {@code @dimen/suw_items_glif_icon_divider_inset} or + * {@code @dimen/suw_items_glif_text_divider_inset}. */ public void setDividerInset(int inset) { mDividerInset = inset; diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java new file mode 100644 index 0000000..3d3b5d3 --- /dev/null +++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardPreferenceLayout.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib; + +import android.content.Context; +import android.os.Bundle; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A layout to be used with {@code PreferenceFragment} in v14 support library. This can be specified + * as the {@code android:layout} in the {@code app:preferenceFragmentStyle} in + * {@code app:preferenceTheme}. + * + * <p />Example: + * <pre>{@code + * <style android:name="MyActivityTheme"> + * <item android:name="preferenceTheme">@style/MyPreferenceTheme</item> + * </style> + * + * <style android:name="MyPreferenceTheme"> + * <item android:name="preferenceFragmentStyle">@style/MyPreferenceFragmentStyle</item> + * </style> + * + * <style android:name="MyPreferenceFragmentStyle"> + * <item android:name="android:layout">@layout/my_preference_layout</item> + * </style> + * }</pre> + * + * where {@code my_preference_layout} is a layout that contains + * {@link com.android.setupwizardlib.SetupWizardPreferenceLayout}. + * + * <p />Example: + * <pre>{@code + * <com.android.setupwizardlib.SetupWizardPreferenceLayout + * xmlns:android="http://schemas.android.com/apk/res/android" + * android:id="@id/list_container" + * android:layout_width="match_parent" + * android:layout_height="match_parent" /> + * }</pre> + * + * <p />Fragments using this layout <em>must</em> delegate {@code onCreateRecyclerView} to the + * implementation in this class: + * {@link #onCreateRecyclerView(android.view.LayoutInflater, android.view.ViewGroup, android.os.Bundle)} + */ +public class SetupWizardPreferenceLayout extends SetupWizardRecyclerLayout { + + private RecyclerView mRecyclerView; + + public SetupWizardPreferenceLayout(Context context) { + super(context); + } + + public SetupWizardPreferenceLayout(Context context, int template, int containerId) { + super(context, template, containerId); + } + + public SetupWizardPreferenceLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SetupWizardPreferenceLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + @Override + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + containerId = R.id.suw_layout_content; + } + return super.findContainer(containerId); + } + + /** + * This method must be called in {@code PreferenceFragment#onCreateRecyclerView}. + */ + public RecyclerView onCreateRecyclerView(LayoutInflater inflater, ViewGroup parent, + Bundle savedInstanceState) { + return mRecyclerView; + } + + @Override + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_preference_template; + } + return super.onInflateTemplate(inflater, template); + } + + @Override + protected void onTemplateInflated() { + // Inflate the recycler view here, so attributes on the decoration views can be applied + // immediately. + final LayoutInflater inflater = LayoutInflater.from(getContext()); + mRecyclerView = (RecyclerView) inflater.inflate(R.layout.suw_preference_recycler_view, + this, false); + initRecyclerView(mRecyclerView); + } +} diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerItemsLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerItemsLayout.java index 97f6589..23e7cef 100644 --- a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerItemsLayout.java +++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerItemsLayout.java @@ -17,133 +17,25 @@ package com.android.setupwizardlib; import android.content.Context; -import android.content.res.TypedArray; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.util.AttributeSet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import com.android.setupwizardlib.items.ItemGroup; -import com.android.setupwizardlib.items.ItemInflater; import com.android.setupwizardlib.items.RecyclerItemAdapter; -import com.android.setupwizardlib.util.RecyclerViewRequireScrollHelper; -import com.android.setupwizardlib.view.HeaderRecyclerView; -import com.android.setupwizardlib.view.NavigationBar; /** - * A SetupWizardLayout for use with {@link com.android.setupwizardlib.items.RecyclerItemAdapter}, - * which displays a list of items using a RecyclerView. The items XML file can be specified through - * {@code android:entries} attribute in the layout. - * - * @see com.android.setupwizardlib.SetupWizardItemsLayout + * @deprecated Use {@link com.android.setupwizardlib.SetupWizardRecyclerLayout} */ -public class SetupWizardRecyclerItemsLayout extends SetupWizardLayout { - - private static final String TAG = "RecyclerItemsLayout"; - - private RecyclerItemAdapter mAdapter; - private RecyclerView mRecyclerView; - - private TextView mHeaderTextView; - private View mDecorationView; +@Deprecated +public class SetupWizardRecyclerItemsLayout extends SetupWizardRecyclerLayout { public SetupWizardRecyclerItemsLayout(Context context, AttributeSet attrs) { super(context, attrs); - init(context, attrs, 0); } public SetupWizardRecyclerItemsLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(context, attrs, defStyleAttr); - } - - private void init(Context context, AttributeSet attrs, int defStyleAttr) { - final TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.SuwSetupWizardRecyclerItemsLayout, defStyleAttr, 0); - final int xml = a.getResourceId( - R.styleable.SuwSetupWizardRecyclerItemsLayout_android_entries, 0); - if (xml != 0) { - final ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml); - mAdapter = new RecyclerItemAdapter(inflated); - mAdapter.setHasStableIds(a.getBoolean( - R.styleable.SuwSetupWizardRecyclerItemsLayout_suwHasStableIds, false)); - setAdapter(mAdapter); - } - a.recycle(); } public RecyclerItemAdapter getAdapter() { - return mAdapter; - } - - public void setAdapter(RecyclerItemAdapter adapter) { - mAdapter = adapter; - getRecyclerView().setAdapter(adapter); - } - - public RecyclerView getRecyclerView() { - return mRecyclerView; - } - - @Override - protected ViewGroup findContainer(int containerId) { - if (containerId == 0) { - containerId = R.id.suw_recycler_view; - } - return super.findContainer(containerId); - } - - @Override - protected void onTemplateInflated() { - mRecyclerView = (RecyclerView) findViewById(R.id.suw_recycler_view); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - mRecyclerView.addItemDecoration(DividerItemDecoration.getDefault(getContext())); - if (mRecyclerView instanceof HeaderRecyclerView) { - final View header = ((HeaderRecyclerView) mRecyclerView).getHeader(); - mHeaderTextView = (TextView) header.findViewById(R.id.suw_layout_title); - mDecorationView = header.findViewById(R.id.suw_layout_decor); - } - } - - @Override - protected View onInflateTemplate(LayoutInflater inflater, int template) { - if (template == 0) { - template = R.layout.suw_recycler_template; - } - return super.onInflateTemplate(inflater, template); - } - - @Override - protected TextView getHeaderTextView() { - if (mHeaderTextView != null) { - return mHeaderTextView; - } else { - return super.getHeaderTextView(); - } - } - - @Override - protected View getDecorationView() { - if (mDecorationView != null) { - return mDecorationView; - } else { - return super.getDecorationView(); - } - } - - @Override - public void requireScrollToBottom() { - final NavigationBar navigationBar = getNavigationBar(); - final RecyclerView recyclerView = getRecyclerView(); - if (navigationBar != null && recyclerView != null) { - RecyclerViewRequireScrollHelper.requireScroll(navigationBar, recyclerView); - } else { - Log.e(TAG, "Both suw_layout_navigation_bar and suw_recycler_view must exist in" - + " the template to require scrolling."); - } + return (RecyclerItemAdapter) super.getAdapter(); } } diff --git a/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java new file mode 100644 index 0000000..5159bae --- /dev/null +++ b/library/full-support/src/com/android/setupwizardlib/SetupWizardRecyclerLayout.java @@ -0,0 +1,212 @@ +/* + * 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 com.android.setupwizardlib; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.setupwizardlib.items.ItemGroup; +import com.android.setupwizardlib.items.ItemInflater; +import com.android.setupwizardlib.items.RecyclerItemAdapter; +import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper; +import com.android.setupwizardlib.util.RecyclerViewRequireScrollHelper; +import com.android.setupwizardlib.view.HeaderRecyclerView; +import com.android.setupwizardlib.view.NavigationBar; + +/** + * A setup wizard layout for use with {@link android.support.v7.widget.RecyclerView}. + * {@code android:entries} can also be used to specify an + * {@link com.android.setupwizardlib.items.ItemHierarchy} to be used with this layout in XML. + * + * @see SetupWizardItemsLayout + */ +public class SetupWizardRecyclerLayout extends SetupWizardLayout { + + private static final String TAG = "RecyclerLayout"; + + private RecyclerView.Adapter mAdapter; + private RecyclerView mRecyclerView; + private View mHeader; + + private DividerItemDecoration mDividerDecoration; + private Drawable mDefaultDivider; + private Drawable mDivider; + private int mDividerInset; + + public SetupWizardRecyclerLayout(Context context) { + this(context, 0, 0); + } + + public SetupWizardRecyclerLayout(Context context, int template, int containerId) { + super(context, template, containerId); + init(context, null, 0); + } + + public SetupWizardRecyclerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public SetupWizardRecyclerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwSetupWizardRecyclerItemsLayout, defStyleAttr, 0); + final int xml = a.getResourceId( + R.styleable.SuwSetupWizardRecyclerItemsLayout_android_entries, 0); + if (xml != 0) { + final ItemGroup inflated = (ItemGroup) new ItemInflater(context).inflate(xml); + mAdapter = new RecyclerItemAdapter(inflated); + mAdapter.setHasStableIds(a.getBoolean( + R.styleable.SuwSetupWizardRecyclerItemsLayout_suwHasStableIds, false)); + setAdapter(mAdapter); + } + int dividerInset = a.getDimensionPixelSize( + R.styleable.SuwSetupWizardRecyclerItemsLayout_suwDividerInset, 0); + if (dividerInset == 0) { + dividerInset = getResources() + .getDimensionPixelSize(R.dimen.suw_items_icon_divider_inset); + } + setDividerInset(dividerInset); + a.recycle(); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mDivider == null) { + // Update divider in case layout direction has just been resolved + updateDivider(); + } + } + + public RecyclerView.Adapter getAdapter() { + return mAdapter; + } + + public void setAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + getRecyclerView().setAdapter(adapter); + } + + public RecyclerView getRecyclerView() { + return mRecyclerView; + } + + @Override + protected ViewGroup findContainer(int containerId) { + if (containerId == 0) { + containerId = R.id.suw_recycler_view; + } + return super.findContainer(containerId); + } + + @Override + protected void onTemplateInflated() { + initRecyclerView((RecyclerView) findViewById(R.id.suw_recycler_view)); + } + + protected void initRecyclerView(RecyclerView recyclerView) { + mRecyclerView = recyclerView; + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + if (mRecyclerView instanceof HeaderRecyclerView) { + mHeader = ((HeaderRecyclerView) mRecyclerView).getHeader(); + } + mDividerDecoration = DividerItemDecoration.getDefault(getContext()); + mRecyclerView.addItemDecoration(mDividerDecoration); + } + + @Override + protected View onInflateTemplate(LayoutInflater inflater, int template) { + if (template == 0) { + template = R.layout.suw_recycler_template; + } + return super.onInflateTemplate(inflater, template); + } + + @Override + protected View findManagedViewById(int id) { + if (mHeader != null) { + final View view = mHeader.findViewById(id); + if (view != null) { + return view; + } + } + return super.findViewById(id); + } + + @Override + public void requireScrollToBottom() { + final NavigationBar navigationBar = getNavigationBar(); + final RecyclerView recyclerView = getRecyclerView(); + if (navigationBar != null && recyclerView != null) { + RecyclerViewRequireScrollHelper.requireScroll(navigationBar, recyclerView); + } else { + Log.e(TAG, "Both suw_layout_navigation_bar and suw_recycler_view must exist in" + + " the template to require scrolling."); + } + } + + /** + * Sets the start inset of the divider. This will use the default divider drawable set in the + * theme and inset it {@code inset} pixels to the right (or left in RTL layouts). + * + * @param inset The number of pixels to inset on the "start" side of the list divider. Typically + * this will be either {@code @dimen/suw_items_icon_divider_inset} or + * {@code @dimen/suw_items_text_divider_inset}. + */ + public void setDividerInset(int inset) { + mDividerInset = inset; + updateDivider(); + } + + public int getDividerInset() { + return mDividerInset; + } + + private void updateDivider() { + boolean shouldUpdate = true; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + shouldUpdate = isLayoutDirectionResolved(); + } + if (shouldUpdate) { + if (mDefaultDivider == null) { + mDefaultDivider = mDividerDecoration.getDivider(); + } + mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(mDefaultDivider, + mDividerInset /* start */, 0 /* top */, 0 /* end */, 0 /* bottom */, this); + mDividerDecoration.setDivider(mDivider); + } + } + + public Drawable getDivider() { + return mDivider; + } +} diff --git a/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java b/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java index ae39cc0..632fee2 100644 --- a/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java +++ b/library/full-support/src/com/android/setupwizardlib/items/RecyclerItemAdapter.java @@ -17,7 +17,10 @@ package com.android.setupwizardlib.items; import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,9 +35,7 @@ import com.android.setupwizardlib.R; public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder> implements ItemHierarchy.Observer { - private static final int[] SELECTABLE_ITEM_BACKGROUND = new int[] { - R.attr.selectableItemBackground - }; + private static final String TAG = "RecyclerItemAdapter"; public interface OnItemSelectedListener { void onItemSelected(IItem item); @@ -75,8 +76,26 @@ public class RecyclerItemAdapter extends RecyclerView.Adapter<ItemViewHolder> final ItemViewHolder viewHolder = new ItemViewHolder(view); final TypedArray typedArray = parent.getContext() - .obtainStyledAttributes(SELECTABLE_ITEM_BACKGROUND); - view.setBackgroundDrawable(typedArray.getDrawable(0)); + .obtainStyledAttributes(R.styleable.SuwRecyclerItemAdapter); + Drawable selectableItemBackground = typedArray.getDrawable( + R.styleable.SuwRecyclerItemAdapter_android_selectableItemBackground); + if (selectableItemBackground == null) { + selectableItemBackground = typedArray.getDrawable( + R.styleable.SuwRecyclerItemAdapter_selectableItemBackground); + } + + final Drawable background = typedArray.getDrawable( + R.styleable.SuwRecyclerItemAdapter_android_colorBackground); + + if (selectableItemBackground == null || background == null) { + Log.e(TAG, "Cannot resolve required attributes." + + " selectableItemBackground=" + selectableItemBackground + + " background=" + background); + } else { + final Drawable[] layers = { background, selectableItemBackground }; + view.setBackgroundDrawable(new LayerDrawable(layers)); + } + typedArray.recycle(); view.setOnClickListener(new View.OnClickListener() { diff --git a/library/full-support/src/com/android/setupwizardlib/util/GlifPreferenceDelegate.java b/library/full-support/src/com/android/setupwizardlib/util/GlifPreferenceDelegate.java index 914dca9..1c16168 100644 --- a/library/full-support/src/com/android/setupwizardlib/util/GlifPreferenceDelegate.java +++ b/library/full-support/src/com/android/setupwizardlib/util/GlifPreferenceDelegate.java @@ -36,7 +36,10 @@ import com.android.setupwizardlib.view.HeaderRecyclerView; * instance and delegate {@code PreferenceFragment#onCreateRecyclerView} to it. Then call * {@code PreferenceFragment#setDivider} to {@link #getDividerDrawable(android.content.Context)} in * order to make sure the correct inset is applied to the dividers. + * + * @deprecated Use {@link com.android.setupwizardlib.GlifPreferenceLayout} */ +@Deprecated public class GlifPreferenceDelegate { public static final int[] ATTRS_LIST_DIVIDER = new int[]{ android.R.attr.listDivider }; @@ -64,8 +67,8 @@ public class GlifPreferenceDelegate { a.recycle(); final int dividerInset = context.getResources().getDimensionPixelSize( - mHasIcons ? R.dimen.suw_items_icon_divider_inset - : R.dimen.suw_items_text_divider_inset); + mHasIcons ? R.dimen.suw_items_glif_icon_divider_inset + : R.dimen.suw_items_glif_text_divider_inset); return DrawableLayoutDirectionHelper.createRelativeInsetDrawable(defaultDivider, dividerInset /* start */, 0 /* top */, 0 /* end */, 0 /* bottom */, context); diff --git a/library/full-support/test/res/layout/test_recycler_layout.xml b/library/full-support/test/res/layout/test_recycler_layout.xml new file mode 100644 index 0000000..8b7602e --- /dev/null +++ b/library/full-support/test/res/layout/test_recycler_layout.xml @@ -0,0 +1,20 @@ +<!-- + 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. +--> + +<com.android.setupwizardlib.SetupWizardRecyclerLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" /> diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java index 044cad6..a06f6f7 100644 --- a/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java +++ b/library/full-support/test/src/com/android/setupwizardlib/test/DividerItemDecorationTest.java @@ -20,6 +20,9 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Xfermode; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.support.v7.widget.LinearLayoutManager; @@ -49,17 +52,94 @@ public class DividerItemDecorationTest extends AndroidTestCase { } @SmallTest - public void testShouldDrawDividerBelow() { - // Set up the canvas to be drawn - Bitmap bitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444); - Canvas canvas = new Canvas(bitmap); - + public void testShouldDrawDividerBelowWithEitherCondition() { // Set up the item decoration, with 1px red divider line final DividerItemDecoration decoration = new DividerItemDecoration(); Drawable divider = new ColorDrawable(Color.RED); decoration.setDivider(divider); decoration.setDividerHeight(1); + Bitmap bitmap = drawDecoration(decoration, true, true); + + // Draw the expected result on a bitmap + Bitmap expectedBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444); + Canvas expectedCanvas = new Canvas(expectedBitmap); + Paint paint = new Paint(); + paint.setColor(Color.RED); + expectedCanvas.drawRect(0, 5, 20, 6, paint); + expectedCanvas.drawRect(0, 10, 20, 11, paint); + expectedCanvas.drawRect(0, 15, 20, 16, paint); + // Compare the two bitmaps + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, false, true); + // should still be the same. + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, true, false); + // last item should not have a divider below it now + paint.setColor(Color.TRANSPARENT); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + expectedCanvas.drawRect(0, 15, 20, 16, paint); + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, false, false); + // everything should be transparent now + expectedCanvas.drawRect(0, 5, 20, 6, paint); + expectedCanvas.drawRect(0, 10, 20, 11, paint); + assertBitmapEquals(expectedBitmap, bitmap); + + } + + @SmallTest + public void testShouldDrawDividerBelowWithBothCondition() { + // Set up the item decoration, with 1px green divider line + final DividerItemDecoration decoration = new DividerItemDecoration(); + Drawable divider = new ColorDrawable(Color.GREEN); + decoration.setDivider(divider); + decoration.setDividerHeight(1); + decoration.setDividerCondition(DividerItemDecoration.DIVIDER_CONDITION_BOTH); + + Bitmap bitmap = drawDecoration(decoration, true, true); + Paint paint = new Paint(); + paint.setColor(Color.GREEN); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); + Bitmap expectedBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444); + Canvas expectedCanvas = new Canvas(expectedBitmap); + expectedCanvas.drawRect(0, 5, 20, 6, paint); + expectedCanvas.drawRect(0, 10, 20, 11, paint); + expectedCanvas.drawRect(0, 15, 20, 16, paint); + // Should have all the dividers + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, false, true); + paint.setColor(Color.TRANSPARENT); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + expectedCanvas.drawRect(0, 5, 20, 6, paint); + expectedCanvas.drawRect(0, 10, 20, 11, paint); + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, true, false); + // nothing should be drawn now. + expectedCanvas.drawRect(0, 15, 20, 16, paint); + assertBitmapEquals(expectedBitmap, bitmap); + + bitmap.recycle(); + bitmap = drawDecoration(decoration, false, false); + assertBitmapEquals(expectedBitmap, bitmap); + } + + private Bitmap drawDecoration(DividerItemDecoration decoration, final boolean allowDividerAbove, + final boolean allowDividerBelow) { + // Set up the canvas to be drawn + Bitmap bitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444); + Canvas canvas = new Canvas(bitmap); + // Set up recycler view with vertical linear layout manager RecyclerView testRecyclerView = new RecyclerView(getContext()); testRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); @@ -71,8 +151,7 @@ public class DividerItemDecorationTest extends AndroidTestCase { final View itemView = new View(getContext()); itemView.setMinimumWidth(20); itemView.setMinimumHeight(5); - return new RecyclerView.ViewHolder(itemView) { - }; + return ViewHolder.createInstance(itemView, allowDividerAbove, allowDividerBelow); } @Override @@ -87,17 +166,7 @@ public class DividerItemDecorationTest extends AndroidTestCase { testRecyclerView.layout(0, 0, 20, 20); decoration.onDraw(canvas, testRecyclerView, null); - - // Draw the expected result on a bitmap - Bitmap expectedBitmap = Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_4444); - Canvas expectedCanvas = new Canvas(expectedBitmap); - Paint paint = new Paint(); - paint.setColor(Color.RED); - expectedCanvas.drawRect(0, 5, 20, 6, paint); - expectedCanvas.drawRect(0, 10, 20, 11, paint); - - // Compare the two bitmaps - assertBitmapEquals(expectedBitmap, bitmap); + return bitmap; } private void assertBitmapEquals(Bitmap expected, Bitmap actual) { @@ -110,4 +179,32 @@ public class DividerItemDecorationTest extends AndroidTestCase { } } } + + private static class ViewHolder extends RecyclerView.ViewHolder + implements DividerItemDecoration.DividedViewHolder { + + private boolean mAllowDividerAbove; + private boolean mAllowDividerBelow; + + public static ViewHolder createInstance(View itemView, boolean allowDividerAbove, + boolean allowDividerBelow) { + return new ViewHolder(itemView, allowDividerAbove, allowDividerBelow); + } + + private ViewHolder(View itemView, boolean allowDividerAbove, boolean allowDividerBelow) { + super(itemView); + mAllowDividerAbove = allowDividerAbove; + mAllowDividerBelow = allowDividerBelow; + } + + @Override + public boolean isDividerAllowedAbove() { + return mAllowDividerAbove; + } + + @Override + public boolean isDividerAllowedBelow() { + return mAllowDividerBelow; + } + } } diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java new file mode 100644 index 0000000..8ec9b0b --- /dev/null +++ b/library/full-support/test/src/com/android/setupwizardlib/test/GlifPreferenceLayoutTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.setupwizardlib.GlifPreferenceLayout; + +public class GlifPreferenceLayoutTest extends InstrumentationTestCase { + + private Context mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mContext = new ContextThemeWrapper(getInstrumentation().getContext(), + R.style.SuwThemeGlif_Light); + } + + @SmallTest + public void testDefaultTemplate() { + GlifPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + } + + @SmallTest + public void testGetRecyclerView() { + GlifPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + assertNotNull("getRecyclerView should not be null", layout.getRecyclerView()); + } + + @SmallTest + public void testOnCreateRecyclerView() { + GlifPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + final RecyclerView recyclerView = layout.onCreateRecyclerView(LayoutInflater.from(mContext), + layout, null /* savedInstanceState */); + assertNotNull("RecyclerView created should not be null", recyclerView); + } + + @SmallTest + public void testDividerInset() { + GlifPreferenceLayout layout = new TestLayout(mContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + } + assertPreferenceTemplateInflated(layout); + + layout.addView(layout.onCreateRecyclerView(LayoutInflater.from(mContext), layout, + null /* savedInstanceState */)); + + layout.setDividerInset(10); + assertEquals("Divider inset should be 10", 10, layout.getDividerInset()); + + final Drawable divider = layout.getDivider(); + assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable); + } + + private void assertPreferenceTemplateInflated(GlifPreferenceLayout layout) { + View contentContainer = layout.findViewById(R.id.suw_layout_content); + assertTrue("@id/suw_layout_content should be a ViewGroup", + contentContainer instanceof ViewGroup); + + if (layout instanceof TestLayout) { + assertNotNull("Header text view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_title)); + assertNotNull("Icon view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_icon)); + } + } + + // Make some methods public for testing + public static class TestLayout extends GlifPreferenceLayout { + + public TestLayout(Context context) { + super(context); + } + + @Override + public View findManagedViewById(int id) { + return super.findManagedViewById(id); + } + } +} diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java index d51a999..8a908ec 100644 --- a/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java +++ b/library/full-support/test/src/com/android/setupwizardlib/test/GlifRecyclerLayoutTest.java @@ -27,11 +27,8 @@ import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import com.android.setupwizardlib.GlifRecyclerLayout; -import com.android.setupwizardlib.view.HeaderRecyclerView; public class GlifRecyclerLayoutTest extends InstrumentationTestCase { @@ -115,8 +112,9 @@ public class GlifRecyclerLayoutTest extends InstrumentationTestCase { if (layout instanceof TestLayout) { assertNotNull("Header text view should not be null", - ((TestLayout) layout).getHeaderTextView()); - assertNotNull("Icon view should not be null", ((TestLayout) layout).getIconView()); + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_title)); + assertNotNull("Icon view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_icon)); } } @@ -128,13 +126,8 @@ public class GlifRecyclerLayoutTest extends InstrumentationTestCase { } @Override - public TextView getHeaderTextView() { - return super.getHeaderTextView(); - } - - @Override - public ImageView getIconView() { - return super.getIconView(); + public View findManagedViewById(int id) { + return super.findManagedViewById(id); } } } diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java new file mode 100644 index 0000000..f2b55f1 --- /dev/null +++ b/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardPreferenceLayoutTest.java @@ -0,0 +1,109 @@ +/* + * 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 com.android.setupwizardlib.test; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.setupwizardlib.SetupWizardPreferenceLayout; + +public class SetupWizardPreferenceLayoutTest extends InstrumentationTestCase { + + private Context mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mContext = new ContextThemeWrapper(getInstrumentation().getContext(), + R.style.SuwThemeMaterial_Light); + } + + @SmallTest + public void testDefaultTemplate() { + SetupWizardPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + } + + @SmallTest + public void testGetRecyclerView() { + SetupWizardPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + assertNotNull("getRecyclerView should not be null", layout.getRecyclerView()); + } + + @SmallTest + public void testOnCreateRecyclerView() { + SetupWizardPreferenceLayout layout = new TestLayout(mContext); + assertPreferenceTemplateInflated(layout); + final RecyclerView recyclerView = layout.onCreateRecyclerView(LayoutInflater.from(mContext), + layout, null /* savedInstanceState */); + assertNotNull("RecyclerView created should not be null", recyclerView); + } + + @SmallTest + public void testDividerInset() { + SetupWizardPreferenceLayout layout = new TestLayout(mContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + } + assertPreferenceTemplateInflated(layout); + + layout.addView(layout.onCreateRecyclerView(LayoutInflater.from(mContext), layout, + null /* savedInstanceState */)); + + layout.setDividerInset(10); + assertEquals("Divider inset should be 10", 10, layout.getDividerInset()); + + final Drawable divider = layout.getDivider(); + assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable); + } + + private void assertPreferenceTemplateInflated(SetupWizardPreferenceLayout layout) { + View contentContainer = layout.findViewById(R.id.suw_layout_content); + assertTrue("@id/suw_layout_content should be a ViewGroup", + contentContainer instanceof ViewGroup); + + if (layout instanceof TestLayout) { + assertNotNull("Header text view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_title)); + assertNotNull("Decoration view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_decor)); + } + } + + // Make some methods public for testing + public static class TestLayout extends SetupWizardPreferenceLayout { + + public TestLayout(Context context) { + super(context); + } + + @Override + public View findManagedViewById(int id) { + return super.findManagedViewById(id); + } + } +} diff --git a/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java b/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java new file mode 100644 index 0000000..2dbce11 --- /dev/null +++ b/library/full-support/test/src/com/android/setupwizardlib/test/SetupWizardRecyclerLayoutTest.java @@ -0,0 +1,134 @@ +/* + * 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 com.android.setupwizardlib.test; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.support.v7.widget.RecyclerView; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.android.setupwizardlib.SetupWizardRecyclerLayout; + +public class SetupWizardRecyclerLayoutTest extends InstrumentationTestCase { + + private Context mContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mContext = new ContextThemeWrapper(getInstrumentation().getContext(), + R.style.SuwThemeMaterial_Light); + } + + @SmallTest + public void testDefaultTemplate() { + SetupWizardRecyclerLayout layout = new TestLayout(mContext); + assertRecyclerTemplateInflated(layout); + } + + @SmallTest + public void testInflateFromXml() { + LayoutInflater inflater = LayoutInflater.from(mContext); + SetupWizardRecyclerLayout layout = (SetupWizardRecyclerLayout) + inflater.inflate(R.layout.test_recycler_layout, null); + assertRecyclerTemplateInflated(layout); + } + + @SmallTest + public void testGetRecyclerView() { + SetupWizardRecyclerLayout layout = new TestLayout(mContext); + assertRecyclerTemplateInflated(layout); + assertNotNull("getRecyclerView should not be null", layout.getRecyclerView()); + } + + @SmallTest + public void testAdapter() { + SetupWizardRecyclerLayout layout = new TestLayout(mContext); + assertRecyclerTemplateInflated(layout); + + final RecyclerView.Adapter adapter = new RecyclerView.Adapter() { + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int position) { + return new RecyclerView.ViewHolder(new View(parent.getContext())) {}; + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + } + + @Override + public int getItemCount() { + return 0; + } + }; + layout.setAdapter(adapter); + + final RecyclerView.Adapter gotAdapter = layout.getAdapter(); + // Note: The wrapped adapter should be returned, not the HeaderAdapter. + assertSame("Adapter got from SetupWizardLayout should be same as set", + adapter, gotAdapter); + } + + @SmallTest + public void testDividerInset() { + SetupWizardRecyclerLayout layout = new TestLayout(mContext); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + layout.setLayoutDirection(View.LAYOUT_DIRECTION_LTR); + } + assertRecyclerTemplateInflated(layout); + + layout.setDividerInset(10); + assertEquals("Divider inset should be 10", 10, layout.getDividerInset()); + + final Drawable divider = layout.getDivider(); + assertTrue("Divider should be instance of InsetDrawable", divider instanceof InsetDrawable); + } + + private void assertRecyclerTemplateInflated(SetupWizardRecyclerLayout layout) { + View recyclerView = layout.findViewById(R.id.suw_recycler_view); + assertTrue("@id/suw_recycler_view should be a RecyclerView", + recyclerView instanceof RecyclerView); + + if (layout instanceof TestLayout) { + assertNotNull("Header text view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_title)); + assertNotNull("Decoration view should not be null", + ((TestLayout) layout).findManagedViewById(R.id.suw_layout_decor)); + } + } + + // Make some methods public for testing + public static class TestLayout extends SetupWizardRecyclerLayout { + + public TestLayout(Context context) { + super(context); + } + + @Override + public View findManagedViewById(int id) { + return super.findManagedViewById(id); + } + + } +} diff --git a/library/main/res/layout/suw_items_button_bar.xml b/library/main/res/layout/suw_items_button_bar.xml new file mode 100644 index 0000000..48414a2 --- /dev/null +++ b/library/main/res/layout/suw_items_button_bar.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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + style="@style/SuwItemContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:orientation="horizontal" /> diff --git a/library/main/res/layout/suw_items_description.xml b/library/main/res/layout/suw_items_description.xml index 36c8088..8712be3 100644 --- a/library/main/res/layout/suw_items_description.xml +++ b/library/main/res/layout/suw_items_description.xml @@ -21,7 +21,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:paddingTop="@dimen/suw_description_margin_top"> + android:paddingTop="@dimen/suw_description_margin_top" + android:paddingBottom="@dimen/suw_description_margin_bottom_lists"> <FrameLayout android:id="@+id/suw_items_icon_container" diff --git a/library/main/res/layout/suw_list_header.xml b/library/main/res/layout/suw_list_header.xml index b0717bc..3b21082 100644 --- a/library/main/res/layout/suw_list_header.xml +++ b/library/main/res/layout/suw_list_header.xml @@ -21,7 +21,9 @@ android:layout_height="wrap_content" android:clipToPadding="false" android:orientation="vertical" - android:tag="stickyContainer"> + android:elevation="@dimen/suw_header_elevation_hack" + android:tag="stickyContainer" + tools:ignore="UnusedAttribute"> <com.android.setupwizardlib.view.Illustration android:id="@+id/suw_layout_decor" diff --git a/library/main/res/layout/suw_no_scroll_template_card.xml b/library/main/res/layout/suw_no_scroll_template_card.xml index e731497..f6c2cab 100644 --- a/library/main/res/layout/suw_no_scroll_template_card.xml +++ b/library/main/res/layout/suw_no_scroll_template_card.xml @@ -55,14 +55,7 @@ <FrameLayout android:id="@+id/suw_layout_content" android:layout_width="match_parent" - android:layout_height="match_parent"> - - <!-- Temporary solution to work with PreferenceFragment v14 --> - <FrameLayout android:id="@+id/list_container" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - - </FrameLayout> + android:layout_height="match_parent" /> <include layout="@layout/suw_progress_bar_stub" /> diff --git a/library/main/res/layout/suw_no_scroll_template_card_wide.xml b/library/main/res/layout/suw_no_scroll_template_card_wide.xml index b010c31..a09b9b6 100644 --- a/library/main/res/layout/suw_no_scroll_template_card_wide.xml +++ b/library/main/res/layout/suw_no_scroll_template_card_wide.xml @@ -56,14 +56,7 @@ <FrameLayout android:id="@+id/suw_layout_content" android:layout_width="match_parent" - android:layout_height="match_parent"> - - <!-- Temporary solution to work with PreferenceFragment v14 --> - <FrameLayout android:id="@+id/list_container" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - - </FrameLayout> + android:layout_height="match_parent" /> <include layout="@layout/suw_progress_bar_stub" /> diff --git a/library/main/res/layout/suw_no_scroll_template_header.xml b/library/main/res/layout/suw_no_scroll_template_header.xml index e17d6b9..413b329 100644 --- a/library/main/res/layout/suw_no_scroll_template_header.xml +++ b/library/main/res/layout/suw_no_scroll_template_header.xml @@ -56,14 +56,7 @@ android:id="@+id/suw_layout_content" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1"> - - <!-- Temporary solution to work with PreferenceFragment v14 --> - <FrameLayout android:id="@+id/list_container" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - - </FrameLayout> + android:layout_weight="1" /> </LinearLayout> diff --git a/library/main/res/layout/suw_no_scroll_template_header_collapsed.xml b/library/main/res/layout/suw_no_scroll_template_header_collapsed.xml index 16a93d4..f8af7a7 100644 --- a/library/main/res/layout/suw_no_scroll_template_header_collapsed.xml +++ b/library/main/res/layout/suw_no_scroll_template_header_collapsed.xml @@ -45,14 +45,7 @@ <FrameLayout android:id="@+id/suw_layout_content" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_weight="1"> - - <!-- Temporary solution to work with PreferenceFragment v14 --> - <FrameLayout android:id="@+id/list_container" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - - </FrameLayout> + android:layout_weight="1" /> <com.android.setupwizardlib.view.NavigationBar android:id="@+id/suw_layout_navigation_bar" diff --git a/library/main/res/values-be-rBY/strings.xml b/library/main/res/values-be-rBY/strings.xml new file mode 100644 index 0000000..1753d86 --- /dev/null +++ b/library/main/res/values-be-rBY/strings.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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="suw_next_button_label" msgid="7269625133873553978">"Далей"</string> + <string name="suw_back_button_label" msgid="1460929053642711025">"Назад"</string> + <string name="suw_more_button_label" msgid="7769076059705546563">"Яшчэ"</string> +</resources> diff --git a/library/main/res/values-bs-rBA/strings.xml b/library/main/res/values-bs-rBA/strings.xml new file mode 100644 index 0000000..2490af5 --- /dev/null +++ b/library/main/res/values-bs-rBA/strings.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. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + <string name="suw_next_button_label" msgid="7269625133873553978">"Naprijed"</string> + <string name="suw_back_button_label" msgid="1460929053642711025">"Nazad"</string> + <string name="suw_more_button_label" msgid="7769076059705546563">"Još"</string> +</resources> diff --git a/library/main/res/values-v21/styles.xml b/library/main/res/values-v21/styles.xml index 50faf1d..88c39d4 100644 --- a/library/main/res/values-v21/styles.xml +++ b/library/main/res/values-v21/styles.xml @@ -30,6 +30,40 @@ <item name="android:textColor">@android:color/white</item> </style> + <!-- GLIF Card layout (for tablets) --> + + <style name="SuwGlifCardBackground"> + <item name="android:background">?android:attr/colorPrimary</item> + </style> + + <!-- Items styles --> + + <style name="SuwItemContainer"> + <item name="android:minHeight">?android:attr/listPreferredItemHeight</item> + <item name="android:paddingBottom">@dimen/suw_items_padding_vertical</item> + <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item> + <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item> + <item name="android:paddingTop">@dimen/suw_items_padding_vertical</item> + </style> + + <style name="SuwItemTitle"> + <item name="android:textAppearance">?android:attr/textAppearanceListItem</item> + </style> + + <style name="SuwItemSummary"> + <item name="android:textAppearance">?android:attr/textAppearanceListItemSmall</item> + </style> + + <!-- Button styles --> + + <style name="SuwButtonItem" /> + + <style name="SuwButtonItem.Colored"> + <item name="android:buttonStyle">@android:style/Widget.Material.Button</item> + <item name="android:colorButtonNormal">?android:attr/colorAccent</item> + <item name="android:textColor">?android:attr/textColorPrimaryInverse</item> + </style> + <!-- Navigation bar styles --> <style name="SuwNavBarButtonStyle" parent="@android:style/Widget.Material.Button.Borderless"> diff --git a/library/main/res/values/attrs.xml b/library/main/res/values/attrs.xml index 9a88a1b..8501e94 100644 --- a/library/main/res/values/attrs.xml +++ b/library/main/res/values/attrs.xml @@ -22,6 +22,10 @@ <attr name="suwMarginSides" format="dimension|reference" /> <attr name="suwCardBackground" format="color|reference" /> + <attr name="suwDividerCondition"> + <enum name="either" value="0" /> + <enum name="both" value="1" /> + </attr> <attr name="suwListItemIconColor" format="color" /> <attr name="suwNavBarBackgroundColor" format="color" /> <attr name="suwNavBarButtonBackground" format="color|reference" /> @@ -77,6 +81,10 @@ <attr name="suwContainer" format="reference" /> </declare-styleable> + <declare-styleable name="SuwSetupWizardListLayout"> + <attr name="suwDividerInset" /> + </declare-styleable> + <declare-styleable name="SuwSetupWizardItemsLayout"> <attr name="android:entries" /> </declare-styleable> @@ -94,4 +102,17 @@ <attr name="android:visible" /> </declare-styleable> + <declare-styleable name="SuwDividerItemDecoration"> + <attr name="android:listDivider" /> + <attr name="android:dividerHeight" /> + <attr name="suwDividerCondition" /> + </declare-styleable> + + <declare-styleable name="SuwButtonItem"> + <attr name="android:buttonStyle" /> + <attr name="android:enabled" /> + <attr name="android:text" /> + <attr name="android:theme" /> + </declare-styleable> + </resources> diff --git a/library/main/res/values/colors.xml b/library/main/res/values/colors.xml index a4470d1..240d0ac 100644 --- a/library/main/res/values/colors.xml +++ b/library/main/res/values/colors.xml @@ -33,4 +33,8 @@ <color name="suw_navbar_bg_dark">#ff21272b</color> <color name="suw_navbar_bg_light">#ffe4e7e9</color> + <!-- GLIF colors --> + <color name="suw_color_accent_glif_dark">#ff4285f4</color> + <color name="suw_color_accent_glif_light">#ff4285f4</color> + </resources> diff --git a/library/main/res/values/dimens.xml b/library/main/res/values/dimens.xml index d2302e6..55b9a45 100644 --- a/library/main/res/values/dimens.xml +++ b/library/main/res/values/dimens.xml @@ -35,6 +35,7 @@ <dimen name="suw_description_margin_top">24dp</dimen> <dimen name="suw_description_margin_bottom">12dp</dimen> + <dimen name="suw_description_margin_bottom_lists">24dp</dimen> <dimen name="suw_description_line_spacing_extra">4sp</dimen> <dimen name="suw_description_text_size">16sp</dimen> @@ -68,6 +69,8 @@ <!-- Header layout (for phones) --> <dimen name="suw_title_area_elevation">3dp</dimen> + <!-- Hack to force the header (and its shadow) to be drawn on top of the list contents --> + <dimen name="suw_header_elevation_hack">1dp</dimen> <dimen name="suw_header_title_size">24sp</dimen> <dimen name="suw_header_title_margin_bottom">16dp</dimen> @@ -89,8 +92,11 @@ <dimen name="suw_items_padding_vertical">15dp</dimen> <dimen name="suw_items_verbose_padding_vertical">20dp</dimen> - <dimen name="suw_items_icon_divider_inset">72dp</dimen> - <dimen name="suw_items_text_divider_inset">24dp</dimen> + <dimen name="suw_items_icon_divider_inset">88dp</dimen> + <dimen name="suw_items_text_divider_inset">40dp</dimen> + + <dimen name="suw_items_glif_icon_divider_inset">72dp</dimen> + <dimen name="suw_items_glif_text_divider_inset">24dp</dimen> <!-- Extra padding in the bottom to compensate for difference between descent and (top) internal leading --> <dimen name="suw_items_padding_bottom_extra">1dp</dimen> diff --git a/library/main/res/values/styles.xml b/library/main/res/values/styles.xml index acae7c8..7c845a2 100644 --- a/library/main/res/values/styles.xml +++ b/library/main/res/values/styles.xml @@ -132,6 +132,11 @@ <item name="android:textAppearance">@style/TextAppearance.SuwGlifBody</item> </style> + <style name="TextAppearance.SuwItemSummary" parent="android:TextAppearance"> + <item name="android:textSize">16sp</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + </style> + <!-- GLIF layout --> <style name="SuwGlifHeaderTitle" parent="SuwBaseHeaderTitle"> diff --git a/library/main/src/com/android/setupwizardlib/GlifLayout.java b/library/main/src/com/android/setupwizardlib/GlifLayout.java index bf06bf5..c3202ad 100644 --- a/library/main/src/com/android/setupwizardlib/GlifLayout.java +++ b/library/main/src/com/android/setupwizardlib/GlifLayout.java @@ -24,6 +24,7 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION_CODES; import android.util.AttributeSet; +import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -121,7 +122,14 @@ public class GlifLayout extends TemplateLayout { if (template == 0) { template = R.layout.suw_glif_template; } - return super.onInflateTemplate(inflater, template); + try { + return super.onInflateTemplate(inflater, template); + } catch (RuntimeException e) { + // Versions before M throws RuntimeException for unsuccessful attribute resolution + // Versions M+ will throw an InflateException (which extends from RuntimeException) + throw new InflateException("Unable to inflate layout. Are you using " + + "@style/SuwThemeGlif (or its descendant) as your theme?", e); + } } @Override @@ -132,13 +140,22 @@ public class GlifLayout extends TemplateLayout { return super.findContainer(containerId); } + /** + * Same as {@link android.view.View#findViewById(int)}, but may include views that are managed + * by this view but not currently added to the view hierarchy. e.g. recycler view or list view + * headers that are not currently shown. + */ + protected View findManagedViewById(int id) { + return findViewById(id); + } + public ScrollView getScrollView() { - final View view = findViewById(R.id.suw_scroll_view); + final View view = findManagedViewById(R.id.suw_scroll_view); return view instanceof ScrollView ? (ScrollView) view : null; } - protected TextView getHeaderTextView() { - return (TextView) findViewById(R.id.suw_layout_title); + public TextView getHeaderTextView() { + return (TextView) findManagedViewById(R.id.suw_layout_title); } public void setHeaderText(int title) { @@ -182,7 +199,7 @@ public class GlifLayout extends TemplateLayout { } protected ImageView getIconView() { - return (ImageView) findViewById(R.id.suw_layout_icon); + return (ImageView) findManagedViewById(R.id.suw_layout_icon); } public void setPrimaryColor(ColorStateList color) { @@ -198,7 +215,7 @@ public class GlifLayout extends TemplateLayout { private void setGlifPatternColor(ColorStateList color) { if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - final View patternBg = findViewById(R.id.suw_pattern_bg); + final View patternBg = findManagedViewById(R.id.suw_pattern_bg); if (patternBg != null) { final GlifPatternDrawable background = new GlifPatternDrawable(color.getDefaultColor()); @@ -212,18 +229,18 @@ public class GlifLayout extends TemplateLayout { } public boolean isProgressBarShown() { - final View progressBar = findViewById(R.id.suw_layout_progress); + final View progressBar = findManagedViewById(R.id.suw_layout_progress); return progressBar != null && progressBar.getVisibility() == View.VISIBLE; } public void setProgressBarShown(boolean shown) { - final View progressBar = findViewById(R.id.suw_layout_progress); + final View progressBar = findManagedViewById(R.id.suw_layout_progress); if (shown) { if (progressBar != null) { progressBar.setVisibility(View.VISIBLE); } else { final ViewStub progressBarStub = - (ViewStub) findViewById(R.id.suw_layout_progress_stub); + (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub); if (progressBarStub != null) { progressBarStub.inflate(); } @@ -238,7 +255,7 @@ public class GlifLayout extends TemplateLayout { private void setProgressBarColor(ColorStateList color) { if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - final ProgressBar bar = (ProgressBar) findViewById(R.id.suw_layout_progress); + final ProgressBar bar = (ProgressBar) findManagedViewById(R.id.suw_layout_progress); if (bar != null) { bar.setIndeterminateTintList(color); } diff --git a/library/main/src/com/android/setupwizardlib/GlifListLayout.java b/library/main/src/com/android/setupwizardlib/GlifListLayout.java index 77065c8..e5f0d42 100644 --- a/library/main/src/com/android/setupwizardlib/GlifListLayout.java +++ b/library/main/src/com/android/setupwizardlib/GlifListLayout.java @@ -88,7 +88,7 @@ public class GlifListLayout extends GlifLayout { a.getDimensionPixelSize(R.styleable.SuwGlifListLayout_suwDividerInset, 0); if (dividerInset == 0) { dividerInset = getResources() - .getDimensionPixelSize(R.dimen.suw_items_icon_divider_inset); + .getDimensionPixelSize(R.dimen.suw_items_glif_icon_divider_inset); } setDividerInset(dividerInset); a.recycle(); @@ -145,8 +145,8 @@ public class GlifListLayout extends GlifLayout { * theme and inset it {@code inset} pixels to the right (or left in RTL layouts). * * @param inset The number of pixels to inset on the "start" side of the list divider. Typically - * this will be either {@code @dimen/suw_items_icon_divider_inset} or - * {@code @dimen/suw_items_text_divider_inset}. + * this will be either {@code @dimen/suw_items_glif_icon_divider_inset} or + * {@code @dimen/suw_items_glif_text_divider_inset}. */ public void setDividerInset(int inset) { mDividerInset = inset; diff --git a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java index 23db40c..3d0efdf 100644 --- a/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java +++ b/library/main/src/com/android/setupwizardlib/GlifPatternDrawable.java @@ -19,6 +19,7 @@ package com.android.setupwizardlib; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; @@ -58,6 +59,7 @@ public class GlifPatternDrawable extends Drawable { private Paint mPaint; private float[] mTempHsv = new float[3]; private Path mTempPath = new Path(); + private Bitmap mBitmap; public GlifPatternDrawable(int color) { mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -66,6 +68,16 @@ public class GlifPatternDrawable extends Drawable { @Override public void draw(Canvas canvas) { + if (mBitmap == null) { + mBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), + Bitmap.Config.ARGB_8888); + Canvas bitmapCanvas = new Canvas(mBitmap); + renderOnCanvas(bitmapCanvas); + } + canvas.drawBitmap(mBitmap, 0, 0, null); + } + + private void renderOnCanvas(Canvas canvas) { canvas.save(); canvas.clipRect(getBounds()); @@ -189,6 +201,7 @@ public class GlifPatternDrawable extends Drawable { public void setColor(int color) { mColor = color; mPaint.setColor(color); + mBitmap = null; invalidateSelf(); } diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java index f7512a9..ce4896f 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardLayout.java @@ -19,6 +19,7 @@ package com.android.setupwizardlib; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Shader.TileMode; import android.graphics.drawable.BitmapDrawable; @@ -32,10 +33,12 @@ import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.Gravity; +import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; +import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; @@ -48,6 +51,8 @@ public class SetupWizardLayout extends TemplateLayout { private static final String TAG = "SetupWizardLayout"; + private ColorStateList mProgressBarColor; + public SetupWizardLayout(Context context) { super(context, 0, 0); init(null, R.attr.suwLayoutTheme); @@ -147,6 +152,12 @@ public class SetupWizardLayout extends TemplateLayout { @Override protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + Log.w(TAG, "Ignoring restore instance state " + state); + super.onRestoreInstanceState(state); + return; + } + final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); final boolean isProgressBarShown = ss.mIsProgressBarShown; @@ -162,7 +173,14 @@ public class SetupWizardLayout extends TemplateLayout { if (template == 0) { template = R.layout.suw_template; } - return super.onInflateTemplate(inflater, template); + try { + return super.onInflateTemplate(inflater, template); + } catch (RuntimeException e) { + // Versions before M throws RuntimeException for unsuccessful attribute resolution + // Versions M+ will throw an InflateException (which extends from RuntimeException) + throw new InflateException("Unable to inflate layout. Are you using " + + "@style/SuwThemeMaterial (or its descendant) as your theme?", e); + } } @Override @@ -174,12 +192,12 @@ public class SetupWizardLayout extends TemplateLayout { } public NavigationBar getNavigationBar() { - final View view = findViewById(R.id.suw_layout_navigation_bar); + final View view = findManagedViewById(R.id.suw_layout_navigation_bar); return view instanceof NavigationBar ? (NavigationBar) view : null; } public ScrollView getScrollView() { - final View view = findViewById(R.id.suw_bottom_scroll_view); + final View view = findManagedViewById(R.id.suw_bottom_scroll_view); return view instanceof ScrollView ? (ScrollView) view : null; } @@ -194,10 +212,6 @@ public class SetupWizardLayout extends TemplateLayout { } } - protected TextView getHeaderTextView() { - return (TextView) findViewById(R.id.suw_layout_title); - } - public void setHeaderText(int title) { final TextView titleView = getHeaderTextView(); if (titleView != null) { @@ -217,8 +231,8 @@ public class SetupWizardLayout extends TemplateLayout { return titleView != null ? titleView.getText() : null; } - protected View getDecorationView() { - return findViewById(R.id.suw_layout_decor); + public TextView getHeaderTextView() { + return (TextView) findManagedViewById(R.id.suw_layout_title); } /** @@ -230,7 +244,7 @@ public class SetupWizardLayout extends TemplateLayout { * @param drawable The drawable specifying the illustration. */ public void setIllustration(Drawable drawable) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view instanceof Illustration) { final Illustration illustration = (Illustration) view; illustration.setIllustration(drawable); @@ -247,7 +261,7 @@ public class SetupWizardLayout extends TemplateLayout { * @param horizontalTile Resource ID of the horizontally repeating tile for tablet layout. */ public void setIllustration(int asset, int horizontalTile) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view instanceof Illustration) { final Illustration illustration = (Illustration) view; final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); @@ -256,7 +270,7 @@ public class SetupWizardLayout extends TemplateLayout { } private void setIllustration(Drawable asset, Drawable horizontalTile) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view instanceof Illustration) { final Illustration illustration = (Illustration) view; final Drawable illustrationDrawable = getIllustration(asset, horizontalTile); @@ -272,7 +286,7 @@ public class SetupWizardLayout extends TemplateLayout { * @see com.android.setupwizardlib.view.Illustration#setAspectRatio(float) */ public void setIllustrationAspectRatio(float aspectRatio) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view instanceof Illustration) { final Illustration illustration = (Illustration) view; illustration.setAspectRatio(aspectRatio); @@ -290,7 +304,7 @@ public class SetupWizardLayout extends TemplateLayout { * @param paddingTop The top padding in pixels. */ public void setDecorPaddingTop(int paddingTop) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view != null) { view.setPadding(view.getPaddingLeft(), paddingTop, view.getPaddingRight(), view.getPaddingBottom()); @@ -302,7 +316,7 @@ public class SetupWizardLayout extends TemplateLayout { * a bitmap tile and you want it to repeat, use {@link #setBackgroundTile(int)} instead. */ public void setLayoutBackground(Drawable background) { - final View view = getDecorationView(); + final View view = findManagedViewById(R.id.suw_layout_decor); if (view != null) { //noinspection deprecation view.setBackgroundDrawable(background); @@ -361,30 +375,74 @@ public class SetupWizardLayout extends TemplateLayout { } } + /** + * Same as {@link android.view.View#findViewById(int)}, but may include views that are managed + * by this view but not currently added to the view hierarchy. e.g. recycler view or list view + * headers that are not currently shown. + */ + protected View findManagedViewById(int id) { + return findViewById(id); + } + public boolean isProgressBarShown() { - final View progressBar = findViewById(R.id.suw_layout_progress); + final View progressBar = findManagedViewById(R.id.suw_layout_progress); return progressBar != null && progressBar.getVisibility() == View.VISIBLE; } - public void showProgressBar() { - final View progressBar = findViewById(R.id.suw_layout_progress); + /** + * Sets whether the progress bar below the header text is shown or not. The progress bar is + * a lazily inflated ViewStub, which means the progress bar will not actually be part of the + * view hierarchy until the first time this is set to {@code true}. + */ + public void setProgressBarShown(boolean shown) { + final View progressBar = findManagedViewById(R.id.suw_layout_progress); if (progressBar != null) { - progressBar.setVisibility(View.VISIBLE); - } else { - final ViewStub progressBarStub = (ViewStub) findViewById(R.id.suw_layout_progress_stub); + progressBar.setVisibility(shown ? View.VISIBLE : View.GONE); + } else if (shown) { + final ViewStub progressBarStub = + (ViewStub) findManagedViewById(R.id.suw_layout_progress_stub); if (progressBarStub != null) { progressBarStub.inflate(); } + if (mProgressBarColor != null) { + setProgressBarColor(mProgressBarColor); + } } } + /** + * @deprecated Use {@link #setProgressBarShown(boolean)} + */ + @Deprecated + public void showProgressBar() { + setProgressBarShown(true); + } + + /** + * @deprecated Use {@link #setProgressBarShown(boolean)} + */ + @Deprecated public void hideProgressBar() { - final View progressBar = findViewById(R.id.suw_layout_progress); - if (progressBar != null) { - progressBar.setVisibility(View.GONE); + setProgressBarShown(false); + } + + public void setProgressBarColor(ColorStateList color) { + mProgressBarColor = color; + if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + // Suppress lint error caused by + // https://code.google.com/p/android/issues/detail?id=183136 + // noinspection AndroidLintWrongViewCast + final ProgressBar bar = (ProgressBar) findViewById(R.id.suw_layout_progress); + if (bar != null) { + bar.setIndeterminateTintList(color); + } } } + public ColorStateList getProgressBarColor() { + return mProgressBarColor; + } + /* Misc */ protected static class SavedState extends BaseSavedState { diff --git a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java index 2ac8600..3410e9b 100644 --- a/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java +++ b/library/main/src/com/android/setupwizardlib/SetupWizardListLayout.java @@ -18,6 +18,9 @@ package com.android.setupwizardlib; import android.annotation.TargetApi; import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Build.VERSION_CODES; import android.util.AttributeSet; import android.util.Log; @@ -27,6 +30,7 @@ import android.view.ViewGroup; import android.widget.ListAdapter; import android.widget.ListView; +import com.android.setupwizardlib.util.DrawableLayoutDirectionHelper; import com.android.setupwizardlib.util.ListViewRequireScrollHelper; import com.android.setupwizardlib.view.NavigationBar; @@ -34,6 +38,9 @@ public class SetupWizardListLayout extends SetupWizardLayout { private static final String TAG = "SetupWizardListLayout"; private ListView mListView; + private Drawable mDivider; + private Drawable mDefaultDivider; + private int mDividerInset; public SetupWizardListLayout(Context context) { this(context, 0, 0); @@ -45,15 +52,27 @@ public class SetupWizardListLayout extends SetupWizardLayout { public SetupWizardListLayout(Context context, int template, int containerId) { super(context, template, containerId); + init(context, null, 0); } public SetupWizardListLayout(Context context, AttributeSet attrs) { super(context, attrs); + init(context, attrs, 0); } @TargetApi(VERSION_CODES.HONEYCOMB) public SetupWizardListLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr) { + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SuwSetupWizardListLayout, defStyleAttr, 0); + int dividerInset = + a.getDimensionPixelSize(R.styleable.SuwSetupWizardListLayout_suwDividerInset, 0); + setDividerInset(dividerInset); + a.recycle(); } @Override @@ -73,6 +92,15 @@ public class SetupWizardListLayout extends SetupWizardLayout { } @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (mDivider == null) { + // Update divider in case layout direction has just been resolved + updateDivider(); + } + } + + @Override protected void onTemplateInflated() { mListView = (ListView) findViewById(android.R.id.list); } @@ -85,6 +113,7 @@ public class SetupWizardListLayout extends SetupWizardLayout { getListView().setAdapter(adapter); } + @Override public void requireScrollToBottom() { final NavigationBar navigationBar = getNavigationBar(); final ListView listView = getListView(); @@ -95,4 +124,41 @@ public class SetupWizardListLayout extends SetupWizardLayout { + " the template to require scrolling."); } } + + /** + * Sets the start inset of the divider. This will use the default divider drawable set in the + * theme and inset it {@code inset} pixels to the right (or left in RTL layouts). + * + * @param inset The number of pixels to inset on the "start" side of the list divider. Typically + * this will be either {@code @dimen/suw_items_icon_divider_inset} or + * {@code @dimen/suw_items_text_divider_inset}. + */ + public void setDividerInset(int inset) { + mDividerInset = inset; + updateDivider(); + } + + public int getDividerInset() { + return mDividerInset; + } + + private void updateDivider() { + boolean shouldUpdate = true; + if (Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT) { + shouldUpdate = isLayoutDirectionResolved(); + } + if (shouldUpdate) { + final ListView listView = getListView(); + if (mDefaultDivider == null) { + mDefaultDivider = listView.getDivider(); + } + mDivider = DrawableLayoutDirectionHelper.createRelativeInsetDrawable(mDefaultDivider, + mDividerInset /* start */, 0 /* top */, 0 /* end */, 0 /* bottom */, this); + listView.setDivider(mDivider); + } + } + + public Drawable getDivider() { + return mDivider; + } } diff --git a/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java b/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java new file mode 100644 index 0000000..8325232 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/gesture/ConsecutiveTapsGestureDetector.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.gesture; + +import android.graphics.Rect; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Helper class to detect the consective-tap gestures on a view. + * + * <p/>This class is instantiated and used similar to a GestureDetector, where onTouchEvent should + * be called when there are MotionEvents this detector should know about. + */ +public final class ConsecutiveTapsGestureDetector { + + public interface OnConsecutiveTapsListener { + /** + * Callback method when the user tapped on the target view X number of times. + */ + void onConsecutiveTaps(int numOfConsecutiveTaps); + } + + private final View mView; + private final OnConsecutiveTapsListener mListener; + private final int mConsecutiveTapTouchSlopSquare; + + private int mConsecutiveTapsCounter = 0; + private MotionEvent mPreviousTapEvent; + + /** + * @param listener The listener that responds to the gesture. + * @param view The target view that associated with consecutive-tap gesture. + */ + public ConsecutiveTapsGestureDetector( + OnConsecutiveTapsListener listener, + View view) { + mListener = listener; + mView = view; + int doubleTapSlop = ViewConfiguration.get(mView.getContext()).getScaledDoubleTapSlop(); + mConsecutiveTapTouchSlopSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * This method should be called from the relevant activity or view, typically in + * onTouchEvent, onInterceptTouchEvent or dispatchTouchEvent. + * + * @param ev The motion event + */ + public void onTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + Rect viewRect = new Rect(); + int[] leftTop = new int[2]; + mView.getLocationOnScreen(leftTop); + viewRect.set( + leftTop[0], + leftTop[1], + leftTop[0] + mView.getWidth(), + leftTop[1] + mView.getHeight()); + if (viewRect.contains((int) ev.getX(), (int) ev.getY())) { + if (isConsecutiveTap(ev)) { + mConsecutiveTapsCounter++; + } else { + mConsecutiveTapsCounter = 1; + } + mListener.onConsecutiveTaps(mConsecutiveTapsCounter); + } else { + // Touch outside the target view. Reset counter. + mConsecutiveTapsCounter = 0; + } + + if (mPreviousTapEvent != null) { + mPreviousTapEvent.recycle(); + } + mPreviousTapEvent = MotionEvent.obtain(ev); + } + } + + /** + * Resets the consecutive-tap counter to zero. + */ + public void resetCounter() { + mConsecutiveTapsCounter = 0; + } + + /** + * Returns true if the distance between consecutive tap is within + * {@link #mConsecutiveTapTouchSlopSquare}. False, otherwise. + */ + private boolean isConsecutiveTap(MotionEvent currentTapEvent) { + if (mPreviousTapEvent == null) { + return false; + } + + double deltaX = mPreviousTapEvent.getX() - currentTapEvent.getX(); + double deltaY = mPreviousTapEvent.getY() - currentTapEvent.getY(); + return (deltaX * deltaX + deltaY * deltaY <= mConsecutiveTapTouchSlopSquare); + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java b/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java new file mode 100644 index 0000000..55bbe75 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ButtonBarItem.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.items; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.android.setupwizardlib.R; + +import java.util.ArrayList; + +/** + * A list item with one or more buttons, declared as + * {@link com.android.setupwizardlib.items.ButtonItem}. + * + * <p>Example usage: + * <pre>{@code + * <ButtonBarItem> + * + * <ButtonItem + * android:id="@+id/skip_button" + * android:text="@string/skip_button_label /> + * + * <ButtonItem + * android:id="@+id/next_button" + * android:text="@string/next_button_label + * android:theme="@style/SuwButtonItem.Colored" /> + * + * </ButtonBarItem> + * }</pre> + */ +public class ButtonBarItem extends AbstractItem implements ItemInflater.ItemParent { + + private final ArrayList<ButtonItem> mButtons = new ArrayList<>(); + private boolean mVisible = true; + + public ButtonBarItem() { + super(); + } + + public ButtonBarItem(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public int getCount() { + return isVisible() ? 1 : 0; + } + + @Override + public boolean isEnabled() { + // The children buttons are enabled and clickable, but the item itself is not + return false; + } + + @Override + public int getLayoutResource() { + return R.layout.suw_items_button_bar; + } + + public void setVisible(boolean visible) { + mVisible = visible; + } + + public boolean isVisible() { + return mVisible; + } + + public int getViewId() { + return getId(); + } + + @Override + public void onBindView(View view) { + // Note: The efficiency could be improved by trying to recycle the buttons created by + // ButtonItem + final LinearLayout layout = (LinearLayout) view; + layout.removeAllViews(); + + for (ButtonItem buttonItem : mButtons) { + Button button = buttonItem.createButton(layout); + layout.addView(button); + } + + view.setId(getViewId()); + } + + @Override + public void addChild(ItemHierarchy child) { + if (child instanceof ButtonItem) { + mButtons.add((ButtonItem) child); + } else { + throw new UnsupportedOperationException("Cannot add non-button item to Button Bar"); + } + } + + @Override + public ItemHierarchy findItemById(int id) { + if (getId() == id) { + return this; + } + for (ButtonItem button : mButtons) { + final ItemHierarchy item = button.findItemById(id); + if (item != null) { + return item; + } + } + return null; + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/ButtonItem.java b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java new file mode 100644 index 0000000..4faeff4 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/items/ButtonItem.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.items; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import com.android.setupwizardlib.R; + +/** + * Description of a button inside {@link com.android.setupwizardlib.items.ButtonBarItem}. This item + * will not be bound by the adapter, and must be a child of {@code ButtonBarItem}. + */ +public class ButtonItem extends AbstractItem implements View.OnClickListener { + + public interface OnClickListener { + void onClick(ButtonItem item); + } + + private boolean mEnabled = true; + private CharSequence mText; + private int mTheme = R.style.SuwButtonItem; + private OnClickListener mListener; + + private Button mButton; + + public ButtonItem() { + super(); + } + + public ButtonItem(Context context, AttributeSet attrs) { + super(context, attrs); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SuwButtonItem); + mEnabled = a.getBoolean(R.styleable.SuwButtonItem_android_enabled, true); + mText = a.getText(R.styleable.SuwButtonItem_android_text); + mTheme = a.getResourceId(R.styleable.SuwButtonItem_android_theme, R.style.SuwButtonItem); + a.recycle(); + } + + public void setOnClickListener(OnClickListener listener) { + mListener = listener; + } + + public void setText(CharSequence text) { + mText = text; + } + + public CharSequence getText() { + return mText; + } + + /** + * The theme to use for this button. This can be used to create button of a particular style + * (e.g. a colored or borderless button). Typically {@code android:buttonStyle} will be set in + * the theme to change the style applied by the button. + * + * @param theme Resource ID of the theme + */ + public void setTheme(int theme) { + mTheme = theme; + mButton = null; + } + + /** + * @return Resource ID of the theme used by this button. + */ + public int getTheme() { + return mTheme; + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + @Override + public int getCount() { + return 0; + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public int getLayoutResource() { + return 0; + } + + /** + * Do not use this since ButtonItem is not directly part of a list. + */ + @Override + public final void onBindView(View view) { + throw new UnsupportedOperationException("Cannot bind to ButtonItem's view"); + } + + protected Button createButton(ViewGroup parent) { + if (mButton == null) { + Context context = parent.getContext(); + if (mTheme != 0) { + context = new ContextThemeWrapper(context, mTheme); + } + mButton = new Button(context); + mButton.setOnClickListener(this); + } + mButton.setEnabled(mEnabled); + mButton.setText(mText); + return mButton; + } + + @Override + public void onClick(View v) { + if (mListener != null) { + mListener.onClick(this); + } + } +} diff --git a/library/main/src/com/android/setupwizardlib/items/Item.java b/library/main/src/com/android/setupwizardlib/items/Item.java index d03b990..796d533 100644 --- a/library/main/src/com/android/setupwizardlib/items/Item.java +++ b/library/main/src/com/android/setupwizardlib/items/Item.java @@ -138,6 +138,9 @@ public class Item extends AbstractItem { final Drawable icon = getIcon(); if (icon != null) { final ImageView iconView = (ImageView) view.findViewById(R.id.suw_items_icon); + // Set the image drawable to null before setting the state and level to avoid affecting + // any recycled drawable in the ImageView + iconView.setImageDrawable(null); iconView.setImageState(icon.getState(), false /* merge */); iconView.setImageLevel(icon.getLevel()); iconView.setImageDrawable(icon); diff --git a/library/main/src/com/android/setupwizardlib/items/ItemGroup.java b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java index 3e500bb..e449a95 100644 --- a/library/main/src/com/android/setupwizardlib/items/ItemGroup.java +++ b/library/main/src/com/android/setupwizardlib/items/ItemGroup.java @@ -23,7 +23,8 @@ import android.util.SparseIntArray; import java.util.ArrayList; import java.util.List; -public class ItemGroup extends AbstractItemHierarchy implements ItemHierarchy.Observer { +public class ItemGroup extends AbstractItemHierarchy implements ItemInflater.ItemParent, + ItemHierarchy.Observer { /* static section */ @@ -101,6 +102,7 @@ public class ItemGroup extends AbstractItemHierarchy implements ItemHierarchy.Ob /** * Add a child hierarchy to this item group. */ + @Override public void addChild(ItemHierarchy child) { mChildren.add(child); child.registerObserver(this); diff --git a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java index 6bd77ac..cadf1a4 100644 --- a/library/main/src/com/android/setupwizardlib/items/ItemInflater.java +++ b/library/main/src/com/android/setupwizardlib/items/ItemInflater.java @@ -27,6 +27,10 @@ public class ItemInflater extends GenericInflater<ItemHierarchy> { private static final String TAG = "ItemInflater"; + public interface ItemParent { + void addChild(ItemHierarchy child); + } + private final Context mContext; public ItemInflater(Context context) { @@ -44,14 +48,15 @@ public class ItemInflater extends GenericInflater<ItemHierarchy> { * Return the context we are running in, for access to resources, class * loader, etc. */ + @Override public Context getContext() { return mContext; } @Override protected void onAddChildItem(ItemHierarchy parent, ItemHierarchy child) { - if (parent instanceof ItemGroup) { - ((ItemGroup) parent).addChild(child); + if (parent instanceof ItemParent) { + ((ItemParent) parent).addChild(child); } else { throw new IllegalArgumentException("Cannot add child item to " + parent); } diff --git a/library/main/src/com/android/setupwizardlib/span/LinkSpan.java b/library/main/src/com/android/setupwizardlib/span/LinkSpan.java new file mode 100644 index 0000000..e4f9854 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/span/LinkSpan.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.span; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.text.TextPaint; +import android.text.style.ClickableSpan; +import android.util.Log; +import android.view.View; + +/** + * A clickable span that will listen for click events and send it back to the context. To use this + * class, implement {@link com.android.setupwizardlib.span.LinkSpan.OnClickListener} in your + * context (typically your Activity). + * + * <p />Note on accessibility: For TalkBack to be able to traverse and interact with the links, you + * should use {@code LinkAccessibilityHelper} in your {@code TextView} subclass. Optionally you can + * also use {@code RichTextView}, which includes link support. + */ +public class LinkSpan extends ClickableSpan { + + /* + * Implementation note: When the orientation changes, TextView retains a reference to this span + * instead of writing it to a parcel (ClickableSpan is not Parcelable). If this class has any + * reference to the containing Activity (i.e. the activity context, or any views in the + * activity), it will cause memory leak. + */ + + /* static section */ + + private static final String TAG = "LinkSpan"; + + private static final Typeface TYPEFACE_MEDIUM = + Typeface.create("sans-serif-medium", Typeface.NORMAL); + + public interface OnClickListener { + void onClick(LinkSpan span); + } + + /* non-static section */ + + private final String mId; + + public LinkSpan(String id) { + mId = id; + } + + @Override + public void onClick(View view) { + final Context context = view.getContext(); + if (context instanceof OnClickListener) { + ((OnClickListener) context).onClick(this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + view.cancelPendingInputEvents(); + } + } else { + Log.w(TAG, "Dropping click event. No listener attached."); + } + } + + @Override + public void updateDrawState(TextPaint drawState) { + super.updateDrawState(drawState); + drawState.setUnderlineText(false); + drawState.setTypeface(TYPEFACE_MEDIUM); + } + + public String getId() { + return mId; + } +} diff --git a/library/main/src/com/android/setupwizardlib/span/SpanHelper.java b/library/main/src/com/android/setupwizardlib/span/SpanHelper.java new file mode 100644 index 0000000..d75e338 --- /dev/null +++ b/library/main/src/com/android/setupwizardlib/span/SpanHelper.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.span; + +import android.text.Spannable; + +/** + * Contains helper methods for dealing with text spans, e.g. the ones in {@code android.text.style}. + */ +public class SpanHelper { + + /** + * Add {@code newSpan} at the same start and end indices as {@code oldSpan} and remove + * {@code oldSpan} from the {@code spannable}. + */ + public static void replaceSpan(Spannable spannable, Object oldSpan, Object newSpan) { + final int spanStart = spannable.getSpanStart(oldSpan); + final int spanEnd = spannable.getSpanEnd(oldSpan); + spannable.removeSpan(oldSpan); + spannable.setSpan(newSpan, spanStart, spanEnd, 0); + } +} diff --git a/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java index 9d95db5..44bcefc 100644 --- a/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java +++ b/library/main/src/com/android/setupwizardlib/util/SystemBarHelper.java @@ -210,7 +210,7 @@ public class SystemBarHelper { */ public static void setImeInsetView(final View view) { if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - view.setOnApplyWindowInsetsListener(new WindowInsetsListener(view.getContext())); + view.setOnApplyWindowInsetsListener(new WindowInsetsListener()); } } @@ -305,20 +305,20 @@ public class SystemBarHelper { @TargetApi(VERSION_CODES.LOLLIPOP) private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener { - - private int mNavigationBarHeight; - - public WindowInsetsListener(Context context) { - mNavigationBarHeight = - context.getResources().getDimensionPixelSize(R.dimen.suw_navbar_height); - } + private int mBottomOffset; + private boolean mHasCalculatedBottomOffset = false; @Override public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { + if (!mHasCalculatedBottomOffset) { + mBottomOffset = getBottomDistance(view); + mHasCalculatedBottomOffset = true; + } + int bottomInset = insets.getSystemWindowInsetBottom(); final int bottomMargin = Math.max( - insets.getSystemWindowInsetBottom() - mNavigationBarHeight, 0); + insets.getSystemWindowInsetBottom() - mBottomOffset, 0); final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); @@ -339,4 +339,10 @@ public class SystemBarHelper { ); } } + + private static int getBottomDistance(View view) { + int[] coords = new int[2]; + view.getLocationInWindow(coords); + return view.getRootView().getHeight() - coords[1] - view.getHeight(); + } } diff --git a/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java index 7d98a90..10172ce 100644 --- a/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java +++ b/library/main/src/com/android/setupwizardlib/util/WizardManagerHelper.java @@ -123,6 +123,8 @@ public class WizardManagerHelper { */ public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) { dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE)); + dstIntent.putExtra(EXTRA_IS_FIRST_RUN, + srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false)); dstIntent.putExtra(EXTRA_SCRIPT_URI, srcIntent.getStringExtra(EXTRA_SCRIPT_URI)); dstIntent.putExtra(EXTRA_ACTION_ID, srcIntent.getStringExtra(EXTRA_ACTION_ID)); } diff --git a/library/main/src/com/android/setupwizardlib/view/StatusBarBackgroundLayout.java b/library/main/src/com/android/setupwizardlib/view/StatusBarBackgroundLayout.java index 7fdabec..fe0bc8f 100644 --- a/library/main/src/com/android/setupwizardlib/view/StatusBarBackgroundLayout.java +++ b/library/main/src/com/android/setupwizardlib/view/StatusBarBackgroundLayout.java @@ -69,6 +69,16 @@ public class StatusBarBackgroundLayout extends FrameLayout { } @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + if (mLastInsets == null) { + requestApplyInsets(); + } + } + } + + @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { diff --git a/library/platform/res/values-v21/styles.xml b/library/platform/res/values-v21/styles.xml index 5c51493..8c74d58 100644 --- a/library/platform/res/values-v21/styles.xml +++ b/library/platform/res/values-v21/styles.xml @@ -70,8 +70,8 @@ <!-- Placeholder for GLIF dark theme, colors are not updated yet --> <style name="SuwThemeGlif" parent="android:Theme.Material.NoActionBar"> - <item name="android:colorAccent">@color/suw_color_accent_light</item> - <item name="android:colorPrimary">@color/suw_color_accent_light</item> + <item name="android:colorAccent">@color/suw_color_accent_glif_dark</item> + <item name="android:colorPrimary">@color/suw_color_accent_glif_light</item> <item name="android:indeterminateTint">?android:attr/colorPrimary</item> <!-- Specify the indeterminateTintMode to work around a bug in Lollipop --> <item name="android:indeterminateTintMode">src_in</item> @@ -92,8 +92,8 @@ </style> <style name="SuwThemeGlif.Light" parent="android:Theme.Material.Light.NoActionBar"> - <item name="android:colorAccent">@color/suw_color_accent_light</item> - <item name="android:colorPrimary">@color/suw_color_accent_light</item> + <item name="android:colorAccent">@color/suw_color_accent_glif_dark</item> + <item name="android:colorPrimary">@color/suw_color_accent_glif_light</item> <item name="android:indeterminateTint">?android:attr/colorPrimary</item> <!-- Specify the indeterminateTintMode to work around a bug in Lollipop --> <item name="android:indeterminateTintMode">src_in</item> @@ -113,28 +113,4 @@ <item name="suwMarginSides">@dimen/suw_glif_margin_sides</item> </style> - <!-- GLIF Card layout (for tablets) --> - - <style name="SuwGlifCardBackground"> - <item name="android:background">?android:attr/colorPrimary</item> - </style> - - <!-- Items styles --> - - <style name="SuwItemContainer"> - <item name="android:minHeight">?android:attr/listPreferredItemHeight</item> - <item name="android:paddingBottom">@dimen/suw_items_padding_vertical</item> - <item name="android:paddingEnd">?android:attr/listPreferredItemPaddingEnd</item> - <item name="android:paddingStart">?android:attr/listPreferredItemPaddingStart</item> - <item name="android:paddingTop">@dimen/suw_items_padding_vertical</item> - </style> - - <style name="SuwItemTitle"> - <item name="android:textAppearance">?android:attr/textAppearanceListItem</item> - </style> - - <style name="SuwItemSummary"> - <item name="android:textAppearance">?android:attr/textAppearanceListItemSmall</item> - </style> - </resources> diff --git a/library/rules.gradle b/library/rules.gradle index e99c994..8efdd5c 100644 --- a/library/rules.gradle +++ b/library/rules.gradle @@ -96,8 +96,12 @@ android { res.srcDirs = ['test/res'] } + androidTestEclairMr1Compat { + java.srcDirs = ['eclair-mr1/test/src'] + } + androidTestFullSupport { - java.srcDirs = ['full-support/test/src'] + java.srcDirs = ['full-support/test/src', 'eclair-mr1/test/src'] res.srcDirs = ['full-support/test/res'] } } diff --git a/library/self.gradle b/library/self.gradle index 9baa10e..7b3c3df 100644 --- a/library/self.gradle +++ b/library/self.gradle @@ -7,7 +7,7 @@ apply plugin: 'dist' apply from: 'build.gradle' apply from: '../tools/gradle/docs.gradle' -task docs(dependsOn: 'javadocPlatformRelease') +task docs(dependsOn: 'javadocFullSupportRelease') android.lintOptions { abortOnError true diff --git a/library/test/src/com/android/setupwizardlib/test/ButtonBarItemTest.java b/library/test/src/com/android/setupwizardlib/test/ButtonBarItemTest.java new file mode 100644 index 0000000..d3d87e5 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ButtonBarItemTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.test.AndroidTestCase; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.android.setupwizardlib.items.ButtonBarItem; +import com.android.setupwizardlib.items.ButtonItem; +import com.android.setupwizardlib.items.Item; +import com.android.setupwizardlib.items.ItemHierarchy; + +public class ButtonBarItemTest extends AndroidTestCase { + + private ButtonItem mChild1; + private ButtonItem mChild2; + private ButtonItem mChild3; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mChild1 = new ButtonItem(); + mChild2 = new ButtonItem(); + mChild3 = new ButtonItem(); + } + + public void testFindItemById() { + ButtonBarItem item = new ButtonBarItem(); + item.setId(888); + + mChild1.setId(123); + mChild2.setId(456); + mChild3.setId(789); + item.addChild(mChild1); + item.addChild(mChild2); + item.addChild(mChild3); + + assertEquals("Finding 123 should return child1", mChild1, item.findItemById(123)); + assertEquals("Finding 456 should return child2", mChild2, item.findItemById(456)); + assertEquals("Finding 789 should return child3", mChild3, item.findItemById(789)); + + assertEquals("Finding 888 should return ButtonBarItem itself", item, + item.findItemById(888)); + + assertNull("Finding 999 should return null", item.findItemById(999)); + } + + public void testBindEmpty() { + ButtonBarItem item = new ButtonBarItem(); + final ViewGroup layout = createLayout(); + item.onBindView(layout); + + assertEquals("Binding empty ButtonBar should not create any children", 0, + layout.getChildCount()); + } + + public void testBind() { + ButtonBarItem item = new ButtonBarItem(); + + item.addChild(mChild1); + mChild1.setText("child1"); + item.addChild(mChild2); + mChild2.setText("child2"); + item.addChild(mChild3); + mChild3.setText("child3"); + + final ViewGroup layout = createLayout(); + item.onBindView(layout); + + assertEquals("Binding ButtonBar should create 3 children", 3, layout.getChildCount()); + assertEquals("First button should have text \"child1\"", "child1", + ((Button) layout.getChildAt(0)).getText()); + assertEquals("Second button should have text \"child2\"", "child2", + ((Button) layout.getChildAt(1)).getText()); + assertEquals("Third button should have text \"child3\"", "child3", + ((Button) layout.getChildAt(2)).getText()); + } + + public void testAddInvalidChild() { + ButtonBarItem item = new ButtonBarItem(); + + ItemHierarchy invalidChild = new Item(); + + try { + item.addChild(invalidChild); + fail("Adding non ButtonItem to ButtonBarItem should throw exception"); + } catch (UnsupportedOperationException e) { + // pass + } + } + + private ViewGroup createLayout() { + return new LinearLayout(mContext); + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java new file mode 100644 index 0000000..45342d0 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/ButtonItemTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.test.AndroidTestCase; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.LinearLayout; + +import com.android.setupwizardlib.R; +import com.android.setupwizardlib.items.ButtonItem; + +public class ButtonItemTest extends AndroidTestCase { + + private ViewGroup mParent; + + @Override + protected void setUp() throws Exception { + super.setUp(); + mParent = new LinearLayout(getContext()); + } + + public void testDefaultItem() { + ButtonItem item = new ButtonItem(); + + assertTrue("ButtonItem should be enabled by default", item.isEnabled()); + assertEquals("ButtonItem should return count = 0", 0, item.getCount()); + assertEquals("ButtonItem should return layout resource = 0", 0, item.getLayoutResource()); + assertEquals("Default theme should be @style/SuwButtonItem", R.style.SuwButtonItem, + item.getTheme()); + assertNull("Default text should be null", item.getText()); + } + + public void testOnBindView() { + ButtonItem item = new ButtonItem(); + + try { + item.onBindView(new View(getContext())); + fail("Calling onBindView on ButtonItem should throw UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // pass + } + } + + public void testCreateButton() { + TestButtonItem item = new TestButtonItem(); + final Button button = item.createButton(mParent); + + assertTrue("Default button should be enabled", button.isEnabled()); + assertTrue("Default button text should be empty", TextUtils.isEmpty(button.getText())); + } + + public void testSetEnabledTrue() { + TestButtonItem item = new TestButtonItem(); + item.setEnabled(true); + + final Button button = item.createButton(mParent); + assertTrue("ButtonItem should be enabled", item.isEnabled()); + assertTrue("Button should be enabled", button.isEnabled()); + } + + public void testSetEnabledFalse() { + TestButtonItem item = new TestButtonItem(); + item.setEnabled(false); + + final Button button = item.createButton(mParent); + assertFalse("ButtonItem should be disabled", item.isEnabled()); + assertFalse("Button should be disabled", button.isEnabled()); + } + + public void testSetText() { + TestButtonItem item = new TestButtonItem(); + item.setText("lorem ipsum"); + + final Button button = item.createButton(mParent); + assertEquals("ButtonItem text should be \"lorem ipsum\"", "lorem ipsum", item.getText()); + assertEquals("Button text should be \"lorem ipsum\"", "lorem ipsum", button.getText()); + } + + public void testSetTheme() { + TestButtonItem item = new TestButtonItem(); + item.setTheme(12345); + + final Button button = item.createButton(mParent); + assertEquals("ButtonItem theme should be 12345", 12345, item.getTheme()); + button.getContext().getTheme(); + } + + public void testOnClickListener() { + TestButtonItem item = new TestButtonItem(); + final TestOnClickListener listener = new TestOnClickListener(); + item.setOnClickListener(listener); + + assertNull("Clicked item should be null before clicking", listener.clickedItem); + + final Button button = item.createButton(mParent); + button.performClick(); + + assertSame("Clicked item should be set", item, listener.clickedItem); + } + + private static class TestOnClickListener implements ButtonItem.OnClickListener { + + public ButtonItem clickedItem = null; + + @Override + public void onClick(ButtonItem item) { + clickedItem = item; + } + } + + private static class TestButtonItem extends ButtonItem { + + @Override + public Button createButton(ViewGroup parent) { + // Make this method public for testing + return super.createButton(parent); + } + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java index 310dcbe..1ae620d 100644 --- a/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java +++ b/library/test/src/com/android/setupwizardlib/test/GlifLayoutTest.java @@ -23,6 +23,7 @@ import android.os.Build; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.SmallTest; import android.view.ContextThemeWrapper; +import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.widget.ProgressBar; @@ -98,6 +99,21 @@ public class GlifLayoutTest extends InstrumentationTestCase { } } + @SmallTest + public void testWrongTheme() { + // Test the error message when using the wrong theme + mContext = new ContextThemeWrapper(getInstrumentation().getContext(), + android.R.style.Theme); + try { + new GlifLayout(mContext); + fail("Should have thrown InflateException"); + } catch (InflateException e) { + assertEquals("Exception message should mention correct theme to use", + "Unable to inflate layout. Are you using @style/SuwThemeGlif " + + "(or its descendant) as your theme?", e.getMessage()); + } + } + private void assertDefaultTemplateInflated(GlifLayout layout) { View title = layout.findViewById(R.id.suw_layout_title); assertNotNull("@id/suw_layout_title should not be null", title); diff --git a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java index d53f49c..44aff73 100644 --- a/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java +++ b/library/test/src/com/android/setupwizardlib/test/GlifPatternDrawableTest.java @@ -16,9 +16,11 @@ package com.android.setupwizardlib.test; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; +import android.graphics.Paint; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -27,6 +29,45 @@ import com.android.setupwizardlib.GlifPatternDrawable; public class GlifPatternDrawableTest extends AndroidTestCase { @SmallTest + public void testDraw() { + final Bitmap bitmap = Bitmap.createBitmap(1366, 768, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED); + drawable.setBounds(0, 0, 1366, 768); + drawable.draw(canvas); + + assertEquals("Top left pixel should be #ed0000", 0xffed0000, bitmap.getPixel(0, 0)); + assertEquals("Center pixel should be #d30000", 0xffd30000, bitmap.getPixel(683, 384)); + assertEquals("Bottom right pixel should be #c70000", 0xffc70000, + bitmap.getPixel(1365, 767)); + } + + @SmallTest + public void testDrawTwice() { + // Test that the second time the drawable is drawn is also correct, to make sure caching is + // done correctly. + + final Bitmap bitmap = Bitmap.createBitmap(1366, 768, Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(bitmap); + + final GlifPatternDrawable drawable = new GlifPatternDrawable(Color.RED); + drawable.setBounds(0, 0, 1366, 768); + drawable.draw(canvas); + + Paint paint = new Paint(); + paint.setColor(Color.WHITE); + canvas.drawRect(0, 0, 1366, 768, paint); // Erase the entire canvas + + drawable.draw(canvas); + + assertEquals("Top left pixel should be #ed0000", 0xffed0000, bitmap.getPixel(0, 0)); + assertEquals("Center pixel should be #d30000", 0xffd30000, bitmap.getPixel(683, 384)); + assertEquals("Bottom right pixel should be #c70000", 0xffc70000, + bitmap.getPixel(1365, 767)); + } + + @SmallTest public void testScaleToCanvasSquare() { final Canvas canvas = new Canvas(); Matrix expected = new Matrix(canvas.getMatrix()); diff --git a/library/test/src/com/android/setupwizardlib/test/ItemTest.java b/library/test/src/com/android/setupwizardlib/test/ItemTest.java index 68ea881..d9104ef 100644 --- a/library/test/src/com/android/setupwizardlib/test/ItemTest.java +++ b/library/test/src/com/android/setupwizardlib/test/ItemTest.java @@ -42,15 +42,22 @@ public class ItemTest extends AndroidTestCase { item.setTitle("TestTitle"); item.setSummary("TestSummary"); Drawable icon = new ShapeDrawable(); + icon.setLevel(4); item.setIcon(icon); View view = createLayout(); + mIconView.setImageLevel(1); + Drawable recycledIcon = new ShapeDrawable(); + mIconView.setImageDrawable(recycledIcon); + item.onBindView(view); assertEquals("Title should be \"TestTitle\"", "TestTitle", mTitleView.getText().toString()); assertEquals("Summary should be \"TestSummary\"", "TestSummary", mSummaryView.getText().toString()); assertSame("Icon should be the icon shape drawable", icon, mIconView.getDrawable()); + assertEquals("Recycled icon level should not change", 1, recycledIcon.getLevel()); + assertEquals("Icon should be level 4", 4, icon.getLevel()); } @SmallTest diff --git a/library/test/src/com/android/setupwizardlib/test/LinkSpanTest.java b/library/test/src/com/android/setupwizardlib/test/LinkSpanTest.java new file mode 100644 index 0000000..884f8ea --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/LinkSpanTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.content.Context; +import android.content.ContextWrapper; +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.widget.TextView; + +import com.android.setupwizardlib.span.LinkSpan; + +public class LinkSpanTest extends AndroidTestCase { + + @SmallTest + public void testOnClick() { + final TestContext context = new TestContext(getContext()); + final TextView textView = new TextView(context); + final LinkSpan linkSpan = new LinkSpan("test_id"); + + linkSpan.onClick(textView); + + assertSame("Clicked LinkSpan should be passed to setup", linkSpan, context.clickedSpan); + } + + @SmallTest + public void testNonImplementingContext() { + final Context context = getContext(); + final TextView textView = new TextView(context); + final LinkSpan linkSpan = new LinkSpan("test_id"); + + linkSpan.onClick(textView); + + // This would be no-op, because the context doesn't implement LinkSpan.OnClickListener. + // Just check that no uncaught exception here. + } + + private static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener { + + public LinkSpan clickedSpan = null; + + public TestContext(Context base) { + super(base); + } + + @Override + public void onClick(LinkSpan span) { + clickedSpan = span; + } + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/SetupWizardLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/SetupWizardLayoutTest.java index 9e291ac..f71c794 100644 --- a/library/test/src/com/android/setupwizardlib/test/SetupWizardLayoutTest.java +++ b/library/test/src/com/android/setupwizardlib/test/SetupWizardLayoutTest.java @@ -19,9 +19,13 @@ package com.android.setupwizardlib.test; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.os.Parcelable; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.SmallTest; +import android.util.SparseArray; +import android.view.AbsSavedState; import android.view.ContextThemeWrapper; +import android.view.InflateException; import android.view.LayoutInflater; import android.view.View; import android.widget.ProgressBar; @@ -143,6 +147,72 @@ public class SetupWizardLayoutTest extends InstrumentationTestCase { assertFalse("Progress bar should not be shown", layout.isProgressBarShown()); } + @SmallTest + public void testWrongTheme() { + // Test the error message when using the wrong theme + mContext = new ContextThemeWrapper(getInstrumentation().getContext(), + android.R.style.Theme); + try { + new SetupWizardLayout(mContext); + fail("Should have thrown InflateException"); + } catch (InflateException e) { + assertEquals("Exception message should mention correct theme to use", + "Unable to inflate layout. Are you using @style/SuwThemeMaterial " + + "(or its descendant) as your theme?", e.getMessage()); + } + } + + @SmallTest + public void testOnRestoreFromInstanceState() { + final SetupWizardLayout layout = new SetupWizardLayout(mContext); + // noinspection ResourceType + layout.setId(1234); + + SparseArray<Parcelable> container = new SparseArray<>(); + layout.saveHierarchyState(container); + + final SetupWizardLayout layout2 = new SetupWizardLayout(mContext); + // noinspection ResourceType + layout2.setId(1234); + layout2.restoreHierarchyState(container); + + assertFalse("Progress bar should not be shown", layout2.isProgressBarShown()); + } + + @SmallTest + public void testOnRestoreFromInstanceStateProgressBarShown() { + final SetupWizardLayout layout = new SetupWizardLayout(mContext); + // noinspection ResourceType + layout.setId(1234); + + layout.setProgressBarShown(true); + + SparseArray<Parcelable> container = new SparseArray<>(); + layout.saveHierarchyState(container); + + final SetupWizardLayout layout2 = new SetupWizardLayout(mContext); + // noinspection ResourceType + layout2.setId(1234); + layout2.restoreHierarchyState(container); + + assertTrue("Progress bar should be shown", layout2.isProgressBarShown()); + } + + @SmallTest + public void testOnRestoreFromIncompatibleInstanceState() { + final SetupWizardLayout layout = new SetupWizardLayout(mContext); + // noinspection ResourceType + layout.setId(1234); + + SparseArray<Parcelable> container = new SparseArray<>(); + container.put(1234, AbsSavedState.EMPTY_STATE); + layout.restoreHierarchyState(container); + + // SetupWizardLayout shouldn't crash with incompatible Parcelable + + assertFalse("Progress bar should not be shown", layout.isProgressBarShown()); + } + private void assertDefaultTemplateInflated(SetupWizardLayout layout) { View decorView = layout.findViewById(R.id.suw_layout_decor); View navbar = layout.findViewById(R.id.suw_layout_navigation_bar); diff --git a/library/test/src/com/android/setupwizardlib/test/SpanHelperTest.java b/library/test/src/com/android/setupwizardlib/test/SpanHelperTest.java new file mode 100644 index 0000000..819e969 --- /dev/null +++ b/library/test/src/com/android/setupwizardlib/test/SpanHelperTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.setupwizardlib.test; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.text.Annotation; +import android.text.SpannableStringBuilder; + +import com.android.setupwizardlib.span.SpanHelper; + +public class SpanHelperTest extends AndroidTestCase { + + @SmallTest + public void testReplaceSpan() { + SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world"); + Annotation oldSpan = new Annotation("key", "value"); + Annotation newSpan = new Annotation("newkey", "newvalue"); + ssb.setSpan(oldSpan, 2, 5, 0 /* flags */); + + SpanHelper.replaceSpan(ssb, oldSpan, newSpan); + + final Object[] spans = ssb.getSpans(0, ssb.length(), Object.class); + assertEquals("There should be one span in the builder", 1, spans.length); + assertSame("The span should be newSpan", newSpan, spans[0]); + } +} diff --git a/library/test/src/com/android/setupwizardlib/test/StatusBarBackgroundLayoutTest.java b/library/test/src/com/android/setupwizardlib/test/StatusBarBackgroundLayoutTest.java index fe680e4..2cb26ec 100644 --- a/library/test/src/com/android/setupwizardlib/test/StatusBarBackgroundLayoutTest.java +++ b/library/test/src/com/android/setupwizardlib/test/StatusBarBackgroundLayoutTest.java @@ -16,6 +16,7 @@ package com.android.setupwizardlib.test; +import android.content.Context; import android.graphics.drawable.ShapeDrawable; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -32,4 +33,37 @@ public class StatusBarBackgroundLayoutTest extends AndroidTestCase { assertSame("Status bar background drawable should be same as set", drawable, layout.getStatusBarBackground()); } + + @SmallTest + public void testAttachedToWindow() { + // Attaching to window should request apply window inset + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + final TestStatusBarBackgroundLayout layout = + new TestStatusBarBackgroundLayout(getContext()); + layout.mRequestApplyInsets = false; + layout.onAttachedToWindow(); + + assertTrue("Attaching to window should apply window inset", layout.mRequestApplyInsets); + } + } + + private static class TestStatusBarBackgroundLayout extends StatusBarBackgroundLayout { + + boolean mRequestApplyInsets = false; + + TestStatusBarBackgroundLayout(Context context) { + super(context); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + } + + @Override + public void requestApplyInsets() { + super.requestApplyInsets(); + mRequestApplyInsets = true; + } + } } |